# -*- coding: utf-8 -*-
"""
    sphinx.registry
    ~~~~~~~~~~~~~~~

    Sphinx component registry.

    :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS.
    :license: BSD, see LICENSE for details.
"""
from __future__ import print_function

import traceback
import warnings
from inspect import isclass
from types import MethodType

from docutils.parsers.rst import Directive
from pkg_resources import iter_entry_points
from six import iteritems, itervalues

from sphinx.deprecation import RemovedInSphinx30Warning
from sphinx.domains import ObjType
from sphinx.domains.std import GenericObject, Target
from sphinx.errors import ExtensionError, SphinxError, VersionRequirementError
from sphinx.extension import Extension
from sphinx.locale import __
from sphinx.parsers import Parser as SphinxParser
from sphinx.roles import XRefRole
from sphinx.util import logging
from sphinx.util.docutils import directive_helper

if False:
    # For type annotation
    from typing import Any, Callable, Dict, Iterator, List, Tuple, Type, Union  # NOQA
    from docutils import nodes  # NOQA
    from docutils.io import Input  # NOQA
    from docutils.parsers import Parser  # NOQA
    from docutils.transforms import Transform  # NOQA
    from sphinx.application import Sphinx  # NOQA
    from sphinx.builders import Builder  # NOQA
    from sphinx.config import Config  # NOQA
    from sphinx.domains import Domain, Index  # NOQA
    from sphinx.environment import BuildEnvironment  # NOQA
    from sphinx.ext.autodoc import Documenter  # NOQA
    from sphinx.util.typing import RoleFunction, TitleGetter  # NOQA

logger = logging.getLogger(__name__)

# list of deprecated extensions. Keys are extension name.
# Values are Sphinx version that merge the extension.
EXTENSION_BLACKLIST = {
    "sphinxjp.themecore": "1.2"
}  # type: Dict[unicode, unicode]


class SphinxComponentRegistry(object):
    def __init__(self):
        # type: () -> None
        #: special attrgetter for autodoc; class object -> attrgetter
        self.autodoc_attrgettrs = {}    # type: Dict[Type, Callable[[Any, unicode, Any], Any]]

        #: builders; a dict of builder name -> bulider class
        self.builders = {}              # type: Dict[unicode, Type[Builder]]

        #: autodoc documenters; a dict of documenter name -> documenter class
        self.documenters = {}           # type: Dict[unicode, Type[Documenter]]

        #: css_files; a list of tuple of filename and attributes
        self.css_files = []             # type: List[Tuple[unicode, Dict[unicode, unicode]]]

        #: domains; a dict of domain name -> domain class
        self.domains = {}               # type: Dict[unicode, Type[Domain]]

        #: additional directives for domains
        #: a dict of domain name -> dict of directive name -> directive
        self.domain_directives = {}     # type: Dict[unicode, Dict[unicode, Any]]

        #: additional indices for domains
        #: a dict of domain name -> list of index class
        self.domain_indices = {}        # type: Dict[unicode, List[Type[Index]]]

        #: additional object types for domains
        #: a dict of domain name -> dict of objtype name -> objtype
        self.domain_object_types = {}   # type: Dict[unicode, Dict[unicode, ObjType]]

        #: additional roles for domains
        #: a dict of domain name -> dict of role name -> role impl.
        self.domain_roles = {}          # type: Dict[unicode, Dict[unicode, Union[RoleFunction, XRefRole]]]  # NOQA

        #: additional enumerable nodes
        #: a dict of node class -> tuple of figtype and title_getter function
        self.enumerable_nodes = {}      # type: Dict[nodes.Node, Tuple[unicode, TitleGetter]]

        #: HTML inline and block math renderers
        #: a dict of name -> tuple of visit function and depart function
        self.html_inline_math_renderers = {}    # type: Dict[unicode, Tuple[Callable, Callable]]  # NOQA
        self.html_block_math_renderers = {}     # type: Dict[unicode, Tuple[Callable, Callable]]  # NOQA

        #: js_files; list of JS paths or URLs
        self.js_files = []              # type: List[Tuple[unicode, Dict[unicode, unicode]]]

        #: LaTeX packages; list of package names and its options
        self.latex_packages = []        # type: List[Tuple[unicode, unicode]]

        #: post transforms; list of transforms
        self.post_transforms = []       # type: List[Type[Transform]]

        #: source paresrs; file type -> parser class
        self.source_parsers = {}        # type: Dict[unicode, Type[Parser]]

        #: source inputs; file type -> input class
        self.source_inputs = {}         # type: Dict[unicode, Input]

        #: source suffix: suffix -> file type
        self.source_suffix = {}         # type: Dict[unicode, unicode]

        #: custom translators; builder name -> translator class
        self.translators = {}           # type: Dict[unicode, nodes.NodeVisitor]

        #: custom handlers for translators
        #: a dict of builder name -> dict of node name -> visitor and departure functions
        self.translation_handlers = {}  # type: Dict[unicode, Dict[unicode, Tuple[Callable, Callable]]]  # NOQA

        #: additional transforms; list of transforms
        self.transforms = []            # type: List[Type[Transform]]

    def add_builder(self, builder, override=False):
        # type: (Type[Builder], bool) -> None
        logger.debug('[app] adding builder: %r', builder)
        if not hasattr(builder, 'name'):
            raise ExtensionError(__('Builder class %s has no "name" attribute') % builder)
        if builder.name in self.builders and not override:
            raise ExtensionError(__('Builder %r already exists (in module %s)') %
                                 (builder.name, self.builders[builder.name].__module__))
        self.builders[builder.name] = builder

    def preload_builder(self, app, name):
        # type: (Sphinx, unicode) -> None
        if name is None:
            return

        if name not in self.builders:
            entry_points = iter_entry_points('sphinx.builders', name)
            try:
                entry_point = next(entry_points)
            except StopIteration:
                raise SphinxError(__('Builder name %s not registered or available'
                                     ' through entry point') % name)

            self.load_extension(app, entry_point.module_name)

    def create_builder(self, app, name):
        # type: (Sphinx, unicode) -> Builder
        if name not in self.builders:
            raise SphinxError(__('Builder name %s not registered') % name)

        return self.builders[name](app)

    def add_domain(self, domain, override=False):
        # type: (Type[Domain], bool) -> None
        logger.debug('[app] adding domain: %r', domain)
        if domain.name in self.domains and not override:
            raise ExtensionError(__('domain %s already registered') % domain.name)
        self.domains[domain.name] = domain

    def has_domain(self, domain):
        # type: (unicode) -> bool
        return domain in self.domains

    def create_domains(self, env):
        # type: (BuildEnvironment) -> Iterator[Domain]
        for DomainClass in itervalues(self.domains):
            domain = DomainClass(env)

            # transplant components added by extensions
            domain.directives.update(self.domain_directives.get(domain.name, {}))
            domain.roles.update(self.domain_roles.get(domain.name, {}))
            domain.indices.extend(self.domain_indices.get(domain.name, []))
            for name, objtype in iteritems(self.domain_object_types.get(domain.name, {})):
                domain.add_object_type(name, objtype)

            yield domain

    def override_domain(self, domain):
        # type: (Type[Domain]) -> None
        warnings.warn('registry.override_domain() is deprecated. '
                      'Use app.add_domain(domain, override=True) instead.',
                      RemovedInSphinx30Warning)
        self.add_domain(domain, override=True)

    def add_directive_to_domain(self, domain, name, obj, has_content=None, argument_spec=None,
                                override=False, **option_spec):
        # type: (unicode, unicode, Any, bool, Any, bool, Any) -> None
        logger.debug('[app] adding directive to domain: %r',
                     (domain, name, obj, has_content, argument_spec, option_spec))
        if domain not in self.domains:
            raise ExtensionError(__('domain %s not yet registered') % domain)

        directives = self.domain_directives.setdefault(domain, {})
        if name in directives and not override:
            raise ExtensionError(__('The %r directive is already registered to domain %s') %
                                 (name, domain))
        if not isclass(obj) or not issubclass(obj, Directive):
            directives[name] = directive_helper(obj, has_content, argument_spec, **option_spec)
        else:
            directives[name] = obj

    def add_role_to_domain(self, domain, name, role, override=False):
        # type: (unicode, unicode, Union[RoleFunction, XRefRole], bool) -> None
        logger.debug('[app] adding role to domain: %r', (domain, name, role))
        if domain not in self.domains:
            raise ExtensionError(__('domain %s not yet registered') % domain)
        roles = self.domain_roles.setdefault(domain, {})
        if name in roles and not override:
            raise ExtensionError(__('The %r role is already registered to domain %s') %
                                 (name, domain))
        roles[name] = role

    def add_index_to_domain(self, domain, index, override=False):
        # type: (unicode, Type[Index], bool) -> None
        logger.debug('[app] adding index to domain: %r', (domain, index))
        if domain not in self.domains:
            raise ExtensionError(__('domain %s not yet registered') % domain)
        indices = self.domain_indices.setdefault(domain, [])
        if index in indices and not override:
            raise ExtensionError(__('The %r index is already registered to domain %s') %
                                 (index.name, domain))
        indices.append(index)

    def add_object_type(self, directivename, rolename, indextemplate='',
                        parse_node=None, ref_nodeclass=None, objname='',
                        doc_field_types=[], override=False):
        # type: (unicode, unicode, unicode, Callable, nodes.Node, unicode, List, bool) -> None
        logger.debug('[app] adding object type: %r',
                     (directivename, rolename, indextemplate, parse_node,
                      ref_nodeclass, objname, doc_field_types))

        # create a subclass of GenericObject as the new directive
        directive = type(directivename,  # type: ignore
                         (GenericObject, object),
                         {'indextemplate': indextemplate,
                          'parse_node': staticmethod(parse_node),
                          'doc_field_types': doc_field_types})

        self.add_directive_to_domain('std', directivename, directive)
        self.add_role_to_domain('std', rolename, XRefRole(innernodeclass=ref_nodeclass))

        object_types = self.domain_object_types.setdefault('std', {})
        if directivename in object_types and not override:
            raise ExtensionError(__('The %r object_type is already registered') %
                                 directivename)
        object_types[directivename] = ObjType(objname or directivename, rolename)

    def add_crossref_type(self, directivename, rolename, indextemplate='',
                          ref_nodeclass=None, objname='', override=False):
        # type: (unicode, unicode, unicode, nodes.Node, unicode, bool) -> None
        logger.debug('[app] adding crossref type: %r',
                     (directivename, rolename, indextemplate, ref_nodeclass, objname))

        # create a subclass of Target as the new directive
        directive = type(directivename,  # type: ignore
                         (Target, object),
                         {'indextemplate': indextemplate})

        self.add_directive_to_domain('std', directivename, directive)
        self.add_role_to_domain('std', rolename, XRefRole(innernodeclass=ref_nodeclass))

        object_types = self.domain_object_types.setdefault('std', {})
        if directivename in object_types and not override:
            raise ExtensionError(__('The %r crossref_type is already registered') %
                                 directivename)
        object_types[directivename] = ObjType(objname or directivename, rolename)

    def add_source_suffix(self, suffix, filetype, override=False):
        # type: (unicode, unicode, bool) -> None
        logger.debug('[app] adding source_suffix: %r, %r', suffix, filetype)
        if suffix in self.source_suffix and not override:
            raise ExtensionError(__('source_suffix %r is already registered') % suffix)
        else:
            self.source_suffix[suffix] = filetype

    def add_source_parser(self, *args, **kwargs):
        # type: (Any, bool) -> None
        logger.debug('[app] adding search source_parser: %r', args)
        if len(args) == 1:
            # new sytle arguments: (source_parser)
            suffix = None       # type: unicode
            parser = args[0]    # type: Type[Parser]
        else:
            # old style arguments: (suffix, source_parser)
            warnings.warn('app.add_source_parser() does not support suffix argument. '
                          'Use app.add_source_suffix() instead.',
                          RemovedInSphinx30Warning)
            suffix = args[0]
            parser = args[1]

        if suffix:
            self.add_source_suffix(suffix, suffix, override=True)

        if len(parser.supported) == 0:
            warnings.warn('Old source_parser has been detected. Please fill Parser.supported '
                          'attribute: %s' % parser.__name__,
                          RemovedInSphinx30Warning)

        # create a map from filetype to parser
        for filetype in parser.supported:
            if filetype in self.source_parsers and not kwargs.get('override'):
                raise ExtensionError(__('source_parser for %r is already registered') %
                                     filetype)
            else:
                self.source_parsers[filetype] = parser

        # also maps suffix to parser
        #
        # This rescues old styled parsers which does not have ``supported`` filetypes.
        if suffix:
            self.source_parsers[suffix] = parser

    def get_source_parser(self, filetype):
        # type: (unicode) -> Type[Parser]
        try:
            return self.source_parsers[filetype]
        except KeyError:
            raise SphinxError(__('Source parser for %s not registered') % filetype)

    def get_source_parsers(self):
        # type: () -> Dict[unicode, Parser]
        return self.source_parsers

    def create_source_parser(self, app, filename):
        # type: (Sphinx, unicode) -> Parser
        parser_class = self.get_source_parser(filename)
        parser = parser_class()
        if isinstance(parser, SphinxParser):
            parser.set_application(app)
        return parser

    def add_source_input(self, input_class, override=False):
        # type: (Type[Input], bool) -> None
        for filetype in input_class.supported:
            if filetype in self.source_inputs and not override:
                raise ExtensionError(__('source_input for %r is already registered') %
                                     filetype)
            self.source_inputs[filetype] = input_class

    def get_source_input(self, filetype):
        # type: (unicode) -> Type[Input]
        try:
            return self.source_inputs[filetype]
        except KeyError:
            try:
                # use special source_input for unknown filetype
                return self.source_inputs['*']
            except KeyError:
                raise SphinxError(__('source_input for %s not registered') % filetype)

    def add_translator(self, name, translator, override=False):
        # type: (unicode, Type[nodes.NodeVisitor], bool) -> None
        logger.debug('[app] Change of translator for the %s builder.' % name)
        if name in self.translators and not override:
            raise ExtensionError(__('Translatoro for %r already exists') % name)
        self.translators[name] = translator

    def add_translation_handlers(self, node, **kwargs):
        # type: (nodes.Node, Any) -> None
        logger.debug('[app] adding translation_handlers: %r, %r', node, kwargs)
        for builder_name, handlers in iteritems(kwargs):
            translation_handlers = self.translation_handlers.setdefault(builder_name, {})
            try:
                visit, depart = handlers  # unpack once for assertion
                translation_handlers[node.__name__] = (visit, depart)
            except ValueError:
                raise ExtensionError(__('kwargs for add_node() must be a (visit, depart) '
                                        'function tuple: %r=%r') % builder_name, handlers)

    def get_translator_class(self, builder):
        # type: (Builder) -> Type[nodes.NodeVisitor]
        return self.translators.get(builder.name,
                                    builder.default_translator_class)

    def create_translator(self, builder, *args):
        # type: (Builder, Any) -> nodes.NodeVisitor
        translator_class = self.get_translator_class(builder)
        assert translator_class, "translator not found for %s" % builder.name
        translator = translator_class(*args)

        # transplant handlers for custom nodes to translator instance
        handlers = self.translation_handlers.get(builder.name, None)
        if handlers is None:
            # retry with builder.format
            handlers = self.translation_handlers.get(builder.format, {})

        for name, (visit, depart) in iteritems(handlers):
            setattr(translator, 'visit_' + name, MethodType(visit, translator))
            if depart:
                setattr(translator, 'depart_' + name, MethodType(depart, translator))

        return translator

    def add_transform(self, transform):
        # type: (Type[Transform]) -> None
        logger.debug('[app] adding transform: %r', transform)
        self.transforms.append(transform)

    def get_transforms(self):
        # type: () -> List[Type[Transform]]
        return self.transforms

    def add_post_transform(self, transform):
        # type: (Type[Transform]) -> None
        logger.debug('[app] adding post transform: %r', transform)
        self.post_transforms.append(transform)

    def get_post_transforms(self):
        # type: () -> List[Type[Transform]]
        return self.post_transforms

    def add_documenter(self, objtype, documenter):
        # type: (unicode, Type[Documenter]) -> None
        self.documenters[objtype] = documenter

    def add_autodoc_attrgetter(self, typ, attrgetter):
        # type: (Type, Callable[[Any, unicode, Any], Any]) -> None
        self.autodoc_attrgettrs[typ] = attrgetter

    def add_css_files(self, filename, **attributes):
        self.css_files.append((filename, attributes))

    def add_js_file(self, filename, **attributes):
        # type: (unicode, **unicode) -> None
        logger.debug('[app] adding js_file: %r, %r', filename, attributes)
        self.js_files.append((filename, attributes))  # type: ignore

    def add_latex_package(self, name, options):
        # type: (unicode, unicode) -> None
        logger.debug('[app] adding latex package: %r', name)
        self.latex_packages.append((name, options))

    def add_enumerable_node(self, node, figtype, title_getter=None, override=False):
        # type: (nodes.Node, unicode, TitleGetter, bool) -> None
        logger.debug('[app] adding enumerable node: (%r, %r, %r)', node, figtype, title_getter)
        if node in self.enumerable_nodes and not override:
            raise ExtensionError(__('enumerable_node %r already registered') % node)
        self.enumerable_nodes[node] = (figtype, title_getter)

    def add_html_math_renderer(self, name, inline_renderers, block_renderers):
        # type: (unicode, Tuple[Callable, Callable], Tuple[Callable, Callable]) -> None
        logger.debug('[app] adding html_math_renderer: %s, %r, %r',
                     name, inline_renderers, block_renderers)
        if name in self.html_inline_math_renderers:
            raise ExtensionError(__('math renderer %s is already registred') % name)

        self.html_inline_math_renderers[name] = inline_renderers
        self.html_block_math_renderers[name] = block_renderers

    def load_extension(self, app, extname):
        # type: (Sphinx, unicode) -> None
        """Load a Sphinx extension."""
        if extname in app.extensions:  # alread loaded
            return
        if extname in EXTENSION_BLACKLIST:
            logger.warning(__('the extension %r was already merged with Sphinx since '
                              'version %s; this extension is ignored.'),
                           extname, EXTENSION_BLACKLIST[extname])
            return

        # update loading context
        app._setting_up_extension.append(extname)

        try:
            mod = __import__(extname, None, None, ['setup'])
        except ImportError as err:
            logger.verbose(__('Original exception:\n') + traceback.format_exc())
            raise ExtensionError(__('Could not import extension %s') % extname, err)

        if not hasattr(mod, 'setup'):
            logger.warning(__('extension %r has no setup() function; is it really '
                              'a Sphinx extension module?'), extname)
            metadata = {}  # type: Dict[unicode, Any]
        else:
            try:
                metadata = mod.setup(app)
            except VersionRequirementError as err:
                # add the extension name to the version required
                raise VersionRequirementError(
                    __('The %s extension used by this project needs at least '
                       'Sphinx v%s; it therefore cannot be built with this '
                       'version.') % (extname, err)
                )

        if metadata is None:
            metadata = {}
        elif not isinstance(metadata, dict):
            logger.warning(__('extension %r returned an unsupported object from '
                              'its setup() function; it should return None or a '
                              'metadata dictionary'), extname)
            metadata = {}

        app.extensions[extname] = Extension(extname, mod, **metadata)
        app._setting_up_extension.pop()

    def get_envversion(self, app):
        # type: (Sphinx) -> Dict[unicode, unicode]
        from sphinx.environment import ENV_VERSION
        envversion = {ext.name: ext.metadata['env_version'] for ext in app.extensions.values()
                      if ext.metadata.get('env_version')}
        envversion['sphinx'] = ENV_VERSION
        return envversion


def merge_source_suffix(app, config):
    # type: (Sphinx, Config) -> None
    """Merge source_suffix which specified by user and added by extensions."""
    for suffix, filetype in iteritems(app.registry.source_suffix):
        if suffix not in app.config.source_suffix:
            app.config.source_suffix[suffix] = filetype
        elif app.config.source_suffix[suffix] is None:
            # filetype is not specified (default filetype).
            # So it overrides default filetype by extensions setting.
            app.config.source_suffix[suffix] = filetype

    # copy config.source_suffix to registry
    app.registry.source_suffix = app.config.source_suffix


def setup(app):
    # type: (Sphinx) -> Dict[unicode, Any]
    app.connect('config-inited', merge_source_suffix)

    return {
        'version': 'builtin',
        'parallel_read_safe': True,
        'parallel_write_safe': True,
    }
