"""
    sphinx.ext.autodoc.importer
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~

    Importer utilities for autodoc

    :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
    :license: BSD, see LICENSE for details.
"""

import importlib
import traceback
import warnings
from typing import Any, Callable, Dict, List, Mapping, NamedTuple, Optional, Tuple

from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias
from sphinx.pycode import ModuleAnalyzer, PycodeError
from sphinx.util import logging
from sphinx.util.inspect import (getannotations, getmro, getslots, isclass, isenumclass,
                                 safe_getattr)

if False:
    # For type annotation
    from typing import Type  # NOQA

logger = logging.getLogger(__name__)


def mangle(subject: Any, name: str) -> str:
    """mangle the given name."""
    try:
        if isclass(subject) and name.startswith('__') and not name.endswith('__'):
            return "_%s%s" % (subject.__name__, name)
    except AttributeError:
        pass

    return name


def unmangle(subject: Any, name: str) -> Optional[str]:
    """unmangle the given name."""
    try:
        if isclass(subject) and not name.endswith('__'):
            prefix = "_%s__" % subject.__name__
            if name.startswith(prefix):
                return name.replace(prefix, "__", 1)
            else:
                for cls in subject.__mro__:
                    prefix = "_%s__" % cls.__name__
                    if name.startswith(prefix):
                        # mangled attribute defined in parent class
                        return None
    except AttributeError:
        pass

    return name


def import_module(modname: str, warningiserror: bool = False) -> Any:
    """
    Call importlib.import_module(modname), convert exceptions to ImportError
    """
    try:
        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", category=ImportWarning)
            with logging.skip_warningiserror(not warningiserror):
                return importlib.import_module(modname)
    except BaseException as exc:
        # Importing modules may cause any side effects, including
        # SystemExit, so we need to catch all errors.
        raise ImportError(exc, traceback.format_exc()) from exc


def import_object(modname: str, objpath: List[str], objtype: str = '',
                  attrgetter: Callable[[Any, str], Any] = safe_getattr,
                  warningiserror: bool = False) -> Any:
    if objpath:
        logger.debug('[autodoc] from %s import %s', modname, '.'.join(objpath))
    else:
        logger.debug('[autodoc] import %s', modname)

    try:
        module = None
        exc_on_importing = None
        objpath = list(objpath)
        while module is None:
            try:
                module = import_module(modname, warningiserror=warningiserror)
                logger.debug('[autodoc] import %s => %r', modname, module)
            except ImportError as exc:
                logger.debug('[autodoc] import %s => failed', modname)
                exc_on_importing = exc
                if '.' in modname:
                    # retry with parent module
                    modname, name = modname.rsplit('.', 1)
                    objpath.insert(0, name)
                else:
                    raise

        obj = module
        parent = None
        object_name = None
        for attrname in objpath:
            parent = obj
            logger.debug('[autodoc] getattr(_, %r)', attrname)
            mangled_name = mangle(obj, attrname)
            obj = attrgetter(obj, mangled_name)
            logger.debug('[autodoc] => %r', obj)
            object_name = attrname
        return [module, parent, object_name, obj]
    except (AttributeError, ImportError) as exc:
        if isinstance(exc, AttributeError) and exc_on_importing:
            # restore ImportError
            exc = exc_on_importing

        if objpath:
            errmsg = ('autodoc: failed to import %s %r from module %r' %
                      (objtype, '.'.join(objpath), modname))
        else:
            errmsg = 'autodoc: failed to import %s %r' % (objtype, modname)

        if isinstance(exc, ImportError):
            # import_module() raises ImportError having real exception obj and
            # traceback
            real_exc, traceback_msg = exc.args
            if isinstance(real_exc, SystemExit):
                errmsg += ('; the module executes module level statement '
                           'and it might call sys.exit().')
            elif isinstance(real_exc, ImportError) and real_exc.args:
                errmsg += '; the following exception was raised:\n%s' % real_exc.args[0]
            else:
                errmsg += '; the following exception was raised:\n%s' % traceback_msg
        else:
            errmsg += '; the following exception was raised:\n%s' % traceback.format_exc()

        logger.debug(errmsg)
        raise ImportError(errmsg) from exc


def get_module_members(module: Any) -> List[Tuple[str, Any]]:
    """Get members of target module."""
    from sphinx.ext.autodoc import INSTANCEATTR

    members = {}  # type: Dict[str, Tuple[str, Any]]
    for name in dir(module):
        try:
            value = safe_getattr(module, name, None)
            members[name] = (name, value)
        except AttributeError:
            continue

    # annotation only member (ex. attr: int)
    try:
        for name in getannotations(module):
            if name not in members:
                members[name] = (name, INSTANCEATTR)
    except AttributeError:
        pass

    return sorted(list(members.values()))


Attribute = NamedTuple('Attribute', [('name', str),
                                     ('directly_defined', bool),
                                     ('value', Any)])


def _getmro(obj: Any) -> Tuple["Type", ...]:
    warnings.warn('sphinx.ext.autodoc.importer._getmro() is deprecated.',
                  RemovedInSphinx40Warning)
    return getmro(obj)


def _getannotations(obj: Any) -> Mapping[str, Any]:
    warnings.warn('sphinx.ext.autodoc.importer._getannotations() is deprecated.',
                  RemovedInSphinx40Warning)
    return getannotations(obj)


def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable,
                       analyzer: ModuleAnalyzer = None) -> Dict[str, Attribute]:
    """Get members and attributes of target object."""
    from sphinx.ext.autodoc import INSTANCEATTR

    # the members directly defined in the class
    obj_dict = attrgetter(subject, '__dict__', {})

    members = {}  # type: Dict[str, Attribute]

    # enum members
    if isenumclass(subject):
        for name, value in subject.__members__.items():
            if name not in members:
                members[name] = Attribute(name, True, value)

        superclass = subject.__mro__[1]
        for name in obj_dict:
            if name not in superclass.__dict__:
                value = safe_getattr(subject, name)
                members[name] = Attribute(name, True, value)

    # members in __slots__
    try:
        __slots__ = getslots(subject)
        if __slots__:
            from sphinx.ext.autodoc import SLOTSATTR

            for name in __slots__:
                members[name] = Attribute(name, True, SLOTSATTR)
    except (AttributeError, TypeError, ValueError):
        pass

    # other members
    for name in dir(subject):
        try:
            value = attrgetter(subject, name)
            directly_defined = name in obj_dict
            name = unmangle(subject, name)
            if name and name not in members:
                members[name] = Attribute(name, directly_defined, value)
        except AttributeError:
            continue

    # annotation only member (ex. attr: int)
    for i, cls in enumerate(getmro(subject)):
        try:
            for name in getannotations(cls):
                name = unmangle(cls, name)
                if name and name not in members:
                    members[name] = Attribute(name, i == 0, INSTANCEATTR)
        except AttributeError:
            pass

    if analyzer:
        # append instance attributes (cf. self.attr1) if analyzer knows
        namespace = '.'.join(objpath)
        for (ns, name) in analyzer.find_attr_docs():
            if namespace == ns and name not in members:
                members[name] = Attribute(name, True, INSTANCEATTR)

    return members


class ClassAttribute:
    """The attribute of the class."""

    def __init__(self, cls: Any, name: str, value: Any, docstring: Optional[str] = None):
        self.class_ = cls
        self.name = name
        self.value = value
        self.docstring = docstring


def get_class_members(subject: Any, objpath: List[str], attrgetter: Callable
                      ) -> Dict[str, ClassAttribute]:
    """Get members and attributes of target class."""
    from sphinx.ext.autodoc import INSTANCEATTR

    # the members directly defined in the class
    obj_dict = attrgetter(subject, '__dict__', {})

    members = {}  # type: Dict[str, ClassAttribute]

    # enum members
    if isenumclass(subject):
        for name, value in subject.__members__.items():
            if name not in members:
                members[name] = ClassAttribute(subject, name, value)

        superclass = subject.__mro__[1]
        for name in obj_dict:
            if name not in superclass.__dict__:
                value = safe_getattr(subject, name)
                members[name] = ClassAttribute(subject, name, value)

    # members in __slots__
    try:
        __slots__ = getslots(subject)
        if __slots__:
            from sphinx.ext.autodoc import SLOTSATTR

            for name, docstring in __slots__.items():
                members[name] = ClassAttribute(subject, name, SLOTSATTR, docstring)
    except (AttributeError, TypeError, ValueError):
        pass

    # other members
    for name in dir(subject):
        try:
            value = attrgetter(subject, name)
            unmangled = unmangle(subject, name)
            if unmangled and unmangled not in members:
                if name in obj_dict:
                    members[unmangled] = ClassAttribute(subject, unmangled, value)
                else:
                    members[unmangled] = ClassAttribute(None, unmangled, value)
        except AttributeError:
            continue

    try:
        for cls in getmro(subject):
            # annotation only member (ex. attr: int)
            try:
                for name in getannotations(cls):
                    name = unmangle(cls, name)
                    if name and name not in members:
                        members[name] = ClassAttribute(cls, name, INSTANCEATTR)
            except AttributeError:
                pass

            # append instance attributes (cf. self.attr1) if analyzer knows
            try:
                modname = safe_getattr(cls, '__module__')
                qualname = safe_getattr(cls, '__qualname__')
                analyzer = ModuleAnalyzer.for_module(modname)
                analyzer.analyze()
                for (ns, name), docstring in analyzer.attr_docs.items():
                    if ns == qualname and name not in members:
                        members[name] = ClassAttribute(cls, name, INSTANCEATTR,
                                                       '\n'.join(docstring))
            except (AttributeError, PycodeError):
                pass
    except AttributeError:
        pass

    return members


from sphinx.ext.autodoc.mock import (MockFinder, MockLoader, _MockModule, _MockObject,  # NOQA
                                     mock)

deprecated_alias('sphinx.ext.autodoc.importer',
                 {
                     '_MockModule': _MockModule,
                     '_MockObject': _MockObject,
                     'MockFinder': MockFinder,
                     'MockLoader': MockLoader,
                     'mock': mock,
                 },
                 RemovedInSphinx40Warning,
                 {
                     '_MockModule': 'sphinx.ext.autodoc.mock._MockModule',
                     '_MockObject': 'sphinx.ext.autodoc.mock._MockObject',
                     'MockFinder': 'sphinx.ext.autodoc.mock.MockFinder',
                     'MockLoader': 'sphinx.ext.autodoc.mock.MockLoader',
                     'mock': 'sphinx.ext.autodoc.mock.mock',
                 })
