"""
A small Sphinx extension that adds Domain objects (eg. Python Classes & Methods) to the TOC Tree.

It dynamically adds them to the already rendered ``app.env.tocs`` dict on the Sphinx environment.
Traditionally this only contains Section's,
we then nest our Domain references inside the already existing Sections.
"""

from docutils import nodes
from sphinx import addnodes
import sphinx.util.logging

LOGGER = sphinx.util.logging.getLogger(__name__)


def _build_toc_node(docname, anchor="anchor", text="test text", bullet=False):
    """
    Create the node structure that Sphinx expects for TOC Tree entries.

    The ``bullet`` argument wraps it in a ``nodes.bullet_list``,
    which is how you nest TOC Tree entries.
    """
    reference = nodes.reference(
        "",
        "",
        internal=True,
        refuri=docname,
        anchorname="#" + anchor,
        *[nodes.Text(text, text)]
    )
    para = addnodes.compact_paragraph("", "", reference)
    ret_list = nodes.list_item("", para)
    return nodes.bullet_list("", ret_list) if bullet else ret_list


def _traverse_parent(node, objtypes):
    """
    Traverse up the node's parents until you hit the ``objtypes`` referenced.

    Can either be a single type,
    or a tuple of types.
    """
    curr_node = node.parent
    while curr_node is not None:
        if isinstance(curr_node, objtypes):
            return curr_node
        curr_node = curr_node.parent
    return None


def _find_toc_node(toc, ref_id, objtype):
    """
    Find the actual TOC node for a ref_id.

    Depends on the object type:
    * Section - First section (refuri) or 2nd+ level section (anchorname)
    * Desc - Just use the anchor name
    """
    for check_node in toc.traverse(nodes.reference):
        if objtype == nodes.section and (
            check_node.attributes["refuri"] == ref_id
            or check_node.attributes["anchorname"] == "#" + ref_id
        ):
            return check_node
        if (
            objtype == addnodes.desc
            and check_node.attributes["anchorname"] == "#" + ref_id
        ):
            return check_node
    return None


def _get_toc_reference(node, toc, docname):
    """
    Logic that understands maps a specific node to it's part of the toctree.

    It takes a specific incoming ``node``,
    and returns the actual TOC Tree node that is said reference.
    """
    if isinstance(node, nodes.section) and isinstance(node.parent, nodes.document):
        # Top Level Section header
        ref_id = docname
        toc_reference = _find_toc_node(toc, ref_id, nodes.section)
    elif isinstance(node, nodes.section):
        # Nested Section header
        ref_id = node.attributes["ids"][0]
        toc_reference = _find_toc_node(toc, ref_id, nodes.section)
    else:
        # Desc node
        try:
            ref_id = node.children[0].attributes["ids"][0]
            toc_reference = _find_toc_node(toc, ref_id, addnodes.desc)
        except (KeyError, IndexError):
            LOGGER.warning(
                "Invalid desc node",
                exc_info=True,
                type="autoapi",
                subtype="toc_reference",
            )
            toc_reference = None

    return toc_reference


def add_domain_to_toctree(app, doctree, docname):
    """
    Add domain objects to the toctree dynamically.

    This should be attached to the ``doctree-resolved`` event.
    This works by:

    * Finding each domain node (addnodes.desc)
    * Figuring out it's parent that will be in the toctree
         (nodes.section, or a previously added addnodes.desc)
    * Finding that parent in the TOC Tree based on it's ID
    * Taking that element in the TOC Tree,
      and finding it's parent that is a TOC Listing (nodes.bullet_list)
    * Adding the new TOC element for our specific node as a child of that nodes.bullet_list
        * This checks that bullet_list's last child,
        and checks that it is also a nodes.bullet_list,
        effectively nesting it under that element
    """
    toc = app.env.tocs[docname]
    for desc_node in doctree.traverse(addnodes.desc):
        try:
            ref_id = desc_node.children[0].attributes["ids"][0]
        except (KeyError, IndexError):
            # autodoc-style directives already add nodes to the toc.
            continue
        try:
            # Python domain object
            ref_text = desc_node[0].attributes["fullname"].split(".")[-1].split("(")[0]
        except (KeyError, IndexError):
            # TODO[eric]: Support other Domains and ways of accessing this data
            # Use `astext` for other types of domain objects
            ref_text = desc_node[0].astext().split(".")[-1].split("(")[0]
        # This is the actual object that will exist in the TOC Tree
        # Sections by default, and other Desc nodes that we've previously placed.
        parent_node = _traverse_parent(
            node=desc_node, objtypes=(addnodes.desc, nodes.section)
        )

        if parent_node:
            toc_reference = _get_toc_reference(parent_node, toc, docname)
            if toc_reference:
                # Get the last child of our parent's bullet list, this is where "we" live.
                toc_insertion_point = _traverse_parent(
                    toc_reference, nodes.bullet_list
                )[-1]
                # Ensure we're added another bullet list so that we nest inside the parent,
                # not next to it
                if len(toc_insertion_point) > 1 and isinstance(
                    toc_insertion_point[1], nodes.bullet_list
                ):
                    to_add = _build_toc_node(docname, anchor=ref_id, text=ref_text)
                    toc_insertion_point = toc_insertion_point[1]
                else:
                    to_add = _build_toc_node(
                        docname,
                        anchor=ref_id,
                        text=ref_text,
                        bullet=True,
                    )
                toc_insertion_point.append(to_add)
