##############################################################################
#
# Copyright (c) 2005 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Deprecation Support

This module provides utilities to ease the development of backward-compatible
code.
"""
__docformat__ = "reStructuredText"
import sys
import types
import warnings


str_and_sequence_types = (str, list, tuple)


class ShowSwitch:
    """Simple stack-based switch."""

    def __init__(self):
        self.stack = []

    def on(self):
        self.stack.pop()

    def off(self):
        self.stack.append(False)

    def reset(self):
        self.stack = []

    def __call__(self):
        return self.stack == []

    def __repr__(self):
        return '<ShowSwitch %s>' % (self() and 'on' or 'off')


# This attribute can be used to temporarily deactivate deprecation
# warnings, so that backward-compatibility code can import other
# backward-compatibility components without warnings being produced.
__show__ = ShowSwitch()


class Suppressor:

    def __enter__(self):
        __show__.off()

    def __exit__(self, *ignored):
        __show__.on()


ogetattr = object.__getattribute__


class DeprecationProxy:

    def __init__(self, module):
        self.__original_module = module
        self.__deprecated = {}

    def deprecate(self, names, message, cls=DeprecationWarning):
        """Deprecate the given names."""
        if not isinstance(names, (tuple, list)):
            names = (names,)
        for name in names:
            self.__deprecated[name] = (message, cls)

    def __getattribute__(self, name):
        if name == 'deprecate' or name.startswith('_DeprecationProxy__'):
            return ogetattr(self, name)

        if name == '__class__':
            return types.ModuleType

        if name in ogetattr(self, '_DeprecationProxy__deprecated'):
            if __show__():
                msg, cls = self.__deprecated[name]
                warnings.warn(name + ': ' + msg, cls, 2)

        return getattr(ogetattr(self, '_DeprecationProxy__original_module'),
                       name)

    def __setattr__(self, name, value):
        if name.startswith('_DeprecationProxy__'):
            return object.__setattr__(self, name, value)

        setattr(self.__original_module, name, value)

    def __delattr__(self, name):
        if name.startswith('_DeprecationProxy__'):
            return object.__delattr__(self, name)

        delattr(self.__original_module, name)


class DeprecatedModule:

    def __init__(self, module, msg, cls=DeprecationWarning):
        self.__original_module = module
        self.__msg = msg
        self.__cls = cls

    def __getattribute__(self, name):
        if name.startswith('_DeprecatedModule__'):
            return ogetattr(self, name)

        if name == '__class__':
            return types.ModuleType

        if __show__():
            warnings.warn(self.__msg, self.__cls, 2)

        return getattr(ogetattr(self, '_DeprecatedModule__original_module'),
                       name)

    def __setattr__(self, name, value):
        if name.startswith('_DeprecatedModule__'):
            return object.__setattr__(self, name, value)
        setattr(self.__original_module, name, value)

    def __delattr__(self, name):
        if name.startswith('_DeprecatedModule__'):
            return object.__delattr__(self, name)
        delattr(self.__original_module, name)


class DeprecatedGetProperty:

    def __init__(self, prop, message, cls=DeprecationWarning):
        self.message = message
        self.prop = prop
        self.cls = cls

    def __get__(self, inst, klass):
        if __show__():
            warnings.warn(self.message, self.cls, 2)
        return self.prop.__get__(inst, klass)


class DeprecatedGetSetProperty(DeprecatedGetProperty):

    def __set__(self, inst, prop):
        if __show__():
            warnings.warn(self.message, self.cls, 2)
        self.prop.__set__(inst, prop)


class DeprecatedGetSetDeleteProperty(DeprecatedGetSetProperty):

    def __delete__(self, inst):
        if __show__():
            warnings.warn(self.message, self.cls, 2)
        self.prop.__delete__(inst)


def DeprecatedMethod(method, message, cls=DeprecationWarning):

    def deprecated_method(*args, **kw):
        if __show__():
            warnings.warn(message, cls, 2)
        return method(*args, **kw)

    return deprecated_method


def deprecated(specifier, message, cls=DeprecationWarning):
    """Deprecate the given names."""

    # A string specifier (or list of strings) means we're called
    # top-level in a module and are to deprecate things inside this
    # module
    if isinstance(specifier, str_and_sequence_types):
        globals = sys._getframe(1).f_globals
        modname = globals['__name__']

        if not isinstance(sys.modules[modname], DeprecationProxy):
            sys.modules[modname] = DeprecationProxy(sys.modules[modname])
        sys.modules[modname].deprecate(specifier, message, cls)

    # Anything else can mean the specifier is a function/method,
    # module, or just an attribute of a class
    elif isinstance(specifier, types.FunctionType):
        return DeprecatedMethod(specifier, message, cls)
    elif isinstance(specifier, types.ModuleType):
        return DeprecatedModule(specifier, message, cls)
    else:
        prop = specifier
        if hasattr(prop, '__get__') and hasattr(prop, '__set__') and \
                hasattr(prop, '__delete__'):
            return DeprecatedGetSetDeleteProperty(prop, message, cls)
        elif hasattr(prop, '__get__') and hasattr(prop, '__set__'):
            return DeprecatedGetSetProperty(prop, message, cls)
        elif hasattr(prop, '__get__'):
            return DeprecatedGetProperty(prop, message, cls)


class deprecate:
    """Deprecation decorator"""

    def __init__(self, msg, cls=DeprecationWarning):
        self.msg = msg
        self.cls = cls

    def __call__(self, func):
        return DeprecatedMethod(func, self.msg, self.cls)


def moved(to_location, unsupported_in=None, cls=DeprecationWarning):
    old = sys._getframe(1).f_globals['__name__']
    message = f'{old} has moved to {to_location}.'
    if unsupported_in:
        message += " Import of {} will become unsupported in {}".format(
            old, unsupported_in)

    warnings.warn(message, cls, 3)
    __import__(to_location)

    fromdict = sys.modules[to_location].__dict__
    tomod = sys.modules[old]
    tomod.__doc__ = message

    for name, v in fromdict.items():
        if name not in tomod.__dict__:
            setattr(tomod, name, v)
