#!/usr/bin/env python3
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

#
# JupyterLab documentation build configuration file, created by
# sphinx-quickstart on Thu Jan  4 15:10:23 2018.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))

import json
import os
import shutil
import sys
import time
from collections import ChainMap
from functools import partial
from pathlib import Path
from subprocess import check_call

HERE = Path(__file__).parent.resolve()

# Add the _ext directory to Python path for custom extensions
sys.path.insert(0, str(HERE / "_ext"))

# -- General configuration ------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
    "myst_parser",
    "sphinx.ext.intersphinx",
    "sphinx.ext.mathjax",
    "sphinx_copybutton",
    "typedoc_links",  # Custom extension for TypeDoc API links
]

myst_enable_extensions = ["html_image", "colon_fence", "substitution"]
myst_heading_anchors = 3

# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]

# The file extensions of source files.
# Sphinx considers the files with this suffix as sources.
# The value can be a dictionary mapping file extensions to file types.
source_suffix = {".md": "markdown"}

# The master toctree document.
master_doc = "index"

# General information about the project.
project = "JupyterLab"
copyright = f"2018-{time.localtime().tm_year}, Project Jupyter"  # noqa
author = "Project Jupyter"


# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.

_version_py = HERE.parent.parent / "jupyterlab" / "_version.py"
version_ns = {}

exec(_version_py.read_text(), version_ns)  # noqa

# The short X.Y version.
version = "{0:d}.{1:d}".format(*version_ns["version_info"])  # noqa
# The full version, including alpha/beta/rc tags.
release = version_ns["__version__"]


# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
language = "en"  # Must be set from the command line to generate various languages

locale_dirs = ["locale/"]
gettext_compact = False

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = [
    "api/media/*.md",
]

# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False


# build js docs and stage them to the build directory
def build_api_docs(out_dir: Path):
    """build js api docs"""
    docs = HERE.parent
    root = docs.parent
    docs_api = docs / "source" / "api"
    api_index = docs_api / "index.html"
    # is this an okay way to specify jlpm
    # without installing jupyterlab first?
    jlpm = ["node", str(root / "jupyterlab" / "staging" / "yarn.js")]

    if api_index.exists():
        # avoid rebuilding docs because it takes forever
        # `make clean` to force a rebuild
        pass
    else:
        check_call(jlpm, cwd=str(root))  # noqa S603
        check_call([*jlpm, "build:packages"], cwd=str(root))  # noqa S603
        check_call([*jlpm, "docs"], cwd=str(root))  # noqa S603

    dest_dir = out_dir / "api"
    if dest_dir.exists():
        shutil.rmtree(str(dest_dir))

    shutil.copytree(str(docs_api), str(dest_dir))


# Copy frontend files for snippet inclusion
FILES_LIST = [  # File paths should be relative to jupyterlab root folder
    "galata/test/documentation/data/custom-jupyter.css",
    "galata/test/documentation/data/custom-markdown.css",
    "packages/settingregistry/src/plugin-schema.json",
]
SNIPPETS_FOLDER = "snippets"


def copy_code_files(temp_folder: Path):
    """Copy files in the temp_folder"""
    docs = HERE.parent
    root = docs.parent

    for file in FILES_LIST:
        target = temp_folder / file
        if not target.parent.exists():
            target.parent.mkdir(parents=True, exist_ok=True)
        shutil.copyfile(str(root / file), str(target))

        # Split plugin schema to ease documentation maintenance
        if file == "packages/settingregistry/src/plugin-schema.json":
            schema = json.loads(Path(target).read_text())

            partial_schema = ChainMap(schema.get("definitions", {}), schema.get("properties", {}))
            for key in partial_schema:
                fragment = target.parent / f"{key}.json"
                with fragment.open("w") as f:
                    json.dump({key: partial_schema[key]}, f, indent=2)


IMAGES_FOLDER = "images"
AUTOMATED_SCREENSHOTS_FOLDER = "galata/test/documentation"


def copy_automated_screenshots(temp_folder: Path) -> list[Path]:
    """Copy PlayWright automated screenshots in documentation folder.

    Args:
        temp_folder: Target directory in which to copy the file
    Returns:
        List of copied files
    """
    docs = HERE.parent
    root = docs.parent

    src = root / AUTOMATED_SCREENSHOTS_FOLDER

    copied_files = []
    for img in src.rglob("*.png"):
        target = temp_folder / (img.name.replace("-documentation-linux", ""))
        shutil.copyfile(str(img), str(target))
        copied_files.append(target)

    return copied_files


COMMANDS_LIST_PATH = "commands.test.ts-snapshots/commandsList-documentation-linux.json"
COMMANDS_LIST_DOC = "user/commands_list.md"
PLUGINS_LIST_PATH = "plugins.test.ts-snapshots/plugins-documentation-linux.json"
PLUGINS_LIST_DOC = "extension/plugins_list.md"
TOKENS_LIST_PATH = "plugins.test.ts-snapshots/tokens-documentation-linux.json"
TOKENS_LIST_DOC = "extension/tokens_list.md"
TOKENS_BLACKLIST = ["@jupyterlab/services-extension:default-content-provider"]


def _clean_command_data(command: dict) -> None:
    """Clean up command data by ensuring required keys exist and formatting values."""
    for key in ("id", "label", "caption"):
        if key not in command:
            command[key] = ""
        else:
            command[key] = command[key].replace("\n", " ")


def _format_shortcuts(shortcuts: list) -> str:
    """Format shortcuts as HTML kbd elements."""
    return " ".join(f"<kbd>{shortcut}</kbd>" for shortcut in shortcuts) if shortcuts else ""


def _format_command_arguments(args_schema: dict) -> str:
    """Format command arguments section."""
    if "properties" not in args_schema or not args_schema["properties"]:
        return "**Arguments:** None\n\n"

    template = "**Arguments:**\n\n"
    required_args = set(args_schema.get("required", []))

    for arg_name, arg_info in args_schema["properties"].items():
        arg_desc = arg_info.get("description", "")
        arg_type = arg_info.get("type", "")

        template += f"- **{arg_name}**"

        if isinstance(arg_type, list):
            template += f" (`{', '.join(arg_type)}`)"
        elif arg_type:
            template += f" (`{arg_type}`)"

        if arg_name in required_args:
            template += " *(required)*"

        if "enum" in arg_info:
            enum_values = ", ".join(f'`"{v}"`' for v in arg_info["enum"])
            template += f" - Options: {enum_values}"

        if arg_desc:
            template += f": {arg_desc}"

        template += "\n"

    return template + "\n"


def _format_scope_name(scope: str) -> str:
    """Format scope name for display."""
    if scope.endswith("-extension"):
        scope = scope[: -len("-extension")]

    compound_suffixes = ["browser", "manager", "menu", "editor", "console", "viewer", "search"]
    for suffix in compound_suffixes:
        if scope.endswith(suffix) and scope != suffix:
            prefix = scope[: -len(suffix)]
            scope = f"{prefix}-{suffix}"
            break

    words = scope.split("-")
    formatted_words = []

    for word in words:
        if word.lower() in ["ui", "css", "html", "pdf", "csv", "tsv", "toc", "lsp"]:
            formatted_words.append(word.upper())
        elif word.lower() == "javascript":
            formatted_words.append("JavaScript")
        elif word.lower() == "markdown":
            formatted_words.append("Markdown")
        else:
            formatted_words.append(word.capitalize())

    return " ".join(formatted_words)


def _format_single_command(command: dict, is_last: bool = False) -> str:
    """Format a single command entry."""
    _clean_command_data(command)

    shortcuts_text = _format_shortcuts(command.get("shortcuts", []))
    template = f"## `{command['id']}`\n\n"

    if command.get("label"):
        template += f"**Label:** {command['label']}\n\n"

    if shortcuts_text:
        template += f"**Shortcuts:** {shortcuts_text}\n\n"

    if "args" in command:
        template += _format_command_arguments(command["args"])
    else:
        template += "**Arguments:** None\n\n"

    if not is_last:
        template += "---\n\n"

    return template


def document_commands_list(temp_folder: Path) -> None:
    """Generate the command list documentation page from application extraction."""
    list_path = HERE.parent.parent / AUTOMATED_SCREENSHOTS_FOLDER / COMMANDS_LIST_PATH
    commands_list = json.loads(list_path.read_text())

    commands_by_scope = {}
    for command in commands_list:
        command_id = command.get("id", "")
        scope = command_id.split(":")[0] if ":" in command_id else "Other"

        display_scope = _format_scope_name(scope)

        if display_scope not in commands_by_scope:
            commands_by_scope[display_scope] = []
        commands_by_scope[display_scope].append(command)

        sorted_scopes = sorted(commands_by_scope.keys(), key=str.lower)

    template = ""
    for scope_idx, scope in enumerate(sorted_scopes):
        commands = sorted(commands_by_scope[scope], key=lambda c: c.get("id", ""))

        template += f"# {scope}\n\n"

        for i, command in enumerate(commands):
            is_last_command_in_scope = i == len(commands) - 1
            is_last_scope = scope_idx == len(sorted_scopes) - 1
            is_last_overall = is_last_command_in_scope and is_last_scope
            template += _format_single_command(command, is_last=is_last_overall)

        if scope_idx != len(sorted_scopes) - 1:
            template += "\n"

    (temp_folder / COMMANDS_LIST_DOC).write_text(template)


def document_plugins_tokens_list(list_path: Path, output_path: Path) -> None:
    """Generate the plugins list documentation page from application extraction."""
    items = json.loads(list_path.read_text())

    template = ""

    for _name, _description in items.items():
        if _name in TOKENS_BLACKLIST:
            continue

        # Normalize line continuation indentation to 2 spaces (prettier standard for markdown lists)
        _description = _description.replace("\n    ", "\n  ")
        template += f"- `{_name}`: {_description}\n"

    output_path.write_text(template)


# -- Options for HTML output ----------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
html_theme = "pydata_sphinx_theme"
html_logo = "_static/logo-rectangle.svg"
html_favicon = "_static/logo-icon.png"
# Theme options are theme-specific and customize the look and feel of a theme
# further.  For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {
    "announcement": '🚀 JupyterLab 4.5.0 is now available · <a href="https://jupyterlab.rtfd.io/en/latest/getting_started/installation.html">INSTALL</a> · <a href="https://jupyterlab.rtfd.io/en/latest/getting_started/changelog.html#v4-5">RELEASE NOTES</a>',
    "icon_links": [
        {
            "name": "jupyter.org",
            "url": "https://jupyter.org",
            "icon": "_static/jupyter_logo.svg",
            "type": "local",
        },
        {
            "name": "GitHub",
            "url": "https://github.com/jupyterlab/jupyterlab",
            "icon": "fab fa-github-square",
        },
        {
            "name": "Discourse",
            "url": "https://discourse.jupyter.org/c/jupyterlab/17",
            "icon": "fab fa-discourse",
        },
        {
            "name": "Zulip",
            "url": "https://jupyter.zulipchat.com/#narrow/channel/469762-jupyterlab",
            "icon": "fa-solid fa-comments",
        },
    ],
    "logo": {
        "image_light": "_static/logo-rectangle.svg",
        "image_dark": "_static/logo-rectangle-dark.svg",
        "alt_text": "JupyterLab",
    },
    "use_edit_page_button": True,
    "navbar_align": "left",
    "navbar_start": ["navbar-logo", "version-switcher"],
    "navigation_with_keys": False,
    "footer_start": ["copyright.html"],
    "footer_end": ["privacy_footer.html", "theme-version"],
    "switcher": {
        # Trick to get the documentation version switcher to always points to the latest version without being corrected by the integrity check;
        # otherwise older versions won't list newer versions
        "json_url": "/".join(
            ("https://jupyterlab.readthedocs.io/en", "latest", "_static/switcher.json")
        ),
        "version_match": os.environ.get("READTHEDOCS_VERSION", "latest"),
    },
}

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]

# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# This is required for the alabaster theme
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
html_sidebars = {
    "index": [],  # Home page has no sidebar so there's more room for content
    "**": ["sidebar-nav-bs.html"],
}

# Output for github to be used in links
html_context = {
    "github_user": "jupyterlab",  # Username
    "github_repo": "jupyterlab",  # Repo name
    "github_version": "4.5.x",  # Version
    "doc_path": "docs/source/",  # Path in the checkout to the docs root
}

# -- Options for HTMLHelp output ------------------------------------------

# Output file base name for HTML help builder.
htmlhelp_basename = "JupyterLabdoc"


# -- Options for LaTeX output ---------------------------------------------

latex_elements = {
    # The paper size ('letterpaper' or 'a4paper').
    #
    # 'papersize': 'letterpaper',
    # The font size ('10pt', '11pt' or '12pt').
    #
    # 'pointsize': '10pt',
    # Additional stuff for the LaTeX preamble.
    #
    # 'preamble': '',
    # Latex figure (float) alignment
    #
    # 'figure_align': 'htbp',
}

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
#  author, documentclass [howto, manual, or own class]).
latex_documents = [
    (
        master_doc,
        "JupyterLab.tex",
        "JupyterLab Documentation",
        "Project Jupyter",
        "manual",
    ),
]


# -- Options for manual page output ---------------------------------------

# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [(master_doc, "jupyterlab", "JupyterLab Documentation", [author], 1)]


# -- Options for Texinfo output -------------------------------------------

# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
#  dir menu entry, description, category)
texinfo_documents = [
    (
        master_doc,
        "JupyterLab",
        "JupyterLab Documentation",
        author,
        "JupyterLab",
        "One line description of project.",
        "Miscellaneous",
    ),
]


# -- Options for Epub output ----------------------------------------------

# Bibliographic Dublin Core info.
epub_title = project
epub_author = author
epub_publisher = author
epub_copyright = copyright

# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#
# epub_identifier = ''

# A unique identification for the text.
#
# epub_uid = ''

# A list of files that should not be packed into the epub file.
epub_exclude_files = ["search.html"]


# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}


def setup(app):
    # Enable Plausible.io stats
    app.add_js_file("https://plausible.io/js/pa-Tem97Eeu4LJFfSRY89aW1.js", loading_method="async")
    app.add_js_file(
        filename=None,
        body="window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};plausible.init({hashBasedRouting:true})",
    )

    dest = HERE / "getting_started/changelog.md"
    shutil.copy(str(HERE.parent.parent / "CHANGELOG.md"), str(dest))
    app.add_css_file("css/custom.css")  # may also be an URL
    # Skip we are dealing with internationalization
    outdir = Path(app.outdir)
    if outdir.name != "gettext":
        build_api_docs(outdir)

    copy_code_files(Path(app.srcdir) / SNIPPETS_FOLDER)
    tmp_files = copy_automated_screenshots(Path(app.srcdir) / IMAGES_FOLDER)

    def clean_code_files(tmp_files, app, exception):
        """Remove temporary folder."""
        try:
            shutil.rmtree(str(Path(app.srcdir) / SNIPPETS_FOLDER))
        except Exception:  # noqa S110
            pass

        for f in tmp_files:
            f.unlink()

    src_dir = Path(app.srcdir)
    document_commands_list(src_dir)
    document_plugins_tokens_list(
        HERE.parent.parent / AUTOMATED_SCREENSHOTS_FOLDER / PLUGINS_LIST_PATH,
        src_dir / PLUGINS_LIST_DOC,
    )
    document_plugins_tokens_list(
        HERE.parent.parent / AUTOMATED_SCREENSHOTS_FOLDER / TOKENS_LIST_PATH,
        src_dir / TOKENS_LIST_DOC,
    )

    app.connect("build-finished", partial(clean_code_files, tmp_files))

    # Inherit some Jupyter branding CSS rules from the Jupyter Documentation
    app.add_css_file("https://docs.jupyter.org/en/latest/_static/jupyter.css")
