"""
Support for generating documentation files for OmniGraph Nodes.

The documentation is written in the reStructuredText format, consistent with API documentation.
Note that the script ../make_docs_toc.py relies on parsing the files so if the format changes it must be updated.

Exported Methods:
    generate_documention

Exported Constants
    RE_OGN_DOC_FILENAME
    RE_OGN_NAME_INFO
    RE_OGN_DESCRIPTION_TITLE
    RE_OGN_INPUTS_TITLE
    RE_OGN_BODY_MARKER
"""
import re
from contextlib import suppress
from typing import List, Optional

from .attributes.AttributeManager import AttributeManager
from .nodes import NodeInterfaceGenerator
from .utils import GeneratorConfiguration, MetadataKeys, UnimplementedError, logger, rst_table, rst_title

__all__ = [
    "generate_documentation",
    "RE_OGN_DOC_FILENAME",
    "RE_OGN_NAME_INFO",
    "RE_OGN_DESCRIPTION_TITLE",
    "RE_OGN_INPUTS_TITLE",
    "RE_OGN_BODY_MARKER",
]

# Special restructuredText markers so that the parser can quickly find the start of any section
RST_ID_DOC = "GENERATED - Documentation"

# Pattern to recognize the name of OGN documentation files
RE_OGN_DOC_FILENAME = re.compile("Ogn.*.rst$")

# Pattern to recognize the name of the node in the file (must coordinate with the output of generate_documentation.py)
RE_OGN_NAME_INFO = re.compile("OmniGraph Node ([^ ]+)$")

# Patterns for title lines in the file
RE_OGN_DESCRIPTION_TITLE = re.compile("Description$")
RE_OGN_INPUTS_TITLE = re.compile("Inputs$")

# Pattern marking the start of the node body
RE_OGN_BODY_MARKER = re.compile(f"^.. _({RST_ID_DOC} .*):")

# Pattern to recognize .ogn files that are part of the internal tutorials
RE_TUTORIAL_FILE = re.compile(".*(omni.graph.core/tutorials.*)")


# ======================================================================
class NodeDocumentationGenerator(NodeInterfaceGenerator):
    """Manage the functions required to generate a C++ interface for a node"""

    def __init__(self, configuration: GeneratorConfiguration):
        """Set up the generator and output the documentation for the node

        Just passes the initialization on to the parent class. See the argument and exception descriptions there.
        """
        logger.info("Creating NodeDocumentationGenerator")
        super().__init__(configuration)

    # ----------------------------------------------------------------------
    def interface_file_name(self) -> str:
        """Return the path to the name of the documentation file"""
        return self.base_name + ".rst"

    # ----------------------------------------------------------------------
    def generate_attributes_documentation(self, attributes: List[AttributeManager]):
        """Write out documentation code corresponding to the node

        Args:
            attributes: List of attributes whose documentation is to be generated

        Raises:
            NodeGenerationError: When there is a failure in the generation of the documentation file
        """
        logger.info("Generating documentation for %s attributes", len(attributes))
        # RST tables are very particular about sizing so first find out how big the columns need to be
        attribute_table = [["Name", "Type", "Default", "Required?", "Descripton"]]
        for attribute in attributes:
            name = attribute.name
            try:
                type_name = attribute.ogn_type()
            except AttributeError:
                type_name = "[Unsupported]"
            try:
                default_value = attribute.default
            except UnimplementedError:
                default_value = "[Unsupported]"
            required = "**Y**" if attribute.is_required else ""
            description = attribute.description
            if isinstance(description, list):
                description = " ".join(description)
            description = description.replace("\n", " ")
            attribute_table.append([name, type_name, default_value, required, description])
            # If there is any metadata add it in name/value pairs below the attribute definition
            for key, value in attribute.metadata.items():
                if key != MetadataKeys.DESCRIPTION:
                    attribute_table.append(["", key, value, "", ""])
        self.out.write(rst_table(attribute_table))

    # ----------------------------------------------------------------------
    def generate_code(self, title: str, code_file_path: str, code_type: str, code_id: str):
        """Generate a code block with the given title containing the contents of the given file

        If the file does not exist then a message to that effect is emitted and no code block is generated
        """
        logger.info("Generating code titled '%s' of type '%s'", title, code_type)
        self.out.write(rst_title(title, 0))
        self.out.write()

        try:
            self.out.write(f".. _{code_id}:\n")
            self.out.write(f".. code:: {code_type}")
            self.out.write()
            self.out.indent()
            with open(code_file_path, "r", encoding="utf-8") as code_fd:
                for code_line in code_fd:
                    self.out.write(code_line.rstrip())
            self.out.exdent()
            self.out.write()
        except FileNotFoundError:
            relative_path = RE_TUTORIAL_FILE.match(code_file_path).group(1)
            self.out.write(f"File not found: {relative_path}")
            return

    # ----------------------------------------------------------------------
    def pre_interface_generation(self):
        """Generate the documentation setup, which is just the link to the top of the generated documentation"""
        self.out.write(f".. _{RST_ID_DOC} _ogn{self.node_interface.name}:\n")

    # ----------------------------------------------------------------------
    def generate_node_interface(self):
        """Generate the documentation for the node"""
        logger.info("Generating documentation for node %s", self.node_interface.name)
        node_name = self.node_interface.name
        self.out.write(rst_title(f"OmniGraph Node {node_name}", 0))

        # Gather the node metadata for reporting
        node_metadata_table = [["Name", "Value"]]
        node_metadata_table.append(["Version", self.node_interface.version])
        node_metadata_table.append(["Extension", self.extension])
        if self.node_interface.icon_path is not None:
            node_metadata_table.append(["Icon", self.node_interface.icon_path])
        node_metadata_table.append(["Has State?", self.node_interface.has_state])
        node_metadata_table.append(["Implementation Language", self.node_interface.language])
        node_metadata_table.append(["Default Memory Type", self.node_interface.memory_type])
        excluded = self.node_interface.excluded_generators
        exclusions = ", ".join(excluded) if excluded else "None"
        node_metadata_table.append(["Generated Code Exclusions", exclusions])
        for key, value in self.node_interface.metadata.items():
            # Some metadata appears in other locations already so skip it here
            if key not in [MetadataKeys.EXTENSION, MetadataKeys.DESCRIPTION]:
                node_metadata_table.append([key, value])
        node_metadata_table.append(["Generated Class Name", f"{self.base_name}Database"])
        node_metadata_table.append(["Python Module", f"{self.module}"])
        self.out.write(rst_title(f"{node_name} Properties", 1))
        self.out.write(rst_table(node_metadata_table))

        # The "node_name" here, and in attribute titles, is kind of redundant. It is mainly here to prevent a whole
        # bunch of duplicate tag errors in the documentation generator, which does not seem to have a way to turn
        # off automatic link generation for a given heading.
        self.out.write(rst_title(f"{node_name} Description", 1))
        self.out.write(self.node_interface.description)

        # Files in the tutorials/ directory have more detailed documentation available elsewhere so link to it
        with suppress(TypeError):
            if RE_TUTORIAL_FILE.match(self.node_file_path):
                self.out.write()
                self.out.write(f"See the accompanying explanation and annotated code at :ref:`ogn{node_name}`")

        attributes = self.node_interface.all_input_attributes()
        if attributes:
            self.out.write(rst_title(f"{node_name} Inputs", 1))
            self.generate_attributes_documentation(attributes)
        attributes = self.node_interface.all_output_attributes()
        if attributes:
            self.out.write(rst_title(f"{node_name} Outputs", 1))
            self.generate_attributes_documentation(attributes)
        attributes = self.node_interface.all_state_attributes()
        if attributes:
            self.out.write(rst_title(f"{node_name} State", 1))
            self.generate_attributes_documentation(attributes)


# ======================================================================
def generate_documentation(configuration: GeneratorConfiguration) -> Optional[str]:
    """Create support files for the documentation of a node

    Args:
        configuration: Information defining how and where the documentation will be generated

    Returns:
        String containing the generated documentation or None if its generation was not enabled

    Raises:
        NodeGenerationError: When there is a failure in the generation of the documentation file
    """
    if not configuration.node_interface.can_generate("docs"):
        return None
    logger.info("Generating documentation")
    generator = NodeDocumentationGenerator(configuration)
    generator.generate_interface()
    return str(generator.out)
