from __future__ import annotations
import importlib
import inspect
import re
from typing import TYPE_CHECKING, Dict, List, NamedTuple, Optional, Sequence, Tuple
from docutils import nodes
from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment
from sphinx.locale import _
from sphinx.util.docutils import SphinxDirective
from sphinx.util.typing import OptionSpec
if TYPE_CHECKING:
    from .builder import DPYHTML5Translator
class attributetable(nodes.General, nodes.Element):
    pass
class attributetablecolumn(nodes.General, nodes.Element):
    pass
class attributetabletitle(nodes.TextElement):
    pass
class attributetableplaceholder(nodes.General, nodes.Element):
    pass
class attributetablebadge(nodes.TextElement):
    pass
class attributetable_item(nodes.Part, nodes.Element):
    pass
def visit_attributetable_node(self: DPYHTML5Translator, node: attributetable) -> None:
    class_ = node["python-class"]
    self.body.append(f'
')
def visit_attributetablecolumn_node(self: DPYHTML5Translator, node: attributetablecolumn) -> None:
    self.body.append(self.starttag(node, "div", CLASS="py-attribute-table-column"))
def visit_attributetabletitle_node(self: DPYHTML5Translator, node: attributetabletitle) -> None:
    self.body.append(self.starttag(node, "span"))
def visit_attributetablebadge_node(self: DPYHTML5Translator, node: attributetablebadge) -> None:
    attributes = {
        "class": "py-attribute-table-badge",
        "title": node["badge-type"],
    }
    self.body.append(self.starttag(node, "span", **attributes))
def visit_attributetable_item_node(self: DPYHTML5Translator, node: attributetable_item) -> None:
    self.body.append(self.starttag(node, "li", CLASS="py-attribute-table-entry"))
def depart_attributetable_node(self: DPYHTML5Translator, node: attributetable) -> None:
    self.body.append("
")
def depart_attributetablecolumn_node(self: DPYHTML5Translator, node: attributetablecolumn) -> None:
    self.body.append("")
def depart_attributetabletitle_node(self: DPYHTML5Translator, node: attributetabletitle) -> None:
    self.body.append("")
def depart_attributetablebadge_node(self: DPYHTML5Translator, node: attributetablebadge) -> None:
    self.body.append("")
def depart_attributetable_item_node(self: DPYHTML5Translator, node: attributetable_item) -> None:
    self.body.append("")
_name_parser_regex = re.compile(r"(?P[\w.]+\.)?(?P\w+)")
class PyAttributeTable(SphinxDirective):
    has_content = False
    required_arguments = 1
    optional_arguments = 0
    final_argument_whitespace = False
    option_spec: OptionSpec = {}
    def parse_name(self, content: str) -> Tuple[str, str]:
        match = _name_parser_regex.match(content)
        if match is None:
            raise RuntimeError(
                f"content {content} somehow doesn't match regex in {self.env.docname}."
            )
        path, name = match.groups()
        if path:
            modulename = path.rstrip(".")
        else:
            modulename = self.env.temp_data.get("autodoc:module")
            if not modulename:
                modulename = self.env.ref_context.get("py:module")
        if modulename is None:
            raise RuntimeError(f"modulename somehow None for {content} in {self.env.docname}.")
        return modulename, name
    def run(self) -> List[attributetableplaceholder]:
        """If you're curious on the HTML this is meant to generate:
        
        However, since this requires the tree to be complete
        and parsed, it'll need to be done at a different stage and then
        replaced.
        """
        content = self.arguments[0].strip()
        node = attributetableplaceholder("")
        modulename, name = self.parse_name(content)
        node["python-doc"] = self.env.docname
        node["python-module"] = modulename
        node["python-class"] = name
        node["python-full-name"] = f"{modulename}.{name}"
        return [node]
def build_lookup_table(env: Optional[BuildEnvironment]) -> Dict[str, List[str]]:
    # Given an environment, load up a lookup table of
    # full-class-name: objects
    result = {}
    domain = env.domains["py"]  # type: ignore
    ignored = {
        "data",
        "exception",
        "module",
        "class",
    }
    for fullname, _, objtype, docname, _, _ in domain.get_objects():
        if objtype in ignored:
            continue
        classname, _, child = fullname.rpartition(".")
        try:
            result[classname].append(child)
        except KeyError:
            result[classname] = [child]
    return result
class TableElement(NamedTuple):
    fullname: str
    label: str
    badge: Optional[attributetablebadge]
def process_attributetable(app: Sphinx, doctree: nodes.Node, fromdocname: str) -> None:
    env = app.builder.env  # type: ignore
    lookup = build_lookup_table(env)
    for node in doctree.traverse(attributetableplaceholder):
        modulename, classname, fullname = (
            node["python-module"],
            node["python-class"],
            node["python-full-name"],
        )
        groups = get_class_results(lookup, modulename, classname, fullname)
        table = attributetable("")
        for label, subitems in groups.items():
            if not subitems:
                continue
            table.append(class_results_to_node(label, sorted(subitems, key=lambda c: c.label)))
        table["python-class"] = fullname
        if not table:
            node.replace_self([])
        else:
            node.replace_self([table])
def get_class_results(
    lookup: Dict[str, List[str]], modulename: str, name: str, fullname: str
) -> Dict[str, List[TableElement]]:
    module = importlib.import_module(modulename)
    cls = getattr(module, name)
    groups: Dict[str, List[TableElement]] = {
        _("Attributes"): [],
        _("Methods"): [],
    }
    try:
        members = lookup[fullname]
    except KeyError:
        return groups
    for attr in members:
        attrlookup = f"{fullname}.{attr}"
        key = _("Attributes")
        badge = None
        label = attr
        value = None
        for base in cls.__mro__:
            value = base.__dict__.get(attr)
            if value is not None:
                break
        if value is not None:
            doc = value.__doc__ or ""
            if inspect.iscoroutinefunction(value) or doc.startswith("|coro|"):
                key = _("Methods")
                badge = attributetablebadge("async", "async")
                badge["badge-type"] = _("coroutine")
            elif isinstance(value, classmethod):
                key = _("Methods")
                label = f"{name}.{attr}"
                badge = attributetablebadge("cls", "cls")
                badge["badge-type"] = _("classmethod")
            elif inspect.isfunction(value):
                if doc.startswith(("A decorator", "A shortcut decorator")):
                    # finicky but surprisingly consistent
                    key = _("Methods")
                    badge = attributetablebadge("@", "@")
                    badge["badge-type"] = _("decorator")
                elif inspect.isasyncgenfunction(value):
                    key = _("Methods")
                    badge = attributetablebadge("async for", "async for")
                    badge["badge-type"] = _("async iterable")
                else:
                    key = _("Methods")
                    badge = attributetablebadge("def", "def")
                    badge["badge-type"] = _("method")
        groups[key].append(TableElement(fullname=attrlookup, label=label, badge=badge))
    return groups
def class_results_to_node(key: str, elements: Sequence[TableElement]) -> attributetablecolumn:
    title = attributetabletitle(key, key)
    ul = nodes.bullet_list("")
    for element in elements:
        ref = nodes.reference(
            "",
            "",
            internal=True,
            refuri=f"#{element.fullname}",
            anchorname="",
            *[nodes.Text(element.label)],
        )
        para = addnodes.compact_paragraph("", "", ref)
        if element.badge is not None:
            ul.append(attributetable_item("", element.badge, para))
        else:
            ul.append(attributetable_item("", para))
    return attributetablecolumn("", title, ul)
def setup(app: Sphinx) -> None:
    app.add_directive("attributetable", PyAttributeTable)
    app.add_node(attributetable, html=(visit_attributetable_node, depart_attributetable_node))
    app.add_node(
        attributetablecolumn,
        html=(visit_attributetablecolumn_node, depart_attributetablecolumn_node),
    )
    app.add_node(
        attributetabletitle, html=(visit_attributetabletitle_node, depart_attributetabletitle_node)
    )
    app.add_node(
        attributetablebadge, html=(visit_attributetablebadge_node, depart_attributetablebadge_node)
    )
    app.add_node(
        attributetable_item, html=(visit_attributetable_item_node, depart_attributetable_item_node)
    )
    app.add_node(attributetableplaceholder)
    app.connect("doctree-resolved", process_attributetable)