##############################################################################
#
# Copyright (c) 2006 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.
#
##############################################################################
"""Modules with defered attributes
"""
import sys
import warnings

import zope.proxy


class Deferred:

    def __init__(self, name, specifier):
        self.__name__ = name
        self.specifier = specifier

    _import_chicken = {}, {}, ['*']

    def get(self):

        specifier = self.specifier
        if ':' in specifier:
            module, name = specifier.split(':')
        else:
            module, name = specifier, ''

        v = __import__(module, *self._import_chicken)
        if name:
            for n in name.split('.'):
                v = getattr(v, n)
        return v


class DeferredAndDeprecated(Deferred):

    def __init__(self, name, specifier, message):
        super().__init__(name, specifier)
        self.message = message

    def get(self):
        warnings.warn(
            self.__name__ + " is deprecated. " + self.message,
            DeprecationWarning, stacklevel=3)

        return super().get()


class ModuleProxy(zope.proxy.ProxyBase):
    __slots__ = ('__deferred_definitions__', '__doc__')

    def __init__(self, module):
        super().__init__(module)
        self.__deferred_definitions__ = {}
        self.__doc__ = module.__doc__

    def __getattr__(self, name):
        try:
            get = self.__deferred_definitions__[name]
        except KeyError:
            raise AttributeError(name)
        v = get.get()
        setattr(self, name, v)
        try:
            del self.__deferred_definitions__[name]
        except KeyError:  # pragma: no cover
            pass
        return v


def initialize(level=1):
    """Prepare a module to support deferred imports.

    Modules do not need to call this directly, because the
    `define*` and `deprecated*` functions call it.

    This is intended to be called from the module to be prepared.
    The implementation wraps a proxy around the module and replaces
    the entry in sys.modules with the proxy.  It does no harm to
    call this function more than once for a given module, because
    this function does not re-wrap a proxied module.

    The level parameter specifies a relative stack depth.
    When this function is called directly by the module, level should be 1.
    When this function is called by a helper function, level should
    increase with the depth of the stack.

    Returns nothing when level is 1; otherwise returns the proxied module.
    """
    __name__ = sys._getframe(level).f_globals['__name__']
    module = sys.modules[__name__]
    if type(module) is not ModuleProxy:
        module = ModuleProxy(module)
        sys.modules[__name__] = module

    if level == 1:
        return
    return module


def define(**names):
    """Define deferred imports using keyword parameters.

    Each parameter specifies the importable name and how to import it.
    Use `module:name` syntax to import a name from a module, or `module`
    (no colon) to import a module.
    """
    module = initialize(2)
    __deferred_definitions__ = module.__deferred_definitions__
    for name, specifier in names.items():
        __deferred_definitions__[name] = Deferred(name, specifier)


def defineFrom(from_name, *names):
    """Define deferred imports from a particular module.

    The from_name specifies which module to import.
    The rest of the parameters specify names to import from that module.
    """
    module = initialize(2)
    __deferred_definitions__ = module.__deferred_definitions__
    for name in names:
        specifier = from_name + ':' + name
        __deferred_definitions__[name] = Deferred(name, specifier)


def deprecated(message, **names):
    """Define deferred and deprecated imports using keyword parameters.

    The first use of each name will generate a deprecation warning with
    the given message.

    Each parameter specifies the importable name and how to import it.
    Use `module:name` syntax to import a name from a module, or `module`
    (no colon) to import a module.
    """
    module = initialize(2)
    __deferred_definitions__ = module.__deferred_definitions__
    for name, specifier in names.items():
        __deferred_definitions__[name] = DeferredAndDeprecated(
            name, specifier, message)


def deprecatedFrom(message, from_name, *names):
    """Define deferred and deprecated imports from a particular module.

    The first use of each name will generate a deprecation warning with
    the given message.

    The from_name specifies which module to import.
    The rest of the parameters specify names to import from that module.
    """
    module = initialize(2)
    __deferred_definitions__ = module.__deferred_definitions__
    for name in names:
        specifier = from_name + ':' + name
        __deferred_definitions__[name] = DeferredAndDeprecated(
            name, specifier, message)
