"""A directive for documenting type aliases and other module-level attributes."""

from __future__ import annotations

from typing import TYPE_CHECKING

from docutils import nodes
from docutils.parsers.rst import Directive
from docutils.statemachine import ViewList

from manim.utils.docbuild.module_parsing import parse_module_attributes

if TYPE_CHECKING:
    from sphinx.application import Sphinx

__all__ = ["AliasAttrDocumenter"]


ALIAS_DOCS_DICT, DATA_DICT, TYPEVAR_DICT = parse_module_attributes()
ALIAS_LIST = [
    alias_name
    for module_dict in ALIAS_DOCS_DICT.values()
    for category_dict in module_dict.values()
    for alias_name in category_dict
]


def smart_replace(base: str, alias: str, substitution: str) -> str:
    """Auxiliary function for substituting type aliases into a base
    string, when there are overlaps between the aliases themselves.

    Parameters
    ----------
    base
        The string in which the type aliases will be located and
        replaced.
    alias
        The substring to be substituted.
    substitution
        The string which will replace every occurrence of ``alias``.

    Returns
    -------
    str
        The new string after the alias substitution.
    """
    occurrences = []
    len_alias = len(alias)
    len_base = len(base)

    def condition(char: str) -> bool:
        return not char.isalnum() and char != "_"

    start = 0
    i = 0
    while True:
        i = base.find(alias, start)
        if i == -1:
            break
        if (i == 0 or condition(base[i - 1])) and (
            i + len_alias == len_base or condition(base[i + len_alias])
        ):
            occurrences.append(i)
        start = i + len_alias

    for o in occurrences[::-1]:
        base = base[:o] + substitution + base[o + len_alias :]

    return base


def setup(app: Sphinx) -> None:
    app.add_directive("autoaliasattr", AliasAttrDocumenter)


class AliasAttrDocumenter(Directive):
    """Directive which replaces Sphinx's Autosummary for module-level
    attributes: instead, it manually crafts a new "Type Aliases"
    section, where all the module-level attributes which are explicitly
    annotated as :class:`TypeAlias` are considered as such, for their
    use all around the Manim docs.

    These type aliases are separated from the "regular" module-level
    attributes, which get their traditional "Module Attributes"
    section autogenerated with Sphinx's Autosummary under "Type
    Aliases".

    See ``docs/source/_templates/autosummary/module.rst`` to watch
    this directive in action.

    See :func:`~.parse_module_attributes` for more information on how
    the modules are parsed to obtain the :class:`TypeAlias` information
    and separate it from the other attributes.
    """

    objtype = "autoaliasattr"
    required_arguments = 1
    has_content = True

    def run(self) -> list[nodes.Element]:
        module_name = self.arguments[0]
        # not present in the keys of the DICTs
        module_name = module_name.removeprefix("manim.")
        module_alias_dict = ALIAS_DOCS_DICT.get(module_name, None)
        module_attrs_list = DATA_DICT.get(module_name, None)
        module_typevars = TYPEVAR_DICT.get(module_name, None)

        content = nodes.container()

        # Add "Type Aliases" section
        if module_alias_dict is not None:
            module_alias_section = nodes.section(ids=[f"{module_name}.alias"])
            content += module_alias_section

            # Use a rubric (title-like), just like in `module.rst`
            module_alias_section += nodes.rubric(text="Type Aliases")

            # category_name: str
            # category_dict: AliasCategoryDict = dict[str, AliasInfo]
            for category_name, category_dict in module_alias_dict.items():
                category_section = nodes.section(
                    ids=[category_name.lower().replace(" ", "_")]
                )
                module_alias_section += category_section
                # category_name can be possibly "" for uncategorized aliases
                if category_name:
                    category_section += nodes.title(text=category_name)

                category_alias_container = nodes.container()
                category_section += category_alias_container

                # alias_name: str
                # alias_info: AliasInfo = dict[str, str]
                #   Contains "definition": str
                #   Can possibly contain "doc": str
                for alias_name, alias_info in category_dict.items():
                    # Replace all occurrences of type aliases in the
                    # definition for automatic cross-referencing!
                    alias_def = alias_info["definition"]
                    for A in ALIAS_LIST:
                        alias_def = smart_replace(alias_def, A, f":class:`~.{A}`")

                    # Using the `.. class::` directive is CRUCIAL, since
                    # function/method parameters are always annotated via
                    # classes - therefore Sphinx expects a class
                    unparsed = ViewList(
                        [
                            f".. class:: {alias_name}",
                            "",
                            "    .. parsed-literal::",
                            "",
                            f"        {alias_def}",
                            "",
                        ]
                    )

                    if "doc" in alias_info:
                        # Replace all occurrences of type aliases in
                        # the docs for automatic cross-referencing!
                        alias_doc = alias_info["doc"]
                        for A in ALIAS_LIST:
                            alias_doc = alias_doc.replace(f"`{A}`", f":class:`~.{A}`")

                        # also hyperlink the TypeVars from that module
                        if module_typevars is not None:
                            for T in module_typevars:
                                alias_doc = alias_doc.replace(f"`{T}`", f":class:`{T}`")

                        # Add all the lines with 4 spaces behind, to consider all the
                        # documentation as a paragraph INSIDE the `.. class::` block
                        doc_lines = alias_doc.split("\n")
                        unparsed.extend(ViewList([f"    {line}" for line in doc_lines]))

                    # Parse the reST text into a fresh container
                    # https://www.sphinx-doc.org/en/master/extdev/markupapi.html#parsing-directive-content-as-rest
                    alias_container = nodes.container()
                    self.state.nested_parse(unparsed, 0, alias_container)
                    category_alias_container += alias_container

        # then add the module TypeVars section
        if module_typevars is not None:
            module_typevars_section = nodes.section(ids=[f"{module_name}.typevars"])
            content += module_typevars_section

            # Use a rubric (title-like), just like in `module.rst`
            module_typevars_section += nodes.rubric(text="TypeVar's")

            # name: str
            # definition: TypeVarDict = dict[str, str]
            for name, definition in module_typevars.items():
                # Using the `.. class::` directive is CRUCIAL, since
                # function/method parameters are always annotated via
                # classes - therefore Sphinx expects a class
                unparsed = ViewList(
                    [
                        f".. class:: {name}",
                        "",
                        "    .. parsed-literal::",
                        "",
                        f"        {definition}",
                        "",
                    ]
                )

                # Parse the reST text into a fresh container
                # https://www.sphinx-doc.org/en/master/extdev/markupapi.html#parsing-directive-content-as-rest
                typevar_container = nodes.container()
                self.state.nested_parse(unparsed, 0, typevar_container)
                module_typevars_section += typevar_container

        # Then, add the traditional "Module Attributes" section
        if module_attrs_list is not None:
            module_attrs_section = nodes.section(ids=[f"{module_name}.data"])
            content += module_attrs_section

            # Use the same rubric (title-like) as in `module.rst`
            module_attrs_section += nodes.rubric(text="Module Attributes")
            # Let Sphinx Autosummary do its thing as always
            # Add all the attribute names with 4 spaces behind, so that
            # they're considered as INSIDE the `.. autosummary::` block
            unparsed = ViewList(
                [
                    ".. autosummary::",
                    *(f"    {attr}" for attr in module_attrs_list),
                ]
            )

            # Parse the reST text into a fresh container
            # https://www.sphinx-doc.org/en/master/extdev/markupapi.html#parsing-directive-content-as-rest
            data_container = nodes.container()
            self.state.nested_parse(unparsed, 0, data_container)
            module_attrs_section += data_container

        return [content]
