import enum
import functools
import typing as ta
import weakref

from .impl import new_cache
from .types import Cache


C = ta.TypeVar('C', bound=ta.Callable)
CC = ta.Callable[[C], C]


class Scope(enum.Enum):
    INSTANCE = 'INSTANCE'
    CLASS = 'CLASS'
    STATIC = 'STATIC'


class _HashedSeq(list):
    __slots__ = ['hash_value']

    def __init__(
            self,
            tup: ta.Tuple,
            hasher: ta.Callable[[ta.Any], int] = hash
    ) -> None:
        super().__init__()

        self[:] = tup
        self.hash_value = hasher(tup)

    def __hash__(self):
        return self.hash_value


def _make_key(
        args: ta.Tuple,
        kwargs: ta.Dict[str, ta.Any],
        typed: bool,
        kwd_mark=(object(),),
        fasttypes={int, str, frozenset, type(None)},
        tuple=tuple,
        type=type,
        len=len
) -> ta.Any:
    key = args
    if kwargs:
        key += kwd_mark
        for item in kwargs.items():
            key += item
    if typed:
        key += tuple(type(v) for v in args)
        if kwargs:
            key += tuple(type(v) for v in kwargs.values())
    elif len(key) == 1 and type(key[0]) in fasttypes:
        return key[0]
    return _HashedSeq(key)


class Ignore:

    def __init__(self, value: ta.Any) -> None:
        super().__init__()

        self._value = value


def ignore(value: ta.Any) -> ta.Any:
    return Ignore(value)


class _CacheDescriptor:

    def __init__(
            self,
            fn: ta.Callable,
            scope: Scope,
            typed: bool,
            **kwargs
    ) -> None:
        super().__init__()

        self._fn = fn
        functools.update_wrapper(self, fn)
        self._scope = scope
        self._typed = typed
        self._kwargs = kwargs
        self.__static: Cache = None
        self.__static_built: ta.Callable = None
        self._by_class: ta.MutableMapping[ta.Type, Cache] = weakref.WeakKeyDictionary() if scope == Scope.CLASS else None  # noqa
        self._name = None
        self._unary = kwargs.get('identity_keys', False) or kwargs.get('weak_keys', False)

    def __set_name__(self, owner, name):
        if self._name is None:
            self._name = name

    @property
    def _static(self) -> Cache:
        if self.__static is None:
            self.__static = new_cache(**self._kwargs)
        return self.__static

    def _build(self, fn: ta.Callable, cache: Cache):
        def miss(key, result):
            if isinstance(result, Ignore):
                return result._value
            else:
                cache[key] = result
                return result

        if self._unary:
            @functools.wraps(fn)
            def inner(key):
                try:
                    return cache[key]
                except KeyError:
                    pass
                return miss(key, fn(key))

        else:
            @functools.wraps(fn)
            def inner(*args, **kwargs):
                key = _make_key(args, kwargs, self._typed)
                try:
                    return cache[key]
                except KeyError:
                    pass
                return miss(key, fn(*args, **kwargs))

        return inner

    def __get__(self, instance, owner=None):
        if self._scope == Scope.STATIC:
            cache = self._static

        elif self._scope == Scope.CLASS:
            if owner is None:
                raise TypeError
            try:
                cache = self._by_class[owner]
            except KeyError:
                cache = self._by_class[owner] = new_cache(**self._kwargs)

        elif self._scope == Scope.INSTANCE:
            if instance is not None:
                cache = new_cache()
            else:
                @functools.wraps(self._fn)
                def trampoline(this, *args, **kwargs):
                    return self.__get__(this, owner)(*args, **kwargs)
                return trampoline

        else:
            raise TypeError

        fn = self._build(self._fn.__get__(instance, owner), cache)

        if self._scope == Scope.CLASS:
            setattr(owner, self._name, fn)
        elif self._scope == Scope.INSTANCE:
            setattr(instance, self._name, fn)

        return fn

    def __call__(self, *args, **kwargs):
        if self.__static_built is None:
            self.__static_built = self._build(self._fn, self._static)
        return self.__static_built(*args, **kwargs)


def cache(
        scope: ta.Union[Scope, str] = Scope.INSTANCE,
        typed: bool = False,
        **kwargs
) -> CC:
    if not isinstance(scope, Scope):
        scope = getattr(Scope, scope.upper())

    def inner(fn):
        return _CacheDescriptor(fn, scope, typed, **kwargs)
    return ta.cast(CC, inner)
