"""
passlib.utils.decor -- helper decorators & properties
"""

import types
from functools import update_wrapper, wraps
from warnings import warn

__all__ = [
    "classproperty",
    "hybrid_method",
    "memoize_single_value",
    "memoized_property",
    "deprecated_function",
    "deprecated_method",
]


class classproperty:
    """Function decorator which acts like a combination of classmethod+property (limited to read-only properties)"""

    def __init__(self, func):
        # XXX: rename to .fget to match property?
        self.__func__ = func

    def __get__(self, obj, cls):
        return self.__func__(cls)


class hybrid_method:
    """
    decorator which invokes function with class if called as class method,
    and with object if called at instance level.
    """

    def __init__(self, func):
        # XXX: rename to .fget to match property?
        self.func = func
        update_wrapper(self, func)

    def __get__(self, obj, cls):
        if obj is None:
            obj = cls
        return types.MethodType(self.func, obj)


def memoize_single_value(func):
    """
    decorator for function which takes no args,
    and memoizes result.  exposes a ``.clear_cache`` method
    to clear the cached value.
    """
    cache = {}

    @wraps(func)
    def wrapper():
        try:
            return cache[True]
        except KeyError:
            pass
        value = cache[True] = func()
        return value

    def clear_cache():
        cache.pop(True, None)

    wrapper.clear_cache = clear_cache

    return wrapper


class memoized_property:
    """
    decorator which invokes method once, then replaces attr with result
    """

    def __init__(self, func):
        self.__func__ = func
        self.__name__ = func.__name__
        self.__doc__ = func.__doc__

    def __get__(self, obj, cls):
        if obj is None:
            return self
        value = self.__func__(obj)
        setattr(obj, self.__name__, value)
        return value

    def clear_cache(self, obj):
        """
        class-level helper to clear stored value (if any).

        usage: :samp:`type(self).{attr}.clear_cache(self)`
        """
        obj.__dict__.pop(self.__name__, None)

    def peek_cache(self, obj, default=None):
        """
        class-level helper to peek at stored value

        usage: :samp:`value = type(self).{attr}.clear_cache(self)`
        """
        return obj.__dict__.get(self.__name__, default)


# works but not used
##class memoized_class_property(object):
##    """function decorator which calls function as classmethod,
##    and replaces itself with result for current and all future invocations.
##    """
##    def __init__(self, func):
##        self.im_func = func
##
##    def __get__(self, obj, cls):
##        func = self.im_func
##        value = func(cls)
##        setattr(cls, func.__name__, value)
##        return value
##
##    @property
##    def __func__(self):
##        "py3 compatible alias"


def deprecated_function(
    msg=None,
    deprecated=None,
    removed=None,
    updoc=True,
    replacement=None,
    _is_method=False,
    func_module=None,
):
    """decorator to deprecate a function.

    :arg msg: optional msg, default chosen if omitted
    :kwd deprecated: version when function was first deprecated
    :kwd removed: version when function will be removed
    :kwd replacement: alternate name / instructions for replacing this function.
    :kwd updoc: add notice to docstring (default ``True``)
    """
    if msg is None:
        if _is_method:
            msg = "the method %(mod)s.%(klass)s.%(name)s() is deprecated"
        else:
            msg = "the function %(mod)s.%(name)s() is deprecated"
        if deprecated:
            msg += " as of Passlib %(deprecated)s"
        if removed:
            msg += ", and will be removed in Passlib %(removed)s"
        if replacement:
            msg += f", use {replacement} instead"
        msg += "."

    def build(func):
        is_classmethod = _is_method and isinstance(func, classmethod)
        if is_classmethod:
            func = func.__func__
        opts = dict(
            mod=func_module or func.__module__,
            name=func.__name__,
            deprecated=deprecated,
            removed=removed,
        )
        if _is_method:

            def wrapper(*args, **kwds):
                tmp = opts.copy()
                klass = args[0] if is_classmethod else args[0].__class__
                tmp.update(klass=klass.__name__, mod=klass.__module__)
                warn(msg % tmp, DeprecationWarning, stacklevel=2)
                return func(*args, **kwds)
        else:
            text = msg % opts

            def wrapper(*args, **kwds):
                warn(text, DeprecationWarning, stacklevel=2)
                return func(*args, **kwds)

        update_wrapper(wrapper, func)
        if (
            updoc
            and (deprecated or removed)
            and wrapper.__doc__
            and ".. deprecated::" not in wrapper.__doc__
        ):
            txt = deprecated or ""
            if removed or replacement:
                txt += "\n    "
                if removed:
                    txt += f"and will be removed in version {removed}"
                if replacement:
                    if removed:
                        txt += ", "
                    txt += f"use {replacement} instead"
                txt += "."
            if not wrapper.__doc__.strip(" ").endswith("\n"):
                wrapper.__doc__ += "\n"
            wrapper.__doc__ += f"\n.. deprecated:: {txt}\n"
        if is_classmethod:
            wrapper = classmethod(wrapper)
        return wrapper

    return build


def deprecated_method(
    msg=None, deprecated=None, removed=None, updoc=True, replacement=None
):
    """decorator to deprecate a method.

    :arg msg: optional msg, default chosen if omitted
    :kwd deprecated: version when method was first deprecated
    :kwd removed: version when method will be removed
    :kwd replacement: alternate name / instructions for replacing this method.
    :kwd updoc: add notice to docstring (default ``True``)
    """
    return deprecated_function(
        msg, deprecated, removed, updoc, replacement, _is_method=True
    )
