import hashlib
import json
import os
import sqlite3
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
import base64


class SqliteCipher:
    """
    Main cipher class
    """

    def __init__(
        self, database_path="pysqlitecipher.db", check_same_thread=False, password=None
    ):
        self.database_path = database_path
        self.check_same_thread = check_same_thread

        # main sqlite3 connection object
        self.sql_object = sqlite3.connect(
            database_path, check_same_thread=check_same_thread
        )

        # password is essential, so it should not be None
        if password is None:
            raise RuntimeError("password is not passed")

        # storing into object
        self.password = str(password)

        # check if the tableNames table exist in data base if not exist create one and insert it into tableNames table to know that this table exist
        self.sql_object.execute(
            "CREATE TABLE IF NOT EXISTS tableNames (table_name TEXT, secured INT);"
        )
        self.sql_object.commit()

        if not self.check_table_exist2("tableNames"):
            self.sql_object.execute(
                "INSERT INTO tableNames (table_name, secured) VALUES ('tableNames', 0);"
            )
            self.sql_object.commit()

        # check if the authenticationTable exist, if not create table
        if not self.check_table_exist2("authenticationTable"):
            self.sql_object.execute(
                "CREATE TABLE authenticationTable (SHA512_pass TEXT, salt BLOB);"
            )
            self.sql_object.execute(
                "INSERT INTO tableNames (table_name, secured) VALUES ('authenticationTable', 0);"
            )
            self.sql_object.commit()

            # converting password to SHA512
            sha512_pass = hashlib.sha512(self.password.encode()).hexdigest()

            # getting a random key from fernet
            salt = os.urandom(16)

            # adding sha512 password and encrypted key to data base
            self.sql_object.execute(
                "INSERT INTO authenticationTable (SHA512_pass, salt) VALUES (?, ?);",
                [sha512_pass, sqlite3.Binary(salt)],
            )
            self.sql_object.commit()
        else:
            # validate the password passed to password stored in data base
            # converting password to SHA512
            sha512_pass = hashlib.sha512(self.password.encode()).hexdigest()
            # getting the password from data base
            cursor_from_sql = self.sql_object.execute(
                "SELECT * FROM authenticationTable;"
            )
            for i in cursor_from_sql:
                sha512_pass_from_db = i[0]

            # validating and raising error if not match
            if sha512_pass_from_db != sha512_pass:
                raise RuntimeError(
                    "password does not match to password used to create data base"
                )

        # getting the encrypted key from db
        cursor_from_sql = self.sql_object.execute("SELECT * FROM authenticationTable;")
        for i in cursor_from_sql:
            salt = i[1]

        # initialising fernet module
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=480000,
            backend=default_backend(),
        )

        key = base64.urlsafe_b64encode(kdf.derive(bytes(self.password, "utf-8")))
        self.cipher_suite = Fernet(key)

        self.md5_pass = hashlib.md5(self.password.encode()).hexdigest()

    @classmethod
    def sha512_converter(cls, password):
        """
        Convert a string to SHA512
        :param cls: class
        :param password: string to convert
        :returns: sha512 hash from string
        """
        return hashlib.sha512(password.encode()).hexdigest()

    @classmethod
    def get_verifier(cls, database_path, check_same_thread):
        """
        Get the sha512 from db to verify it on your own
        :param cls: class
        :param database_path: path to database
        :param check_same_thread: whether to check same thread
        :returns: sha512 pass from database
        """
        # main sqlite3 connection object
        sql_object = sqlite3.connect(database_path, check_same_thread=check_same_thread)

        # getting the password from data base
        cursor_from_sql = sql_object.execute("SELECT * FROM authenticationTable;")
        for i in cursor_from_sql:
            sha512_pass_from_db = i[0]

        return sha512_pass_from_db

    def check_table_exist(self, table_name):
        """
        Check if a table exist or not
        :param table_name: Table name to check existence for
        :return: %True if found, %False otherwise
        """
        # table name should be only str type
        if table_name is None:
            raise ValueError("Table name cannot be None in check_table_exist method")

        try:
            table_name = str(table_name).strip()
        except ValueError as exc:
            raise ValueError(
                "Table name passed in check_table_exist function cannot be converted to string"
            ) from exc

        # getting all tablenames in data base
        result = self.sql_object.execute("SELECT * FROM tableNames;")

        exist = False
        table_list = []

        # if the table exist then a list will be returned and for loop we run at least once
        for i in result:
            if i[1] == 1:
                table_list.append(self.decryptor(i[0]))
            if i[1] == 0:
                table_list.append(i[0])

        for i in table_list:
            if i == table_name:
                exist = True
                break

        return exist

    def check_table_exist2(self, table_name):
        """
        Check if a table exist or not (without match name with secured table)
        :param table_name: Table name to check existence for
        :return: %True if found, %False otherwise
        """

        # table name should be only str type
        if table_name is None:
            raise ValueError("Table name cannot be None in check_table_exist2 method")

        try:
            table_name = str(table_name).strip()
        except ValueError as exc:
            raise ValueError(
                "Table name passed in check_table_exist2 function cannot be converted to string"
            ) from exc

        # getting all tablenames in data base
        result = self.sql_object.execute("SELECT * FROM tableNames;")

        exist = False

        # if the table exist then a list will be returned and for loop we run at least once
        for i in result:
            if i[1] == 0:
                if i[0] == table_name:
                    exist = True
                    break

        return exist

    def encryptor(self, string):
        """
        Encrypt the passed string
        :param string: string to encrypt
        :return: encrypted string
        TODO: Combine with binary
        """
        # encrypting
        string_to_pass = bytes(string, "utf-8")
        encoded_text = self.cipher_suite.encrypt(string_to_pass)

        return encoded_text.decode("utf-8")

    def decryptor(self, string):
        """
        Decrypt the passed string
        :param string: string to decrypt
        :return: decrypted string
        """
        # decrypting
        string_to_pass = bytes(string, "utf-8")
        decoded_text = self.cipher_suite.decrypt(string_to_pass)
        return decoded_text.decode("utf-8")

    def encryptor_binary(self, bytes_data):
        """
        Encrypt the passed bytes
        :param bytes_data: bytes to encrypt
        :return: encrypted bytes
        """
        return self.cipher_suite.encrypt(bytes_data)

    def decryptor_binary(self, bytes_data):
        """
        Decrypt the passed bytes
        :param bytes_data: bytes to decrypt
        :return: decoded bytes
        """

        return self.cipher_suite.decrypt(bytes_data)

    def create_table(self, table_name, col_list, make_secure=False, commit=True):
        """
        function to create a table

        col_list should be like this -
        [
            [colname, datatype],
            [colname2, datatype],
        ]

        dataType allowed = TEXT, REAL, INT

        tags for data type
        TEXT - T
        REAL - R
        INT - I
        JSON - J
        LIST - L
        BLOB - B

        example  = [
            ["rollno", "INT"],
            ["name", "TEXT"],
        ]

        if make_secure is True table name, col list should be encrypted before
        add to data base this time while adding created table to tableNames
        table make secure = 1 to know that this table as to deal with encryption
        before performing any operation
        """

        # table name should be only str type
        if table_name is None:
            raise ValueError("Table name cannot be None in create_table method")

        try:
            table_name = str(table_name).strip()
        except ValueError as exc:
            raise ValueError(
                "Table name passed in create_table function cannot be converted to string"
            ) from exc

        # collist should not be empty
        if len(col_list) < 1:
            raise ValueError("col list contains no value in create_table method")

        # table should not exist in database already
        if self.check_table_exist(table_name):
            raise ValueError("table name already exist in data base")

        # if table has to be secured, encrypt table name
        if make_secure:
            table_name = self.encryptor(table_name)
            table_name = "'" + table_name + "'"
            self.sql_object.execute(
                f"INSERT INTO tableNames (table_name, secured) VALUES ({table_name}, 1);"
            )
        else:
            table_name = "'" + table_name + "'"
            self.sql_object.execute(
                f"INSERT INTO tableNames (table_name, secured) VALUES ({table_name}, 0);"
            )

        # init string to execute in sqlite connector
        # by default a primary key ID is used to perform update, delete operations
        # this ID is automatically maintained, so no need to pass it in insert list while inserting data to table
        if make_secure:
            string_to_execute = f"CREATE TABLE {table_name} ( '{self.encryptor('ID_I')}' TEXT PRIMARY KEY NOT NULL, "
        else:
            string_to_execute = (
                f"CREATE TABLE {table_name} ( 'ID_I' TEXT PRIMARY KEY NOT NULL, "
            )

        # traverse col list to add colname and data types to string_to_execute
        for i in col_list:
            # i[0] = colname
            # i[1] = datatype

            colname = i[0]

            # add data type tags to the col names
            if i[1] == "INT":
                colname = colname + "_I"
            elif i[1] == "REAL":
                colname = colname + "_R"
            elif i[1] == "JSON":
                colname = colname + "_J"
            elif i[1] == "LIST":
                colname = colname + "_L"
            elif i[1] == "BLOB":
                colname = colname + "_B"
            # TEXT will be default data type
            else:
                colname = colname + "_T"

            if make_secure:
                colname = self.encryptor(colname)

            # converting colname to 'colname'
            colname = "'" + colname + "'"

            if i[1] == "BLOB":
                # BLOB data type
                string_to_execute = string_to_execute + colname + " BLOB"
            else:
                # only TEXT data type is allowed as encryptor only returns string type
                string_to_execute = string_to_execute + colname + " TEXT"

            string_to_execute = string_to_execute + ", "

        # remove extra, at the back
        string_to_execute = string_to_execute[:-3] + ");"

        # creating the table using sql connector
        self.sql_object.execute(string_to_execute)

        if commit:
            self.sql_object.commit()

    def check_if_table_is_secured(self, table_name, raise_error=True):
        """
        Check if a table is meant to be secured and return encrypted name if so
        if raise_error=True then error will be raised when table is not found

        This function will return
        a) table_name in encrypted if secure is enabled
        b) secure tag 1 or 0
        c) None, None if the table does not exist
        """

        # table name should be only str type
        if table_name is None:
            raise ValueError("Table name cannot be None in create_table method")

        try:
            table_name = str(table_name).strip()
        except ValueError as exc:
            raise ValueError(
                "Table name passed in create_table function cannot be converted to string"
            ) from exc

        # getting all tablenames in data base
        result = self.sql_object.execute("SELECT * FROM tableNames;")

        for i in result:
            if i[1] == 1:
                if table_name == self.decryptor(i[0]):
                    return table_name, i[0], True
            elif i[1] == 0:
                if table_name == i[0]:
                    return i[0], None, False

        if raise_error:
            raise ValueError("{}, no such table in data base".format(table_name))

        return None, None, None

    def get_all_table_names(self):
        """
        Return the list of all table names paired with encrypted table_name for
        table which have secure tag=1 and None for secure tag=0
        """
        # getting all tablenames in data base
        result = self.sql_object.execute("SELECT * FROM tableNames;")

        result_list = []
        # adding data to result list
        for i in result:
            if i[1] == 1:
                # decrypting tableNames if they are decrypted
                result_list.append([self.decryptor(i[0]), i[0]])
            if i[1] == 0:
                result_list.append([i[0], None])

        return result_list

    def get_col_names(self, table_name):
        """
        Get the list of col names in a table
        """
        table_name, enc_table_name, secured = self.check_if_table_is_secured(table_name)

        if secured:
            result = self.sql_object.execute(f"SELECT * FROM '{enc_table_name}';")

            # decrypting col names if they are encrypted and removing data type tag along with it
            col_list = [
                [self.decryptor(description[0])[:-2], description[0]]
                for description in result.description
            ]
        else:
            result = self.sql_object.execute(f"SELECT * FROM '{table_name}';")

            col_list = [
                [description[0][:-2], None] for description in result.description
            ]

        return col_list

    def describe_table(self, table_name):
        """
        Get the list of col names along with their data type
        """

        table_name, enc_table_name, secured = self.check_if_table_is_secured(table_name)

        if secured:
            result = self.sql_object.execute(f"SELECT * FROM '{enc_table_name}';")

            # decrypting col names if they are encrypted and removing data type tag along with it
            col_list = [
                [self.decryptor(description[0]), description[0]]
                for description in result.description
            ]
        else:
            result = self.sql_object.execute(f"SELECT * FROM '{table_name}';")

            col_list = [
                [description[0], description[0]] for description in result.description
            ]

        final_col_list = []

        # identifying data type from tag
        for i in col_list:
            if i[0][-1] == "I":
                final_col_list.append([i[0][:-2], "INT", i[1]])
            elif i[0][-1] == "R":
                final_col_list.append([i[0][:-2], "REAL", i[1]])
            elif i[0][-1] == "L":
                final_col_list.append([i[0][:-2], "LIST", i[1]])
            elif i[0][-1] == "J":
                final_col_list.append([i[0][:-2], "JSON", i[1]])
            elif i[0][-1] == "B":
                final_col_list.append([i[0][:-2], "BLOB", i[1]])
            elif i[0][-1] == "T":
                final_col_list.append([i[0][:-2], "TEXT", i[1]])

        return final_col_list

    def insert_into_table(self, table_name, insert_list, commit=True):
        """
        Insert data into table. Insert should contain values of all col
        else None is add to that col
        """
        table_name, enc_table_name, secured = self.check_if_table_is_secured(table_name)

        insert_list = list(insert_list)

        # init string to exe
        if secured:
            string_to_execute = f"INSERT INTO '{enc_table_name}' ( "
        else:
            string_to_execute = f"INSERT INTO '{table_name}' ( "

        # adding columns to string_to_execute
        col_list = self.describe_table(table_name)

        for i in col_list:
            string_to_execute = string_to_execute + " '{}',".format(i[2])

        # getting the result from table
        if secured:
            result = self.sql_object.execute(f"SELECT * FROM '{enc_table_name}';")
        else:
            result = self.sql_object.execute(f"SELECT * FROM '{table_name}';")

        # getting the last ID value
        last_key_from_table = None

        for i in result:
            last_key_from_table = i[0]

        # if the table is secured we need to perform encryption decryption operations
        if secured:
            # if no data in table
            if last_key_from_table is None:
                # init ID as 0
                last_key_from_table = self.encryptor("0")
            else:
                # else get ID, decrypt it, increment it,  encrypt it back
                last_key_from_table = self.decryptor(last_key_from_table)
                last_key_from_table = int(last_key_from_table) + 1
                last_key_from_table = self.encryptor(str(last_key_from_table))
        # if table is not secured we just skip encryption and decryption
        else:
            if last_key_from_table is None:
                last_key_from_table = "0"
            else:
                last_key_from_table = int(last_key_from_table) + 1
                last_key_from_table = str(last_key_from_table)

        # adding the ID value to value list
        string_to_execute = string_to_execute[:-1] + ") VALUES ( '{}', ".format(
            last_key_from_table
        )

        # adding None if the the insert_list as less value than col list
        if len(col_list) - 1 > len(insert_list):
            for i in range(len(col_list) - len(insert_list) - 1):
                insert_list.append("None")

        blob_parameters = []

        # ID col is already been handled
        col_list = col_list[1:]

        # adding the insertion value to string to exe
        for i, j in zip(insert_list, col_list):
            # if the data is blob type then it need to be passed as a parameter list
            if j[1] == "BLOB":
                if secured:
                    i = self.encryptor_binary(i)

                string_to_execute = string_to_execute + "?, "
                blob_parameters.append(sqlite3.Binary(i))
            # convert the list and json data into json string
            elif j[1] == "LIST" or j[1] == "JSON":
                i = json.dumps(i)
                if secured:
                    i = self.encryptor(i)

                string_to_execute = string_to_execute + "'" + str(i) + "', "
            # rest data is converted to string
            else:
                if secured:
                    i = self.encryptor(str(i))

                string_to_execute = string_to_execute + "'" + str(i) + "', "

        string_to_execute = string_to_execute[:-2] + ");"

        self.sql_object.execute(string_to_execute, blob_parameters)

        if commit:
            self.sql_object.commit()

    def get_data_from_table(
        self, table_name, raise_conversion_error=True, omit_id=False, col_type=False
    ):
        """
        Get the all data from table
        returns two variables
        a) col list containing names of cols
        b) value list containing values in form of sublist
            value_list( row1(col1Data, col2Data), row2(col1Data, col2Data) ) = [ [col1Data, col2Data], [col1Data, col2Data] ]
        sometimes module can receive a unexpected data type like int in col of
        list data type then if raise_conversion_error is True then error is raised
        else the exact string is returned
        """

        def raise_conversion_error_function(value, to):
            raise ValueError(f"{value} cannot be converted to {to}")

        table_name, enc_table_name, secured = self.check_if_table_is_secured(table_name)

        table_description = self.describe_table(table_name)

        # generating col list
        col_list = []
        for i in table_description:
            if col_type:
                col_list.append([i[0], i[1]])
            else:
                col_list.append(i[0])

        # getting data from data base
        if secured:
            result = self.sql_object.execute(f"SELECT * FROM '{enc_table_name}';")
        else:
            result = self.sql_object.execute(f"SELECT * FROM '{table_name}';")

        # adding data to value list and decrypting it if required
        value_list = []

        for row in result:
            temp_list = []

            for i, j in zip(row, table_description):
                if j[1] == "BLOB":
                    if secured:
                        i = self.decryptor_binary(i)
                elif j[1] == "LIST":
                    if secured:
                        i = self.decryptor(i)

                    # trying to convert to desired data type
                    try:
                        i = list(json.loads(i))
                    except TypeError:
                        if raise_conversion_error:
                            raise_conversion_error_function()
                        else:
                            i = str(i)
                elif j[1] == "JSON":
                    if secured:
                        i = self.decryptor(i)

                    try:
                        i = dict(json.loads(i))
                    except TypeError:
                        if raise_conversion_error:
                            raise_conversion_error_function()
                        else:
                            i = str(i)
                else:
                    if secured:
                        i = self.decryptor(i)

                    if j[1] == "INT":
                        try:
                            i = int(i)
                        except TypeError:
                            if raise_conversion_error:
                                raise_conversion_error_function()
                            else:
                                i = str(i)
                    elif j[1] == "REAL":
                        try:
                            i = float(i)
                        except TypeError:
                            if raise_conversion_error:
                                raise_conversion_error_function()
                            else:
                                i = str(i)
                    elif j[1] == "TEXT":
                        i = str(i)

                temp_list.append(i)

            value_list.append(temp_list)

        # if the user does not want ID col which is auto maintained and inserted to be returned
        if omit_id:
            col_list = col_list[1:]

            new_value_list = []

            for i in value_list:
                new_value_list.append(i[1:])

            value_list = new_value_list

        return col_list, value_list

    def delete_data_in_table(
        self, table_name, id_value, commit=True, raise_error=True, update_id=True
    ):
        """
        Delete a row based on ID value
        if raise_error is True, a error will be raised if ID is not found,
        but this may result in performance impact as now function as check for
        ID before deletion
        """
        # setting up table names
        table_name, enc_table_name, secured = self.check_if_table_is_secured(table_name)

        table_description = self.describe_table(table_name)

        # col ID name
        id_name = table_description[0][2]

        # we have to make a statement like this
        # "DELETE from COMPANY where ID = 2;"

        if secured:
            # getting data from table to find the corresponding encrypted ID
            result = self.sql_object.execute(f"SELECT * FROM '{enc_table_name}';")

            found = False

            for i in result:
                # if the ID passed is same as found in data base, then pick up the encrypted version of it from data base
                if int(self.decryptor(i[0])) == int(id_value):
                    id_value = i[0]
                    found = True
                    break

            # raise error if ID not found
            if raise_error and not found:
                raise RuntimeError(f"ID = {id_value} not found while deletion process")

            string_to_execute = (
                f"""DELETE from '{enc_table_name}' where "{id_name}"='{id_value}';"""
            )
        else:
            # raise error if ID not found
            if raise_error:
                # getting data from db to check if ID is present in data base
                result = self.sql_object.execute(f"SELECT * FROM '{table_name}';")

                found = False

                for i in result:
                    # if present make found = True
                    if int(i[0]) == int(id_value):
                        found = True
                        break

                # raise error
                if not found:
                    raise RuntimeError(
                        f"ID = {id_value} not found while deletion process"
                    )

            string_to_execute = (
                f"DELETE from '{table_name}' WHERE {id_name}='{id_value}';"
            )

        # exe command
        result = self.sql_object.execute(string_to_execute)

        # if update id is required
        if update_id:
            self.update_ids(table_name, False)

        # commit if wanted
        if commit:
            self.sql_object.commit()

    def update_ids(self, table_name, commit=True):
        # assume ID we total 7 IDS
        # lets say we delete ID = 5 from table then table will still have 0 1 2 3 4 6 ID row, which is not in order
        # we will traverse the data in table and start from 0
        # if at 0 ID is 0, then fine else make it 0
        # if at 1 ID is 1, then fine else make it 1
        # and so on
        _, result = self.get_data_from_table(table_name, omit_id=False)

        count = 0
        for i in result:
            if i[0] != count:
                self.update_in_table(table_name, i[0], "ID", count, False)

            count = count + 1

        # commit if wanted
        if commit:
            self.sql_object.commit()

    def update_in_table(
        self, table_name, id_value, col_name, col_value, commit=True, raise_error=True
    ):
        """
        statement to make like
        "UPDATE COMPANY set SALARY = 25000.00 where ID = 1"
        """

        # setting up table names
        table_name, enc_table_name, secured = self.check_if_table_is_secured(table_name)

        table_description = self.describe_table(table_name)

        # col ID name
        id_name = table_description[0][2]

        # checking if the col name is present in data base
        # if present getting its real name
        col_found = False
        col_data = None
        for i in table_description:
            if i[0] == col_name:
                col_found = True
                col_data = i[1]
                col_name = i[2]
                break

        # raise error if col not found
        if not col_found:
            raise RuntimeError(
                f"no such column - {col_name} in table - {table_name} while updating"
            )

        # list for binary parameters
        blob_parameters = []

        # converting to string and encrypting if needed
        if col_data in ("LIST", "JSON"):
            col_value = json.dumps(col_value)
            if secured:
                col_value = self.encryptor(col_value)
        elif col_data == "BLOB":
            if secured:
                col_value = self.encryptor_binary(col_value)

            blob_parameters.append(sqlite3.Binary(col_value))
        else:
            col_value = str(col_value)
            if secured:
                col_value = self.encryptor(col_value)

        if secured:
            # getting data from table to find the corresponding encrypted ID
            result = self.sql_object.execute(f"SELECT * FROM '{enc_table_name}';")

            found = False

            for i in result:
                # if the ID passed is same as found in data base, then pick up the encrypted version of it from data base
                if int(self.decryptor(i[0])) == int(id_value):
                    id_value = i[0]
                    found = True
                    break

            # raise error if ID not found
            if raise_error and not found:
                raise RuntimeError(f"ID = {id_value} not found while deletion process")

            if col_data == "BLOB":
                string_to_execute = (
                    """UPDATE '{}' set "{}" = ? where "{}"='{}';""".format(
                        enc_table_name, col_name, id_name, id_value
                    )
                )
            else:
                string_to_execute = (
                    """UPDATE '{}' set "{}" = '{}' where "{}"='{}';""".format(
                        enc_table_name, col_name, col_value, id_name, id_value
                    )
                )
        else:
            # raise error if ID not found
            if raise_error:
                # getting data from db to check if ID is present in data base
                result = self.sql_object.execute(f"SELECT * FROM '{table_name}';")

                found = False

                for i in result:
                    # if present make found = True
                    if int(i[0]) == int(id_value):
                        found = True
                        break

                # raise error
                if not found:
                    raise RuntimeError(
                        "ID = {} not found while deletion process".format(id_value)
                    )

            if col_data == "BLOB":
                string_to_execute = (
                    """UPDATE '{}' set "{}" = ? where "{}"='{}';""".format(
                        enc_table_name, col_name, id_name, id_value
                    )
                )
            else:
                string_to_execute = (
                    """UPDATE '{}' set "{}" = '{}' where "{}"='{}';""".format(
                        table_name, col_name, col_value, id_name, id_value
                    )
                )

        # exe command
        result = self.sql_object.execute(string_to_execute, blob_parameters)

        # commit if wanted
        if commit:
            self.sql_object.commit()

    def change_password(self, new_pass):
        """
        Change the password in data base
        we need to encrypt the key using new password and change it in data base
        we need to change the SHA512 value in data base
        """
        # FIXME
        return
        new_pass = str(new_pass)

        # update the encryption in everyTable
        table_list = self.get_all_table_names()

        count = 0
        final_count = len(table_list[2:])

        table_data = []

        for i in table_list[2:]:
            _, _, temp_secured = self.check_if_table_is_secured(i[0])
            if temp_secured == 1:
                col_list, result = self.get_data_from_table(
                    i[0], omit_id=True, col_type=True
                )

                table_data.append([i[0], col_list, result])

                self.sql_object.execute(f"DROP TABLE IF EXISTS '{i[1]}'")
                to_delete = f"""DELETE from 'tableNames' where "table_name"='{i[1]}';"""
                self.sql_object.execute(to_delete)
                self.sql_object.commit()

            yield count, final_count

            count = count + 1

        table_list = self.get_all_table_names()

        # converting password to SHA512
        old_sha512_pass = hashlib.sha512(self.password.encode()).hexdigest()
        new_sha512_pass = hashlib.sha512(new_pass.encode()).hexdigest()

        # change the key
        # string_to_execute = """UPDATE authenticationTable set encryptedKey = '{}' where SHA512_pass = '{}'""".format(encrypted_key, old_sha512_pass)
        # self.sql_object.execute(string_to_execute)
        # self.sql_object.commit()

        # change the sha512 value
        string_to_execute = """UPDATE authenticationTable set SHA512_pass = '{}' where SHA512_pass = '{}'""".format(
            new_sha512_pass, old_sha512_pass
        )
        self.sql_object.execute(string_to_execute)
        self.sql_object.commit()

        self.__init__(self.database_path, self.check_same_thread, new_pass)

        count = 0
        final_count = len(table_data)

        for i in table_data:
            self.create_table(i[0], i[1], True)

            for j in i[2]:
                self.insert_into_table(i[0], j)

            yield count, final_count

            count = count + 1

    def rename_column(self, table_name, col_name, new_name):
        # setting up table names
        table_name, enc_table_name, secured = self.check_if_table_is_secured(table_name)

        table_description = self.describe_table(table_name)

        # checking if the col name is present in data base
        # if present getting its real name
        col_found = False
        col_data = None
        for i in table_description:
            if i[0] == col_name:
                col_found = True
                col_data = i[1]
                col_name = i[2]
                break

        # raise error if col not found
        if not col_found:
            raise RuntimeError(
                f"no such column - {col_name} in table - {table_name} while updating"
            )

        if col_data == "TEXT":
            new_name += "_T"

        string_to_execute = """ALTER TABLE '{}' RENAME COLUMN '{}' TO '{}'""".format(
            enc_table_name, col_name, self.encryptor(new_name)
        )
        self.sql_object.execute(string_to_execute)
        self.sql_object.commit()
