# Copyright Spack Project Developers. See COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)

# flake8: noqa
# -*- coding: utf-8 -*-
#
# Spack documentation build configuration file, created by
# sphinx-quickstart on Mon Dec  9 15:32:41 2013.
#
# 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.

import os
import subprocess
import sys
from glob import glob
from typing import List

from docutils.statemachine import StringList
from pygments.formatters.html import HtmlFormatter
from pygments.lexer import RegexLexer, default
from pygments.token import *
from sphinx.domains.python import PythonDomain
from sphinx.ext.apidoc import main as sphinx_apidoc
from sphinx.highlighting import PygmentsBridge
from sphinx.parsers import RSTParser

# -- Spack customizations -----------------------------------------------------
# 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.
link_name = os.path.abspath("_spack_root")
if not os.path.exists(link_name):
    os.symlink(os.path.abspath("../../.."), link_name, target_is_directory=True)

# Add the Spack bin directory to the path so that we can use its output in docs.
os.environ["SPACK_ROOT"] = os.path.abspath("_spack_root")
os.environ["SPACK_USER_CONFIG_PATH"] = os.path.abspath(".spack")
os.environ["PATH"] += os.pathsep + os.path.abspath("_spack_root/bin")

# Set an environment variable so that colify will print output like it would to
# a terminal.
os.environ["COLIFY_SIZE"] = "25x120"
os.environ["COLUMNS"] = "120"

sys.path[0:0] = [
    os.path.abspath("_spack_root/lib/spack/"),
    os.path.abspath(".spack/spack-packages/repos"),
]

# Init the package repo with all git history, so "Last updated on" is accurate.
subprocess.call(["spack", "repo", "update"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if os.path.exists(".spack/spack-packages/.git/shallow"):
    subprocess.call(
        ["git", "fetch", "--unshallow"],
        cwd=".spack/spack-packages",
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )

# Generate a command index if an update is needed
subprocess.call(
    [
        "spack",
        "commands",
        "--format=rst",
        "--header=command_index.in",
        "--update=command_index.rst",
        *glob("*rst"),
    ]
)

#
# Run sphinx-apidoc
#
# Remove any previous API docs
# Read the Docs doesn't clean up after previous builds
# Without this, the API Docs will never actually update
#
apidoc_args = [
    "--force",  # Overwrite existing files
    "--no-toc",  # Don't create a table of contents file
    "--output-dir=.",  # Directory to place all output
    "--module-first",  # emit module docs before submodule docs
]
sphinx_apidoc(
    apidoc_args
    + [
        "_spack_root/lib/spack/spack",
        "_spack_root/lib/spack/spack/vendor",
        "_spack_root/lib/spack/spack/test",
        "_spack_root/lib/spack/spack/package.py",
    ]
)
sphinx_apidoc(
    apidoc_args
    + [
        "--implicit-namespaces",
        ".spack/spack-packages/repos/spack_repo",
        ".spack/spack-packages/repos/spack_repo/builtin/packages",
    ]
)


class NoWhitespaceHtmlFormatter(HtmlFormatter):
    """HTML formatter that suppresses redundant span elements for Text.Whitespace tokens."""

    def _get_css_classes(self, ttype):
        # For Text.Whitespace return an empty string, which avoids <span class="w"> </span>
        # elements from being generated.
        return "" if ttype is Text.Whitespace else super()._get_css_classes(ttype)


class CustomPygmentsBridge(PygmentsBridge):
    def get_formatter(self, **options):
        return NoWhitespaceHtmlFormatter(**options)


# Use custom HTML formatter to avoid redundant <span class="w"> </span> elements.
# See https://github.com/pygments/pygments/issues/1905#issuecomment-3170486995.
PygmentsBridge.html_formatter = NoWhitespaceHtmlFormatter


from spack.llnl.util.lang import classproperty
from spack.spec_parser import SpecTokens

# replace classproperty.__get__ to return `self` so Sphinx can document it correctly. Otherwise
# it evaluates the callback, and it documents the result, which is not what we want.
classproperty.__get__ = lambda self, instance, owner: self


class SpecLexer(RegexLexer):
    """A custom lexer for Spack spec strings and spack commands."""

    name = "Spack spec"
    aliases = ["spec"]
    filenames = []
    tokens = {
        "root": [
            # Looks for `$ command`, which may need spec highlighting.
            (r"^\$\s+", Generic.Prompt, "command"),
            (r"#.*?\n", Comment.Single),
            # Alternatively, we just get a literal spec string, so we move to spec mode. We just
            # look ahead here, without consuming the spec string.
            (r"(?=\S+)", Generic.Prompt, "spec"),
        ],
        "command": [
            # A spack install command is followed by a spec string, which we highlight.
            (
                r"spack(?:\s+(?:-[eC]\s+\S+|--?\S+))*\s+(?:install|uninstall|spec|load|unload|find|info|list|versions|providers|mark|diff|add)(?: +(?:--?\S+)?)*",
                Text,
                "spec",
            ),
            # Comment
            (r"\s+#.*?\n", Comment.Single, "command_output"),
            # Escaped newline should leave us in this mode
            (r".*?\\\n", Text),
            # Otherwise, it's the end of the command
            (r".*?\n", Text, "command_output"),
        ],
        "command_output": [
            (r"^\$\s+", Generic.Prompt, "#pop"),  # new command
            (r"#.*?\n", Comment.Single),  # comments
            (r".*?\n", Generic.Output),  # command output
        ],
        "spec": [
            # New line terminates the spec string
            (r"\s*?$", Text, "#pop"),
            # Dependency, with optional virtual assignment specifier
            (SpecTokens.START_EDGE_PROPERTIES.regex, Name.Variable, "edge_properties"),
            (SpecTokens.DEPENDENCY.regex, Name.Variable),
            # versions
            (SpecTokens.VERSION_HASH_PAIR.regex, Keyword.Pseudo),
            (SpecTokens.GIT_VERSION.regex, Keyword.Pseudo),
            (SpecTokens.VERSION.regex, Keyword.Pseudo),
            # variants
            (SpecTokens.PROPAGATED_BOOL_VARIANT.regex, Name.Function),
            (SpecTokens.BOOL_VARIANT.regex, Name.Function),
            (SpecTokens.PROPAGATED_KEY_VALUE_PAIR.regex, Name.Function),
            (SpecTokens.KEY_VALUE_PAIR.regex, Name.Function),
            # filename
            (SpecTokens.FILENAME.regex, Text),
            # Package name
            (SpecTokens.FULLY_QUALIFIED_PACKAGE_NAME.regex, Name.Class),
            (SpecTokens.UNQUALIFIED_PACKAGE_NAME.regex, Name.Class),
            # DAG hash
            (SpecTokens.DAG_HASH.regex, Text),
            (SpecTokens.WS.regex, Text),
            # Also stop at unrecognized tokens (without consuming them)
            default("#pop"),
        ],
        "edge_properties": [
            (SpecTokens.KEY_VALUE_PAIR.regex, Name.Function),
            (SpecTokens.END_EDGE_PROPERTIES.regex, Name.Variable, "#pop"),
        ],
    }


# Enable todo items
todo_include_todos = True


#
# Disable duplicate cross-reference warnings.
#
class PatchedPythonDomain(PythonDomain):
    def resolve_xref(self, env, fromdocname, builder, type, target, node, contnode):
        if "refspecific" in node:
            del node["refspecific"]
        return super().resolve_xref(env, fromdocname, builder, type, target, node, contnode)


#
# Disable tabs to space expansion in code blocks
# since Makefiles require tabs.
#
class NoTabExpansionRSTParser(RSTParser):
    def parse(self, inputstring, document):
        if isinstance(inputstring, str):
            lines = inputstring.splitlines()
            inputstring = StringList(lines, document.current_source)
        super().parse(inputstring, document)


def add_package_api_version_line(app, what, name: str, obj, options, lines: List[str]):
    """Add versionadded directive to package API docstrings"""
    # We're adding versionadded directive here instead of in spack/package.py because most symbols
    # are re-exported, and we don't want to modify __doc__ of symbols we don't own.
    if name.startswith("spack.package."):
        symbol = name[len("spack.package.") :]
        for version, symbols in spack.package.api.items():
            if symbol in symbols:
                lines.extend(["", f".. versionadded:: {version}"])
                break


def skip_member(app, what, name, obj, skip, options):
    # Do not skip (Make)Executable.__call__
    if name == "__call__" and "Executable" in obj.__qualname__:
        return False
    return skip


def setup(sphinx):
    # autodoc-process-docstring
    sphinx.connect("autodoc-process-docstring", add_package_api_version_line)
    sphinx.connect("autodoc-skip-member", skip_member)
    sphinx.add_domain(PatchedPythonDomain, override=True)
    sphinx.add_source_parser(NoTabExpansionRSTParser, override=True)
    sphinx.add_lexer("spec", SpecLexer)


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

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

# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
    "sphinx.ext.autodoc",
    "sphinx.ext.graphviz",
    "sphinx.ext.intersphinx",
    "sphinx.ext.napoleon",
    "sphinx.ext.todo",
    "sphinx.ext.viewcode",
    "sphinx_copybutton",
    "sphinx_last_updated_by_git",
    "sphinx_sitemap",
    "sphinxcontrib.inkscapeconverter",
    "sphinxcontrib.programoutput",
]

copybutton_exclude = ".linenos, .gp, .go"

# Set default graphviz options
graphviz_dot_args = [
    "-Grankdir=LR",
    "-Gbgcolor=transparent",
    "-Nshape=box",
    "-Nfontname=monaco",
    "-Nfontsize=10",
]

# Get nice vector graphics
graphviz_output_format = "svg"

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

# The suffix of source filenames.
source_suffix = ".rst"

# The encoding of source files.
source_encoding = "utf-8-sig"

# The master toctree document.
master_doc = "index"

# General information about the project.
project = "Spack"
copyright = "Spack Project Developers"

# 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.
#
# The short X.Y version.
import spack
import spack.package

version = ".".join(str(s) for s in spack.spack_version_info[:2])
# The full version, including alpha/beta/rc tags.
release = spack.spack_version

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
# language = None

# Places to look for .po/.mo files for doc translations
# locale_dirs = []

# Sphinx gettext settings
gettext_compact = True
gettext_uuid = False

# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
# today = ''
# Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y'

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ["_build", "_spack_root", ".spack-env", ".spack", ".venv"]

autodoc_mock_imports = ["llnl"]
autodoc_default_options = {"no-value": True}
autodoc_preserve_defaults = True

nitpicky = True
nitpick_ignore = [
    # Python classes that intersphinx is unable to resolve
    ("py:class", "argparse.HelpFormatter"),
    ("py:class", "contextlib.contextmanager"),
    ("py:class", "module"),
    ("py:class", "_io.BufferedReader"),
    ("py:class", "_io.BytesIO"),
    ("py:class", "unittest.case.TestCase"),
    ("py:class", "_frozen_importlib_external.SourceFileLoader"),
    ("py:class", "clingo.Control"),
    ("py:class", "six.moves.urllib.parse.ParseResult"),
    ("py:class", "TextIO"),
    ("py:class", "hashlib._Hash"),
    ("py:class", "concurrent.futures._base.Executor"),
    ("py:class", "multiprocessing.context.BaseContext"),
    ("py:class", "multiprocessing.context.Process"),
    ("py:class", "posix.DirEntry"),
    ("py:class", "spack.vendor.jinja2.Environment"),
    # Spack classes that are private and we don't want to expose
    ("py:class", "spack.repo._PrependFileLoader"),
    ("py:class", "spack_repo.builtin.build_systems._checks.BuilderWithDefaults"),
    # Spack classes that intersphinx is unable to resolve
    ("py:class", "spack.version.StandardVersion"),
    ("py:class", "spack.spec.DependencySpec"),
    ("py:class", "spack.spec.ArchSpec"),
    ("py:class", "spack.spec.InstallStatus"),
    ("py:class", "spack.spec.SpecfileReaderBase"),
    ("py:class", "spack.filesystem_view.SimpleFilesystemView"),
    ("py:class", "spack.traverse.EdgeAndDepth"),
    ("py:class", "spack.vendor.archspec.cpu.microarchitecture.Microarchitecture"),
    ("py:class", "spack.compiler.CompilerCache"),
    # TypeVar that is not handled correctly
    ("py:class", "spack.llnl.util.lang.T"),
    ("py:class", "spack.llnl.util.lang.KT"),
    ("py:class", "spack.llnl.util.lang.VT"),
    ("py:class", "spack.llnl.util.lang.K"),
    ("py:class", "spack.llnl.util.lang.V"),
    ("py:class", "spack.llnl.util.lang.ClassPropertyType"),
    ("py:obj", "spack.llnl.util.lang.KT"),
    ("py:obj", "spack.llnl.util.lang.VT"),
    ("py:obj", "spack.llnl.util.lang.ClassPropertyType"),
    ("py:obj", "spack.llnl.util.lang.K"),
    ("py:obj", "spack.llnl.util.lang.V"),
]

# The reST default role (used for this markup: `text`) to use for all documents.
# default_role = None

# If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True

# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
# add_module_names = True

# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
# show_authors = False

# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []


# -- 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 = "furo"

# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = ["_themes"]

# Google Search Console verification file
html_extra_path = ["google5fda5f94b4ffb8de.html"]

# The name for this set of Sphinx documents.  If None, it defaults to
# "<project> v<release> documentation".
# html_title = None

# A shorter title for the navigation bar.  Default is the same as html_title.
# html_short_title = None

# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
html_theme_options = {
    "sidebar_hide_name": True,
    "light_logo": "spack-logo-text.svg",
    "dark_logo": "spack-logo-white-text.svg",
}

# The name of an image file (within the static path) to use as favicon of the
# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
html_favicon = "_spack_root/share/spack/logo/favicon.ico"

# 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"]

# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
html_last_updated_fmt = "%b %d, %Y"
pygments_style = "default"
pygments_dark_style = "monokai"

# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
# html_use_smartypants = True

# Custom sidebar templates, maps document names to template names.
# html_sidebars = {}

# Additional templates that should be rendered to pages, maps page names to
# template names.
# html_additional_pages = {}

# If false, no module index is generated.
# html_domain_indices = True

# If false, no index is generated.
# html_use_index = True

# If true, the index is split into individual pages for each letter.
# html_split_index = False

# If true, links to the reST sources are added to the pages.
# html_show_sourcelink = True

# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
html_show_sphinx = False

# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
# html_show_copyright = True

# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it.  The value of this option must be the
# base URL from which the finished HTML is served.
# html_use_opensearch = ''

# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None

# Base URL for the documentation, used to generate <link rel="canonical"/> for better indexing
html_baseurl = "https://spack.readthedocs.io/en/latest/"

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

# Sitemap settings
sitemap_show_lastmod = True
sitemap_url_scheme = "{link}"
sitemap_excludes = ["search.html", "_modules/*"]

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

latex_engine = "lualatex"

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': '',
}

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [("index", "Spack.tex", "Spack Documentation", "", "manual")]

# The name of an image file (relative to this directory) to place at the top of
# the title page.
# latex_logo = None

# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False

# If true, show page references after internal links.
# latex_show_pagerefs = False

# If true, show URL addresses after external links.
# latex_show_urls = False

# Documents to append as an appendix to all manuals.
# latex_appendices = []

# If false, no module index is generated.
# latex_domain_indices = True


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

# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [("index", "spack", "Spack Documentation", ["Todd Gamblin"], 1)]

# If true, show URL addresses after external links.
# man_show_urls = False


# -- 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 = [
    (
        "index",
        "Spack",
        "Spack Documentation",
        "Todd Gamblin",
        "Spack",
        "One line description of project.",
        "Miscellaneous",
    )
]

# Documents to append as an appendix to all manuals.
# texinfo_appendices = []

# If false, no module index is generated.
# texinfo_domain_indices = True

# How to display URL addresses: 'footnote', 'no', or 'inline'.
# texinfo_show_urls = 'footnote'


# -- Extension configuration -------------------------------------------------

# sphinx.ext.intersphinx
intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}

rst_epilog = f"""
.. |package_api_version| replace:: v{spack.package_api_version[0]}.{spack.package_api_version[1]}
.. |min_package_api_version| replace:: v{spack.min_package_api_version[0]}.{spack.min_package_api_version[1]}
.. |spack_version| replace:: {spack.spack_version}
"""

html_static_path = ["_static"]
html_css_files = ["css/custom.css"]
html_context = {}

if os.environ.get("READTHEDOCS", "") == "True":
    html_context["READTHEDOCS"] = True
