"""Configuration file for the Sphinx documentation builder.

This file only contains a selection of the most common options. For a full
list see the documentation:
https://www.sphinx-doc.org/en/master/usage/configuration.html
"""

# Authors: The MNE-Python contributors.
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.

import faulthandler
import os
import subprocess
import sys
from datetime import datetime, timezone
from importlib.metadata import metadata
from pathlib import Path

import matplotlib
import sphinx
from intersphinx_registry import get_intersphinx_mapping
from numpydoc import docscrape
from sphinx.config import is_serializable
from sphinx.domains.changeset import versionlabels
from sphinx_gallery.sorting import ExplicitOrder

import mne
import mne.html_templates._templates
from mne.tests.test_docstring_parameters import error_ignores
from mne.utils import (
    linkcode_resolve,
    run_subprocess,
)

assert linkcode_resolve is not None  # avoid flake warnings, used by numpydoc
matplotlib.use("agg")
faulthandler.enable()
os.environ["_MNE_BROWSER_NO_BLOCK"] = "true"
os.environ["MNE_BROWSER_OVERVIEW_MODE"] = "hidden"
os.environ["MNE_BROWSER_THEME"] = "light"
os.environ["MNE_3D_OPTION_THEME"] = "light"
# https://numba.readthedocs.io/en/latest/reference/deprecation.html#deprecation-of-old-style-numba-captured-errors  # noqa: E501
os.environ["NUMBA_CAPTURED_ERRORS"] = "new_style"
mne.html_templates._templates._COLLAPSED = True  # collapse info _repr_html_

# -- Path setup --------------------------------------------------------------

# 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.
curpath = Path(__file__).parent.resolve(strict=True)
sys.path.append(str(curpath / "sphinxext"))

from credit_tools import generate_credit_rst  # noqa: E402
from mne_doc_utils import report_scraper, reset_warnings, sphinx_logger  # noqa: E402

# -- Project information -----------------------------------------------------

project = "MNE"
td = datetime.now(tz=timezone.utc)

# We need to triage which date type we use so that incremental builds work
# (Sphinx looks at variable changes and rewrites all files if some change)
project_copyright = (
    f'2012–{td.year}, MNE Developers. Last updated <time datetime="{td.isoformat()}" class="localized">{td.strftime("%Y-%m-%d %H:%M %Z")}</time>.\n'  # noqa: E501
    """<script type="text/javascript">
function formatTimestamp() {
    document.querySelectorAll("time.localized").forEach(el => {
        const d = new Date(el.getAttribute("datetime"));
        el.textContent = d.toLocaleString("sv-SE", { "timeZoneName": "short" });
    });
}
if (document.readyState !== "loading") {
    formatTimestamp();
} else {
    document.addEventListener("DOMContentLoaded", formatTimestamp);
}
</script>"""
)
if os.getenv("MNE_FULL_DATE", "false").lower() != "true":
    project_copyright = f"2012–{td.year}, MNE Developers. Last updated locally."

# 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 full version, including alpha/beta/rc tags.
release = mne.__version__
sphinx_logger.info(f"Building documentation for MNE {release} ({mne.__file__})")
# The short X.Y version.
version = ".".join(release.split(".")[:2])

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

# If your documentation needs a minimal Sphinx version, state it here.
needs_sphinx = "6.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 = [
    # builtin
    "sphinx.ext.autodoc",
    "sphinx.ext.autosummary",
    "sphinx.ext.coverage",
    "sphinx.ext.doctest",
    "sphinx.ext.graphviz",
    "sphinx.ext.intersphinx",
    "sphinx.ext.linkcode",
    "sphinx.ext.mathjax",
    "sphinx.ext.todo",
    # contrib
    "matplotlib.sphinxext.plot_directive",
    "numpydoc",
    "sphinx_copybutton",
    "sphinx_design",
    "sphinx_gallery.gen_gallery",
    "sphinxcontrib.bibtex",
    "sphinxcontrib.youtube",
    "sphinxcontrib.towncrier.ext",
    # homegrown
    "contrib_avatars",
    "gen_commands",
    "gen_names",
    "gh_substitutions",
    "mne_substitutions",
    "newcontrib_substitutions",
    "unit_role",
    "related_software",
    "directive_formatting",
]

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

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.

# NB: changes here should also be made to the linkcheck target in the Makefile
exclude_patterns = ["_includes", "changes/dev"]

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

# The main toctree document.
master_doc = "index"

# List of documents that shouldn't be included in the build.
unused_docs = []

# List of directories, relative to source directory, that shouldn't be searched
# for source files.
exclude_trees = ["_build"]

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

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

# -- Sphinx-Copybutton configuration -----------------------------------------
copybutton_prompt_text = r">>> |\.\.\. |\$ "
copybutton_prompt_is_regexp = True

# -- sphinxcontrib-towncrier configuration -----------------------------------

towncrier_draft_working_directory = str(curpath.parent)

# -- Intersphinx configuration -----------------------------------------------

intersphinx_mapping = {
    # More niche so didn't upstream to intersphinx_registry
    "nitime": ("https://nipy.org/nitime/", None),
    "mne_bids": ("https://mne.tools/mne-bids/stable", None),
    "mne-connectivity": ("https://mne.tools/mne-connectivity/stable", None),
    "mne-gui-addons": ("https://mne.tools/mne-gui-addons", None),
    "picard": ("https://mind-inria.github.io/picard/", None),
    "eeglabio": ("https://eeglabio.readthedocs.io/en/latest", None),
    "pybv": ("https://pybv.readthedocs.io/en/latest", None),
}
intersphinx_mapping.update(
    get_intersphinx_mapping(
        packages=set(
            """
imageio matplotlib numpy pandas python scipy statsmodels sklearn numba joblib nibabel
seaborn patsy pyvista dipy nilearn pyqtgraph
""".strip().split()
        ),
    )
)


# NumPyDoc configuration -----------------------------------------------------

# Define what extra methods numpydoc will document
docscrape.ClassDoc.extra_public_methods = mne.utils._doc_special_members
numpydoc_class_members_toctree = False
numpydoc_show_inherited_class_members = {
    "mne.Forward": False,
    "mne.Projection": False,
    "mne.SourceSpaces": False,
}
numpydoc_attributes_as_param_list = True
numpydoc_xref_param_type = True
numpydoc_xref_aliases = {
    # Python
    "file-like": ":term:`file-like <python:file object>`",
    "iterator": ":term:`iterator <python:iterator>`",
    "path-like": ":term:`path-like`",
    "array-like": ":term:`array_like <numpy:array_like>`",
    "Path": ":class:`python:pathlib.Path`",
    "bool": ":ref:`bool <python:typebool>`",
    # Matplotlib
    "colormap": ":ref:`colormap <matplotlib:colormaps>`",
    "color": ":doc:`color <matplotlib:api/colors_api>`",
    "Axes": "matplotlib.axes.Axes",
    "Figure": "matplotlib.figure.Figure",
    "Axes3D": "mpl_toolkits.mplot3d.axes3d.Axes3D",
    "ColorbarBase": "matplotlib.colorbar.ColorbarBase",
    # sklearn
    "LeaveOneOut": "sklearn.model_selection.LeaveOneOut",
    "MetadataRequest": "sklearn.utils.metadata_routing.MetadataRequest",
    "estimator": "sklearn.base.BaseEstimator",
    # joblib
    "joblib.Parallel": "joblib.Parallel",
    # nibabel
    "Nifti1Image": "nibabel.nifti1.Nifti1Image",
    "Nifti2Image": "nibabel.nifti2.Nifti2Image",
    "SpatialImage": "nibabel.spatialimages.SpatialImage",
    # MNE
    "Label": "mne.Label",
    "Forward": "mne.Forward",
    "Evoked": "mne.Evoked",
    "Info": "mne.Info",
    "SourceSpaces": "mne.SourceSpaces",
    "Epochs": "mne.Epochs",
    "Layout": "mne.channels.Layout",
    "EvokedArray": "mne.EvokedArray",
    "BiHemiLabel": "mne.BiHemiLabel",
    "AverageTFR": "mne.time_frequency.AverageTFR",
    "AverageTFRArray": "mne.time_frequency.AverageTFRArray",
    "EpochsTFR": "mne.time_frequency.EpochsTFR",
    "EpochsTFRArray": "mne.time_frequency.EpochsTFRArray",
    "RawTFR": "mne.time_frequency.RawTFR",
    "RawTFRArray": "mne.time_frequency.RawTFRArray",
    "Raw": "mne.io.Raw",
    "ICA": "mne.preprocessing.ICA",
    "Covariance": "mne.Covariance",
    "Annotations": "mne.Annotations",
    "DigMontage": "mne.channels.DigMontage",
    "VectorSourceEstimate": "mne.VectorSourceEstimate",
    "VolSourceEstimate": "mne.VolSourceEstimate",
    "VolVectorSourceEstimate": "mne.VolVectorSourceEstimate",
    "MixedSourceEstimate": "mne.MixedSourceEstimate",
    "MixedVectorSourceEstimate": "mne.MixedVectorSourceEstimate",
    "SourceEstimate": "mne.SourceEstimate",
    "Projection": "mne.Projection",
    "ConductorModel": "mne.bem.ConductorModel",
    "Dipole": "mne.Dipole",
    "DipoleFixed": "mne.DipoleFixed",
    "InverseOperator": "mne.minimum_norm.InverseOperator",
    "CrossSpectralDensity": "mne.time_frequency.CrossSpectralDensity",
    "SourceMorph": "mne.SourceMorph",
    "Xdawn": "mne.preprocessing.Xdawn",
    "Report": "mne.Report",
    "TimeDelayingRidge": "mne.decoding.TimeDelayingRidge",
    "Vectorizer": "mne.decoding.Vectorizer",
    "UnsupervisedSpatialFilter": "mne.decoding.UnsupervisedSpatialFilter",
    "TemporalFilter": "mne.decoding.TemporalFilter",
    "SSD": "mne.decoding.SSD",
    "Scaler": "mne.decoding.Scaler",
    "SPoC": "mne.decoding.SPoC",
    "PSDEstimator": "mne.decoding.PSDEstimator",
    "LinearModel": "mne.decoding.LinearModel",
    "FilterEstimator": "mne.decoding.FilterEstimator",
    "EMS": "mne.decoding.EMS",
    "CSP": "mne.decoding.CSP",
    "Beamformer": "mne.beamformer.Beamformer",
    "Transform": "mne.transforms.Transform",
    "Coregistration": "mne.coreg.Coregistration",
    "Figure3D": "mne.viz.Figure3D",
    "EOGRegression": "mne.preprocessing.EOGRegression",
    "Spectrum": "mne.time_frequency.Spectrum",
    "EpochsSpectrum": "mne.time_frequency.EpochsSpectrum",
    "EpochsFIF": "mne.Epochs",
    "EpochsEEGLAB": "mne.Epochs",
    "EpochsKIT": "mne.Epochs",
    "RawANT": "mne.io.Raw",
    "RawBCI2k": "mne.io.Raw",
    "RawBOXY": "mne.io.Raw",
    "RawBrainVision": "mne.io.Raw",
    "RawBTi": "mne.io.Raw",
    "RawCTF": "mne.io.Raw",
    "RawCurry": "mne.io.Raw",
    "RawEDF": "mne.io.Raw",
    "RawEEGLAB": "mne.io.Raw",
    "RawEGI": "mne.io.Raw",
    "RawEximia": "mne.io.Raw",
    "RawEyelink": "mne.io.Raw",
    "RawFIL": "mne.io.Raw",
    "RawGDF": "mne.io.Raw",
    "RawHitachi": "mne.io.Raw",
    "RawKIT": "mne.io.Raw",
    "RawNedf": "mne.io.Raw",
    "RawNeuralynx": "mne.io.Raw",
    "RawNihon": "mne.io.Raw",
    "RawMEF": "mne.io.Raw",
    "RawNIRX": "mne.io.Raw",
    "RawPersyst": "mne.io.Raw",
    "RawSNIRF": "mne.io.Raw",
    "Calibration": "mne.preprocessing.eyetracking.Calibration",
    # dipy
    "dipy.align.AffineMap": "dipy.align.imaffine.AffineMap",
    "dipy.align.DiffeomorphicMap": "dipy.align.imwarp.DiffeomorphicMap",
}
numpydoc_xref_ignore = {
    # words
    "and",
    "as",
    "between",
    "data",
    "instance",
    "instances",
    "input",
    "of",
    "default",
    "same",
    "shape",
    "or",
    "the",
    "with",
    "length",
    "pair",
    "matplotlib",
    "optional",
    "kwargs",
    "in",
    "dtype",
    "object",
    # shapes
    "n_vertices",
    "n_faces",
    "n_channels",
    "m",
    "n",
    "n_events",
    "n_colors",
    "n_times",
    "obj",
    "n_chan",
    "n_epochs",
    "n_picks",
    "n_ch_groups",
    "n_dipoles",
    "n_ica_components",
    "n_pos",
    "n_node_names",
    "n_tapers",
    "n_signals",
    "n_step",
    "n_freqs",
    "wsize",
    "Tx",
    "M",
    "N",
    "p",
    "q",
    "r",
    "n_observations",
    "n_regressors",
    "n_cols",
    "n_frequencies",
    "n_tests",
    "n_samples",
    "n_peaks",
    "n_permutations",
    "nchan",
    "n_points",
    "n_features",
    "n_parts",
    "n_features_new",
    "n_components",
    "n_labels",
    "n_events_in",
    "n_splits",
    "n_scores",
    "n_outputs",
    "n_trials",
    "n_estimators",
    "n_tasks",
    "nd_features",
    "n_classes",
    "n_targets",
    "n_slices",
    "n_hpi",
    "n_fids",
    "n_elp",
    "n_pts",
    "n_tris",
    "n_nodes",
    "n_nonzero",
    "n_events_out",
    "n_segments",
    "n_orient_inv",
    "n_orient_fwd",
    "n_orient",
    "n_dipoles_lcmv",
    "n_dipoles_fwd",
    "n_picks_ref",
    "n_coords",
    "n_meg",
    "n_good_meg",
    "n_moments",
    "n_patterns",
    "n_new_events",
    # sklearn subclasses
    "mapping",
    "to",
    "any",
    "pandas",
    "polars",
    "default",
    # unlinkable
    "CoregistrationUI",
    "mne_qt_browser.figure.MNEQtBrowser",
    # pooch, since its website is unreliable and users will rarely need the links
    "pooch.Unzip",
    "pooch.Untar",
    "pooch.HTTPDownloader",
}
numpydoc_validate = True
numpydoc_validation_checks = {"all"} | set(error_ignores)
numpydoc_validation_exclude = {  # set of regex
    # dict subclasses
    r"\.clear",
    r"\.get$",
    r"\.copy$",
    r"\.fromkeys",
    r"\.items",
    r"\.keys",
    r"\.move_to_end",
    r"\.pop",
    r"\.popitem",
    r"\.setdefault",
    r"\.update",
    r"\.values",
    # list subclasses
    r"\.append",
    r"\.count",
    r"\.extend",
    r"\.index",
    r"\.insert",
    r"\.remove",
    r"\.sort",
    # we currently don't document these properly (probably okay)
    r"\.__getitem__",
    r"\.__contains__",
    r"\.__hash__",
    r"\.__mul__",
    r"\.__sub__",
    r"\.__add__",
    r"\.__iter__",
    r"\.__div__",
    r"\.__neg__",
    # copied from sklearn
    r"mne\.utils\.deprecated",
}


# -- Sphinx-gallery configuration --------------------------------------------

examples_dirs = ["../tutorials", "../examples"]
gallery_dirs = ["auto_tutorials", "auto_examples"]
os.environ["_MNE_BUILDING_DOC"] = "true"

scrapers = (
    "matplotlib",
    "mne_doc_utils.gui_scraper",
    "mne_doc_utils.brain_scraper",
    "pyvista",
    "mne_doc_utils.report_scraper",
    "mne_doc_utils.mne_qt_browser_scraper",
)

compress_images = ("images", "thumbnails")
# let's make things easier on Windows users
# (on Linux and macOS it's easy enough to require this)
if sys.platform.startswith("win"):
    try:
        subprocess.check_call(["optipng", "--version"])
    except Exception:
        compress_images = ()

sphinx_gallery_parallel = int(os.getenv("MNE_DOC_BUILD_N_JOBS", "1"))
sphinx_gallery_conf = {
    "doc_module": ("mne",),
    "reference_url": dict(mne=None),
    "examples_dirs": examples_dirs,
    "subsection_order": ExplicitOrder(
        [
            "../examples/io/",
            "../examples/simulation/",
            "../examples/preprocessing/",
            "../examples/visualization/",
            "../examples/time_frequency/",
            "../examples/stats/",
            "../examples/decoding/",
            "../examples/connectivity/",
            "../examples/forward/",
            "../examples/inverse/",
            "../examples/realtime/",
            "../examples/datasets/",
            "../tutorials/intro/",
            "../tutorials/io/",
            "../tutorials/raw/",
            "../tutorials/preprocessing/",
            "../tutorials/epochs/",
            "../tutorials/evoked/",
            "../tutorials/time-freq/",
            "../tutorials/forward/",
            "../tutorials/inverse/",
            "../tutorials/stats-sensor-space/",
            "../tutorials/stats-source-space/",
            "../tutorials/machine-learning/",
            "../tutorials/clinical/",
            "../tutorials/simulation/",
            "../tutorials/sample-datasets/",
            "../tutorials/visualization/",
            "../tutorials/misc/",
        ]
    ),
    "gallery_dirs": gallery_dirs,
    "default_thumb_file": os.path.join("_static", "mne_helmet.png"),
    "backreferences_dir": "generated",
    "plot_gallery": "True",  # Avoid annoying Unicode/bool default warning
    "thumbnail_size": (160, 112),
    "remove_config_comments": True,
    "min_reported_time": 1.0,
    "abort_on_example_error": False,
    "reset_modules": (
        "matplotlib",
        "mne_doc_utils.reset_modules",
    ),  # called w/each script
    "reset_modules_order": "both",
    "image_scrapers": scrapers,
    "show_memory": sys.platform == "linux" and sphinx_gallery_parallel == 1,
    "line_numbers": False,  # messes with style
    "within_subsection_order": "FileNameSortKey",
    "capture_repr": ("_repr_html_",),
    "junit": os.path.join("..", "test-results", "sphinx-gallery", "junit.xml"),
    "matplotlib_animations": True,
    "compress_images": compress_images,
    "filename_pattern": "^((?!sgskip).)*$",
    "exclude_implicit_doc": {
        r"mne\.io\.read_raw_fif",
        r"mne\.io\.Raw",
        r"mne\.Epochs",
        r"mne.datasets.*",
    },
    "show_api_usage": "unused",
    "api_usage_ignore": (
        "("
        ".*__.*__|"  # built-ins
        ".*Base.*|.*Array.*|mne.Vector.*|mne.Mixed.*|mne.Vol.*|"  # inherited
        "mne.coreg.Coregistration.*|"  # GUI
        # common
        ".*utils.*|.*verbose()|.*copy()|.*update()|.*save()|"
        ".*get_data()|"
        # mixins
        ".*add_channels()|.*add_reference_channels()|"
        ".*anonymize()|.*apply_baseline()|.*apply_function()|"
        ".*apply_hilbert()|.*as_type()|.*decimate()|"
        ".*drop()|.*drop_channels()|.*drop_log_stats()|"
        ".*export()|.*get_channel_types()|"
        ".*get_montage()|.*interpolate_bads()|.*next()|"
        ".*pick()|.*pick_channels()|.*pick_types()|"
        ".*plot_sensors()|.*rename_channels()|"
        ".*reorder_channels()|.*savgol_filter()|"
        ".*set_eeg_reference()|.*set_channel_types()|"
        ".*set_meas_date()|.*set_montage()|.*shift_time()|"
        ".*time_as_index()|.*to_data_frame()|"
        # dictionary inherited
        ".*clear()|.*fromkeys()|.*get()|.*items()|"
        ".*keys()|.*pop()|.*popitem()|.*setdefault()|"
        ".*values()|"
        # sklearn inherited
        ".*apply()|.*decision_function()|.*fit()|"
        ".*fit_transform()|.*get_params()|.*predict()|"
        ".*predict_proba()|.*set_params()|.*transform()|"
        # I/O, also related to mixins
        ".*.remove.*|.*.write.*)"
    ),
    "copyfile_regex": r".*index\.rst",  # allow custom index.rst files
    "parallel": sphinx_gallery_parallel,
}
assert is_serializable(sphinx_gallery_conf)
# Files were renamed from plot_* with:
# find . -type f -name 'plot_*.py' -exec sh -c 'x="{}"; xn=`basename "${x}"`; git mv "$x" `dirname "${x}"`/${xn:5}' \;  # noqa


def append_attr_meth_examples(app, what, name, obj, options, lines):
    """Append SG examples backreferences to method and attr docstrings."""
    # NumpyDoc nicely embeds method and attribute docstrings for us, but it
    # does not respect the autodoc templates that would otherwise insert
    # the .. include:: lines, so we need to do it.
    # Eventually this could perhaps live in SG.
    if what in ("attribute", "method"):
        size = os.path.getsize(
            os.path.join(
                os.path.dirname(__file__),
                "generated",
                f"{name}.examples",
            )
        )
        if size > 0:
            lines += """
.. _sphx_glr_backreferences_{1}:

.. rubric:: Examples using ``{0}``:

.. minigallery:: {1}

""".format(name.split(".")[-1], name).split("\n")


def fix_sklearn_inherited_docstrings(app, what, name, obj, options, lines):
    """Fix sklearn docstrings because they use autolink and we do not."""
    if (
        name.startswith("mne.decoding.") or name.startswith("mne.preprocessing.Xdawn")
    ) and name.endswith(
        (
            ".get_metadata_routing",
            ".fit",
            ".fit_transform",
            ".set_output",
            ".transform",
        )
    ):
        if ":Parameters:" in lines:
            loc = lines.index(":Parameters:")
        else:
            loc = lines.index(":Returns:")
        lines.insert(loc, "")
        lines.insert(loc, ".. default-role:: autolink")
        lines.insert(loc, "")


# -- Other extension configuration -------------------------------------------

# Consider using http://magjac.com/graphviz-visual-editor for this
graphviz_dot_args = [
    "-Gsep=-0.5",
    "-Gpad=0.5",
    "-Nshape=box",
    "-Nfontsize=20",
    "-Nfontname=Open Sans,Arial",
]
graphviz_output_format = "svg"  # for API usage diagrams
user_agent = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Mobile Safari/537.36"  # noqa: E501
# Can eventually add linkcheck_request_headers if needed
linkcheck_ignore = [  # will be compiled to regex
    # 403 Client Error: Forbidden
    "https://doi.org/10.1002/",  # onlinelibrary.wiley.com/doi/10.1002/hbm
    "https://doi.org/10.1017/",  # cambridge.org
    "https://doi.org/10.1016/",  # neuroimage
    "https://doi.org/10.1021/",  # pubs.acs.org/doi/abs
    "https://doi.org/10.1063/",  # pubs.aip.org/aip/jap
    "https://doi.org/10.1073/",  # pnas.org
    "https://doi.org/10.1080/",  # www.tandfonline.com
    "https://doi.org/10.1088/",  # www.tandfonline.com
    "https://doi.org/10.1090/",  # ams.org
    "https://doi.org/10.1093/",  # academic.oup.com/sleep/
    "https://doi.org/10.1098/",  # royalsocietypublishing.org
    "https://doi.org/10.1101/",  # www.biorxiv.org
    "https://doi.org/10.1103/",  # journals.aps.org/rmp
    "https://doi.org/10.1111/",  # onlinelibrary.wiley.com/doi/10.1111/psyp
    "https://doi.org/10.1126/",  # www.science.org
    "https://doi.org/10.1137/",  # epubs.siam.org
    "https://doi.org/10.1145/",  # dl.acm.org
    "https://doi.org/10.1155/",  # www.hindawi.com/journals/cin
    "https://doi.org/10.1161/",  # www.ahajournals.org
    "https://doi.org/10.1162/",  # direct.mit.edu/neco/article/
    "https://doi.org/10.1167/",  # jov.arvojournals.org
    "https://doi.org/10.1177/",  # journals.sagepub.com
    "https://doi.org/10.1523/",  # jneurosci.org
    "https://doi.org/10.3109/",  # www.tandfonline.com
    "https://doi.org/10.3390/",  # mdpi.com
    "https://hms.harvard.edu/",  # doc/funding.rst
    "https://stackoverflow.com/questions/21752259/python-why-pickle",  # doc/help/faq
    "https://blender.org",
    "https://home.alexk101.dev",
    "https://www.mq.edu.au/",
    "https://www.biorxiv.org/content/10.1101/",  # biorxiv.org
    "https://www.researchgate.net/profile/",
    "https://www.intel.com/content/www/us/en/developer/tools/oneapi/onemkl.html",
    r"https://scholar.google.com/scholar\?cites=12188330066413208874&as_ylo=2014",
    r"https://scholar.google.com/scholar\?cites=1521584321377182930&as_ylo=2013",
    "https://www.research.chop.edu/imaging",
    "http://prdownloads.sourceforge.net/optipng",
    "https://sourceforge.net/projects/aespa/files/",
    "https://sourceforge.net/projects/ezwinports/files/",
    r"https://.*\.sourceforge\.net/",
    "https://www.cogsci.nl/smathot",
    "https://www.mathworks.com/products/compiler/matlab-runtime.html",
    "https://medicine.umich.edu/dept/khri/ross-maddox-phd",
    "http://blog.kaggle.com/2015/08/12/july-2015-scripts-of-the-week",
    # 500 server error
    "https://openwetware.org/wiki/Beauchamp:FreeSurfer",
    # 503 Server error
    "https://hal.archives-ouvertes.fr/hal-01848442",
    # Read timed out
    "http://www.cs.ucl.ac.uk/staff/d.barber/brml",
    "https://www.cea.fr",
    "http://www.humanconnectome.org/data",
    "https://www.mail-archive.com/freesurfer@nmr.mgh.harvard.edu",
    "https://surfer.nmr.mgh.harvard.edu/fswiki/mri_normalize",
    "https://launchpad.net",
    # Max retries exceeded
    "https://doi.org/10.7488/ds/1556",
    "https://datashare.is.ed.ac.uk/handle/10283",
    "https://imaging.mrc-cbu.cam.ac.uk/imaging/MniTalairach",
    "https://www.nyu.edu/",
    # Too slow
    "https://speakerdeck.com/dengemann/",
    "https://www.dtu.dk/english/service/phonebook/person",
    "https://www.gnu.org/software/make/",
    "https://www.macports.org/",
    "https://hastie.su.domains/CASI",
    # SSL problems sometimes
    "http://ilabs.washington.edu",
    "https://psychophysiology.cpmc.columbia.edu",
    "https://erc.easme-web.eu",
    "https://www.crnl.fr",
    # Not rendered by linkcheck builder
    r"ides\.html",
]
linkcheck_anchors = False  # saves a bit of time
linkcheck_timeout = 15  # some can be quite slow
linkcheck_retries = 3
linkcheck_report_timeouts_as_broken = False

# autodoc / autosummary
autosummary_generate = True
autodoc_default_options = {"inherited-members": None}

# sphinxcontrib-bibtex
bibtex_bibfiles = ["./references.bib"]
bibtex_style = "unsrt"
bibtex_footbibliography_header = ""


# -- Nitpicky ----------------------------------------------------------------

nitpicky = True
show_warning_types = True
nitpick_ignore = [
    ("py:class", "None.  Remove all items from D."),
    (
        "py:class",
        "v, remove specified key and return the corresponding value.",
    ),  # noqa: E501
    ("py:class", "an object providing a view on D's values"),
    ("py:class", "a shallow copy of D"),
    ("py:class", "(k, v), remove and return some (key, value) pair as a"),
    ("py:class", "_FuncT"),  # type hint used in @verbose decorator
    ("py:class", "mne.utils._logging._FuncT"),
    ("py:class", "None.  Remove all items from od."),
]
nitpick_ignore_regex = [
    ("py:class", "a set-like object providing a view on D's (items|keys)"),
    ("py:class", r"None\.  Update D from (dict|mapping)/iterable E and F\."),
    # Classes whose methods we purposefully do not document
    ("py:.*", r"mne\.io\.BaseRaw.*"),  # use mne.io.Raw
    ("py:.*", r"mne\.BaseEpochs.*"),  # use mne.Epochs
    # Type hints for undocumented types
    ("py:.*", r"mne\.io\..*\.Raw.*"),  # RawEDF etc.
    ("py:.*", r"mne\.epochs\.EpochsFIF.*"),
    ("py:.*", r"mne\.io\..*\.Epochs.*"),  # EpochsKIT etc.
    (  # BaseRaw attributes are documented in Raw
        "py:obj",
        "(filename|metadata|proj|times|tmax|tmin|annotations|ch_names"
        "|compensation_grade|duration|filenames|first_samp|first_time"
        "|last_samp|n_times|proj|times|tmax|tmin)",
    ),
]
suppress_warnings = [
    "image.nonlocal_uri",  # we intentionally link outside
]


# -- Sphinx hacks / overrides ------------------------------------------------

versionlabels["versionadded"] = sphinx.locale._("New in v%s")

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

# 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.
switcher_version_match = "dev" if ".dev" in release else version
html_theme_options = {
    "icon_links": [
        dict(
            name="Discord (office hours)",
            url="https://discord.gg/rKfvxTuATa",
            icon="fa-brands fa-discord fa-fw",
        ),
        dict(
            name="Mastodon",
            url="https://fosstodon.org/@mne",
            icon="fa-brands fa-mastodon fa-fw",
            attributes=dict(rel="me"),
        ),
        dict(
            name="Q&A Forum",
            url="https://mne.discourse.group/",
            icon="fa-brands fa-discourse fa-fw",
        ),
        dict(
            name="Code Repository",
            url="https://github.com/mne-tools/mne-python",
            icon="fa-brands fa-github fa-fw",
        ),
        dict(
            name="Sponsor us on GitHub",
            url="https://github.com/sponsors/mne-tools",
            icon="fa-regular fa-heart fa-fw",
        ),
        dict(
            name="Donate via OpenCollective",
            url="https://opencollective.com/mne-python",
            icon="fa-custom fa-opencollective fa-fw",
        ),
    ],
    "icon_links_label": "External Links",  # for screen reader
    "use_edit_page_button": False,
    "navigation_with_keys": False,
    "show_toc_level": 1,
    "article_header_start": [],  # disable breadcrumbs
    "navbar_end": [
        "theme-switcher",
        "version-switcher",
        "navbar-icon-links",
    ],
    "navbar_align": "left",
    "navbar_persistent": ["search-button"],
    "footer_start": ["copyright"],
    "secondary_sidebar_items": ["page-toc", "edit-this-page"],
    "analytics": dict(google_analytics_id="G-5TBCPCRB6X"),
    "switcher": {
        "json_url": "https://mne.tools/dev/_static/versions.json",
        "version_match": switcher_version_match,
    },
    "back_to_top_button": False,
}

# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
html_logo = "_static/mne_logo_small.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 = "_static/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"]
html_css_files = [
    "style.css",
]
html_js_files = [
    ("js/custom-icons.js", {"defer": "defer"}),
]

# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
html_extra_path = [
    "contributing.html",
    "documentation.html",
    "getting_started.html",
    "install_mne_python.html",
]

# Custom sidebar templates, maps document names to template names.
html_sidebars = {
    "index": ["sidebar-quicklinks.html"],
}

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

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

# accommodate different logo shapes (width values in rem)
xs = "2"
sm = "2.5"
md = "3"
lg = "4.5"
xl = "5"
xxl = "6"
# variables to pass to HTML templating engine
html_context = {
    "default_mode": "auto",
    # next 3 are for the "edit this page" button
    "github_user": "mne-tools",
    "github_repo": "mne-python",
    "github_version": "main",
    "doc_path": "doc",
    "funders": [
        dict(img="nih.svg", size="3", title="National Institutes of Health"),
        dict(img="nsf.png", size="3.5", title="US National Science Foundation"),
        dict(
            img="erc.svg",
            size="3.5",
            title="European Research Council",
            klass="only-light",
        ),
        dict(
            img="erc-dark.svg",
            size="3.5",
            title="European Research Council",
            klass="only-dark",
        ),
        dict(img="doe.svg", size="3", title="US Department of Energy"),
        dict(img="anr.svg", size="3.5", title="Agence Nationale de la Recherche"),
        dict(
            img="cds.svg",
            size="1.75",
            title="Paris-Saclay Center for Data Science",
            klass="only-light",
        ),
        dict(
            img="cds-dark.svg",
            size="1.75",
            title="Paris-Saclay Center for Data Science",
            klass="only-dark",
        ),
        dict(img="google.svg", size="2.25", title="Google"),
        dict(img="amazon.svg", size="2.5", title="Amazon"),
        dict(img="czi.svg", size="2.5", title="Chan Zuckerberg Initiative"),
    ],
    "institutions": [
        dict(
            name="Massachusetts General Hospital",
            img="MGH.svg",
            url="https://www.massgeneral.org/",
            size=sm,
        ),
        dict(
            name="Athinoula A. Martinos Center for Biomedical Imaging",
            img="Martinos.png",
            url="https://martinos.org/",
            size=md,
        ),
        dict(
            name="Harvard Medical School",
            img="Harvard.png",
            url="https://hms.harvard.edu/",
            size=sm,
        ),
        dict(
            name="Massachusetts Institute of Technology",
            img="MIT.svg",
            url="https://web.mit.edu/",
            size=md,
        ),
        dict(
            name="New York University",
            img="NYU.svg",
            url="https://www.nyu.edu/",
            size=xs,
            klass="only-light",
        ),
        dict(
            name="New York University",
            img="NYU-dark.svg",
            url="https://www.nyu.edu/",
            size=xs,
            klass="only-dark",
        ),
        dict(
            name="Commissariat à l´énergie atomique et aux énergies alternatives",
            img="CEA.png",
            url="http://www.cea.fr/",
            size=md,
        ),
        dict(
            name="Aalto-yliopiston perustieteiden korkeakoulu",
            img="Aalto.svg",
            url="https://sci.aalto.fi/",
            size=md,
            klass="only-light",
        ),
        dict(
            name="Aalto-yliopiston perustieteiden korkeakoulu",
            img="Aalto-dark.svg",
            url="https://sci.aalto.fi/",
            size=md,
            klass="only-dark",
        ),
        dict(
            name="Télécom ParisTech",
            img="Telecom_Paris_Tech.svg",
            url="https://www.telecom-paris.fr/",
            size=md,
        ),
        dict(
            name="University of Washington",
            img="Washington.svg",
            url="https://www.washington.edu/",
            size=md,
            klass="only-light",
        ),
        dict(
            name="University of Washington",
            img="Washington-dark.svg",
            url="https://www.washington.edu/",
            size=md,
            klass="only-dark",
        ),
        dict(
            name="Institut du Cerveau et de la Moelle épinière",
            img="ICM.jpg",
            url="https://icm-institute.org/",
            size=md,
        ),
        dict(
            name="Boston University", img="BU.svg", url="https://www.bu.edu/", size=lg
        ),
        dict(
            name="Institut national de la santé et de la recherche médicale",
            img="Inserm.svg",
            url="https://www.inserm.fr/",
            size=xl,
            klass="only-light",
        ),
        dict(
            name="Institut national de la santé et de la recherche médicale",
            img="Inserm-dark.svg",
            url="https://www.inserm.fr/",
            size=xl,
            klass="only-dark",
        ),
        dict(
            name="Forschungszentrum Jülich",
            img="Julich.svg",
            url="https://www.fz-juelich.de/",
            size=xl,
            klass="only-light",
        ),
        dict(
            name="Forschungszentrum Jülich",
            img="Julich-dark.svg",
            url="https://www.fz-juelich.de/",
            size=xl,
            klass="only-dark",
        ),
        dict(
            name="Technische Universität Ilmenau",
            img="Ilmenau.svg",
            url="https://www.tu-ilmenau.de/",
            size=xxl,
            klass="only-light",
        ),
        dict(
            name="Technische Universität Ilmenau",
            img="Ilmenau-dark.svg",
            url="https://www.tu-ilmenau.de/",
            size=xxl,
            klass="only-dark",
        ),
        dict(
            name="Berkeley Institute for Data Science",
            img="BIDS.svg",
            url="https://bids.berkeley.edu/",
            size=lg,
            klass="only-light",
        ),
        dict(
            name="Berkeley Institute for Data Science",
            img="BIDS-dark.svg",
            url="https://bids.berkeley.edu/",
            size=lg,
            klass="only-dark",
        ),
        dict(
            name="Institut national de recherche en informatique et en automatique",
            img="inria.png",
            url="https://www.inria.fr/",
            size=xl,
        ),
        dict(
            name="Aarhus Universitet",
            img="Aarhus.svg",
            url="https://www.au.dk/",
            size=xl,
            klass="only-light",
        ),
        dict(
            name="Aarhus Universitet",
            img="Aarhus-dark.svg",
            url="https://www.au.dk/",
            size=xl,
            klass="only-dark",
        ),
        dict(
            name="Karl-Franzens-Universität Graz",
            img="Graz.svg",
            url="https://www.uni-graz.at/",
            size=md,
        ),
        dict(
            name="SWPS Uniwersytet Humanistycznospołeczny",
            img="SWPS.svg",
            url="https://www.swps.pl/",
            size=xl,
            klass="only-light",
        ),
        dict(
            name="SWPS Uniwersytet Humanistycznospołeczny",
            img="SWPS-dark.svg",
            url="https://www.swps.pl/",
            size=xl,
            klass="only-dark",
        ),
        dict(
            name="Max-Planck-Institut für Bildungsforschung",
            img="MPIB.svg",
            url="https://www.mpib-berlin.mpg.de/",
            size=xxl,
            klass="only-light",
        ),
        dict(
            name="Max-Planck-Institut für Bildungsforschung",
            img="MPIB-dark.svg",
            url="https://www.mpib-berlin.mpg.de/",
            size=xxl,
            klass="only-dark",
        ),
        dict(
            name="Macquarie University",
            img="Macquarie.svg",
            url="https://www.mq.edu.au/",
            size=lg,
            klass="only-light",
        ),
        dict(
            name="Macquarie University",
            img="Macquarie-dark.svg",
            url="https://www.mq.edu.au/",
            size=lg,
            klass="only-dark",
        ),
        dict(
            name="AE Studio",
            img="AE-Studio-light.svg",
            url="https://ae.studio/",
            size=xxl,
            klass="only-light",
        ),
        dict(
            name="AE Studio",
            img="AE-Studio-dark.svg",
            url="https://ae.studio/",
            size=xxl,
            klass="only-dark",
        ),
        dict(
            name="Children’s Hospital of Philadelphia Research Institute",
            img="CHOP.svg",
            url="https://www.research.chop.edu/imaging",
            size=xxl,
            klass="only-light",
        ),
        dict(
            name="Children’s Hospital of Philadelphia Research Institute",
            img="CHOP-dark.svg",
            url="https://www.research.chop.edu/imaging",
            size=xxl,
            klass="only-dark",
        ),
        dict(
            name="Donders Institute for Brain, Cognition and Behaviour at Radboud University",  # noqa E501
            img="Donders.png",
            url="https://www.ru.nl/donders/",
            size=xl,
        ),
        dict(
            name="Fondation Campus Biotech Geneva",
            img="FCBG.svg",
            url="https://fcbg.ch/",
            size=sm,
        ),
    ],
    # \u00AD is an optional hyphen (not rendered unless needed)
    # If these are changed, the Makefile should be updated, too
    "carousel": [
        dict(
            title="Source Estimation",
            text="Distributed, sparse, mixed-norm, beam\u00adformers, dipole fitting, and more.",  # noqa E501
            url="auto_tutorials/inverse/index.html",
            img="sphx_glr_30_mne_dspm_loreta_008.gif",
            alt="dSPM",
        ),
        dict(
            title="Machine Learning",
            text="Advanced decoding models including time general\u00adiza\u00adtion.",
            url="auto_tutorials/machine-learning/50_decoding.html",
            img="sphx_glr_50_decoding_006.png",
            alt="Decoding",
        ),
        dict(
            title="Encoding Models",
            text="Receptive field estima\u00adtion with optional smooth\u00adness priors.",  # noqa E501
            url="auto_tutorials/machine-learning/30_strf.html",
            img="sphx_glr_30_strf_001.png",
            alt="STRF",
        ),
        dict(
            title="Statistics",
            text="Parametric and non-parametric, permutation tests and clustering.",
            url="auto_tutorials/stats-source-space/index.html",
            img="sphx_glr_20_cluster_1samp_spatiotemporal_001.png",
            alt="Clusters",
        ),
        dict(
            title="Connectivity",
            text="All-to-all spectral and effective connec\u00adtivity measures.",
            url="https://mne.tools/mne-connectivity/stable/auto_examples/mne_inverse_label_connectivity.html",  # noqa E501
            img="https://mne.tools/mne-connectivity/stable/_images/sphx_glr_mne_inverse_label_connectivity_001.png",  # noqa E501
            alt="Connectivity",
        ),
        dict(
            title="Data Visualization",
            text="Explore your data from multiple perspectives.",
            url="auto_tutorials/evoked/20_visualize_evoked.html",
            img="sphx_glr_20_visualize_evoked_010.png",
            alt="Visualization",
        ),
    ],
}

# Output file base name for HTML help builder.
htmlhelp_basename = "mne-doc"


# -- Options for plot_directive ----------------------------------------------

# Adapted from SciPy
plot_include_source = True
plot_formats = [("png", 96)]
plot_html_show_formats = False
plot_html_show_source_link = False
font_size = 13 * 72 / 96.0  # 13 px
plot_rcparams = {
    "font.size": font_size,
    "axes.titlesize": font_size,
    "axes.labelsize": font_size,
    "xtick.labelsize": font_size,
    "ytick.labelsize": font_size,
    "legend.fontsize": font_size,
    "figure.figsize": (6, 5),
    "figure.subplot.bottom": 0.2,
    "figure.subplot.left": 0.2,
    "figure.subplot.right": 0.9,
    "figure.subplot.top": 0.85,
    "figure.subplot.wspace": 0.4,
    "text.usetex": False,
}


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

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

# The name of an image file (relative to this directory) to place at the top of
# the title page.
latex_logo = "_static/logo.png"

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

# -- Warnings management -----------------------------------------------------
reset_warnings(None, None)

# -- Fontawesome support -----------------------------------------------------
brand_icons = ("apple", "linux", "windows", "discourse", "python")
fixed_width_icons = (
    # homepage:
    "book",
    "code-branch",
    "newspaper",
    "circle-question",
    "quote-left",
    # contrib guide:
    "bug-slash",
    "comment",
    "computer-mouse",
    "hand-sparkles",
    "pencil",
    "text-slash",
    "universal-access",
    "wand-magic-sparkles",
    "discourse",
    "python",
)
other_icons = (
    "hand-paper",
    "question",
    "rocket",
    "server",
    "code",
    "desktop",
    "terminal",
    "cloud-arrow-down",
    "wrench",
    "hourglass-half",
)
icon_class = dict()
for icon in brand_icons + fixed_width_icons + other_icons:
    icon_class[icon] = ("fa-brands",) if icon in brand_icons else ("fa-solid",)
    icon_class[icon] += ("fa-fw",) if icon in fixed_width_icons else ()

rst_prolog = ""
for icon, classes in icon_class.items():
    rst_prolog += f"""
.. |{icon}| raw:: html

    <i class="{" ".join(classes + (f"fa-{icon}",))}"></i>
"""

rst_prolog += """
.. |ensp| unicode:: U+2002 .. EN SPACE

.. include:: /links.inc
.. include:: /changes/names.inc

.. currentmodule:: mne
"""

# -- Dependency info ----------------------------------------------------------

min_py = metadata("mne")["Requires-Python"].lstrip(" =<>")
rst_prolog += f"\n.. |min_python_version| replace:: {min_py}\n"

# -- website redirects --------------------------------------------------------

# Static list created 2021/04/13 based on what we needed to redirect,
# since we don't need to add redirects for examples added after this date.
needed_plot_redirects = {
    # tutorials
    "10_epochs_overview.py",
    "10_evoked_overview.py",
    "10_overview.py",
    "10_preprocessing_overview.py",
    "10_raw_overview.py",
    "10_reading_meg_data.py",
    "15_handling_bad_channels.py",
    "20_event_arrays.py",
    "20_events_from_raw.py",
    "20_reading_eeg_data.py",
    "20_rejecting_bad_data.py",
    "20_visualize_epochs.py",
    "20_visualize_evoked.py",
    "30_annotate_raw.py",
    "30_epochs_metadata.py",
    "30_filtering_resampling.py",
    "30_info.py",
    "30_reading_fnirs_data.py",
    "35_artifact_correction_regression.py",
    "40_artifact_correction_ica.py",
    "40_autogenerate_metadata.py",
    "40_sensor_locations.py",
    "40_visualize_raw.py",
    "45_projectors_background.py",
    "50_artifact_correction_ssp.py",
    "50_configure_mne.py",
    "50_epochs_to_data_frame.py",
    "55_setting_eeg_reference.py",
    "59_head_positions.py",
    "60_make_fixed_length_epochs.py",
    "60_maxwell_filtering_sss.py",
    "70_fnirs_processing.py",
    # examples
    "3d_to_2d.py",
    "brainstorm_data.py",
    "channel_epochs_image.py",
    "cluster_stats_evoked.py",
    "compute_csd.py",
    "compute_mne_inverse_epochs_in_label.py",
    "compute_mne_inverse_raw_in_label.py",
    "compute_mne_inverse_volume.py",
    "compute_source_psd_epochs.py",
    "covariance_whitening_dspm.py",
    "custom_inverse_solver.py",
    "decoding_csp_eeg.py",
    "decoding_csp_timefreq.py",
    "decoding_spatio_temporal_source.py",
    "decoding_spoc_CMC.py",
    "decoding_time_generalization_conditions.py",
    "decoding_unsupervised_spatial_filter.py",
    "decoding_xdawn_eeg.py",
    "define_target_events.py",
    "dics_source_power.py",
    "eeg_csd.py",
    "eeg_on_scalp.py",
    "eeglab_head_sphere.py",
    "elekta_epochs.py",
    "ems_filtering.py",
    "eog_artifact_histogram.py",
    "evoked_arrowmap.py",
    "evoked_ers_source_power.py",
    "evoked_topomap.py",
    "evoked_whitening.py",
    "fdr_stats_evoked.py",
    "find_ref_artifacts.py",
    "fnirs_artifact_removal.py",
    "forward_sensitivity_maps.py",
    "gamma_map_inverse.py",
    "hf_sef_data.py",
    "ica_comparison.py",
    "interpolate_bad_channels.py",
    "label_activation_from_stc.py",
    "label_from_stc.py",
    "label_source_activations.py",
    "left_cerebellum_volume_source.py",
    "limo_data.py",
    "linear_model_patterns.py",
    "linear_regression_raw.py",
    "meg_sensors.py",
    "mixed_norm_inverse.py",
    "mixed_source_space_inverse.py",
    "mne_cov_power.py",
    "mne_helmet.py",
    "mne_inverse_coherence_epochs.py",
    "mne_inverse_envelope_correlation.py",
    "mne_inverse_envelope_correlation_volume.py",
    "mne_inverse_psi_visual.py",
    "morph_surface_stc.py",
    "morph_volume_stc.py",
    "movement_compensation.py",
    "movement_detection.py",
    "multidict_reweighted_tfmxne.py",
    "muscle_detection.py",
    "opm_data.py",
    "otp.py",
    "parcellation.py",
    "psf_ctf_label_leakage.py",
    "psf_ctf_vertices.py",
    "psf_ctf_vertices_lcmv.py",
    "publication_figure.py",
    "rap_music.py",
    "trap_music.py",
    "read_inverse.py",
    "read_neo_format.py",
    "read_noise_covariance_matrix.py",
    "read_stc.py",
    "receptive_field_mtrf.py",
    "resolution_metrics.py",
    "resolution_metrics_eegmeg.py",
    "roi_erpimage_by_rt.py",
    "sensor_noise_level.py",
    "sensor_permutation_test.py",
    "sensor_regression.py",
    "shift_evoked.py",
    "simulate_evoked_data.py",
    "simulate_raw_data.py",
    "simulated_raw_data_using_subject_anatomy.py",
    "snr_estimate.py",
    "source_label_time_frequency.py",
    "source_power_spectrum.py",
    "source_power_spectrum_opm.py",
    "source_simulator.py",
    "source_space_morphing.py",
    "source_space_snr.py",
    "source_space_time_frequency.py",
    "ssd_spatial_filters.py",
    "ssp_projs_sensitivity_map.py",
    "temporal_whitening.py",
    "time_frequency_erds.py",
    "time_frequency_global_field_power.py",
    "time_frequency_mixed_norm_inverse.py",
    "time_frequency_simulated.py",
    "topo_compare_conditions.py",
    "topo_customized.py",
    "vector_mne_solution.py",
    "virtual_evoked.py",
    "xdawn_denoising.py",
    "xhemi.py",
}
api_redirects = {
    "connectivity",
    "covariance",
    "creating_from_arrays",
    "datasets",
    "decoding",
    "events",
    "export",
    "file_io",
    "forward",
    "inverse",
    "logging",
    "most_used_classes",
    "mri",
    "preprocessing",
    "python_reference",
    "reading_raw_data",
    "realtime",
    "report",
    "sensor_space",
    "simulation",
    "source_space",
    "statistics",
    "time_frequency",
    "visualization",
}
ex = "auto_examples"
co = "connectivity"
mne_conn = "https://mne.tools/mne-connectivity/stable"
tu = "auto_tutorials"
pr = "preprocessing"
di = "discussions"
sm = "source-modeling"
fw = "forward"
nv = "inverse"
sn = "stats-sensor-space"
sr = "stats-source-space"
sd = "sample-datasets"
ml = "machine-learning"
tf = "time-freq"
si = "simulation"
vi = "visualization"
custom_redirects = {
    # Custom redirects (one HTML path to another, relative to outdir)
    # can be added here as fr->to key->value mappings
    "install/contributing": "development/contributing",
    "overview/cite": "documentation/cite",
    "overview/get_help": "help/index",
    "overview/roadmap": "development/roadmap",
    "whats_new": "development/whats_new",
    f"{tu}/evoked/plot_eeg_erp": f"{tu}/evoked/30_eeg_erp",
    f"{tu}/evoked/plot_whitened": f"{tu}/evoked/40_whitened",
    f"{tu}/misc/plot_modifying_data_inplace": f"{tu}/intro/15_inplace",
    f"{tu}/misc/plot_report": f"{tu}/intro/70_report",
    f"{tu}/misc/plot_seeg": f"{tu}/clinical/20_seeg",
    f"{tu}/misc/plot_ecog": f"{tu}/clinical/30_ecog",
    f"{tu}/{ml}/plot_receptive_field": f"{tu}/{ml}/30_strf",
    f"{tu}/{ml}/plot_sensors_decoding": f"{tu}/{ml}/50_decoding",
    f"{tu}/{sm}/plot_background_freesurfer": f"{tu}/{fw}/10_background_freesurfer",
    f"{tu}/{sm}/plot_source_alignment": f"{tu}/{fw}/20_source_alignment",
    f"{tu}/{sm}/plot_forward": f"{tu}/{fw}/30_forward",
    f"{tu}/{sm}/plot_eeg_no_mri": f"{tu}/{fw}/35_eeg_no_mri",
    f"{tu}/{sm}/plot_background_freesurfer_mne": f"{tu}/{fw}/50_background_freesurfer_mne",  # noqa E501
    f"{tu}/{sm}/plot_fix_bem_in_blender": f"{tu}/{fw}/80_fix_bem_in_blender",
    f"{tu}/{sm}/plot_compute_covariance": f"{tu}/{fw}/90_compute_covariance",
    f"{tu}/{sm}/plot_object_source_estimate": f"{tu}/{nv}/10_stc_class",
    f"{tu}/{sm}/plot_dipole_fit": f"{tu}/{nv}/20_dipole_fit",
    f"{tu}/{sm}/plot_mne_dspm_source_localization": f"{tu}/{nv}/30_mne_dspm_loreta",
    f"{tu}/{sm}/plot_dipole_orientations": f"{tu}/{nv}/35_dipole_orientations",
    f"{tu}/{sm}/plot_mne_solutions": f"{tu}/{nv}/40_mne_fixed_free",
    f"{tu}/{sm}/plot_beamformer_lcmv": f"{tu}/{nv}/50_beamformer_lcmv",
    f"{tu}/{sm}/plot_visualize_stc": f"{tu}/{nv}/60_visualize_stc",
    f"{tu}/{sm}/plot_eeg_mri_coords": f"{tu}/{nv}/70_eeg_mri_coords",
    f"{tu}/{sd}/plot_brainstorm_phantom_elekta": f"{tu}/{nv}/80_brainstorm_phantom_elekta",  # noqa E501
    f"{tu}/{sd}/plot_brainstorm_phantom_ctf": f"{tu}/{nv}/85_brainstorm_phantom_ctf",
    f"{tu}/{sd}/plot_phantom_4DBTi": f"{tu}/{nv}/90_phantom_4DBTi",
    f"{tu}/{sd}/plot_brainstorm_auditory": f"{tu}/io/60_ctf_bst_auditory",
    f"{tu}/{sd}/plot_sleep": f"{tu}/clinical/60_sleep",
    f"{tu}/{di}/plot_background_filtering": f"{tu}/{pr}/25_background_filtering",
    f"{tu}/{di}/plot_background_statistics": f"{tu}/{sn}/10_background_stats",
    f"{tu}/{sn}/plot_stats_cluster_erp": f"{tu}/{sn}/20_erp_stats",
    f"{tu}/{sn}/plot_stats_cluster_1samp_test_time_frequency": f"{tu}/{sn}/40_cluster_1samp_time_freq",  # noqa E501
    f"{tu}/{sn}/plot_stats_cluster_time_frequency": f"{tu}/{sn}/50_cluster_between_time_freq",  # noqa E501
    f"{tu}/{sn}/plot_stats_spatio_temporal_cluster_sensors": f"{tu}/{sn}/75_cluster_ftest_spatiotemporal",  # noqa E501
    f"{tu}/{sr}/plot_stats_cluster_spatio_temporal": f"{tu}/{sr}/20_cluster_1samp_spatiotemporal",  # noqa E501
    f"{tu}/{sr}/plot_stats_cluster_spatio_temporal_2samp": f"{tu}/{sr}/30_cluster_ftest_spatiotemporal",  # noqa E501
    f"{tu}/{sr}/plot_stats_cluster_spatio_temporal_repeated_measures_anova": f"{tu}/{sr}/60_cluster_rmANOVA_spatiotemporal",  # noqa E501
    f"{tu}/{sr}/plot_stats_cluster_time_frequency_repeated_measures_anova": f"{tu}/{sn}/70_cluster_rmANOVA_time_freq",  # noqa E501
    f"{tu}/{tf}/plot_sensors_time_frequency": f"{tu}/{tf}/20_sensors_time_frequency",
    f"{tu}/{tf}/plot_ssvep": f"{tu}/{tf}/50_ssvep",
    f"{tu}/{si}/plot_creating_data_structures": f"{tu}/{si}/10_array_objs",
    f"{tu}/{si}/plot_point_spread": f"{tu}/{si}/70_point_spread",
    f"{tu}/{si}/plot_dics": f"{tu}/{si}/80_dics",
    f"{tu}/{tf}/plot_eyetracking": f"{tu}/{pr}/90_eyetracking_data",
    f"{ex}/{co}/mne_inverse_label_connectivity": f"{mne_conn}/{ex}/mne_inverse_label_connectivity",  # noqa E501
    f"{ex}/{co}/cwt_sensor_connectivity": f"{mne_conn}/{ex}/cwt_sensor_connectivity",
    f"{ex}/{co}/mixed_source_space_connectivity": f"{mne_conn}/{ex}/mixed_source_space_connectivity",  # noqa E501
    f"{ex}/{co}/mne_inverse_coherence_epochs": f"{mne_conn}/{ex}/mne_inverse_coherence_epochs",  # noqa E501
    f"{ex}/{co}/mne_inverse_connectivity_spectrum": f"{mne_conn}/{ex}/mne_inverse_connectivity_spectrum",  # noqa E501
    f"{ex}/{co}/mne_inverse_envelope_correlation_volume": f"{mne_conn}/{ex}/mne_inverse_envelope_correlation_volume",  # noqa E501
    f"{ex}/{co}/mne_inverse_envelope_correlation": f"{mne_conn}/{ex}/mne_inverse_envelope_correlation",  # noqa E501
    f"{ex}/{co}/mne_inverse_psi_visual": f"{mne_conn}/{ex}/mne_inverse_psi_visual",
    f"{ex}/{co}/sensor_connectivity": f"{mne_conn}/{ex}/sensor_connectivity",
    f"{ex}/{vi}/publication_figure": f"{tu}/{vi}/10_publication_figure",
    f"{ex}/{vi}/sensor_noise_level": f"{tu}/{pr}/50_artifact_correction_ssp",
}

# Adapted from sphinxcontrib/redirects (BSD-2-Clause)
REDIRECT_TEMPLATE = """\
<!DOCTYPE HTML>
<html lang="en-US">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="refresh" content="1; url={to}">
        <script type="text/javascript">
            window.location.href = "{to}"
        </script>
        <title>Page Redirection</title>
    </head>
    <body>
        If you are not redirected automatically, follow this <a href='{to}'>link</a>.
    </body>
</html>"""


def check_existing_redirect(path):
    """Make sure existing HTML files are redirects, before overwriting."""
    if path.is_file():
        with open(path) as fid:
            for _ in range(8):
                next(fid)
            line = fid.readline()
            if "Page Redirection" not in line:
                raise RuntimeError(
                    "Attempted overwrite of HTML file with a redirect, where the "
                    "original file was not already a redirect."
                )


def _check_valid_builder(app, exception):
    valid_builder = isinstance(app.builder, sphinx.builders.html.StandaloneHTMLBuilder)
    return valid_builder and exception is None


def make_gallery_redirects(app, exception):
    """Make HTML redirects for our sphinx gallery pages."""
    if not _check_valid_builder(app, exception):
        return
    sg_conf = app.config["sphinx_gallery_conf"]
    for src_dir, out_dir in zip(sg_conf["examples_dirs"], sg_conf["gallery_dirs"]):
        root = (Path(app.srcdir) / src_dir).resolve()
        fnames = [
            pyfile.relative_to(root)
            for pyfile in root.rglob(r"**/*.py")
            if pyfile.name in needed_plot_redirects
        ]
        # plot_ redirects
        for fname in fnames:
            dirname = Path(app.outdir) / out_dir / fname.parent
            to_fname = fname.with_suffix(".html").name
            fr_fname = f"plot_{to_fname}"
            to_path = dirname / to_fname
            fr_path = dirname / fr_fname
            assert to_path.is_file(), (fname, to_path)
            with open(fr_path, "w") as fid:
                fid.write(REDIRECT_TEMPLATE.format(to=to_fname))
        sphinx_logger.info(
            f"Added {len(fnames):3d} HTML plot_* redirects for {out_dir}"
        )


def make_api_redirects(app, exception):
    """Make HTML redirects for our API pages."""
    if not _check_valid_builder(app, exception):
        return

    for page in api_redirects:
        fname = f"{page}.html"
        fr_path = Path(app.outdir) / fname
        to_path = Path(app.outdir) / "api" / fname
        # allow overwrite if existing file is just a redirect
        check_existing_redirect(fr_path)
        with open(fr_path, "w") as fid:
            fid.write(REDIRECT_TEMPLATE.format(to=to_path))
    sphinx_logger.info(f"Added {len(api_redirects):3d} HTML API redirects")


def make_custom_redirects(app, exception):
    """Make HTML redirects for miscellaneous pages."""
    if not _check_valid_builder(app, exception):
        return

    for _fr, _to in custom_redirects.items():
        fr = f"{_fr}.html"
        to = f"{_to}.html"
        fr_path = Path(app.outdir) / fr
        check_existing_redirect(fr_path)
        if to.startswith("http"):
            to_path = to
        else:
            to_path = Path(app.outdir) / to
            assert to_path.is_file(), to_path
        # recreate folders that no longer exist
        defunct_gallery_folders = (
            "misc",
            "discussions",
            "source-modeling",
            "sample-datasets",
            "connectivity",
        )
        parts = fr_path.relative_to(Path(app.outdir)).parts
        if (
            len(parts) > 1  # whats_new violates this
            and parts[1] in defunct_gallery_folders
            and not fr_path.parent.exists()
        ):
            os.makedirs(fr_path.parent, exist_ok=True)
        # write the redirect
        with open(fr_path, "w") as fid:
            fid.write(REDIRECT_TEMPLATE.format(to=to_path))
    sphinx_logger.info(f"Added {len(custom_redirects):3d} HTML custom redirects")


def make_version(app, exception):
    """Make a text file with the git version."""
    if not (
        isinstance(app.builder, sphinx.builders.html.StandaloneHTMLBuilder)
        and exception is None
    ):
        return
    try:
        stdout, _ = run_subprocess(["git", "rev-parse", "HEAD"], verbose=False)
    except Exception as exc:
        sphinx_logger.warning(f"Failed to write _version.txt: {exc}")
        return
    with open(os.path.join(app.outdir, "_version.txt"), "w") as fid:
        fid.write(stdout)
    sphinx_logger.info(f'Added "{stdout.rstrip()}" > _version.txt')


# -- Connect our handlers to the main Sphinx app ---------------------------


def setup(app):
    """Set up the Sphinx app."""
    app.connect("autodoc-process-docstring", append_attr_meth_examples)
    app.connect("autodoc-process-docstring", fix_sklearn_inherited_docstrings)
    # High prio, will happen before SG
    app.connect("builder-inited", generate_credit_rst, priority=10)
    app.connect("builder-inited", report_scraper.set_dirs, priority=20)
    app.connect("build-finished", make_gallery_redirects)
    app.connect("build-finished", make_api_redirects)
    app.connect("build-finished", make_custom_redirects)
    app.connect("build-finished", make_version)
