# -*- coding: utf-8 -*-
"""
    sphinx.ext.mathbase
    ~~~~~~~~~~~~~~~~~~~

    Set up math support in source files and LaTeX/text output.

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

from docutils import nodes, utils
from docutils.nodes import make_id
from docutils.parsers.rst import Directive, directives

from sphinx.config import string_classes
from sphinx.domains import Domain
from sphinx.locale import __
from sphinx.roles import XRefRole
from sphinx.util import logging
from sphinx.util.nodes import make_refnode, set_source_info

if False:
    # For type annotation
    from typing import Any, Callable, Dict, Iterable, List, Tuple  # NOQA
    from docutils.parsers.rst.states import Inliner  # NOQA
    from sphinx.application import Sphinx  # NOQA
    from sphinx.builders import Builder  # NOQA
    from sphinx.environment import BuildEnvironment  # NOQA

logger = logging.getLogger(__name__)


class math(nodes.Inline, nodes.TextElement):
    pass


class displaymath(nodes.Part, nodes.Element):
    pass


class eqref(nodes.Inline, nodes.TextElement):
    pass


class EqXRefRole(XRefRole):
    def result_nodes(self, document, env, node, is_ref):
        # type: (nodes.Node, BuildEnvironment, nodes.Node, bool) -> Tuple[List[nodes.Node], List[nodes.Node]]  # NOQA
        node['refdomain'] = 'math'
        return [node], []


class MathDomain(Domain):
    """Mathematics domain."""
    name = 'math'
    label = 'mathematics'

    initial_data = {
        'objects': {},  # labelid -> (docname, eqno)
    }  # type: Dict[unicode, Dict[unicode, Tuple[unicode, int]]]
    dangling_warnings = {
        'eq': 'equation not found: %(target)s',
    }

    def clear_doc(self, docname):
        # type: (unicode) -> None
        for equation_id, (doc, eqno) in list(self.data['objects'].items()):
            if doc == docname:
                del self.data['objects'][equation_id]

    def merge_domaindata(self, docnames, otherdata):
        # type: (Iterable[unicode], Dict) -> None
        for labelid, (doc, eqno) in otherdata['objects'].items():
            if doc in docnames:
                self.data['objects'][labelid] = (doc, eqno)

    def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
        # type: (BuildEnvironment, unicode, Builder, unicode, unicode, nodes.Node, nodes.Node) -> nodes.Node  # NOQA
        assert typ == 'eq'
        docname, number = self.data['objects'].get(target, (None, None))
        if docname:
            if builder.name == 'latex':
                newnode = eqref('', **node.attributes)
                newnode['docname'] = docname
                newnode['target'] = target
                return newnode
            else:
                # TODO: perhaps use rather a sphinx-core provided prefix here?
                node_id = make_id('equation-%s' % target)
                if env.config.math_numfig and env.config.numfig:
                    if docname in env.toc_fignumbers:
                        number = env.toc_fignumbers[docname]['displaymath'].get(node_id, ())
                        number = '.'.join(map(str, number))
                    else:
                        number = ''
                try:
                    eqref_format = env.config.math_eqref_format or "({number})"
                    title = nodes.Text(eqref_format.format(number=number))
                except KeyError as exc:
                    logger.warning('Invalid math_eqref_format: %r', exc,
                                   location=node)
                    title = nodes.Text("(%d)" % number)
                    title = nodes.Text("(%d)" % number)
                return make_refnode(builder, fromdocname, docname, node_id, title)
        else:
            return None

    def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode):
        # type: (BuildEnvironment, unicode, Builder, unicode, nodes.Node, nodes.Node) -> List[nodes.Node]  # NOQA
        refnode = self.resolve_xref(env, fromdocname, builder, 'eq', target, node, contnode)
        if refnode is None:
            return []
        else:
            return [refnode]

    def get_objects(self):
        # type: () -> List
        return []

    def add_equation(self, env, docname, labelid):
        # type: (BuildEnvironment, unicode, unicode) -> int
        equations = self.data['objects']
        if labelid in equations:
            path = env.doc2path(equations[labelid][0])
            msg = __('duplicate label of equation %s, other instance in %s') % (labelid, path)
            raise UserWarning(msg)
        else:
            eqno = self.get_next_equation_number(docname)
            equations[labelid] = (docname, eqno)
            return eqno

    def get_next_equation_number(self, docname):
        # type: (unicode) -> int
        targets = [eq for eq in self.data['objects'].values() if eq[0] == docname]
        return len(targets) + 1


def get_node_equation_number(writer, node):
    if writer.builder.config.math_numfig and writer.builder.config.numfig:
        figtype = 'displaymath'
        if writer.builder.name == 'singlehtml':
            key = u"%s/%s" % (writer.docnames[-1], figtype)
        else:
            key = figtype

        id = node['ids'][0]
        number = writer.builder.fignumbers.get(key, {}).get(id, ())
        number = '.'.join(map(str, number))
    else:
        number = node['number']

    return number


def wrap_displaymath(math, label, numbering):
    # type: (unicode, unicode, bool) -> unicode
    def is_equation(part):
        # type: (unicode) -> unicode
        return part.strip()

    if label is None:
        labeldef = ''
    else:
        labeldef = r'\label{%s}' % label
        numbering = True

    parts = list(filter(is_equation, math.split('\n\n')))
    equations = []
    if len(parts) == 0:
        return ''
    elif len(parts) == 1:
        if numbering:
            begin = r'\begin{equation}' + labeldef
            end = r'\end{equation}'
        else:
            begin = r'\begin{equation*}' + labeldef
            end = r'\end{equation*}'
        equations.append('\\begin{split}%s\\end{split}\n' % parts[0])
    else:
        if numbering:
            begin = r'\begin{align}%s\!\begin{aligned}' % labeldef
            end = r'\end{aligned}\end{align}'
        else:
            begin = r'\begin{align*}%s\!\begin{aligned}' % labeldef
            end = r'\end{aligned}\end{align*}'
        for part in parts:
            equations.append('%s\\\\\n' % part.strip())

    return '%s\n%s%s' % (begin, ''.join(equations), end)


def math_role(role, rawtext, text, lineno, inliner, options={}, content=[]):
    # type: (unicode, unicode, unicode, int, Inliner, Dict, List[unicode]) -> Tuple[List[nodes.Node], List[nodes.Node]]  # NOQA
    latex = utils.unescape(text, restore_backslashes=True)
    return [math(latex=latex)], []


def is_in_section_title(node):
    # type: (nodes.Node) -> bool
    """Determine whether the node is in a section title"""
    from sphinx.util.nodes import traverse_parent

    for ancestor in traverse_parent(node):
        if isinstance(ancestor, nodes.title) and \
           isinstance(ancestor.parent, nodes.section):
            return True
    return False


class MathDirective(Directive):

    has_content = True
    required_arguments = 0
    optional_arguments = 1
    final_argument_whitespace = True
    option_spec = {
        'label': directives.unchanged,
        'name': directives.unchanged,
        'nowrap': directives.flag,
    }

    def run(self):
        # type: () -> List[nodes.Node]
        latex = '\n'.join(self.content)
        if self.arguments and self.arguments[0]:
            latex = self.arguments[0] + '\n\n' + latex
        node = displaymath()
        node['latex'] = latex
        node['number'] = None
        node['label'] = None
        if 'name' in self.options:
            node['label'] = self.options['name']
        if 'label' in self.options:
            node['label'] = self.options['label']
        node['nowrap'] = 'nowrap' in self.options
        node['docname'] = self.state.document.settings.env.docname
        ret = [node]
        set_source_info(self, node)
        if hasattr(self, 'src'):
            node.source = self.src
        self.add_target(ret)
        return ret

    def add_target(self, ret):
        # type: (List[nodes.Node]) -> None
        node = ret[0]
        env = self.state.document.settings.env

        # assign label automatically if math_number_all enabled
        if node['label'] == '' or (env.config.math_number_all and not node['label']):
            seq = env.new_serialno('sphinx.ext.math#equations')
            node['label'] = "%s:%d" % (env.docname, seq)

        # no targets and numbers are needed
        if not node['label']:
            return

        # register label to domain
        domain = env.get_domain('math')
        try:
            eqno = domain.add_equation(env, env.docname, node['label'])
            node['number'] = eqno

            # add target node
            node_id = make_id('equation-%s' % node['label'])
            target = nodes.target('', '', ids=[node_id])
            self.state.document.note_explicit_target(target)
            ret.insert(0, target)
        except UserWarning as exc:
            self.state_machine.reporter.warning(exc.args[0], line=self.lineno)


def latex_visit_math(self, node):
    # type: (nodes.NodeVisitor, math) -> None
    if is_in_section_title(node):
        protect = r'\protect'
    else:
        protect = ''
    equation = protect + r'\(' + node['latex'] + protect + r'\)'
    self.body.append(equation)
    raise nodes.SkipNode


def latex_visit_displaymath(self, node):
    # type: (nodes.NodeVisitor, displaymath) -> None
    if not node['label']:
        label = None
    else:
        label = "equation:%s:%s" % (node['docname'], node['label'])

    if node['nowrap']:
        if label:
            self.body.append(r'\label{%s}' % label)
        self.body.append(node['latex'])
    else:
        self.body.append(wrap_displaymath(node['latex'], label,
                                          self.builder.config.math_number_all))
    raise nodes.SkipNode


def latex_visit_eqref(self, node):
    # type: (nodes.NodeVisitor, eqref) -> None
    label = "equation:%s:%s" % (node['docname'], node['target'])
    eqref_format = self.builder.config.math_eqref_format
    if eqref_format:
        try:
            ref = '\\ref{%s}' % label
            self.body.append(eqref_format.format(number=ref))
        except KeyError as exc:
            logger.warning('Invalid math_eqref_format: %r', exc,
                           location=node)
            self.body.append('\\eqref{%s}' % label)
    else:
        self.body.append('\\eqref{%s}' % label)
    raise nodes.SkipNode


def text_visit_math(self, node):
    # type: (nodes.NodeVisitor, math) -> None
    self.add_text(node['latex'])
    raise nodes.SkipNode


def text_visit_displaymath(self, node):
    # type: (nodes.NodeVisitor, displaymath) -> None
    self.new_state()
    self.add_text(node['latex'])
    self.end_state()
    raise nodes.SkipNode


def man_visit_math(self, node):
    # type: (nodes.NodeVisitor, math) -> None
    self.body.append(node['latex'])
    raise nodes.SkipNode


def man_visit_displaymath(self, node):
    # type: (nodes.NodeVisitor, displaymath) -> None
    self.visit_centered(node)


def man_depart_displaymath(self, node):
    # type: (nodes.NodeVisitor, displaymath) -> None
    self.depart_centered(node)


def texinfo_visit_math(self, node):
    # type: (nodes.NodeVisitor, math) -> None
    self.body.append('@math{' + self.escape_arg(node['latex']) + '}')
    raise nodes.SkipNode


def texinfo_visit_displaymath(self, node):
    # type: (nodes.NodeVisitor, displaymath) -> None
    if node.get('label'):
        self.add_anchor(node['label'], node)
    self.body.append('\n\n@example\n%s\n@end example\n\n' %
                     self.escape_arg(node['latex']))


def texinfo_depart_displaymath(self, node):
    # type: (nodes.NodeVisitor, displaymath) -> None
    pass


def setup_math(app, htmlinlinevisitors, htmldisplayvisitors):
    # type: (Sphinx, Tuple[Callable, Any], Tuple[Callable, Any]) -> None
    app.add_config_value('math_number_all', False, 'env')
    app.add_config_value('math_eqref_format', None, 'env', string_classes)
    app.add_config_value('math_numfig', True, 'env')
    app.add_domain(MathDomain)
    app.add_node(math, override=True,
                 latex=(latex_visit_math, None),
                 text=(text_visit_math, None),
                 man=(man_visit_math, None),
                 texinfo=(texinfo_visit_math, None),
                 html=htmlinlinevisitors)
    app.add_enumerable_node(displaymath, 'displaymath',
                            latex=(latex_visit_displaymath, None),
                            text=(text_visit_displaymath, None),
                            man=(man_visit_displaymath, man_depart_displaymath),
                            texinfo=(texinfo_visit_displaymath, texinfo_depart_displaymath),
                            html=htmldisplayvisitors)
    app.add_node(eqref, latex=(latex_visit_eqref, None))
    app.add_role('math', math_role)
    app.add_role('eq', EqXRefRole(warn_dangling=True))
    app.add_directive('math', MathDirective)
