"""
    sphinx.roles
    ~~~~~~~~~~~~

    Handlers for additional ReST roles.

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

import re
import warnings
from typing import Any, Dict, List, Tuple

from docutils import nodes, utils
from docutils.nodes import Element, Node, TextElement, system_message
from docutils.parsers.rst.states import Inliner

from sphinx import addnodes
from sphinx.deprecation import RemovedInSphinx40Warning
from sphinx.locale import _
from sphinx.util import ws_re
from sphinx.util.docutils import ReferenceRole, SphinxRole
from sphinx.util.nodes import process_index_entry, set_role_source_info, split_explicit_title
from sphinx.util.typing import RoleFunction

if False:
    # For type annotation
    from typing import Type  # for python3.5.1

    from sphinx.application import Sphinx
    from sphinx.environment import BuildEnvironment


generic_docroles = {
    'command': addnodes.literal_strong,
    'dfn': nodes.emphasis,
    'kbd': nodes.literal,
    'mailheader': addnodes.literal_emphasis,
    'makevar': addnodes.literal_strong,
    'manpage': addnodes.manpage,
    'mimetype': addnodes.literal_emphasis,
    'newsgroup': addnodes.literal_emphasis,
    'program': addnodes.literal_strong,  # XXX should be an x-ref
    'regexp': nodes.literal,
}


# -- generic cross-reference role ----------------------------------------------

class XRefRole(ReferenceRole):
    """
    A generic cross-referencing role.  To create a callable that can be used as
    a role function, create an instance of this class.

    The general features of this role are:

    * Automatic creation of a reference and a content node.
    * Optional separation of title and target with `title <target>`.
    * The implementation is a class rather than a function to make
      customization easier.

    Customization can be done in two ways:

    * Supplying constructor parameters:
      * `fix_parens` to normalize parentheses (strip from target, and add to
        title if configured)
      * `lowercase` to lowercase the target
      * `nodeclass` and `innernodeclass` select the node classes for
        the reference and the content node

    * Subclassing and overwriting `process_link()` and/or `result_nodes()`.
    """

    nodeclass = addnodes.pending_xref   # type: Type[Element]
    innernodeclass = nodes.literal      # type: Type[TextElement]

    def __init__(self, fix_parens: bool = False, lowercase: bool = False,
                 nodeclass: "Type[Element]" = None, innernodeclass: "Type[TextElement]" = None,
                 warn_dangling: bool = False) -> None:
        self.fix_parens = fix_parens
        self.lowercase = lowercase
        self.warn_dangling = warn_dangling
        if nodeclass is not None:
            self.nodeclass = nodeclass
        if innernodeclass is not None:
            self.innernodeclass = innernodeclass

        super().__init__()

    def _fix_parens(self, env: "BuildEnvironment", has_explicit_title: bool, title: str,
                    target: str) -> Tuple[str, str]:
        warnings.warn('XRefRole._fix_parens() is deprecated.',
                      RemovedInSphinx40Warning, stacklevel=2)
        if not has_explicit_title:
            if title.endswith('()'):
                # remove parentheses
                title = title[:-2]
            if env.config.add_function_parentheses:
                # add them back to all occurrences if configured
                title += '()'
        # remove parentheses from the target too
        if target.endswith('()'):
            target = target[:-2]
        return title, target

    def update_title_and_target(self, title: str, target: str) -> Tuple[str, str]:
        if not self.has_explicit_title:
            if title.endswith('()'):
                # remove parentheses
                title = title[:-2]
            if self.config.add_function_parentheses:
                # add them back to all occurrences if configured
                title += '()'
        # remove parentheses from the target too
        if target.endswith('()'):
            target = target[:-2]
        return title, target

    def run(self) -> Tuple[List[Node], List[system_message]]:
        if ':' not in self.name:
            self.refdomain, self.reftype = '', self.name
            self.classes = ['xref', self.reftype]
        else:
            self.refdomain, self.reftype = self.name.split(':', 1)
            self.classes = ['xref', self.refdomain, '%s-%s' % (self.refdomain, self.reftype)]

        if self.disabled:
            return self.create_non_xref_node()
        else:
            return self.create_xref_node()

    def create_non_xref_node(self) -> Tuple[List[Node], List[system_message]]:
        text = utils.unescape(self.text[1:])
        if self.fix_parens:
            self.has_explicit_title = False  # treat as implicit
            text, target = self.update_title_and_target(text, "")

        node = self.innernodeclass(self.rawtext, text, classes=self.classes)
        return self.result_nodes(self.inliner.document, self.env, node, is_ref=False)

    def create_xref_node(self) -> Tuple[List[Node], List[system_message]]:
        target = self.target
        title = self.title
        if self.lowercase:
            target = target.lower()
        if self.fix_parens:
            title, target = self.update_title_and_target(title, target)

        # create the reference node
        options = {'refdoc': self.env.docname,
                   'refdomain': self.refdomain,
                   'reftype': self.reftype,
                   'refexplicit': self.has_explicit_title,
                   'refwarn': self.warn_dangling}
        refnode = self.nodeclass(self.rawtext, **options)
        self.set_source_info(refnode)

        # determine the target and title for the class
        title, target = self.process_link(self.env, refnode, self.has_explicit_title,
                                          title, target)
        refnode['reftarget'] = target
        refnode += self.innernodeclass(self.rawtext, title, classes=self.classes)

        return self.result_nodes(self.inliner.document, self.env, refnode, is_ref=True)

    # methods that can be overwritten

    def process_link(self, env: "BuildEnvironment", refnode: Element, has_explicit_title: bool,
                     title: str, target: str) -> Tuple[str, str]:
        """Called after parsing title and target text, and creating the
        reference node (given in *refnode*).  This method can alter the
        reference node and must return a new (or the same) ``(title, target)``
        tuple.
        """
        return title, ws_re.sub(' ', target)

    def result_nodes(self, document: nodes.document, env: "BuildEnvironment", node: Element,
                     is_ref: bool) -> Tuple[List[Node], List[system_message]]:
        """Called before returning the finished nodes.  *node* is the reference
        node if one was created (*is_ref* is then true), else the content node.
        This method can add other nodes and must return a ``(nodes, messages)``
        tuple (the usual return value of a role function).
        """
        return [node], []


class AnyXRefRole(XRefRole):
    def process_link(self, env: "BuildEnvironment", refnode: Element, has_explicit_title: bool,
                     title: str, target: str) -> Tuple[str, str]:
        result = super().process_link(env, refnode, has_explicit_title, title, target)
        # add all possible context info (i.e. std:program, py:module etc.)
        refnode.attributes.update(env.ref_context)
        return result


def indexmarkup_role(typ: str, rawtext: str, text: str, lineno: int, inliner: Inliner,
                     options: Dict = {}, content: List[str] = []
                     ) -> Tuple[List[Node], List[system_message]]:
    """Role for PEP/RFC references that generate an index entry."""
    warnings.warn('indexmarkup_role() is deprecated.  Please use PEP or RFC class instead.',
                  RemovedInSphinx40Warning, stacklevel=2)
    env = inliner.document.settings.env
    if not typ:
        assert env.temp_data['default_role']
        typ = env.temp_data['default_role'].lower()
    else:
        typ = typ.lower()

    has_explicit_title, title, target = split_explicit_title(text)
    title = utils.unescape(title)
    target = utils.unescape(target)
    targetid = 'index-%s' % env.new_serialno('index')
    indexnode = addnodes.index()
    targetnode = nodes.target('', '', ids=[targetid])
    inliner.document.note_explicit_target(targetnode)
    if typ == 'pep':
        indexnode['entries'] = [
            ('single', _('Python Enhancement Proposals; PEP %s') % target,
             targetid, '', None)]
        anchor = ''
        anchorindex = target.find('#')
        if anchorindex > 0:
            target, anchor = target[:anchorindex], target[anchorindex:]
        if not has_explicit_title:
            title = "PEP " + utils.unescape(title)
        try:
            pepnum = int(target)
        except ValueError:
            msg = inliner.reporter.error('invalid PEP number %s' % target,
                                         line=lineno)
            prb = inliner.problematic(rawtext, rawtext, msg)
            return [prb], [msg]
        ref = inliner.document.settings.pep_base_url + 'pep-%04d' % pepnum
        sn = nodes.strong(title, title)
        rn = nodes.reference('', '', internal=False, refuri=ref + anchor,
                             classes=[typ])
        rn += sn
        return [indexnode, targetnode, rn], []
    elif typ == 'rfc':
        indexnode['entries'] = [
            ('single', 'RFC; RFC %s' % target, targetid, '', None)]
        anchor = ''
        anchorindex = target.find('#')
        if anchorindex > 0:
            target, anchor = target[:anchorindex], target[anchorindex:]
        if not has_explicit_title:
            title = "RFC " + utils.unescape(title)
        try:
            rfcnum = int(target)
        except ValueError:
            msg = inliner.reporter.error('invalid RFC number %s' % target,
                                         line=lineno)
            prb = inliner.problematic(rawtext, rawtext, msg)
            return [prb], [msg]
        ref = inliner.document.settings.rfc_base_url + inliner.rfc_url % rfcnum
        sn = nodes.strong(title, title)
        rn = nodes.reference('', '', internal=False, refuri=ref + anchor,
                             classes=[typ])
        rn += sn
        return [indexnode, targetnode, rn], []
    else:
        raise ValueError('unknown role type: %s' % typ)


class PEP(ReferenceRole):
    def run(self) -> Tuple[List[Node], List[system_message]]:
        target_id = 'index-%s' % self.env.new_serialno('index')
        entries = [('single', _('Python Enhancement Proposals; PEP %s') % self.target,
                    target_id, '', None)]

        index = addnodes.index(entries=entries)
        target = nodes.target('', '', ids=[target_id])
        self.inliner.document.note_explicit_target(target)

        try:
            refuri = self.build_uri()
            reference = nodes.reference('', '', internal=False, refuri=refuri, classes=['pep'])
            if self.has_explicit_title:
                reference += nodes.strong(self.title, self.title)
            else:
                title = "PEP " + self.title
                reference += nodes.strong(title, title)
        except ValueError:
            msg = self.inliner.reporter.error('invalid PEP number %s' % self.target,
                                              line=self.lineno)
            prb = self.inliner.problematic(self.rawtext, self.rawtext, msg)
            return [prb], [msg]

        return [index, target, reference], []

    def build_uri(self) -> str:
        base_url = self.inliner.document.settings.pep_base_url
        ret = self.target.split('#', 1)
        if len(ret) == 2:
            return base_url + 'pep-%04d#%s' % (int(ret[0]), ret[1])
        else:
            return base_url + 'pep-%04d' % int(ret[0])


class RFC(ReferenceRole):
    def run(self) -> Tuple[List[Node], List[system_message]]:
        target_id = 'index-%s' % self.env.new_serialno('index')
        entries = [('single', 'RFC; RFC %s' % self.target, target_id, '', None)]

        index = addnodes.index(entries=entries)
        target = nodes.target('', '', ids=[target_id])
        self.inliner.document.note_explicit_target(target)

        try:
            refuri = self.build_uri()
            reference = nodes.reference('', '', internal=False, refuri=refuri, classes=['rfc'])
            if self.has_explicit_title:
                reference += nodes.strong(self.title, self.title)
            else:
                title = "RFC " + self.title
                reference += nodes.strong(title, title)
        except ValueError:
            msg = self.inliner.reporter.error('invalid RFC number %s' % self.target,
                                              line=self.lineno)
            prb = self.inliner.problematic(self.rawtext, self.rawtext, msg)
            return [prb], [msg]

        return [index, target, reference], []

    def build_uri(self) -> str:
        base_url = self.inliner.document.settings.rfc_base_url
        ret = self.target.split('#', 1)
        if len(ret) == 2:
            return base_url + self.inliner.rfc_url % int(ret[0]) + '#' + ret[1]
        else:
            return base_url + self.inliner.rfc_url % int(ret[0])


_amp_re = re.compile(r'(?<!&)&(?![&\s])')


def menusel_role(typ: str, rawtext: str, text: str, lineno: int, inliner: Inliner,
                 options: Dict = {}, content: List[str] = []
                 ) -> Tuple[List[Node], List[system_message]]:
    warnings.warn('menusel_role() is deprecated. '
                  'Please use MenuSelection or GUILabel class instead.',
                  RemovedInSphinx40Warning, stacklevel=2)
    env = inliner.document.settings.env
    if not typ:
        assert env.temp_data['default_role']
        typ = env.temp_data['default_role'].lower()
    else:
        typ = typ.lower()

    text = utils.unescape(text)
    if typ == 'menuselection':
        text = text.replace('-->', '\N{TRIANGULAR BULLET}')
    spans = _amp_re.split(text)

    node = nodes.inline(rawtext=rawtext)
    for i, span in enumerate(spans):
        span = span.replace('&&', '&')
        if i == 0:
            if len(span) > 0:
                textnode = nodes.Text(span)
                node += textnode
            continue
        accel_node = nodes.inline()
        letter_node = nodes.Text(span[0])
        accel_node += letter_node
        accel_node['classes'].append('accelerator')
        node += accel_node
        textnode = nodes.Text(span[1:])
        node += textnode

    node['classes'].append(typ)
    return [node], []


class GUILabel(SphinxRole):
    amp_re = re.compile(r'(?<!&)&(?![&\s])')

    def run(self) -> Tuple[List[Node], List[system_message]]:
        node = nodes.inline(rawtext=self.rawtext, classes=[self.name])
        spans = self.amp_re.split(self.text)
        node += nodes.Text(spans.pop(0))
        for span in spans:
            span = span.replace('&&', '&')

            letter = nodes.Text(span[0])
            accelerator = nodes.inline('', '', letter, classes=['accelerator'])
            node += accelerator
            node += nodes.Text(span[1:])

        return [node], []


class MenuSelection(GUILabel):
    BULLET_CHARACTER = '\N{TRIANGULAR BULLET}'

    def run(self) -> Tuple[List[Node], List[system_message]]:
        self.text = self.text.replace('-->', self.BULLET_CHARACTER)
        return super().run()


_litvar_re = re.compile('{([^}]+)}')
parens_re = re.compile(r'(\\*{|\\*})')


def emph_literal_role(typ: str, rawtext: str, text: str, lineno: int, inliner: Inliner,
                      options: Dict = {}, content: List[str] = []
                      ) -> Tuple[List[Node], List[system_message]]:
    warnings.warn('emph_literal_role() is deprecated. '
                  'Please use EmphasizedLiteral class instead.',
                  RemovedInSphinx40Warning, stacklevel=2)
    env = inliner.document.settings.env
    if not typ:
        assert env.temp_data['default_role']
        typ = env.temp_data['default_role'].lower()
    else:
        typ = typ.lower()

    retnode = nodes.literal(role=typ.lower(), classes=[typ])
    parts = list(parens_re.split(utils.unescape(text)))
    stack = ['']
    for part in parts:
        matched = parens_re.match(part)
        if matched:
            backslashes = len(part) - 1
            if backslashes % 2 == 1:    # escaped
                stack[-1] += "\\" * int((backslashes - 1) / 2) + part[-1]
            elif part[-1] == '{':       # rparen
                stack[-1] += "\\" * int(backslashes / 2)
                if len(stack) >= 2 and stack[-2] == "{":
                    # nested
                    stack[-1] += "{"
                else:
                    # start emphasis
                    stack.append('{')
                    stack.append('')
            else:                       # lparen
                stack[-1] += "\\" * int(backslashes / 2)
                if len(stack) == 3 and stack[1] == "{" and len(stack[2]) > 0:
                    # emphasized word found
                    if stack[0]:
                        retnode += nodes.Text(stack[0], stack[0])
                    retnode += nodes.emphasis(stack[2], stack[2])
                    stack = ['']
                else:
                    # emphasized word not found; the rparen is not a special symbol
                    stack.append('}')
                    stack = [''.join(stack)]
        else:
            stack[-1] += part
    if ''.join(stack):
        # remaining is treated as Text
        text = ''.join(stack)
        retnode += nodes.Text(text, text)

    return [retnode], []


class EmphasizedLiteral(SphinxRole):
    parens_re = re.compile(r'(\\\\|\\{|\\}|{|})')

    def run(self) -> Tuple[List[Node], List[system_message]]:
        children = self.parse(self.text)
        node = nodes.literal(self.rawtext, '', *children,
                             role=self.name.lower(), classes=[self.name])

        return [node], []

    def parse(self, text: str) -> List[Node]:
        result = []  # type: List[Node]

        stack = ['']
        for part in self.parens_re.split(text):
            if part == '\\\\':  # escaped backslash
                stack[-1] += '\\'
            elif part == '{':
                if len(stack) >= 2 and stack[-2] == "{":  # nested
                    stack[-1] += "{"
                else:
                    # start emphasis
                    stack.append('{')
                    stack.append('')
            elif part == '}':
                if len(stack) == 3 and stack[1] == "{" and len(stack[2]) > 0:
                    # emphasized word found
                    if stack[0]:
                        result.append(nodes.Text(stack[0], stack[0]))
                    result.append(nodes.emphasis(stack[2], stack[2]))
                    stack = ['']
                else:
                    # emphasized word not found; the rparen is not a special symbol
                    stack.append('}')
                    stack = [''.join(stack)]
            elif part == '\\{':  # escaped left-brace
                stack[-1] += '{'
            elif part == '\\}':  # escaped right-brace
                stack[-1] += '}'
            else:  # others (containing escaped braces)
                stack[-1] += part

        if ''.join(stack):
            # remaining is treated as Text
            text = ''.join(stack)
            result.append(nodes.Text(text, text))

        return result


_abbr_re = re.compile(r'\((.*)\)$', re.S)


def abbr_role(typ: str, rawtext: str, text: str, lineno: int, inliner: Inliner,
              options: Dict = {}, content: List[str] = []
              ) -> Tuple[List[Node], List[system_message]]:
    warnings.warn('abbr_role() is deprecated.  Please use Abbrevation class instead.',
                  RemovedInSphinx40Warning, stacklevel=2)
    text = utils.unescape(text)
    m = _abbr_re.search(text)
    if m is None:
        return [nodes.abbreviation(text, text, **options)], []
    abbr = text[:m.start()].strip()
    expl = m.group(1)
    options = options.copy()
    options['explanation'] = expl
    return [nodes.abbreviation(abbr, abbr, **options)], []


class Abbreviation(SphinxRole):
    abbr_re = re.compile(r'\((.*)\)$', re.S)

    def run(self) -> Tuple[List[Node], List[system_message]]:
        options = self.options.copy()
        matched = self.abbr_re.search(self.text)
        if matched:
            text = self.text[:matched.start()].strip()
            options['explanation'] = matched.group(1)
        else:
            text = self.text

        return [nodes.abbreviation(self.rawtext, text, **options)], []


def index_role(typ: str, rawtext: str, text: str, lineno: int, inliner: Inliner,
               options: Dict = {}, content: List[str] = []
               ) -> Tuple[List[Node], List[system_message]]:
    warnings.warn('index_role() is deprecated.  Please use Index class instead.',
                  RemovedInSphinx40Warning, stacklevel=2)
    # create new reference target
    env = inliner.document.settings.env
    targetid = 'index-%s' % env.new_serialno('index')
    targetnode = nodes.target('', '', ids=[targetid])
    # split text and target in role content
    has_explicit_title, title, target = split_explicit_title(text)
    title = utils.unescape(title)
    target = utils.unescape(target)
    # if an explicit target is given, we can process it as a full entry
    if has_explicit_title:
        entries = process_index_entry(target, targetid)
    # otherwise we just create a "single" entry
    else:
        # but allow giving main entry
        main = ''
        if target.startswith('!'):
            target = target[1:]
            title = title[1:]
            main = 'main'
        entries = [('single', target, targetid, main, None)]
    indexnode = addnodes.index()
    indexnode['entries'] = entries
    set_role_source_info(inliner, lineno, indexnode)
    textnode = nodes.Text(title, title)
    return [indexnode, targetnode, textnode], []


class Index(ReferenceRole):
    def run(self) -> Tuple[List[Node], List[system_message]]:
        warnings.warn('Index role is deprecated.', RemovedInSphinx40Warning, stacklevel=2)
        target_id = 'index-%s' % self.env.new_serialno('index')
        if self.has_explicit_title:
            # if an explicit target is given, process it as a full entry
            title = self.title
            entries = process_index_entry(self.target, target_id)
        else:
            # otherwise we just create a single entry
            if self.target.startswith('!'):
                title = self.title[1:]
                entries = [('single', self.target[1:], target_id, 'main', None)]
            else:
                title = self.title
                entries = [('single', self.target, target_id, '', None)]

        index = addnodes.index(entries=entries)
        target = nodes.target('', '', ids=[target_id])
        text = nodes.Text(title, title)
        self.set_source_info(index)
        return [index, target, text], []


specific_docroles = {
    # links to download references
    'download': XRefRole(nodeclass=addnodes.download_reference),
    # links to anything
    'any': AnyXRefRole(warn_dangling=True),

    'pep': PEP(),
    'rfc': RFC(),
    'guilabel': GUILabel(),
    'menuselection': MenuSelection(),
    'file': EmphasizedLiteral(),
    'samp': EmphasizedLiteral(),
    'abbr': Abbreviation(),
}  # type: Dict[str, RoleFunction]


def setup(app: "Sphinx") -> Dict[str, Any]:
    from docutils.parsers.rst import roles

    for rolename, nodeclass in generic_docroles.items():
        generic = roles.GenericRole(rolename, nodeclass)
        role = roles.CustomRole(rolename, generic, {'classes': [rolename]})
        roles.register_local_role(rolename, role)

    for rolename, func in specific_docroles.items():
        roles.register_local_role(rolename, func)

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