from __future__ import annotations

from gi.repository import Gio, GLib

import itertools
import logging
import os
import sqlite3
from threading import current_thread, Lock
from typing import List, Optional
import unicodedata

import iotas.config_manager


class DbCursor:
    """
    Database cursor.
    """

    def __init__(self, obj, commit: bool = False):
        self.__obj = obj
        self.__commit = commit
        self.__cursor = None

    def __enter__(self):
        thread_id = current_thread().getName()
        app = Gio.Application.get_default()
        if thread_id in app.cursors.keys():
            cursor = app.cursors[thread_id]
            return cursor
        else:
            self.__cursor = self.__obj.get_cursor()
            return self.__cursor

    def __exit__(self, type, value, traceback) -> None:
        if self.__cursor is not None:
            if self.__commit:
                self.__obj.acquire_lock()
                self.__cursor.commit()
                self.__obj.release_lock()
            self.__cursor.close()
        self.__cursor = None


class DbLock:
    """
    Database lock.
    """

    def __init__(self):
        self.__lock = Lock()
        self.__locked = False

    def acquire(self) -> None:
        """Acquire lock."""
        self.__locked = True
        self.__lock.acquire()

    def release(self) -> None:
        """Release lock."""
        self.__locked = False
        self.__lock.release()

    @property
    def locked(self) -> bool:
        return self.__locked


class Database:
    """
    Base database object.
    """

    NOTES_DATA_PATH = GLib.get_user_data_dir() + "/iotas"
    DB_PATH = "{}/notes.db".format(NOTES_DATA_PATH)
    VERSION = 1

    __create_note = """CREATE TABLE note (id INTEGER PRIMARY KEY,
                                          title TEXT NOT NULL,
                                          content TEXT NOT NULL,
                                          excerpt TEXT NOT NULL,
                                          last_modified INT NOT NULL,
                                          favourite BOOLEAN NOT NULL,
                                          category TEXT NOT NULL,
                                          remote_id INT NOT NULL,
                                          dirty BOOLEAN NOT NULL,
                                          locally_deleted BOOLEAN NOT NULL,
                                          etag TEXT,
                                          read_only BOOLEAN NOT NULL DEFAULT false)"""
    __create_note_modified_idx = """CREATE index idx_note_last_modified ON note(last_modified)"""
    __create_note_remote_id_idx = """CREATE index idx_note_remote_id ON note(remote_id)"""

    # A migration is a tuple comprising the query and whether we want to resync from the server
    __migrations = {}
    __migrations[1] = ("ALTER TABLE note ADD COLUMN read_only BOOLEAN NOT NULL DEFAULT false", True)

    def __init__(self):
        self.__thread_lock = DbLock()
        f = Gio.File.new_for_path(self.DB_PATH)
        if not f.query_exists():
            try:
                d = Gio.File.new_for_path(self.NOTES_DATA_PATH)
                if not d.query_exists():
                    d.make_directory_with_parents()
                with DbCursor(self, True) as sql:
                    sql.execute(self.__create_note)
                    sql.execute(self.__create_note_modified_idx)
                    sql.execute(self.__create_note_remote_id_idx)
                    sql.execute("PRAGMA user_version=%s" % self.VERSION)
            except Exception as e:
                logging.error("DB init error: {}".format(e))
        else:
            current_version = self.__get_version()
            if current_version < self.VERSION:
                self.__run_migrations(current_version)
            elif current_version > self.VERSION:
                versions = f"v{current_version} > v{self.VERSION}"
                logging.warning(f"Database schema version is newer than expected ({versions})")

    def acquire_lock(self):
        self.__thread_lock.acquire()

    def release_lock(self):
        self.__thread_lock.release()

    def execute(self, request: str) -> List:
        """Execute a query

        :param str request: The request
        :return: A list of ids, if applicable
        :rtype: List
        """
        try:
            ids = []
            db = Gio.Application.get_default().db
            with DbCursor(db) as sql:
                result = sql.execute(request)
                ids += list(itertools.chain(*result))
                return ids
        except Exception as e:
            logging.error("DB execute error: {} for {}".format(e, request))
        return []

    def get_cursor(self) -> Optional[DbCursor]:
        """Get a cursor.

        :return: The cursor
        :rtype: Optional[DbCursor]
        """
        try:
            c = sqlite3.connect(self.DB_PATH)
            c.create_function("noaccents", 1, noaccents)
            return c
        except Exception as e:
            logging.error("Couldn't get cursor: {}".format(e))
            exit(-1)

    def trash(self) -> None:
        """Trash the database, remove the file."""
        os.remove(self.DB_PATH)

    def __get_version(self) -> Optional[int]:
        request = "PRAGMA user_version"
        try:
            version = None
            with DbCursor(self) as sql:
                result = sql.execute(request)
                row = result.fetchone()
                if isinstance(row, tuple) and len(row) == 1:
                    version = row[0]
        except Exception as e:
            logging.error("DB execute error: {} for {}".format(e, request))
        return version

    def __set_version(self, version: int) -> None:
        if not isinstance(version, int):
            return
        request = f"PRAGMA user_version = {version}"
        try:
            with DbCursor(self) as sql:
                sql.execute(request)
        except Exception as e:
            logging.error("DB execute error: {} for {}".format(e, request))

    def __run_migrations(self, current_version: int) -> None:
        logging.info(
            "Running database schema migration from v{} to v{}".format(
                current_version, self.VERSION
            )
        )
        resync_requested = False
        while current_version != self.VERSION:
            request, resync = self.__migrations[current_version + 1]
            try:
                with DbCursor(self) as sql:
                    sql.execute(request)
            except Exception as e:
                logging.error("DB execute error: {} for {}".format(e, request))
                break
            current_version += 1
            if resync:
                resync_requested = True

        if resync_requested:
            iotas.config_manager.set_perform_full_server_refresh(True)

        self.__set_version(current_version)


def noaccents(string: str) -> str:
    """Produce unaccented form of string

    :param str string: Input string
    :return: Unaccented form
    :rtype: str
    """
    nfkd_form = unicodedata.normalize("NFKD", string)
    v = "".join([c for c in nfkd_form if not unicodedata.combining(c)])
    return v.lower()
