# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Auto-generate the Kconfig reference in //docs/sphinx/os/zephyr/kconfig.rst"""


import re
from typing import Iterable

import docutils
from docutils.core import publish_doctree
from sphinx.application import Sphinx
from sphinx.addnodes import document


try:
    import kconfiglib  # type: ignore

    KCONFIGLIB_AVAILABLE = True
except ImportError:
    KCONFIGLIB_AVAILABLE = False


# Determine whether the docs are being built with Bazel or GN.
is_bazel_build = True
try:
    # pylint: disable=unused-import
    from python.runfiles import runfiles  # type: ignore

    # pylint: enable=unused-import
except ImportError:
    is_bazel_build = False


class MissingKconfigRoot(Exception):
    pass


def rst_to_doctree(rst: str) -> Iterable[docutils.nodes.Node]:
    """Convert raw reStructuredText into doctree nodes."""
    # TODO: b/288127315 - Properly resolve references within the rst so that
    # links are generated more robustly.
    while ':ref:`module-' in rst:
        rst = re.sub(
            r':ref:`module-(.*?)`', r'`\1 <https://pigweed.dev/\1>`_', rst
        )
    doctree = publish_doctree(rst)
    return doctree.children


def create_source_paragraph(name_and_loc: str) -> Iterable[docutils.nodes.Node]:
    """Convert kconfiglib's name and location string into a source code link."""
    if not is_bazel_build:
        name_and_loc = name_and_loc.replace('pw_docgen_tree/', '')
    start = name_and_loc.index('pw_')
    end = name_and_loc.index(':')
    file_path = name_and_loc[start:end]
    url = f'https://cs.opensource.google/pigweed/pigweed/+/main:{file_path}'
    link = f'`//{file_path} <{url}>`_'
    return rst_to_doctree(f'Source: {link}')


def process_node(
    node: kconfiglib.MenuNode, parent: docutils.nodes.Node
) -> None:
    """Recursively generate documentation for the Kconfig nodes."""
    while node:
        if node.item == kconfiglib.MENU:
            name = node.prompt[0]
            # All auto-generated sections must have an ID or else the
            # get_secnumber() function in Sphinx's HTML5 writer throws an
            # IndexError.
            menu_section = docutils.nodes.section(ids=[name])
            menu_section += docutils.nodes.title(text=f'{name} options')
            if node.list:
                process_node(node.list, menu_section)
            parent += menu_section
        elif isinstance(node.item, kconfiglib.Symbol):
            name = f'CONFIG_{node.item.name}'
            symbol_section = docutils.nodes.section(ids=[name])
            symbol_section += docutils.nodes.title(text=name)
            symbol_section += docutils.nodes.paragraph(
                text=f'Type: {kconfiglib.TYPE_TO_STR[node.item.type]}'
            )
            if node.item.defaults:
                try:
                    default_value = node.item.defaults[0][0].str_value
                    symbol_section += docutils.nodes.paragraph(
                        text=f'Default value: {default_value}'
                    )
                # If the data wasn't found, just contine trying to process
                # rest of the documentation for the node.
                except IndexError:
                    pass
            if node.item.ranges:
                try:
                    low = node.item.ranges[0][0].str_value
                    high = node.item.ranges[0][1].str_value
                    symbol_section += docutils.nodes.paragraph(
                        text=f'Range of valid values: {low} to {high}'
                    )
                except IndexError:
                    pass
            if node.prompt:
                try:
                    symbol_section += docutils.nodes.paragraph(
                        text=f'Description: {node.prompt[0]}'
                    )
                except IndexError:
                    pass
            if node.help:
                symbol_section += rst_to_doctree(node.help)
            if node.list:
                process_node(node.list, symbol_section)
            symbol_section += create_source_paragraph(node.item.name_and_loc)
            parent += symbol_section
        # TODO: b/288127315 - Render choices?
        # elif isinstance(node.item, kconfiglib.Choice):
        node = node.next


def generate_kconfig_reference(
    app: Sphinx, doctree: document, docname: str
) -> None:
    """Parse the Kconfig and kick off the doc generation process."""
    # Only run this logic on one specific page.
    if 'docs/os/zephyr/kconfig' not in docname:
        return
    root = None
    # Get the last `section` node in the doc. This is where we'll append the
    # auto-generated content.
    for child in doctree.children:
        if isinstance(child, docutils.nodes.section):
            root = child
    if not root:
        raise MissingKconfigRoot
    file_path = f'{app.confdir}/Kconfig.zephyr'
    kconfig = kconfiglib.Kconfig(file_path)
    # There's no need to process kconfig.top_node (the main menu) or
    # kconfig.top_node.list (ZEPHYR_PIGWEED_MODULE) because they don't
    # contain data that should be documented.
    first_data_node = kconfig.top_node.list.next
    process_node(first_data_node, root)


def setup(app: Sphinx) -> dict[str, bool]:
    """Initialize the Sphinx extension."""
    if KCONFIGLIB_AVAILABLE:
        app.connect('doctree-resolved', generate_kconfig_reference)
    return {'parallel_read_safe': True, 'parallel_write_safe': True}
