"""A cache for the REST results borrows heavily from the ideas here:
https://github.com/ubershmekel/filecache/tree/master/filecache
"""
from pathlib import Path
from collections import namedtuple
from time import time
import hashlib
import pickle
import os
import codecs
import shelve
import atexit


CACHE_ROOT = os.path.join(os.environ['HOME'], ".cache", "ensembl_cache")
Path(CACHE_ROOT).mkdir(parents=True, exist_ok=True)
CACHE_PATH = os.path.join(CACHE_ROOT, "ensembl.cache")

MINUTE = 60
HOUR = 60 * MINUTE
DAY = 24 * HOUR
MONTH = 30 * DAY
YEAR = 365 * DAY

CacheEntry = namedtuple("CacheEntry", ['time', 'data'])


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class DummyCache(object):
    """A dummy cache that does not do anything but provide an interface.

    Parameters
    ----------
    *args
        Ignored.
    **kwargs
        Ignored.
    """
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __init__(self, *args, **kwargs):
        pass

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __len__(self):
        """Length will always be 0
        """
        return 0

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @staticmethod
    def get_cache_for_url(url):
        """Generate a cache file name based on the input URL.

        Parameters
        ----------
        args : `url`
            The input URL.

        Returns
        -------
        cache_file : `str`
            The cache file for the URL. Filenames are based on an SHA1 checksum
            of the URL.
        """
        # Ensure the URLs all end the same
        if not url.endswith('/'):
            url = r'{url}/'

        d = hashlib.sha1(url.encode())
        file_basename = d.hexdigest()
        return os.path.join(CACHE_PATH, file_basename)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @staticmethod
    def get_key(args):
        """Get a string based on function args/kwargs that that can be used as a
        key for the cache.

        Parameters
        ----------
        args : `tuple`
            The arguments that can be used as a str key for Shelve.
        """
        return codecs.encode(
            pickle.dumps(
                args,
                protocol=0
            ), "base64").decode()

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def purge_cache(self, *args, **kwargs):
        """A dummy purge cache method.

        Parameters
        ----------
        *args
            Ignored.
        **kwargs
            Ignored.
        """
        pass

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def add_to_cache(self, *args, **kwargs):
        """A dummy add to cache method.

        Parameters
        ----------
        *args
            Ignored.
        **kwargs
            Ignored.
        """
        pass

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def get_from_cache(self, *args, **kwargs):
        """A dummy get from cache method.

        Parameters
        ----------
        *args
            Ignored.
        **kwargs
            Ignored.
        """
        return None


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class ShelveCache(DummyCache):
    """Create a file cache using Python's Shelve module.

    Parameters
    ----------
    cache_path : `str`
        The path to the cache file. This may or may not exist.
    seconds_cutoff : `int`, optional, default: `NoneType`
        Only entries < this will be deemed valid cache entries and
        returned. If ``NoneType``, then any matching entry is returned.
    purge : `bool`, optional, default: `True`
        Go through the cache on setup and remove any entries >
        ``seconds_cutoff`` old.
    """
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __init__(self, cache_path, seconds_cutoff=None, purge=True):
        cache_db = shelve.open(cache_path)
        atexit.register(cache_db.close)
        self.cache_db = cache_db
        self.seconds_cutoff = seconds_cutoff

        if purge is True:
            self.purge_cache()

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __len__(self):
        """The number of entries in the cache.
        """
        return len(self.cache_db)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def purge_cache(self):
        """Go through the cache on setup and remove any entries >
        ``ShelveCache.seconds_cutoff`` old.

        Returns
        -------
        n_deleted : `int`
            The number of entries purged from the cache.
        """
        to_remove = []
        if self.seconds_cutoff is not None and self.seconds_cutoff > 0:
            cur_time = time()

            for key in self.cache_db.keys():
                entry = self.cache_db[key]
                if cur_time - entry.time >= self.seconds_cutoff:
                    to_remove.append(key)

            for i in to_remove:
                del self.cache_db[i]
        return len(to_remove)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def get_from_cache(self, key):
        """Get data from the cache (if it exists).

        Parameters
        ----------
        key : `str`
            The key that the data was stored under. This will usualy be the
            result of the ``ShelveCache.get_key`` call.

        Returns
        -------
        data : `Any`
            The data that was stored in the cache. If it is ``NoneType``, then
            no matching data exists in the cache.
        """
        if key in self.cache_db:
            entry = self.cache_db[key]
            if self.seconds_cutoff is None or \
               time() - entry.time < self.seconds_cutoff:
                return entry.data
        return None

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def add_to_cache(self, key, data):
        """Add the data to the cache.

        Parameters
        ----------
        key : `str`
            The key that the data will be stored under. This will usualy be the
            result of the ``ShelveCache.get_key`` call.
        data : `Any`
            The data to add, must be pickle-able.
        """
        self.cache_db[key] = CacheEntry(data=data, time=time())
        self.cache_db.sync()


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def get_cache(cache_path, url=None, **kwargs):
    """Setup the cache depending on the ``cache_path``.

    Parameters
    ----------
    cache_path : `str` or `bool` or `NoneType`
        The path to the cache, `False` or `NoneType` means you do not want to
        implement a cache. `True` means the cache filename will be generated
        from the `url`, A string is interpreted as a file name.
    url : `str`, optional, default: `NoneType`
        If the `cache_path` is set to `True`, then the `url` must be defined.
    **kwargs
        Any keyword arguments for the cache.

    Returns
    -------
    cache : `ShelveCache` or `DummyCache`
        If a string ``cache_path`` is given then a ``ShelveCache`` object is
        returned, if ``NoneType`` then a ``DummyCache`` is returned, this
        implements the exact same interface but does nothing.
    """
    if cache_path is False or cache_path is None:
        return DummyCache(cache_path, **kwargs)

    if cache_path is True:
        if url is None:
            raise TypeError("url must be defined if cache is set to True")
        cache_path = ShelveCache.get_cache_for_url(url)

    return ShelveCache(cache_path, **kwargs)
