import logging
import threading
from functools import _make_key, wraps

from ocyan.core.fender import config

logger = logging.getLogger(__name__)

DISABLE_METHOD_REQUEST_CACHE = config.get("core", "disable_method_request_cache", False)
DISABLE_TEMPLATETAG_REQUEST_CACHE = config.get(
    "core", "disable_templatetag_request_cache", False
)

__all__ = ["RequestCache", "RequestLibrary"]


class RequestCache(threading.local):
    CACHE_STORAGE_REQUEST_KEY = "_request_cache"

    class CacheForKey(object):
        def __init__(self, cache, cache_key):
            self.cache = cache
            self.cache_key = cache_key

        def __bool__(self):
            return self.cache_key in self.cache

        def get(self):
            return self.cache.get(self.cache_key)

        def set(self, val):
            self.cache.set(self.cache_key, val)

        def clear(self):
            self.cache.delete(self.cache_key)

    @classmethod
    def cache_for_request(cls, request):
        if not hasattr(request, cls.CACHE_STORAGE_REQUEST_KEY):
            setattr(request, cls.CACHE_STORAGE_REQUEST_KEY, cls())

        return getattr(request, cls.CACHE_STORAGE_REQUEST_KEY)

    @classmethod
    def cache_for_key(cls, request, *args, **kwargs):
        """
        create a request cache object for a single key

        >>> import argparse
        >>> request = argparse.Namespace()
        >>> c = RequestCache.cache_for_key(request, "harrie")
        >>> bool(c)
        False
        >>> c.set(1)
        >>> bool(c)
        True
        >>> c.get()
        1
        >>> c.set("klaas")
        >>> bool(c)
        True
        >>> c.get()
        'klaas'
        >>> c.clear()
        >>> bool(c)
        False
        >>> c.get()
        >>> c.set("adje")
        >>> d = RequestCache.cache_for_key(None, "harrie")
        >>> bool(d)
        False
        >>> d.set(1)
        >>> bool(d)
        True
        >>> d.get()
        1
        >>> f = RequestCache.cache_for_key(request, "harrie")
        >>> bool(f)
        True
        >>> f.get()
        'adje'
        """
        cache_key = cls.make_key(*args, **kwargs)
        if request is None:
            return cls.CacheForKey(cls(), cache_key)

        return cls.CacheForKey(cls.cache_for_request(request), cache_key)

    def clear(self):
        self.__dict__.clear()

    def __contains__(self, value):
        return value in self.__dict__

    def set(self, name, value):
        self.__dict__[name] = value

    def get(self, name):
        return self.__dict__.get(name)

    def delete(self, name):
        self.__dict__.pop(name, None)

    @classmethod
    def make_key(cls, *args, **kwargs):
        return _make_key(args, kwargs, False)

    @classmethod
    def once_per_request(cls, method):
        if DISABLE_METHOD_REQUEST_CACHE:
            return method

        @wraps(method)
        def _wrapper(self, *args, **kwargs):
            if self.request is not None:
                cache = cls.cache_for_request(self.request)
                method_name = getattr(method, "_decorated_function", method).__name__

                if hasattr(self, "make_key"):
                    key = self.make_key(method_name, *args, **kwargs)
                else:
                    key = cache.make_key(method_name, *args, **kwargs)

                if key not in cache:
                    value = method(self, *args, **kwargs)
                    cache.set(key, value)
                    return value

                return cache.get(key)

            return method(self, *args, **kwargs)

        return _wrapper


class ContextRequestTemplateTag(object):
    """
    Templatetag that is forced to take context to read the request from
    """

    def __init__(self, tag_decorator, *args, takes_context=None, name=None):
        self.func_takes_context = takes_context
        self.tag_decorator = tag_decorator(*args, takes_context=True, name=name)

    def __call__(self, func):
        def contextrequesttemplatetag_wrapper(context, *args, **kwargs):
            request = context.get("request")
            if request is None:
                logger.warning(
                    "Useless application of RequestLibrary, no request in context"
                )
                if self.func_takes_context:
                    return func(context, *args, **kwargs)
                else:
                    return func(*args, **kwargs)

            # we are good, compute cache key and try cached value
            cache = RequestCache.cache_for_request(request)
            func_name = getattr(func, "_decorated_function", func).__name__
            key = cache.make_key(func_name, *args, **kwargs)

            if key not in cache:
                if self.func_takes_context:
                    value = func(context, *args, **kwargs)
                else:
                    value = func(*args, **kwargs)
                cache.set(key, value)
                return value  # immediately return the value

            return cache.get(key)

        return self.tag_decorator(contextrequesttemplatetag_wrapper)


class RequestTemplateTag(object):
    """
    Templatetag that take request as the first argument
    """

    def __init__(self, tag_decorator, *args, takes_context=None, name=None):
        assert (
            not takes_context
        ), "RequestTemplateTag can not take request and context at the same time"
        self.tag_decorator = tag_decorator(*args, takes_context=False, name=name)

    def __call__(self, func):
        def requesttemplatetag_wrapper(request, *args, **kwargs):
            cache = RequestCache.cache_for_request(request)
            func_name = getattr(func, "_decorated_function", func).__name__
            key = cache.make_key(func_name, *args, **kwargs)

            if key not in cache:
                value = func(request, *args, **kwargs)
                cache.set(key, value)
                return value

            return cache.get(key)

        return self.tag_decorator(requesttemplatetag_wrapper)


class RequestLibrary(object):
    def __init__(self, library):
        self.library = library

    def simple_tag(self, func=None, takes_context=None, name=None, takes_request=False):
        if DISABLE_TEMPLATETAG_REQUEST_CACHE:
            return func

        if takes_request:
            tag = RequestTemplateTag(
                self.library.simple_tag, takes_context=takes_context, name=name
            )
        else:
            tag = ContextRequestTemplateTag(
                self.library.simple_tag, takes_context=takes_context, name=name
            )

        if func is not None:
            return tag(func)
        return tag

    def inclusion_tag(
        self, filename, func=None, takes_context=None, name=None, takes_request=False
    ):
        if DISABLE_TEMPLATETAG_REQUEST_CACHE:
            return func

        if takes_request:
            tag = RequestTemplateTag(
                self.library.inclusion_tag,
                filename,
                takes_context=takes_context,
                name=name,
            )
        else:
            tag = ContextRequestTemplateTag(
                self.library.inclusion_tag, takes_context=takes_context, name=name
            )

        if func is not None:
            return tag(func)

        return tag
