import inspect
import pickle
import sys
from abc import abstractmethod

import six
from decorator import decorator

from omniduct.duct import Duct
from omniduct.utils.config import config
from omniduct.utils.docs import quirk_docs

from ..utils.debug import logger

config.register('cache_fail_hard',
                description='Raise exception if cache fails to save.',
                default=False)


def cached_method(id_str,
                  cache=lambda self: self.cache,
                  id_duct=lambda self, kwargs: "{}.{}".format(self.__class__.__name__, self.name),
                  use_cache=lambda self, kwargs: kwargs.pop('use_cache', True),
                  renew=lambda self, kwargs: kwargs.pop('renew', False),
                  format=lambda self, kwargs: None,
                  serializer=lambda format: pickle.dump,  # Serializers accept obj, file handle and format.
                  deserializer=lambda format: pickle.load):  # Deserializers accept file handle and format.
    @decorator
    def wrapped(method, self, *args, **kwargs):
        if six.PY3 and not hasattr(sys, 'pypy_version_info'):
            arguments = inspect.signature(method).parameters.keys()
        else:
            arguments = inspect.getargspec(method).args
        kwargs.update(dict(zip(list(arguments)[1:], args)))

        _cache = cache(self)
        _use_cache = use_cache(self, kwargs)
        _renew = renew(self, kwargs)
        _format = format(self, kwargs)

        if _cache is None or not _use_cache:
            return method(self, **kwargs)

        _id_duct = id_duct(self, kwargs)
        _id_str = id_str(self, kwargs)

        if _renew or not _cache.has_key(_id_duct, _id_str):  # noqa: has_key is not of a dictionary here
            value = method(self, **kwargs)
            try:
                _cache.set(
                    id_duct=_id_duct,
                    id_str=_id_str,
                    value=value,
                    serializer=serializer(_format)
                )
            except Exception:  # Remove any lingering (perhaps partial) cache files
                _cache.clear(
                    id_duct=_id_duct,
                    id_str=_id_str
                )
                logger.warning("Failed to save results to cache. If needed, please save them manually.")
                if config.cache_fail_hard:
                    raise

            return value

        logger.caveat('Loaded from cache')

        return _cache.get(
            id_duct=_id_duct,
            id_str=_id_str,
            deserializer=deserializer(_format)
        )
    return wrapped


class Cache(Duct):
    """
    `Cache` is an abstract subclass of `Duct` that provides a common
    API for all cache clients, which in turn will be subclasses of this
    class.

    Note: This will likely be refactored soon to be more powerful, including the
    ability to manage the resource usage of the cache.
    """

    DUCT_TYPE = Duct.Type.CACHE

    @quirk_docs('_init', mro=True)
    def __init__(self, **kwargs):
        """
        This is a shim __init__ function that passes all arguments onto
        `self._init`, which is implemented by subclasses. This allows subclasses
        to instantiate themselves with arbitrary parameters.
        """
        Duct.__init_with_kwargs__(self, kwargs)
        self._init(**kwargs)

    @abstractmethod
    def _init(self):
        pass

    @abstractmethod
    def clear(self, id_duct, id_str):
        pass

    @abstractmethod
    def clear_all(self, id_duct=None):
        pass

    @abstractmethod
    def get(self, id_duct, id_str, deserializer=pickle.load):
        pass

    @abstractmethod
    def has_key(self, id_duct, id_str):
        pass

    @abstractmethod
    def keys(self, id_duct):
        pass

    @abstractmethod
    def set(self, id_duct, id_str, value, serializer=pickle.dump):
        pass
