# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017-2019 BasioMeusPuga

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import os
import pickle
import sqlite3
import logging

from PyQt5 import QtCore

logger = logging.getLogger(__name__)


class DatabaseInit:
    def __init__(self, location_prefix):
        self.database_path = os.path.join(location_prefix, 'Lector.db')

        self.books_table_columns = {
            'id': 'INTEGER PRIMARY KEY',
            'Title': 'TEXT',
            'Author': 'TEXT',
            'Year': 'INTEGER',
            'DateAdded': 'BLOB',
            'Path': 'TEXT',
            'Position': 'BLOB',
            'ISBN': 'TEXT',
            'Tags': 'TEXT',
            'Hash': 'TEXT',
            'LastAccessed': 'BLOB',
            'Bookmarks': 'BLOB',
            'CoverImage': 'BLOB',
            'Addition': 'TEXT',
            'Annotations': 'BLOB'}

        self.directories_table_columns = {
            'id': 'INTEGER PRIMARY KEY',
            'Path': 'TEXT',
            'Name': 'TEXT',
            'Tags': 'TEXT',
            'CheckState': 'INTEGER'}

        if os.path.exists(self.database_path):
            self.check_columns()
        else:
            self.create_database()

    def create_database(self):
        self.database = sqlite3.connect(self.database_path)

        column_string = ', '.join(
            [i[0] + ' ' + i[1] for i in self.books_table_columns.items()])
        self.database.execute(f"CREATE TABLE books ({column_string})")

        # CheckState is the standard QtCore.Qt.Checked / Unchecked
        column_string = ', '.join(
            [i[0] + ' ' + i[1] for i in self.directories_table_columns.items()])
        self.database.execute(f"CREATE TABLE directories ({column_string})")

        self.database.commit()
        self.database.close()

    def check_columns(self):
        self.database = sqlite3.connect(self.database_path)

        database_return = self.database.execute("PRAGMA table_info(books)").fetchall()
        database_columns = [i[1] for i in database_return]

        # This allows for addition of a column without having to reform the database
        commit_required = False
        for i in self.books_table_columns.items():
            if i[0] not in database_columns:
                commit_required = True
                info_string = f'Database: Adding column "{i[0]}"'
                logger.info(info_string)
                sql_command = f"ALTER TABLE books ADD COLUMN {i[0]} {i[1]}"
                self.database.execute(sql_command)

        if commit_required:
            self.database.commit()


class DatabaseFunctions:
    def __init__(self, location_prefix):
        database_path = os.path.join(location_prefix, 'Lector.db')
        self.database = sqlite3.connect(database_path)

    def set_library_paths(self, data_iterable):
        self.database.execute("DELETE FROM directories")

        for i in data_iterable:
            path = i[0]
            name = i[1]
            tags = i[2]
            is_checked = i[3]

            if not os.path.exists(path):
                continue  # Remove invalid paths from the database

            sql_command = (
                "INSERT INTO directories (Path, Name, Tags, CheckState)\
                 VALUES (?, ?, ?, ?)")
            self.database.execute(sql_command, [path, name, tags, is_checked])

        self.database.commit()
        self.database.close()

    def add_to_database(self, data):
        # data is expected to be a dictionary
        # with keys corresponding to the book hash
        # and corresponding items containing
        # whatever else needs insertion
        # Haha I said insertion

        # Add the current datetime value to each file's database entry
        # current_time = datetime.datetime.now()
        current_datetime = QtCore.QDateTime().currentDateTime()
        current_datetime_bin = sqlite3.Binary(pickle.dumps(current_datetime))

        for i in data.items():
            book_hash = i[0]
            title = i[1]['title']
            author = i[1]['author']
            year = i[1]['year']
            path = i[1]['path']
            cover = i[1]['cover_image']
            isbn = i[1]['isbn']
            addition_mode = i[1]['addition_mode']
            tags = i[1]['tags']
            if tags:
                # Is a list. Needs to be a string
                tags = ', '.join([str(j) for j in tags])
            else:
                # Is still a list. Needs to be None.
                tags = None

            sql_command_add = (
                "INSERT OR REPLACE INTO \
                books (Title, Author, Year, DateAdded, Path, \
                ISBN, Tags, Hash, CoverImage, Addition) \
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")

            cover_insert = None
            if cover:
                cover_insert = sqlite3.Binary(cover)

            self.database.execute(
                sql_command_add,
                [title, author, year, current_datetime_bin,
                 path, isbn, tags, book_hash, cover_insert,
                 addition_mode])

        self.database.commit()
        self.database.close()

    def fetch_data(self, columns, table, selection_criteria, equivalence, fetch_one=False):
        # columns is a tuple that will be passed as a comma separated list
        # table is a string that will be used as is
        # selection_criteria is a dictionary which contains the name of a column linked
        # to a corresponding value for selection

        # Example:
        # Name and AltName are expected to be the same
        # sel_dict = {
        #     'Name': 'sav',
        #     'AltName': 'sav'
        # }
        # data = DatabaseFunctions().fetch_data(('Name',), 'books', sel_dict)
        try:
            column_list = ','.join(columns)
            sql_command_fetch = f"SELECT {column_list} FROM {table}"
            if selection_criteria:
                sql_command_fetch += " WHERE"

                if equivalence == 'EQUALS':
                    for i in selection_criteria.keys():
                        search_parameter = selection_criteria[i]
                        sql_command_fetch += f" {i} = '{search_parameter}' OR"

                elif equivalence == 'LIKE':
                    for i in selection_criteria.keys():
                        search_parameter = "'%" + selection_criteria[i] + "%'"
                        sql_command_fetch += f" {i} LIKE {search_parameter} OR"

                sql_command_fetch = sql_command_fetch[:-3]  # Truncate the last OR

            # book data is returned as a list of tuples
            data = self.database.execute(sql_command_fetch).fetchall()
            self.database.close()

            if data:
                # Because this is the result of a fetchall(), we need an
                # ugly hack (tm) to get correct results
                if fetch_one:
                    return data[0][0]

                return data
            else:
                return None

        except Exception as e:
            error_string = 'SQLite is in wretched rebellion @ data fetching handling'
            logger.critical(error_string + f' {type(e).__name__} Arguments: {e.args}')

    def fetch_covers_only(self, hash_list):
        parameter_marks = ','.join(['?' for i in hash_list])
        sql_command = f"SELECT Hash, CoverImage from books WHERE Hash IN ({parameter_marks})"
        data = self.database.execute(sql_command, hash_list).fetchall()
        self.database.close()
        return data

    def modify_metadata(self, metadata_dict, book_hash):
        def generate_binary(column, data):
            if column in ('Position', 'LastAccessed', 'Bookmarks', 'Annotations'):
                return sqlite3.Binary(pickle.dumps(data))
            elif column == 'CoverImage':
                return sqlite3.Binary(data)
            else:
                return data

        sql_command = 'UPDATE books SET '
        update_data = []
        for i in metadata_dict.items():
            sql_command += i[0] + ' = ?, '
            bin_data = generate_binary(i[0], i[1])
            update_data.append(bin_data)

        sql_command = sql_command[:-2]
        sql_command += ' WHERE Hash = ?'
        update_data.append(book_hash)

        try:
            self.database.execute(
                sql_command, update_data)
        except sqlite3.OperationalError as e:
            error_string = 'SQLite is in wretched rebellion @ metadata handling'
            logger.critical(error_string + f' {type(e).__name__} Arguments: {e.args}')

        self.database.commit()
        self.database.close()

    def delete_from_database(self, column_name, target_data):
        # target_data is an iterable

        if column_name == '*':
            self.database.execute(
                "DELETE FROM books WHERE NOT Addition = 'manual'")
        else:
            sql_command = f"DELETE FROM books WHERE {column_name} = ?"
            for i in target_data:
                self.database.execute(sql_command, (i,))

        self.database.commit()
        self.database.close()

    def vacuum_database(self):
        self.database.execute("VACUUM")
        return True