# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import importlib
from pathlib import Path

from docutils import nodes
from docutils.parsers.rst import Directive
from mots.config import FileConfig
from mots.directory import Directory
from mots.export import export_to_format
from sphinx.util.docstrings import prepare_docstring
from sphinx.util.docutils import ReferenceRole


def function_reference(f, attr, args, doc):
    lines = []

    lines.extend(
        [
            f,
            "-" * len(f),
            "",
        ]
    )

    docstring = prepare_docstring(doc)

    lines.extend(
        [
            docstring[0],
            "",
        ]
    )

    arg_types = []

    for t in args:
        if isinstance(t, list):
            inner_types = [t2.__name__ for t2 in t]
            arg_types.append(" | ".join(inner_types))
            continue

        arg_types.append(t.__name__)

    arg_s = "(%s)" % ", ".join(arg_types)

    lines.extend(
        [
            ":Arguments: %s" % arg_s,
            "",
        ]
    )

    lines.extend(docstring[1:])
    lines.append("")

    return lines


def variable_reference(v, st_type, in_type, doc):
    lines = [
        v,
        "-" * len(v),
        "",
    ]

    docstring = prepare_docstring(doc)

    lines.extend(
        [
            docstring[0],
            "",
        ]
    )

    lines.extend(
        [
            ":Storage Type: ``%s``" % st_type.__name__,
            ":Input Type: ``%s``" % in_type.__name__,
            "",
        ]
    )

    lines.extend(docstring[1:])
    lines.append("")

    return lines


def special_reference(v, func, typ, doc):
    lines = [
        v,
        "-" * len(v),
        "",
    ]

    docstring = prepare_docstring(doc)

    lines.extend(
        [
            docstring[0],
            "",
            ":Type: ``%s``" % typ.__name__,
            "",
        ]
    )

    lines.extend(docstring[1:])
    lines.append("")

    return lines


def format_module(m):
    lines = []

    lines.extend(
        [
            ".. note::",
            "   moz.build files' implementation includes a ``Path`` class.",
        ]
    )
    path_docstring_minus_summary = prepare_docstring(m.Path.__doc__)[2:]
    lines.extend(["   " + line for line in path_docstring_minus_summary])

    for subcontext, cls in sorted(m.SUBCONTEXTS.items()):
        lines.extend(
            [
                ".. _mozbuild_subcontext_%s:" % subcontext,
                "",
                "Sub-Context: %s" % subcontext,
                "=============" + "=" * len(subcontext),
                "",
            ]
        )
        lines.extend(prepare_docstring(cls.__doc__))
        if lines[-1]:
            lines.append("")

        for k, v in sorted(cls.VARIABLES.items()):
            lines.extend(variable_reference(k, *v))

    lines.extend(
        [
            "Variables",
            "=========",
            "",
        ]
    )

    for v in sorted(m.VARIABLES):
        lines.extend(variable_reference(v, *m.VARIABLES[v]))

    lines.extend(
        [
            "Functions",
            "=========",
            "",
        ]
    )

    for func in sorted(m.FUNCTIONS):
        lines.extend(function_reference(func, *m.FUNCTIONS[func]))

    lines.extend(
        [
            "Special Variables",
            "=================",
            "",
        ]
    )

    for v in sorted(m.SPECIAL_VARIABLES):
        lines.extend(special_reference(v, *m.SPECIAL_VARIABLES[v]))

    return lines


def find_mots_config_path(app):
    """Find and return mots config path if it exists."""
    base_path = Path(app.srcdir).parent
    config_path = base_path / "mots.yaml"
    if config_path.exists():
        return config_path


def export_mots(config_path):
    """Load mots configuration and export it to file."""
    # Load from disk and initialize configuration and directory.
    config = FileConfig(config_path)
    config.load()
    directory = Directory(config)
    directory.load()

    # Fetch file format (i.e., "rst") and export path.
    frmt = config.config["export"]["format"]
    path = config_path.parent / config.config["export"]["path"]

    # Generate output.
    output = export_to_format(directory, frmt)

    # Create export directory if it does not exist.
    path.parent.mkdir(parents=True, exist_ok=True)

    # Write changes to disk.
    with path.open("w", encoding="utf-8") as f:
        f.write(output)


class MozbuildSymbols(Directive):
    """Directive to insert mozbuild sandbox symbol information."""

    required_arguments = 1

    def run(self):
        module = importlib.import_module(self.arguments[0])
        fname = module.__file__
        if fname.endswith(".pyc"):
            fname = fname[0:-1]

        self.state.document.settings.record_dependencies.add(fname)

        # We simply format out the documentation as rst then feed it back
        # into the parser for conversion. We don't even emit ourselves, so
        # there's no record of us.
        self.state_machine.insert_input(format_module(module), fname)

        return []


class Searchfox(ReferenceRole):
    """Role which links a relative path from the source to it's searchfox URL.

    Can be used like:

        See :searchfox:`browser/base/content/browser-places.js` for more details.

    Will generate a link to
    ``https://searchfox.org/mozilla-central/source/browser/base/content/browser-places.js``

    The example above will use the path as the text, to use custom text:

        See :searchfox:`this file <browser/base/content/browser-places.js>` for
        more details.

    To specify a different source tree:

        See :searchfox:`mozilla-beta:browser/base/content/browser-places.js`
        for more details.
    """

    def run(self):
        base = "https://searchfox.org/{source}/source/{path}"

        if ":" in self.target:
            source, path = self.target.split(":", 1)
        else:
            source = "mozilla-central"
            path = self.target

        url = base.format(source=source, path=path)

        if self.has_explicit_title:
            title = self.title
        else:
            title = path

        node = nodes.reference(self.rawtext, title, refuri=url, **self.options)
        return [node], []


def setup(app):
    from moztreedocs import manager

    app.add_directive("mozbuildsymbols", MozbuildSymbols)
    app.add_role("searchfox", Searchfox())

    # Unlike typical Sphinx installs, our documentation is assembled from
    # many sources and staged in a common location. This arguably isn't a best
    # practice, but it was the easiest to implement at the time.
    #
    # Here, we invoke our custom code for staging/generating all our
    # documentation.

    # Export and write "governance" documentation to disk.
    config_path = find_mots_config_path(app)
    if config_path:
        export_mots(config_path)

    manager.generate_docs(app)
    app.srcdir = Path(manager.staging_dir)
