# Copyright (c) 2018 Hubert Kario
#
# See the LICENSE file for legal information regarding use of this file.
"""Methods for deprecating old names for arguments or attributes."""

import warnings
import inspect
from functools import wraps


def deprecated_class_name(old_name, warn="Class name '{old_name}' is deprecated, please use '{new_name}'"):
    """
    Class decorator to deprecate a use of class.

    :param str old_name: the deprecated name that will be registered, but
       will raise warnings if used.

    :param str warn: DeprecationWarning format string for informing the
       user what is the current class name, uses 'old_name' for the deprecated
       keyword name and the 'new_name' for the current one.
       Example: "Old name: {old_nam}, use '{new_name}' instead".
    """

    def _wrap(obj):
        assert callable(obj)

        def _warn():
            warnings.warn(warn.format(old_name=old_name, new_name=obj.__name__), DeprecationWarning, stacklevel=3)

        def _wrap_with_warn(func, is_inspect):
            @wraps(func)
            def _func(*args, **kwargs):
                if is_inspect:
                    # XXX: If use another name to call,
                    # you will not get the warning.
                    # we do this instead of subclassing or metaclass as
                    # we want to isinstance(new_name(), old_name) and
                    # isinstance(old_name(), new_name) to work
                    frame = inspect.currentframe().f_back
                    code = inspect.getframeinfo(frame).code_context
                    if [line for line in code if "{0}(".format(old_name) in line]:
                        _warn()
                else:
                    _warn()
                return func(*args, **kwargs)

            return _func

        # Make old name available.
        frame = inspect.currentframe().f_back
        if old_name in frame.f_globals:
            raise NameError("Name '{0}' already in use.".format(old_name))

        if inspect.isclass(obj):
            obj.__init__ = _wrap_with_warn(obj.__init__, True)
            placeholder = obj
        else:
            placeholder = _wrap_with_warn(obj, False)

        frame.f_globals[old_name] = placeholder

        return obj

    return _wrap


def deprecated_params(names, warn="Param name '{old_name}' is deprecated, please use '{new_name}'"):
    """Decorator to translate obsolete names and warn about their use.

    :param dict names: dictionary with pairs of new_name: old_name
        that will be used for translating obsolete param names to new names

    :param str warn: DeprecationWarning format string for informing the user
        what is the current parameter name, uses 'old_name' for the
        deprecated keyword name and 'new_name' for the current one.
        Example: "Old name: {old_name}, use {new_name} instead".
    """

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for new_name, old_name in names.items():
                if old_name in kwargs:
                    if new_name in kwargs:
                        raise TypeError("got multiple values for keyword argument '{0}'".format(new_name))
                    warnings.warn(warn.format(old_name=old_name, new_name=new_name), DeprecationWarning, stacklevel=2)
                    kwargs[new_name] = kwargs.pop(old_name)
            return func(*args, **kwargs)

        return wrapper

    return decorator


def deprecated_instance_attrs(names, warn="Attribute '{old_name}' is deprecated, please use '{new_name}'"):
    """Decorator to deprecate class instance attributes.

    Translates all names in `names` to use new names and emits warnings
    if the translation was necessary. Does apply only to instance variables
    and attributes (won't modify behaviour of class variables, static methods,
    etc.

    :param dict names: dictionary with paris of new_name: old_name that will
        be used to translate the calls
    :param str warn: DeprecationWarning format string for informing the user
        what is the current parameter name, uses 'old_name' for the
        deprecated keyword name and 'new_name' for the current one.
        Example: "Old name: {old_name}, use {new_name} instead".
    """
    # reverse the dict as we're looking for old attributes, not new ones
    names = dict((j, i) for i, j in names.items())

    def decorator(clazz):
        def getx(self, name, __old_getx=getattr(clazz, "__getattr__", None)):
            if name in names:
                warnings.warn(warn.format(old_name=name, new_name=names[name]), DeprecationWarning, stacklevel=2)
                return getattr(self, names[name])
            if __old_getx:
                if hasattr(__old_getx, "__func__"):
                    return __old_getx.__func__(self, name)
                return __old_getx(self, name)
            raise AttributeError("'{0}' object has no attribute '{1}'".format(clazz.__name__, name))

        getx.__name__ = "__getattr__"
        clazz.__getattr__ = getx

        def setx(self, name, value, __old_setx=getattr(clazz, "__setattr__")):
            if name in names:
                warnings.warn(warn.format(old_name=name, new_name=names[name]), DeprecationWarning, stacklevel=2)
                setattr(self, names[name], value)
            else:
                __old_setx(self, name, value)

        setx.__name__ = "__setattr__"
        clazz.__setattr__ = setx

        def delx(self, name, __old_delx=getattr(clazz, "__delattr__")):
            if name in names:
                warnings.warn(warn.format(old_name=name, new_name=names[name]), DeprecationWarning, stacklevel=2)
                delattr(self, names[name])
            else:
                __old_delx(self, name)

        delx.__name__ = "__delattr__"
        clazz.__delattr__ = delx

        return clazz

    return decorator


def deprecated_attrs(names, warn="Attribute '{old_name}' is deprecated, please use '{new_name}'"):
    """Decorator to deprecate all specified attributes in class.

    Translates all names in `names` to use new names and emits warnings
    if the translation was necessary.

    Note: uses metaclass magic so is incompatible with other metaclass uses

    :param dict names: dictionary with paris of new_name: old_name that will
        be used to translate the calls
    :param str warn: DeprecationWarning format string for informing the user
        what is the current parameter name, uses 'old_name' for the
        deprecated keyword name and 'new_name' for the current one.
        Example: "Old name: {old_name}, use {new_name} instead".
    """

    # prepare metaclass for handling all the class methods, class variables
    # and static methods (as they don't go through instance's __getattr__)
    class DeprecatedProps(type):
        pass

    metaclass = deprecated_instance_attrs(names, warn)(DeprecatedProps)

    def wrapper(cls):
        cls = deprecated_instance_attrs(names, warn)(cls)

        # apply metaclass
        orig_vars = cls.__dict__.copy()
        slots = orig_vars.get("__slots__")
        if slots is not None:
            if isinstance(slots, str):
                slots = [slots]
            for slots_var in slots:
                orig_vars.pop(slots_var)
        orig_vars.pop("__dict__", None)
        orig_vars.pop("__weakref__", None)
        return metaclass(cls.__name__, cls.__bases__, orig_vars)

    return wrapper


def deprecated_method(message):
    """Decorator for deprecating methods.

    :param ste message: The message you want to display.
    """

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            warnings.warn(
                "{0} is a deprecated method. {1}".format(func.__name__, message), DeprecationWarning, stacklevel=2
            )
            return func(*args, **kwargs)

        return wrapper

    return decorator
