"""
Generates the logic nodes reference page for Leenkx3D's wiki:
https://github.com/leenkx3d/leenkx/wiki/reference

USAGE:
    First, generate the node screenshots (1). After that, open a
    terminal in the folder of this script and execute the following
    command (Blender must have the Leenkx add-on activated of course):

    path/to/blender.exe -b -P make_node_reference.py"

    This will create markdown files containing the reference in the
    `/output` folder relative to this script. You can copy the content
    from those files into the logic node reference articles. DO NOT
    commit the generated files to the leenkx_tools repo!

    Todo: Create a GitHub action to automatically update the reference
    for each release.

    (1) https://github.com/leenkx3d/leenkx_wiki_images/blob/master/logic_nodes/make_screenshots.py
        Please also read the usage notes in that file!
"""
import ensurepip
import itertools
import os
import subprocess
import sys
from typing import List

import bpy
from nodeitems_utils import NodeItem

from lnx.logicnode import lnx_nodes
import lnx.props

ensurepip.bootstrap()
os.environ.pop("PIP_REQ_TRACKER", None)
subprocess.check_output([sys.executable, '-m', 'pip', 'install', '--upgrade', 'markdownmaker'])

# If pip wants an update, toggle this flag before execution
UPDATE_PIP = False
if UPDATE_PIP:
    subprocess.check_output([sys.executable, '-m', 'pip', 'install', '--upgrade', 'pip'])

from markdownmaker.document import Document
from markdownmaker.markdownmaker import *

PY_NODE_DIR = "https://github.com/leenkx3d/leenkx/blob/main/blender/lnx.logicnode/"
HX_NODE_DIR = "https://github.com/leenkx3d/leenkx/blob/main/Sources/leenkx/logicnode/"
IMG_DIR = "https://github.com/leenkx3d/leenkx_wiki_images/raw/master/logic_nodes/"

OUTPUT_DIR = os.path.abspath(__file__)
OUTPUT_DIR = os.path.join(os.path.dirname(OUTPUT_DIR), "output")


def get_anchor(text: str) -> str:
    """Gets the GitHub anchor id for a link."""
    return "#" + text.lower().replace(" ", "-")


def make_node_link(nodename: str) -> str:
    """Create a link to a node given by the name of the node"""
    return Link(label=InlineCode(nodename), url=get_anchor(nodename))


def get_nodetype(typename: str):
    """Convert the type name to the actual type."""
    return bpy.types.bpy_struct.bl_rna_get_subclass_py(typename)


def format_desc(description_text: str, *, indented=False) -> str:
    """Format the raw description string."""
    out = ""

    # Indentation for list items is 2 spaces per markdown standard in
    # this case. For now, sub-lists are indented one level only, more
    # is not required currently
    line_start = "  " if indented else ""

    # Whether the last line was empty (ignore multiple empty lines)
    last_empty = False

    for line in description_text.splitlines():
        line = line.strip()

        # List item. Explicitly check for space after "-", might be a negative number else
        if line.startswith("- "):
            out += "\n" + line_start + line
            last_empty = False

        elif line == "":
            if last_empty:
                continue
            out += "\n"
            last_empty = True

        else:
            if last_empty:
                out += "\n" + line_start + line  # Create a full empty line above the start of a paragraph
            else:
                out += " " + line  # Combine one paragraph to a single line
            last_empty = False

    # Remove any left-over whitespace at the beginning/end
    return out.strip()


def generate_node_documentation(doc: Document, nodeitem: NodeItem, category: lnx_nodes.LnxNodeCategory):
    nodetype = get_nodetype(nodeitem.nodetype)
    docstring: str = nodetype.__doc__
    if docstring is not None:
        doc_parts = docstring.split("@")

        # Show docstring until the first "@"
        node_description = doc_parts[0].rstrip("\n")
        node_description = format_desc(node_description)

        deprecation_note = Optional()
        doc.add(deprecation_note)
        doc.add(Paragraph(node_description))

        has_see = False
        has_inputs = False
        has_outputs = False
        has_options = False
        see_list = []
        input_list = []
        output_list = []
        option_list = []
        for part in doc_parts:
            # Reference to other logic nodes
            if part.startswith("seeNode "):
                if not has_see:
                    has_see = True
                    doc.add(Paragraph(Bold("See also:")))
                    doc.add(UnorderedList(see_list))

                see_list.append(Italic(make_node_link(part[8:].rstrip())))

            # General references
            elif part.startswith("see "):
                if not has_see:
                    has_see = True
                    doc.add(Paragraph(Bold("See also:")))
                    doc.add(UnorderedList(see_list))

                see_list.append(Italic(part[4:].rstrip()))

        # Add node screenshot
        image_file = IMG_DIR + category.name.lower() + "/" + nodeitem.nodetype + ".jpg"
        doc.add(Image(url=image_file, alt_text=nodeitem.label + " node"))

        for part in doc_parts:
            # Input sockets
            if part.startswith("input "):
                if not has_inputs:
                    has_inputs = True
                    doc.add(Paragraph(Bold("Inputs:")))
                    doc.add(UnorderedList(input_list))

                socket_name, description = part[6:].split(":", 1)
                description = format_desc(description, indented=True)
                input_list.append(f"{InlineCode(socket_name)}: {description}")

            # Output sockets
            elif part.startswith("output "):
                if not has_outputs:
                    has_outputs = True
                    doc.add(Paragraph(Bold("Outputs:")))
                    doc.add(UnorderedList(output_list))

                socket_name, description = part[7:].split(":", 1)
                description = format_desc(description, indented=True)
                output_list.append(f"{InlineCode(socket_name)}: {description}")

            # Other UI options
            elif part.startswith("option "):
                if not has_options:
                    has_options = True
                    doc.add(Paragraph(Bold("Options:")))
                    doc.add(UnorderedList(option_list))

                option_name, description = part[7:].split(":", 1)
                description = format_desc(description, indented=True)
                option_list.append(f"{InlineCode(option_name)}: {description}")

            elif part.startswith("deprecated "):
                alternatives, message = part[11:].split(":", 1)

                message = " ".join(message.split()).replace("\n", "")
                if not message.endswith(".") and not message == "":
                    message += "."

                links = []
                for alternative in alternatives.split(","):
                    if alternative == "":
                        continue
                    links.append(str(make_node_link(alternative)))

                if len(links) > 0:
                    alternatives = f"Please use the following node(s) instead: {', '.join(links)}."
                    message = alternatives + " " + message

                deprecation_note.content = Quote(f"{Bold('DEPRECATED.')} This node is deprecated and will be removed in future versions of Leenkx. {message}")

        # Link to sources
        node_file_py = "/".join(nodetype.__module__.split(".")[2:]) + ".py"
        node_file_hx = nodetype.bl_idname[2:] + ".hx"  # Discard LN prefix

        pylink = Link(label="Python", url=PY_NODE_DIR + node_file_py)
        hxlink = Link(label="Haxe", url=HX_NODE_DIR + node_file_hx)

        doc.add(Paragraph(f"{Bold('Sources:')} {pylink} | {hxlink}"))


def build_page(section_name: str = ""):
    is_mainpage = section_name == ""

    doc = Document()

    doc.add(Header("Logic Nodes Reference" + ("" if is_mainpage else f": {Italic(section_name.capitalize())} nodes")))

    doc.add(Paragraph(Italic(
        "This reference was generated automatically. Please do not edit the"
        " page directly, instead change the docstrings of the nodes in their"
        f" {Link(label='Python files', url='https://github.com/leenkx3d/leenkx/tree/main/blender/lnx.logicnode')}"
        f" or the {Link(label='generator script', url='https://github.com/leenkx3d/leenkx_tools/blob/main/mkdocs/make_node_reference.py')}"
        f" and {Link(label='open a pull request', url='https://github.com/leenkx3d/leenkx/wiki/contribute#creating-a-pull-request')}."
        " Thank you for contributing!")))
    doc.add(Paragraph(Italic(f"This reference was built for {Bold(f'Leenkx {lnx.props.lnx_version}')}.")))

    doc.add(HorizontalRule())

    with HeaderSubLevel(doc):

        # Table of contents
        doc.add(Header("Node Categories"))

        category_items: List[Node] = []
        for section, section_categories in lnx_nodes.category_items.items():
            # Ignore empty sections ("default" e.g)
            if len(section_categories) > 0:
                section_title = Bold(section.capitalize())
                if section_name == section:
                    # Highlight current page
                    section_title = Italic(section_title)
                category_items.append(section_title)
                url = f"https://github.com/leenkx3d/leenkx/wiki/reference_{section}"
                category_items.append(UnorderedList([Link(c.name, url + get_anchor(c.name)) for c in section_categories]))

        doc.add(UnorderedList(category_items))

        # Page content
        if not is_mainpage:
            for category in lnx_nodes.category_items[section_name]:
                doc.add(Header(category.name))

                if category.description != "":
                    doc.add(Paragraph(category.description))

                with HeaderSubLevel(doc):
                    # Sort nodes alphabetically and discard section order
                    iterator = itertools.chain(category.get_all_nodes(), category.deprecated_nodes)
                    for nodeitem in sorted(iterator, key=lambda n: n.label):
                        doc.add(Header(nodeitem.label))

                        generate_node_documentation(doc, nodeitem, category)

    filename = "reference.md" if is_mainpage else f"reference_{section_name}.md"
    with open(os.path.join(OUTPUT_DIR, filename), "w") as out_file:
        out_file.write(doc.write())


def run():
    print("Generating documentation...")

    if not os.path.exists(OUTPUT_DIR):
        os.mkdir(OUTPUT_DIR)

    # Main page
    build_page()

    # Section sub-pages
    for section_name in lnx_nodes.category_items.keys():
        if section_name == 'default':
            continue
        build_page(section_name)


if __name__ == "__main__":
    run()