"""Configuration file for the Sphinx documentation builder."""

import os
import sys
from pathlib import Path

import aeon

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

project = "aeon"
copyright = "The aeon developers (BSD-3 License)"
author = "aeon developers"

version = aeon.__version__
release = aeon.__version__

github_tag = f"v{version}"

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

# local sphinx extensions
sys.path.append(str(Path(__file__).parent / "_sphinxext"))

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here.
on_readthedocs = os.environ.get("READTHEDOCS") == "True"
if not on_readthedocs:
    sys.path.insert(0, os.path.abspath(""))
else:
    rtd_version = os.environ.get("READTHEDOCS_VERSION")
    if rtd_version == "latest":
        github_tag = "main"

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

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
    "numpydoc",
    "sphinx.ext.autosummary",
    "sphinx.ext.autodoc",
    "sphinx.ext.autosectionlabel",
    "sphinx.ext.intersphinx",
    "sphinx.ext.linkcode",
    "sphinxext.opengraph",
    "nbsphinx",
    "sphinx_design",
    "sphinx_issues",
    "sphinx_copybutton",
    "versionwarning.extension",
    "myst_parser",
    # local extensions (_sphinxext/)
    "sphinx_remove_toctrees",
]

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

# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
source_suffix = {
    ".rst": "restructuredtext",
    ".md": "markdown",
}

# The main toctree document.
master_doc = "index"

# 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.
# Usually you set "language" from the command line for these cases.
language = "en"

# 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.
exclude_patterns = [
    "_build",
    ".ipynb_checkpoints",
    "Thumbs.db",
    ".DS_Store",
]

# A boolean that decides whether module names are prepended to all object names (for
# object types where a “module” of some kind is defined), e.g. for py:function
# directives.
add_module_names = False

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

# -- numpydoc --

# see http://stackoverflow.com/q/12206334/562769
#     https://github.com/numpy/numpydoc/issues/69
# Lots of warnings
numpydoc_show_class_members = True

# Whether to create a Sphinx table of contents for the lists of class methods and
# attributes. If a table of contents is made, Sphinx expects each entry to have a
# separate page.
# We remove the toctree for API for speed and do not want separate pages for this
numpydoc_class_members_toctree = False

# Whether to produce plot:: directives for Examples sections that contain
# 'import matplotlib' or 'from matplotlib import'.
numpydoc_use_plots = True

# Options for the ::plot directive:
# https://matplotlib.org/stable/api/sphinxext_plot_directive_api.html
plot_formats = ["png"]
plot_include_source = True
plot_html_show_formats = False
plot_html_show_source_link = False

# This will produce warnings for docstring errors.
numpydoc_validation_checks = {
    "all",
    "GL01",  # docstring starts after opening quotes
    "ES01",  # no extended summary
    "SA01",  # no see also section
    "EX01",  # no examples
}

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

# -- autosummary and autodoc --

# generate autosummary even if no references
autosummary_generate = True

# Members and inherited-members default to showing methods and attributes from a
# class or those inherited.
# Member-order orders the documentation in the order of how the members are defined in
# the source code.
autodoc_default_options = {
    "class-doc-from": "class",
    "members": True,
    "exclude-members": "get_metadata_routing, set_output",
    "inherited-members": True,
    "show-inheritance": True,
}

# -- autosectionlabel --

autosectionlabel_maxdepth = 4

# -- intersphinx --

intersphinx_mapping = {
    "python": ("https://docs.python.org/3/", None),
    "numpy": ("https://numpy.org/doc/stable/", None),
    "scipy": ("https://docs.scipy.org/doc/scipy/", None),
    "matplotlib": ("https://matplotlib.org/stable/", None),
    "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None),
    "joblib": ("https://joblib.readthedocs.io/en/latest/", None),
    "scikit-learn": ("https://scikit-learn.org/stable/", None),
}

# -- nbsphinx --

nbsphinx_execute = "never"  # whether to run notebooks
nbsphinx_allow_errors = False
nbsphinx_timeout = 600  # seconds, set to -1 to disable timeout

# Binder launch button
current_file = "{{ env.doc2path( env.docname, base=None) }}"
binder_url = (
    f"https://mybinder.org/v2/gh/aeon-toolkit/aeon/{github_tag}?filepath={current_file}"
)

# link to original notebook
notebook_url = f"https://github.com/aeon-toolkit/aeon/tree/{github_tag}/{current_file}"

# add to the bottom of each notebook page
nbsphinx_epilog = f"""
----

Generated using nbsphinx_. The Jupyter notebook can be found here_.

|Binder|_

.. _here: {notebook_url}
.. _nbsphinx: https://nbsphinx.readthedocs.io/
.. |binder| image:: https://mybinder.org/badge_logo.svg
.. _Binder: {binder_url}
"""

# -- sphinx_issues --

issues_github_path = "aeon-toolkit/aeon"

# -- sphinx_copybutton --

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

# -- MyST parser --

# "colon_fence" and "html_image" recommended by sphinx_design when using the MyST Parser
myst_enable_extensions = ["colon_fence", "html_image", "attrs_inline"]

myst_heading_anchors = 4

# --  sphinx-remove-toctrees --

# see https://github.com/pradyunsg/furo/pull/674
# we use an in-built alternative currently due to a bug. See sphinx_remove_toctrees.py
# extension issue https://github.com/executablebooks/sphinx-remove-toctrees/issues/9
remove_from_toctrees = [
    "api_reference/auto_generated/*",  # including all api pages significantly slows
    # down the documentation build
]

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

# The theme to use for HTML.
html_theme = "furo"

# 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 = {
    "sidebar_hide_name": True,
    "top_of_page_button": "edit",
    "source_repository": "https://github.com/aeon-toolkit/aeon/",
    "source_branch": "main",
    "source_directory": "docs/",
    "light_css_variables": {
        "color-brand-primary": "#005E80",
        "color-brand-content": "#F05F05",
    },
    "dark_css_variables": {
        "color-brand-primary": "#00ACEB",
        "color-brand-content": "#FB9456",
    },
    "footer_icons": [
        {
            "name": "Slack",
            "url": "https://join.slack.com/t/aeon-toolkit/shared_invite/zt-36dlmbouu-vajTShUYAHopSXUUVtHGzw",  # noqa: E501
            "html": """
            <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
                <path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zM361.5 580.2c0 27.8-22.5 50.4-50.3 50.4-13.3 0-26.1-5.3-35.6-14.8-9.4-9.5-14.7-22.3-14.7-35.6 0-27.8 22.5-50.4 50.3-50.4h50.3v50.4zm134 134.4c0 27.8-22.5 50.4-50.3 50.4-27.8 0-50.3-22.6-50.3-50.4V580.2c0-27.8 22.5-50.4 50.3-50.4 13.3 0 26.1 5.3 35.6 14.8s14.7 22.3 14.7 35.6v134.4zm-50.2-218.4h-134c-27.8 0-50.3-22.6-50.3-50.4 0-27.8 22.5-50.4 50.3-50.4h134c27.8 0 50.3 22.6 50.3 50.4-.1 27.9-22.6 50.4-50.3 50.4zm0-134.4c-13.3 0-26.1-5.3-35.6-14.8S395 324.8 395 311.4c0-27.8 22.5-50.4 50.3-50.4 27.8 0 50.3 22.6 50.3 50.4v50.4h-50.3zm83.7-50.4c0-27.8 22.5-50.4 50.3-50.4 27.8 0 50.3 22.6 50.3 50.4v134.4c0 27.8-22.5 50.4-50.3 50.4-27.8 0-50.3-22.6-50.3-50.4V311.4zM579.3 765c-27.8 0-50.3-22.6-50.3-50.4v-50.4h50.3c27.8 0 50.3 22.6 50.3 50.4 0 27.8-22.5 50.4-50.3 50.4zm134-134.4h-134c-13.3 0-26.1-5.3-35.6-14.8S529 593.6 529 580.2c0-27.8 22.5-50.4 50.3-50.4h134c27.8 0 50.3 22.6 50.3 50.4 0 27.8-22.5 50.4-50.3 50.4zm0-134.4H663v-50.4c0-27.8 22.5-50.4 50.3-50.4s50.3 22.6 50.3 50.4c0 27.8-22.5 50.4-50.3 50.4z"></path>
            </svg>
            """,  # noqa: E501
            "class": "",
        },
        {
            "name": "LinkedIn",
            "url": "https://www.linkedin.com/company/aeon-toolkit/",
            "html": """
            <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
                <path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zM349.3 793.7H230.6V411.9h118.7v381.8zm-59.3-434a68.8 68.8 0 1 1 68.8-68.8c-.1 38-30.9 68.8-68.8 68.8zm503.7 434H675.1V608c0-44.3-.8-101.2-61.7-101.2-61.7 0-71.2 48.2-71.2 98v188.9H423.7V411.9h113.8v52.2h1.6c15.8-30 54.5-61.7 112.3-61.7 120.2 0 142.3 79.1 142.3 181.9v209.4z"></path>
            </svg>
            """,  # noqa: E501
            "class": "",
        },
        {
            "name": "X/Twitter",
            "url": "https://twitter.com/aeon_toolkit",
            "html": """
            <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
                <path d="M928 254.3c-30.6 13.2-63.9 22.7-98.2 26.4a170.1 170.1 0 0 0 75-94 336.64 336.64 0 0 1-108.2 41.2A170.1 170.1 0 0 0 672 174c-94.5 0-170.5 76.6-170.5 170.6 0 13.2 1.6 26.4 4.2 39.1-141.5-7.4-267.7-75-351.6-178.5a169.32 169.32 0 0 0-23.2 86.1c0 59.2 30.1 111.4 76 142.1a172 172 0 0 1-77.1-21.7v2.1c0 82.9 58.6 151.6 136.7 167.4a180.6 180.6 0 0 1-44.9 5.8c-11.1 0-21.6-1.1-32.2-2.6C211 652 273.9 701.1 348.8 702.7c-58.6 45.9-132 72.9-211.7 72.9-14.3 0-27.5-.5-41.2-2.1C171.5 822 261.2 850 357.8 850 671.4 850 843 590.2 843 364.7c0-7.4 0-14.8-.5-22.2 33.2-24.3 62.3-54.4 85.5-88.2z"></path>
            </svg>
            """,  # noqa: E501
            "class": "",
        },
        {
            "name": "Medium",
            "url": "https://medium.com/@aeon.toolkit",
            "html": """
            <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
                <path d="M6.158 4h11.684C19.034 4 20 4.966 20 6.158v3.455a2.9 2.9 0 0 0-1.539 1.003c-.472.586-.758 1.377-.828 2.266q-.022.266-.017.532c.041 1.763.88 3.216 2.384 3.55v.878A2.16 2.16 0 0 1 17.842 20H6.158A2.16 2.16 0 0 1 4 17.842V6.158C4 4.966 4.966 4 6.158 4M21 6.158A3.16 3.16 0 0 0 17.842 3H6.158A3.16 3.16 0 0 0 3 6.158v11.684A3.16 3.16 0 0 0 6.158 21h11.684A3.16 3.16 0 0 0 21 17.842zm-1 4.14v1.983h-.616c.039-.867.253-1.58.616-1.983m0 2.364v2.063c-.441-.513-.699-1.25-.653-2.063zM17.697 7.3l.015-.003v-.11h-2.9l-2.69 6.326L9.43 7.187H6.306v.11l.014.003c.529.12.798.298.798.94v7.52c0 .642-.27.82-.8.94l-.013.002v.11h2.12v-.11L8.41 16.7c-.529-.12-.798-.298-.798-.94V8.676l3.458 8.137h.196l3.559-8.364v7.496c-.046.508-.312.665-.791.773l-.014.003v.109h3.692v-.11l-.015-.002c-.48-.108-.752-.265-.797-.773l-.003-7.705h.003c0-.642.269-.82.797-.94"></path>
            </svg>
            """,  # noqa: E501
            "class": "",
        },
        {
            "name": "GitHub",
            "url": "https://github.com/aeon-toolkit/aeon",
            "html": """
            <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
                <path d="M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9a127.5 127.5 0 0 1 38.1 91v112.5c.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z"></path>
            </svg>
            """,  # noqa: E501
            "class": "",
        },
    ],
}

# logos
html_logo = "images/logo/aeon-logo-blue-2-compact.png"
html_favicon = "images/logo/aeon-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 = ["css/custom.css"]

html_show_sourcelink = False

# -- Documentation functions and setup ---------------------------------------


def linkcode_resolve(domain, info):
    """Return URL to source code corresponding.

    Parameters
    ----------
    domain : str
    info : dict

    Returns
    -------
    url : str
    """

    def find_source():
        # try to find the file and line number, based on code from numpy:
        # https://github.com/numpy/numpy/blob/main/doc/source/conf.py#L286

        import inspect
        import os

        # Get the top-level object from the module name
        obj = sys.modules[info["module"]]

        # Traverse dotted path (e.g., module.submodule.Class.method)
        for part in info["fullname"].split("."):
            obj = getattr(obj, part)

        # Unwrapping decorators (if any), so we can get the true
        # source function
        if inspect.isfunction(obj):
            obj = inspect.unwrap(obj)

        # Get the source filename
        try:
            fn = inspect.getsourcefile(obj)
        except TypeError:
            fn = None

        # If no source file is found, return None (no link)
        if not fn:
            return None

        # Make filename relative to the aeon source directory
        startdir = Path(aeon.__file__).parent.parent
        try:
            fn = os.path.relpath(fn, start=startdir).replace(os.path.sep, "/")
        except ValueError:
            return None

        # Filter out files not in the aeon package
        # (e.g., inherited from sklearn)
        if not fn.startswith("aeon/"):
            return None

        # Get line range of the object
        source, lineno = inspect.getsourcelines(obj)
        return fn, lineno, lineno + len(source) - 1

    if domain != "py" or not info["module"]:
        return None
    try:
        result = find_source()
        if not result:
            return None
        filename = "%s#L%d-L%d" % result
    except Exception:
        filename = info["module"].replace(".", "/") + ".py"
    return "https://github.com/aeon-toolkit/aeon/blob/{}/{}".format(
        github_tag,
        filename,
    )


def _make_estimator_overview(app):
    """Make estimator overview table."""
    import pandas as pd

    from aeon.utils.discovery import all_estimators

    def _does_not_start_with_underscore(input_string):
        return not input_string.startswith("_")

    # Columns for the output table
    base_columns = ["Estimator name", "Module", "Method family"]

    capabilities_to_include = {
        "multivariate": "Mult.",
        "unequal_length": "Uneq.",
        "missing_values": "Miss.",
    }

    # Initialize data dictionary with base columns
    data = {col: [] for col in base_columns}
    # Add abbreviated columns
    data.update({abbreviation: [] for abbreviation in capabilities_to_include.values()})

    for estimator_name, estimator_class in all_estimators(include_sklearn=False):
        algorithm_type = "::".join(str(estimator_class).split(".")[1:-2])
        # fetch tags
        tag_dict = estimator_class.get_class_tags()

        # includes part of class string
        modpath = str(estimator_class)[8:-2]
        path_parts = modpath.split(".")
        # joins strings excluding starting with '_'
        clean_path = ".".join(list(filter(_does_not_start_with_underscore, path_parts)))
        # adds html link reference
        estimator_name_as_link = str(
            '<a href="api_reference/auto_generated/'
            + clean_path
            + '.html">'
            + estimator_name
            + "</a>"
        )
        algorithm_type = algorithm_type.split("::")
        data["Estimator name"].append(estimator_name_as_link)
        data["Module"].append(algorithm_type[0])
        if len(algorithm_type) > 1:
            data["Method family"].append("/".join(algorithm_type[1:]))
        else:
            data["Method family"].append("N/A")
        for capability_name, abbreviation in capabilities_to_include.items():
            _val = tag_dict.get(f"capability:{capability_name}")

            # For case where tag is not included output as not supported
            if not _val or _val is None:
                data[abbreviation].append("\u274c")
            else:
                data[abbreviation].append("\u2705")

    df = pd.DataFrame(data).sort_values(
        by=["Module", "Method family", "Estimator name"]
    )

    df_str = """
<!-- DataTables CSS -->
<link rel="stylesheet" type="text/css"
href="https://cdn.datatables.net/1.13.7/css/jquery.dataTables.min.css">

<style>
.dataTables_wrapper {
    padding: 10px 0;
}

.dataTables_wrapper table.dataTable tbody tr {
    background-color: var(--color-background-primary);
    color: var(--color-foreground-primary);
}

.dataTables_wrapper table.dataTable tbody tr.odd {
    background-color: var(--color-background-secondary);
}

.dataTables_wrapper table.dataTable tbody tr:hover {
    background-color: var(--color-background-hover);
}

.dataTables_wrapper table.dataTable a {
    color: var(--color-brand-content);
}

.dataTables_length select {
    background-color: var(--color-background-primary) !important;
    color: var(--color-foreground-primary) !important;
    border: 1px solid var(--color-background-border) !important;
}

.dataTables_length select option {
    background-color: var(--color-background-primary);
    color: var(--color-foreground-primary);
}

.dataTables_length select option:checked {
    background-color: var(--color-background-hover);
    color: var(--color-foreground-primary);
}

.dataTables_length select:focus {
    outline-color: var(--color-background-border);
}
</style>
"""

    df_str += df.to_markdown(index=False, tablefmt="github")

    df_str += """
<!-- DataTables JS -->
<script type="text/javascript"
 src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
<script type="text/javascript"
 src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script>

<script>
$(document).ready(function() {
    $('table').DataTable({
        pageLength: 10,
        order: [[1, 'asc'], [2, 'asc'], [0, 'asc']],
        language: {
            search: 'Search:',
            lengthMenu: 'Show _MENU_ entries'
        }
    });
});
</script>
"""

    with open("estimator_overview_table.md", "w", encoding="utf-8") as file:
        file.write(df_str)


def _add_estimator_capabilities_table(app, pagename, templatename, context, doctree):
    """Add estimator capabilities table to HTML page."""
    if "title" not in context or "body" not in context:
        return

    if '<span class="caption-text">Capabilities</span>' in context["body"]:
        return

    from aeon.utils.discovery import all_estimators

    estimators = all_estimators(include_sklearn=False)

    for estimator_name, estimator_class in estimators:
        if estimator_name == context["title"]:
            tags = estimator_class.get_class_tags()

            capabilities = {
                key.split(":")[1]: value
                for key, value in tags.items()
                if key.startswith("capability:")
            }

            html_output = """
            <div class="table-wrapper docutils container" id="id3">
            <table class="docutils align-default" id="id3">
            <caption>
            <span class="caption-text">Capabilities</span>
            <a class="headerlink" href="#id3" title="Link to this table">¶</a>
            </caption>
            <tbody>
            """

            for idx, (key, value) in enumerate(capabilities.items()):
                row_class = "row-odd" if idx % 2 == 0 else "row-even"
                formatted_key = key.replace("_", " ").title()
                if value is True:
                    formatted_value = "Yes"
                elif value is False:
                    formatted_value = "No"
                elif value is None:
                    formatted_value = "Not Set"
                else:
                    formatted_value = str(value)

                html_output += f"""
            <tr class="{row_class}">
                <th class="stub"><p>{formatted_key}</p></th>
                <td><p>{formatted_value}</p></td>
            </tr>
            """

            html_output += """
            </tbody>
            </table>
            </div>
            """

            html_content = context["body"]

            # Function to insert table into HTML content
            # Look for existing NOTES section outside methods
            start_methods = html_content.find('<dl class="py method">')
            section_before_methods = html_content[:start_methods]

            # Look for Notes section
            notes_heading = '<p class="rubric">Notes</p>'
            notes_pos = section_before_methods.find(notes_heading)

            if notes_pos != -1:
                # Notes exists, insert table after it
                insert_pos = notes_pos + len(notes_heading)
                context["body"] = (
                    html_content[:insert_pos]
                    + "\n"
                    + html_output
                    + html_content[insert_pos:]
                )
            else:
                # Need to create Notes section
                # Find position before References or Examples or Methods
                # whichever comes first
                ref_pos = section_before_methods.find(
                    '<p class="rubric">References</p>'
                )
                ex_pos = section_before_methods.find('<p class="rubric">Examples</p>')

                positions = [
                    pos for pos in [ref_pos, ex_pos, start_methods] if pos != -1
                ]
                insert_pos = min(positions) if positions else start_methods

                new_section = f'\n<p class="rubric">Notes</p>\n{html_output}\n'
                context["body"] = (
                    html_content[:insert_pos] + new_section + html_content[insert_pos:]
                )


def setup(app):
    """Set up sphinx builder.

    Parameters
    ----------
    app : Sphinx application object
    """
    app.connect("builder-inited", _make_estimator_overview)
    app.connect("html-page-context", _add_estimator_capabilities_table)
