"""This module implements decorators for implementing other decorators
as well as some commonly used decorators.

"""

from functools import partial
from inspect import isclass, signature

from .__wrapt__ import (
    BaseObjectProxy,
    BoundFunctionWrapper,
    CallableObjectProxy,
    FunctionWrapper,
)
from .arguments import formatargspec

# Adapter wrapper for the wrapped function which will overlay certain
# properties from the adapter function onto the wrapped function so that
# functions such as inspect.getfullargspec(), inspect.signature() and
# inspect.getsource() return the correct results one would expect.


class _AdapterFunctionCode(CallableObjectProxy):

    def __init__(self, wrapped_code, adapter_code):
        # Explicit class in super() is used because the proxy overrides
        # __class__ and MRO-related methods to delegate to the wrapped
        # object, which can interfere with bare super().
        super(_AdapterFunctionCode, self).__init__(wrapped_code)
        self._self_adapter_code = adapter_code

    @property
    def co_argcount(self):
        return self._self_adapter_code.co_argcount

    @property
    def co_code(self):
        return self._self_adapter_code.co_code

    @property
    def co_flags(self):
        return self._self_adapter_code.co_flags

    @property
    def co_kwonlyargcount(self):
        return self._self_adapter_code.co_kwonlyargcount

    @property
    def co_varnames(self):
        return self._self_adapter_code.co_varnames


class _AdapterFunctionSurrogate(CallableObjectProxy):

    def __init__(self, wrapped, adapter):
        # Explicit class in super() is used because the proxy overrides
        # __class__ and MRO-related methods to delegate to the wrapped
        # object, which can interfere with bare super().
        super(_AdapterFunctionSurrogate, self).__init__(wrapped)
        self._self_adapter = adapter

    @property
    def __code__(self):
        return _AdapterFunctionCode(
            self.__wrapped__.__code__, self._self_adapter.__code__
        )

    @property
    def __defaults__(self):
        return self._self_adapter.__defaults__

    @property
    def __kwdefaults__(self):
        return self._self_adapter.__kwdefaults__

    @property
    def __signature__(self):
        return signature(self._self_adapter)


class _BoundAdapterFunctionWrapper(BoundFunctionWrapper):

    @property
    def __func__(self):
        return _AdapterFunctionSurrogate(
            self.__wrapped__.__func__, self._self_parent._self_adapter
        )

    @property
    def __signature__(self):
        return signature(self._self_parent._self_adapter)


class _AdapterFunctionWrapper(FunctionWrapper):

    __bound_function_wrapper__ = _BoundAdapterFunctionWrapper

    def __init__(self, *args, **kwargs):
        adapter = kwargs.pop("adapter")
        # Explicit class in super() is used because the proxy overrides
        # __class__ and MRO-related methods to delegate to the wrapped
        # object, which can interfere with bare super().
        super(_AdapterFunctionWrapper, self).__init__(*args, **kwargs)
        self._self_surrogate = _AdapterFunctionSurrogate(self.__wrapped__, adapter)
        self._self_adapter = adapter

    @property
    def __code__(self):
        return self._self_surrogate.__code__

    @property
    def __defaults__(self):
        return self._self_surrogate.__defaults__

    @property
    def __kwdefaults__(self):
        return self._self_surrogate.__kwdefaults__

    @property
    def __signature__(self):
        return self._self_surrogate.__signature__


class AdapterFactory:
    def __call__(self, wrapped):
        raise NotImplementedError()


class _DelegatedAdapterFactory(AdapterFactory):
    def __init__(self, factory):
        # Explicit class in super() for consistency with proxy subclasses.
        super(_DelegatedAdapterFactory, self).__init__()
        self.factory = factory

    def __call__(self, wrapped):
        return self.factory(wrapped)


adapter_factory = _DelegatedAdapterFactory

# Decorator for creating other decorators. This decorator and the
# wrappers which they use are designed to properly preserve any name
# attributes, function signatures etc, in addition to the wrappers
# themselves acting like a transparent proxy for the original wrapped
# function so the wrapper is effectively indistinguishable from the
# original wrapped function.


def decorator(wrapper=None, /, *, enabled=None, adapter=None, proxy=FunctionWrapper):
    """
    The decorator should be supplied with a single positional argument
    which is the `wrapper` function to be used to implement the
    decorator. This may be preceded by a step whereby the keyword
    arguments are supplied to customise the behaviour of the
    decorator. The `adapter` argument is used to optionally denote a
    separate function which is notionally used by an adapter
    decorator. In that case parts of the function `__code__` and
    `__defaults__` attributes are used from the adapter function
    rather than those of the wrapped function. This allows for the
    argument specification from `inspect.getfullargspec()` and similar
    functions to be overridden with a prototype for a different
    function than what was wrapped. The `enabled` argument provides a
    way to enable/disable the use of the decorator. If the type of
    `enabled` is a boolean, then it is evaluated immediately and the
    wrapper not even applied if it is `False`. If not a boolean, it will
    be evaluated when the wrapper is called for an unbound wrapper,
    and when binding occurs for a bound wrapper. When being evaluated,
    if `enabled` is callable it will be called to obtain the value to
    be checked. If `False`, the wrapper will not be called and instead
    the original wrapped function will be called directly instead.
    The `proxy` argument provides a way of passing a custom version of
    the `FunctionWrapper` class used in decorating the function.
    """

    if wrapper is not None:
        # Helper function for creating wrapper of the appropriate
        # time when we need it down below.

        def _build(wrapped, wrapper, enabled=None, adapter=None):
            if adapter:
                if isinstance(adapter, AdapterFactory):
                    adapter = adapter(wrapped)

                if not callable(adapter):
                    ns = {}

                    # Check if the signature argument specification has
                    # annotations. If it does then we need to remember
                    # it but also drop it when attempting to manufacture
                    # a standin adapter function. This is necessary else
                    # it will try and look up any types referenced in
                    # the annotations in the empty namespace we use,
                    # which will fail.

                    annotations = {}

                    if not isinstance(adapter, str):
                        if len(adapter) == 7:
                            annotations = adapter[-1]
                            adapter = adapter[:-1]
                        adapter = formatargspec(*adapter)

                    exec(f"def adapter{adapter}: pass", ns, ns)
                    adapter = ns["adapter"]

                    # Override the annotations for the manufactured
                    # adapter function so they match the original
                    # adapter signature argument specification.

                    if annotations:
                        adapter.__annotations__ = annotations

                return _AdapterFunctionWrapper(
                    wrapped=wrapped, wrapper=wrapper, enabled=enabled, adapter=adapter
                )

            return proxy(wrapped=wrapped, wrapper=wrapper, enabled=enabled)

        # The wrapper has been provided so return the final decorator.
        # The decorator is itself one of our function wrappers so we
        # can determine when it is applied to functions, instance methods
        # or class methods. This allows us to bind the instance or class
        # method so the appropriate self or cls attribute is supplied
        # when it is finally called.

        def _wrapper(wrapped, instance, args, kwargs):
            # We first check for the case where the decorator was applied
            # to a class type.
            #
            #     @decorator
            #     class mydecoratorclass:
            #         def __init__(self, arg=None):
            #             self.arg = arg
            #         def __call__(self, wrapped, instance, args, kwargs):
            #             return wrapped(*args, **kwargs)
            #
            #     @mydecoratorclass(arg=1)
            #     def function():
            #         pass
            #
            # In this case an instance of the class is to be used as the
            # decorator wrapper function. If args was empty at this point,
            # then it means that there were optional keyword arguments
            # supplied to be used when creating an instance of the class
            # to be used as the wrapper function.

            if instance is None and isclass(wrapped) and not args:
                # We still need to be passed the target function to be
                # wrapped as yet, so we need to return a further function
                # to be able to capture it.

                def _capture(target_wrapped):
                    # Now have the target function to be wrapped and need
                    # to create an instance of the class which is to act
                    # as the decorator wrapper function. Before we do that,
                    # we need to first check that use of the decorator
                    # hadn't been disabled by a simple boolean. If it was,
                    # the target function to be wrapped is returned instead.

                    _enabled = enabled
                    if type(_enabled) is bool:
                        if not _enabled:
                            return target_wrapped
                        _enabled = None

                    # Now create an instance of the class which is to act
                    # as the decorator wrapper function. Any arguments had
                    # to be supplied as keyword only arguments so that is
                    # all we pass when creating it.

                    target_wrapper = wrapped(**kwargs)

                    # Finally build the wrapper itself and return it.

                    return _build(target_wrapped, target_wrapper, _enabled, adapter)

                return _capture

            # We should always have the target function to be wrapped at
            # this point as the first (and only) value in args.

            target_wrapped = args[0]

            # Need to now check that use of the decorator hadn't been
            # disabled by a simple boolean. If it was, then target
            # function to be wrapped is returned instead.

            _enabled = enabled
            if type(_enabled) is bool:
                if not _enabled:
                    return target_wrapped
                _enabled = None

            # We now need to build the wrapper, but there are a couple of
            # different cases we need to consider.

            if instance is None:
                if isclass(wrapped):
                    # In this case the decorator was applied to a class
                    # type but optional keyword arguments were not supplied
                    # for initialising an instance of the class to be used
                    # as the decorator wrapper function.
                    #
                    #     @decorator
                    #     class mydecoratorclass:
                    #         def __init__(self, arg=None):
                    #             self.arg = arg
                    #         def __call__(self, wrapped, instance,
                    #                 args, kwargs):
                    #             return wrapped(*args, **kwargs)
                    #
                    #     @mydecoratorclass
                    #     def function():
                    #         pass
                    #
                    # We still need to create an instance of the class to
                    # be used as the decorator wrapper function, but no
                    # arguments are pass.

                    target_wrapper = wrapped()

                else:
                    # In this case the decorator was applied to a normal
                    # function, or possibly a static method of a class.
                    #
                    #     @decorator
                    #     def mydecoratorfuntion(wrapped, instance,
                    #             args, kwargs):
                    #         return wrapped(*args, **kwargs)
                    #
                    #     @mydecoratorfunction
                    #     def function():
                    #         pass
                    #
                    # That normal function becomes the decorator wrapper
                    # function.

                    target_wrapper = wrapper

            else:
                if isclass(instance):
                    # In this case the decorator was applied to a class
                    # method.
                    #
                    #     class myclass:
                    #         @decorator
                    #         @classmethod
                    #         def decoratorclassmethod(cls, wrapped,
                    #                 instance, args, kwargs):
                    #             return wrapped(*args, **kwargs)
                    #
                    #     instance = myclass()
                    #
                    #     @instance.decoratorclassmethod
                    #     def function():
                    #         pass
                    #
                    # This one is a bit strange because binding was actually
                    # performed on the wrapper created by our decorator
                    # factory. We need to apply that binding to the decorator
                    # wrapper function that the decorator factory
                    # was applied to.

                    target_wrapper = wrapper.__get__(None, instance)

                else:
                    # In this case the decorator was applied to an instance
                    # method.
                    #
                    #     class myclass:
                    #         @decorator
                    #         def decoratorclassmethod(self, wrapped,
                    #                 instance, args, kwargs):
                    #             return wrapped(*args, **kwargs)
                    #
                    #     instance = myclass()
                    #
                    #     @instance.decoratorclassmethod
                    #     def function():
                    #         pass
                    #
                    # This one is a bit strange because binding was actually
                    # performed on the wrapper created by our decorator
                    # factory. We need to apply that binding to the decorator
                    # wrapper function that the decorator factory
                    # was applied to.

                    target_wrapper = wrapper.__get__(instance, type(instance))

            # Finally build the wrapper itself and return it.

            return _build(target_wrapped, target_wrapper, _enabled, adapter)

        # We first return our magic function wrapper here so we can
        # determine in what context the decorator factory was used. In
        # other words, it is itself a universal decorator. The decorator
        # function is used as the adapter so that linters see a signature
        # corresponding to the decorator and not the wrapper it is being
        # applied to.

        return _build(wrapper, _wrapper, adapter=decorator)

    else:
        # The wrapper still has not been provided, so we are just
        # collecting the optional keyword arguments. Return the
        # decorator again wrapped in a partial using the collected
        # arguments.

        return partial(decorator, enabled=enabled, adapter=adapter, proxy=proxy)


# Descriptor decorator for automatically binding state to a wrapper.
# When applied to a method decorated with function_wrapper or decorator,
# it intercepts descriptor access so that when the method is accessed
# via an instance, the instance is automatically attached to the
# resulting wrapper using __self_setattr__. This is useful when you
# want the wrapper to carry a reference back to the object that owns
# the wrapper factory method, making state accessible through the
# decorated function without manual setup.


class _StateBindingWrapper(BaseObjectProxy):
    """A descriptor decorator that binds the owner instance to wrappers
    produced by a wrapper factory method.

    When applied on top of a method decorated with ``function_wrapper``
    or ``decorator``, it intercepts the descriptor ``__get__`` so that
    when the method is accessed through an instance, the owner instance
    is automatically attached as an attribute on the resulting wrapper
    via ``__self_setattr__``.

    The *name* keyword argument controls the attribute name under which
    the owner instance is stored on the wrapper (default ``"state"``).
    """

    def __init__(self, *, name="state"):
        # Initialise the proxy with a placeholder. The actual wrapper
        # factory is set later when __call__ is invoked as a decorator.

        super().__init__(None)

        self._self_attr_name = name

    def __call__(self, wrapper_factory):
        # Called when used as a decorator, receiving the wrapper factory
        # (the function_wrapper or decorator-decorated method) to wrap.

        self.__wrapped__ = wrapper_factory

        return self

    def __get__(self, instance, owner):
        # When accessed via the class rather than an instance, return
        # the descriptor itself unchanged. This preserves introspection
        # of the underlying wrapper factory via the object proxy.

        if instance is None:
            return self

        # Bind the underlying wrapper factory to the instance so it
        # receives the correct self/cls when called.

        wrapper_factory = self.__wrapped__.__get__(instance, owner)

        name = self._self_attr_name

        def _bind(func):
            wrapper = wrapper_factory(func)

            # Attach the owner instance to the wrapper so it can be
            # accessed as an attribute of the decorated function.

            wrapper.__self_setattr__(name, instance)

            return wrapper

        return _bind


bind_state_to_wrapper = _StateBindingWrapper
