# Copyright (c) 2019 Intel Corporation
#
# SPDX-License-Identifier: Apache-2.0

# based on https://protips.readthedocs.io/link-roles.html

import re
import subprocess
from collections.abc import Sequence
from pathlib import Path
from typing import Any, Final

from docutils import nodes
from sphinx.util import logging

ZEPHYR_BASE: Final[str] = Path(__file__).parents[3]

try:
    import west.manifest

    try:
        west_manifest = west.manifest.Manifest.from_file()
    except west.util.WestNotFound:
        west_manifest = None
except ImportError:
    west_manifest = None


logger = logging.getLogger(__name__)


def get_github_rev():
    try:
        output = subprocess.check_output(
            "git describe --exact-match", shell=True, stderr=subprocess.DEVNULL
        )
    except subprocess.CalledProcessError:
        return "main"

    return output.strip().decode("utf-8")


def setup(app):
    app.add_role("zephyr_file", modulelink("zephyr"))
    app.add_role("zephyr_raw", modulelink("zephyr", format="raw"))
    app.add_role("module_file", modulelink())

    app.add_config_value("link_roles_manifest_baseurl", None, "env")
    app.add_config_value("link_roles_manifest_project", None, "env")
    app.add_config_value("link_roles_manifest_project_broken_links_ignore_globs", [], "env")

    # The role just creates new nodes based on information in the
    # arguments; its behavior doesn't depend on any other documents.
    return {
        "parallel_read_safe": True,
        "parallel_write_safe": True,
    }


def modulelink(default_module=None, format="blob"):
    def role(
        name: str,
        rawtext: str,
        text: str,
        lineno: int,
        inliner,
        options: dict[str, Any] | None = None,
        content: Sequence[str] = (),
    ):
        if options is None:
            options = {}
        module = default_module
        rev = get_github_rev()
        config = inliner.document.settings.env.app.config
        baseurl = config.link_roles_manifest_baseurl
        source, line = inliner.reporter.get_source_and_line(lineno)
        trace = f"at '{source}:{line}'"

        m = re.search(r"(.*)\s*<(.*)>", text)
        if m:
            link_text = m.group(1)
            link = m.group(2)
        else:
            link_text = text
            link = text

        line_ref = ""
        line_match = re.search(r"(.+?)(?:#(L\d+(?:-L\d+)?))?$", link)
        if line_match and line_match.group(2):
            link = line_match.group(1)
            line_ref = f"?plain=1#{line_match.group(2)}"

        module_match = re.search(r"(.+?):\s*(.+)", link)
        if module_match:
            module = module_match.group(1).strip()
            link = module_match.group(2).strip()

        # Try to get a module repository's GitHub URL from the manifest.
        #
        # This allows e.g. building the docs in downstream Zephyr-based
        # software with forks of the zephyr repository, and getting
        # :zephyr_file: / :zephyr_raw: output that links to the fork,
        # instead of mainline zephyr.
        projects = [p.name for p in west_manifest.projects] if west_manifest else []
        if module in projects:
            project = west_manifest.get_projects([module])[0]
            baseurl = project.url
            rev = project.revision
        # No module provided
        elif module is None:
            raise ValueError(f"Role 'module_file' must take a module as an argument\n\t{trace}")
        # Invalid module provided
        elif module != config.link_roles_manifest_project:
            logger.debug(f"Module {module} not found in the west manifest")
        # Baseurl for manifest project not set
        elif baseurl is None:
            raise ValueError(
                f"Configuration value `link_roles_manifest_baseurl` not set\n\t{trace}"
            )

        if module == config.link_roles_manifest_project:
            p = Path(source).relative_to(inliner.document.settings.env.srcdir)
            if (
                not any(
                    p.match(glob)
                    for glob in config.link_roles_manifest_project_broken_links_ignore_globs
                )
                and not Path(ZEPHYR_BASE, link).exists()
            ):
                logger.warning(f"{link} not found in {config.link_roles_manifest_project} {trace}")

        url = f"{baseurl}/{format}/{rev}/{link}{line_ref}"
        node = nodes.reference(rawtext, link_text, refuri=url, **options)
        return [node], []

    return role
