"""
database.py
===========
The core module for the Database class.
"""

import sys

from itertools import islice
from sqlitedict import SqliteDict
from collections import OrderedDict

from nsaphx.log import LOGGER
from nsaphx.base.utils import human_readible_size

class Database:
    """ Database class
    
    Parameters:
    -----------

    :db_path: str
        Path to the database file.  

    The Database class takes care of the access to the database. It is a 
    wrapper class for the sqlitedict package. The Database class has a 
    in-memory cache to speed up the access to the recentely used values. 
    User can update the cache size. 

    Examples:  
    ---------

    >>> from nsaphx.database import Database  
    >>> db = Database("test.db")  
    >>> db.set_value("key1", "value1")  
    >>> db.get_value("key1")  
    'value1'

    >>> db.delete_value("key1")  
    >>> db.get_value("key1")  
    >>> db.update_cache_size(100)  
    >>> db.close_db()  

    """

    _cache = None
    _cache_size = None 

    def __init__(self, db_path):
        """ 
        Initializes the database.
        Inputs:
            | db_path: path to the database file.
        """

        self.name = db_path
        self._init_reserved_keys()

        if Database._cache is None:       
            Database._cache_size = 1000
            Database._cache = OrderedDict()
            LOGGER.debug(f"In memory cache has been initiated "+\
                         f"with size: {Database._cache_size}.")

    def __str__(self):
        return f"SQLitedict Database: {self.name}"

    def __repr__(self):
        return f"Database(db_path={self.name})"

    def set_value(self, key, value):
        """ 
        Sets the key and given value in the database. If the key exists,
        it will override the value. In that case, it will remove the key from
        the in-memory dictionary. It will be loaded again with the get_value
        command if needed.

        Inputs:

        | key: str 
            hash value (generated by the package)
        | value: Any 
            Any python object

        """
        try:
            with SqliteDict(self.name, autocommit=True) as db:
                db[key] = value
                del Database._cache[key]
        except KeyError:
            LOGGER.debug(f"Tried to delete non-existing {key} on the cache.")
        except RuntimeError as e:
            print(e)
        except Exception:
            LOGGER.warning(f"Tried to set {key} on the database." + 
                           f"Something went wrong.")
        finally:
            return

    def delete_value(self,key):
        """ Deletes the key, and its value from both in-memory dictionary and
        on-disk database. If the key is not found, simply ignores it.
        
        Inputs:
        
        | key: str
            A hash value (generated by the package)
        """
        try:
            with SqliteDict(self.name, autocommit=True) as db:
                reserved_keys = db["RESERVED_KEYS"]
                if key in reserved_keys:
                    LOGGER.debug(f"An attempt to remove {key} was recorded.")
                    print(f"{key} is a Reserved key. Reserved keys are not removable.")
                    return
            
                del db[key]   
            try: 
                del Database._cache[key]
            except Exception as e:
                print(e)         
            LOGGER.debug(f"Value {key} is removed from database.")
        except KeyError:
            LOGGER.warning(f"Tried to delete '{key}' on the database."
             " No such keys on the database.")
        except Exception as e:
            print(e)
        finally:
            db.commit()
            db.close()

    def get_value(self, key):
        """ Returns the value in the following order:
        
        | 1) It will look for the value in the cache and return it, if not found
        | 2) will look for the value in the disk and return it, if not found
        | 3) will return None.
        
        Parameters
        ----------
        key: str
            hash value (generated by the package)

        Returns
        -------
        value: Any | None 
            If found, value, else returns None.         
        """
        value = None
        try:
            value = Database._cache[key]
            LOGGER.debug(f"Key: {key}. Value is loaded from the cache.")
            LOGGER.debug(f"In memory cache size: {len(Database._cache)}")
        except:
            LOGGER.debug(f"Key: {key}. Value is not found in the cache.")

        if value is None:
            try:
                with SqliteDict(self.name, autocommit=True) as db:
                    tmp = db[key]
                if len(Database._cache) >  Database._cache_size - 1:
                    Database._cache.popitem(last=False)
                    LOGGER.debug(f"cache size is more than limit"
                     f"{Database._cache_size}. An item removed, and new item added.")
                Database._cache[key] = tmp
                return tmp
            except RuntimeError as e:
                print(f"RuntimeError: {e}")
                return
            except Exception:
                LOGGER.debug(f"The requested key ({key}) is not in the"
                 " database. Returns None.")
                return None
        else:
            return value

    def summary(self):
        """ 
        Returns a summary of the cache. It includes the length, limit and 
        human readible cache size.  
        """
        try: 
            _db_c_count = len(Database._cache)
            _db_c_limit = Database._cache_size
            _db_c_size_hr = human_readible_size(
                sys.getsizeof(Database._cache))
            _db_name = self.name
            _db_size_hr = human_readible_size(
                sys.getsizeof(self.name))  
            
            print(f"Cache: \n"
                  + f"  length: {_db_c_count}\n"
                  + f"  limit: {_db_c_limit} \n"
                  + f"  size: {_db_c_size_hr}\n" 
                  + f"Database: \n"
                  + f"  name: {_db_name}\n"
                  + f"  size: {_db_size_hr}\n")
        except Exception as e:
            print(e)
      
    def update_cache_size(self, new_size):
        """
        Update the cache size. If the new size is smaller than the current size,
        it will remove the oldest items from the cache.

        Parameters
        ----------
        new_size: int
            A new cache size (this is the number of items, not the size on 
            the disk)
        """
        Database._cache_size = new_size
        if Database._cache_size > new_size:
            keys = list(islice(Database._cache, new_size))
            tmp_cache = OrderedDict()
            for key in keys:
                tmp_cache[key] = Database._cache[key]
            Database._cache = tmp_cache

    def close_db(self):
        """ Commits changes to the database, closes the database, clears the 
        cache.
        """
        Database._cache = None
        LOGGER.info(f"Database ({self.name}) is closed.")

    def _init_reserved_keys(self):
        """ Initializes the reserved keys in the database. There are two 
        reserved keys: RESERVED_KEYS and PROJECTS_LIST."""

        try:
            with SqliteDict(self.name, autocommit=True) as db:
                if "RESERVED_KEYS" not in db:
                    db["RESERVED_KEYS"] = ["RESERVED_KEYS", "PROJECTS_LIST"]
                    LOGGER.debug(f"Reserved keys are initialized.")     
        except RuntimeError as e:
            print(e)
        except Exception:
            LOGGER.debug(f"Reserved keys are already initialized.")
        finally:
            return
