# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html


import datetime
import importlib

# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
import os
import re
import shutil
import sys
from importlib import metadata
from pathlib import Path

from nbdime.diffing.notebooks import diff_notebooks
from nbdime.utils import read_notebook

# sys.path.insert(0, os.path.abspath(os.path.join("..", "..", "src")))
now = datetime.datetime.now()

release = metadata.version("bsk_rl")
project = "BSK-RL v" + release
copyright = str(now.year) + ", Autonomous Vehicle Systems (AVS) Laboratory"
author = "Mark Stephenson"
version = "Version " + release

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

extensions = [
    "sphinx.ext.duration",
    "sphinx.ext.doctest",
    "sphinx.ext.todo",
    "sphinx.ext.autodoc",
    "sphinx.ext.viewcode",
    "sphinx.ext.napoleon",
    "sphinx_rtd_theme",
    "nbsphinx",
    "sphinx.ext.mathjax",
    "sphinxcontrib.youtube",
]

templates_path = ["_templates"]
exclude_patterns = []
source_suffix = ".rst"
master_doc = "index"
autoclass_content = "init"
autodoc_member_order = "bysource"
autodoc_default_options = {
    "undoc-members": None,
}
autodoc_typehints = "both"
# nbsphinx_execute = "never"
nbsphinx_allow_errors = False


nitpicky = True
nitpick_ignore = [
    ("py:obj", "bsk_rl.gym.SatAct"),
    ("py:obj", "bsk_rl.gym.SatObs"),
    ("py:class", "bsk_rl.gym.SatAct"),
    ("py:class", "bsk_rl.gym.SatObs"),
]
nitpick_ignore_regex = [
    ("py:class", r"^(?!bsk_rl).*"),
    ("py:obj", r"^(?!bsk_rl).*"),
]


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


# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

html_theme = "sphinx_rtd_theme"
html_theme_options = {
    "style_nav_header_background": "#CFB87C",
    "navigation_depth": -1,
}
html_static_path = ["_static"]
html_css_files = ["custom.css"]
html_logo = "./_images/static/bsk_rl-logo.png"

add_module_names = False


def skip(app, what, name, obj, would_skip, options):
    return would_skip


def setup(app):
    app.connect("autodoc-skip-member", skip)


def has_all(qual_name):
    try:
        all = importlib.import_module(qual_name).__all__
        if len(all) == 0:
            return False
    except AttributeError:
        return False

    return True


class PackageCrawler:
    def __init__(
        self, base_source_dir, base_doc_dir, filter_all=True, nb_cache_dir=None
    ):
        self.base_source_dir = base_source_dir
        self.base_doc_dir = base_doc_dir
        self.filter_all = filter_all
        self.nb_cache_dir = nb_cache_dir

    def grab_files(self, dir_path):
        dirs_in_dir = [x for x in dir_path.iterdir() if x.is_dir()]
        dir_filters = [
            r".*__pycache__.*",
            r".*\.ruff_cache.*",
            r".*\.egg-info",
            r".*\/simplemaps_worldcities",
            r".*\/_.*",
        ]
        dirs_in_dir = list(
            filter(
                lambda dir: not any(
                    re.match(filter, str(dir)) for filter in dir_filters
                ),
                dirs_in_dir,
            )
        )

        files_in_dir = dir_path.glob("*.py")
        file_filters = [
            r".*__init__\.py",
            r"(.*\/|)_[a-zA-Z0-9_]*\.py",
        ]
        files_in_dir = list(
            filter(
                lambda file: not any(
                    re.match(filter, str(file)) for filter in file_filters
                ),
                files_in_dir,
            )
        )

        if self.filter_all:
            files_in_dir = list(
                filter(
                    lambda file: has_all(
                        str(file.relative_to(self.base_source_dir.parent))
                        .replace("/", ".")
                        .replace(".py", "")
                    ),
                    files_in_dir,
                )
            )

        notebooks_in_dir = dir_path.glob("*.ipynb")

        return (
            sorted(list(files_in_dir)),
            sorted(list(dirs_in_dir)),
            sorted(list(notebooks_in_dir)),
        )

    def generate_index(self, index_path, file_paths, dir_paths, source_dir):
        # Make header
        lines = ""
        qual_name = str(source_dir.relative_to(self.base_source_dir.parent)).replace(
            "/", "."
        )
        lines += ".. _" + qual_name.replace(" ", "_") + ":\n"
        lines += f".. currentmodule:: {qual_name}\n\n"

        # if a _default.rst file exists in a folder, then use it to generate the index.rst file
        try:
            docFileName = source_dir / "_default.rst"
            with open(docFileName, "r") as docFile:
                docContents = docFile.read()
            lines += docContents + "\n\n"
        except FileNotFoundError:  # Auto-generate the index.rst file
            # add page tag

            # pull in folder _doc.rst file if it exists
            docContents = ""
            try:
                docFileName = source_dir / "_doc.rst"
                if os.path.isfile(docFileName):
                    with open(docFileName, "r") as docFile:
                        docContents = docFile.read()
            except FileNotFoundError:
                pass

            # Title the page
            if "\n===" not in docContents:
                # Make title
                doc_title = qual_name
                try:
                    doc_title = importlib.import_module(qual_name).__doc_title__
                except AttributeError:
                    pass
                lines += doc_title + "\n" + "=" * len(doc_title) + "\n\n"

            lines += docContents + "\n\n"

            # Also check for docs in the __init__.py file
            # lines += "\n\nReference\n----------\n\n"
            lines += (
                """.. automodule:: """
                + qual_name
                + """\n   :members:\n   :show-inheritance:\n\n"""
            )

            if len(file_paths) > 0 or len(dir_paths) > 0:
                # lines += "\n\nModules\n----------\n\n"
                pass

            # Add a linking point to all local files
            lines += "\n\n.. toctree::\n   :maxdepth: 1\n   :hidden:\n\n"
            added_names = []
            for file_path in sorted(file_paths):
                file_name = os.path.basename(os.path.normpath(file_path))
                file_name = file_name[: file_name.rfind(".")]

                if file_name not in added_names:
                    lines += "   " + file_name + "\n"
                    added_names.append(file_name)
            lines += "\n"

            # Add a linking point to all local directories
            lines += ".. toctree::\n   :maxdepth: 1\n   :hidden:\n\n"

            for dir_path in sorted(dir_paths):
                dirName = os.path.basename(os.path.normpath(dir_path))
                lines += "   " + dirName + "/index\n"

        with open(os.path.join(index_path, "index.rst"), "w") as f:
            f.write(lines)

    def generate_autodoc(self, doc_path, source_file):
        # Make header
        short_name = source_file.name.replace(".py", "")
        lines = ""
        qual_name = (
            str(source_file.relative_to(self.base_source_dir.parent))
            .replace("/", ".")
            .replace(".py", "")
        )
        lines += ".. _" + qual_name.replace(" ", "_") + ":\n"
        lines += f".. currentmodule:: {qual_name}\n\n"

        # Make title
        doc_title = short_name
        try:
            doc_title = importlib.import_module(qual_name).__doc_title__
        except (AttributeError, ModuleNotFoundError):
            pass
        lines += doc_title + "\n" + "=" * len(doc_title) + "\n\n"

        # Generate the autodoc file
        # lines += f"``{qual_name}``\n\n"
        lines += ".. toctree::\n   :maxdepth: 1\n\n"
        lines += (
            """.. automodule:: """
            + qual_name
            + """\n   :members:\n   :show-inheritance:\n\n"""
        )

        # Write to file
        with open(doc_path / f"{short_name}.rst", "w") as f:
            f.write(lines)

    def run(self, source_dir=None):
        if source_dir is None:
            source_dir = self.base_source_dir

        file_paths, dir_paths, nb_paths = self.grab_files(source_dir)
        index_path = source_dir.relative_to(self.base_source_dir)

        # Populate the index.rst file of the local directory
        os.makedirs(self.base_doc_dir / index_path, exist_ok=True)
        self.generate_index(
            self.base_doc_dir / index_path, file_paths + nb_paths, dir_paths, source_dir
        )

        # Generate the correct auto-doc function for python modules
        for file in file_paths:
            self.generate_autodoc(
                self.base_doc_dir / index_path,
                file,
            )

        for notebook in nb_paths:
            nb_cache = (
                self.nb_cache_dir / self.base_doc_dir / index_path / notebook.name
            )
            if (
                self.nb_cache_dir is not None
                and nb_cache.is_file()
                and (
                    "'source'"
                    not in diff_notebooks(
                        read_notebook(notebook.resolve(), on_null="empty"),
                        read_notebook(nb_cache.resolve(), on_null="empty"),
                    ).__repr__()
                )
                and (
                    "'source'"
                    not in diff_notebooks(
                        read_notebook(nb_cache.resolve(), on_null="empty"),
                        read_notebook(notebook.resolve(), on_null="empty"),
                    ).__repr__()
                )
            ):
                shutil.copy(
                    nb_cache,
                    self.base_doc_dir / index_path / notebook.name,
                )
            else:
                shutil.copy(notebook, self.base_doc_dir / index_path / notebook.name)

        # Recursively go through all directories in source, documenting what is available.
        for dir_path in sorted(dir_paths):
            self.run(
                source_dir=dir_path,
            )

        return


sys.path.append(os.path.abspath("../.."))
nb_cache_dir = Path("../build/doctrees/nbsphinx")
PackageCrawler(
    Path("../../src/bsk_rl/"), Path("./api_reference/"), nb_cache_dir=nb_cache_dir
).run()
PackageCrawler(
    Path("../../examples"),
    Path("./examples/"),
    filter_all=False,
    nb_cache_dir=nb_cache_dir,
).run()
