"""
Notes:
    Based on template code in:
        ~/code/xcookie/xcookie/builders/docs.py
        ~/code/xcookie/xcookie/rc/conf_ext.py

    http://docs.readthedocs.io/en/latest/getting_started.html

    pip install sphinx sphinx-autobuild sphinx_rtd_theme sphinxcontrib-napoleon

    cd ~/code/xdoctest
    mkdir -p docs
    cd docs

    sphinx-quickstart

    # need to edit the conf.py

    # Remove any old auto docs folder and regenerate it.
    rm -rf ~/code/xdoctest/docs/source/auto
    cd ~/code/xdoctest/docs
    sphinx-apidoc --private --separate --force --output-dir ~/code/xdoctest/docs/source/auto ~/code/xdoctest/src/xdoctest '_tokenize.py'
    git add source/auto/*.rst

    # Note: the module should importable before running this
    # (e.g. install it in developer mode or munge the PYTHONPATH)
    make html

    Also:
        To turn on PR checks

        https://docs.readthedocs.io/en/stable/guides/autobuild-docs-for-pull-requests.html

        https://readthedocs.org/dashboard/xdoctest/advanced/

        ensure your github account is connected to readthedocs
        https://readthedocs.org/accounts/social/connections/

        ### For gitlab

        To enable the read-the-docs go to https://readthedocs.org/dashboard/ and login

        The user will need to enable the repo on their readthedocs account:
        https://readthedocs.org/dashboard/import/manual/?

        Enter the following information:
            Set the Repository NAME: xdoctest
            Set the Repository URL: https://github.com/Erotemic/xdoctest

        Make sure you have a .readthedocs.yml file

        For gitlab you also need to setup an integrations. Navigate to:

            https://readthedocs.org/dashboard/xdoctest/integrations/create/

        Then add gitlab incoming webhook and copy the URL (make sure
        you copy the real url and not the text so https is included),
        specifically:

            In the "Integration type:" dropdown menu, select
            "Gitlab incoming webhook"

            Click "Add integration"

            Copy the text in the "Webhook URL" box to be used later.

            Copy the text in the "Secret" box to be used later.

        Then go to

            https://github.com/Erotemic/xdoctest/hooks

            Click "Add new webhook".

            Copy the text previously saved from the "Webhook URL" box
            in the readthedocs form into the "URL" box in the gitlab
            form.

            Copy the text previously saved from the "Secret" box
            in the readthedocs form into the "Secret token" box in the
            gitlab form.

            For trigger permissions select the following checkboxes:
                push events,
                tag push events,
                merge request events
                release events

            Click the "Add webhook" button.

        See Docs for more details https://docs.readthedocs.io/en/stable/integrations.html

        Will also need to activate the main branch:
            https://readthedocs.org/projects/xdoctest/versions/
"""
#
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/stable/config

# -- 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.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))


# -- Project information -----------------------------------------------------
import sphinx_rtd_theme
from os.path import exists
from os.path import dirname
from os.path import join


def parse_version(fpath):
    """
    Statically parse the version number from a python file
    """
    import ast
    if not exists(fpath):
        raise ValueError('fpath={!r} does not exist'.format(fpath))
    with open(fpath, 'r') as file_:
        sourcecode = file_.read()
    pt = ast.parse(sourcecode)
    class VersionVisitor(ast.NodeVisitor):
        def visit_Assign(self, node):
            for target in node.targets:
                if getattr(target, 'id', None) == '__version__':
                    self.version = node.value.s
    visitor = VersionVisitor()
    visitor.visit(pt)
    return visitor.version

project = 'xdoctest'
copyright = '2025, Jon Crall'
author = 'Jon Crall'
modname = 'xdoctest'

repo_dpath = dirname(dirname(dirname(__file__)))
mod_dpath = join(repo_dpath, 'src/xdoctest')
src_dpath = dirname(mod_dpath)
modpath = join(mod_dpath, '__init__.py')
release = parse_version(modpath)
version = '.'.join(release.split('.')[0:2])

# Hack to ensure the module is importable
# sys.path.insert(0, os.path.abspath(src_dpath))

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

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

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
    # 'autoapi.extension',
    'sphinx.ext.autodoc',
    'sphinx.ext.autosummary',
    'sphinx.ext.intersphinx',
    'sphinx.ext.napoleon',
    'sphinx.ext.todo',
    'sphinx.ext.viewcode',
    'myst_parser',  # For markdown docs
    'sphinx.ext.imgconverter',  # For building latexpdf
    'sphinx.ext.githubpages',
    # 'sphinxcontrib.redirects',
    'sphinxcontrib.jquery',  # Fix for search
    'sphinx_reredirects',
]

todo_include_todos = True
napoleon_google_docstring = True
napoleon_use_param = False
napoleon_use_ivar = True

#autoapi_type = 'python'
#autoapi_dirs = [mod_dpath]

autodoc_inherit_docstrings = False

# Hack for geowatch, todo configure
autosummary_mock_imports = [
    'geowatch.utils.lightning_ext._jsonargparse_ext_ge_4_24_and_lt_4_xx',
    'geowatch.utils.lightning_ext._jsonargparse_ext_ge_4_22_and_lt_4_24',
    'geowatch.utils.lightning_ext._jsonargparse_ext_ge_4_21_and_lt_4_22',
    'geowatch.tasks.fusion.datamodules.temporal_sampling.affinity_sampling',
    'geowatch.tasks.depth_pcd.model',
    'geowatch.tasks.cold.export_change_map',
]

autodoc_default_options = {  # Document callable classes
    'special-members': '__call__'}

autodoc_member_order = 'bysource'
autoclass_content = 'both'
# autodoc_mock_imports = ['torch', 'torchvision', 'visdom']

# autoapi_modules = {
#     modname: {
#         'override': False,
#         'output': 'auto'
#     }
# }
# autoapi_dirs = [f'../../src/{modname}']
# autoapi_keep_files = True

# References:
# https://stackoverflow.com/questions/21538983/specifying-targets-for-intersphinx-links-to-numpy-scipy-and-matplotlib

intersphinx_mapping = {
    # 'pytorch': ('http://pytorch.org/docs/master/', None),
    'python': ('https://docs.python.org/3', None),
    'click': ('https://click.palletsprojects.com/', None),
    # 'xxhash': ('https://pypi.org/project/xxhash/', None),
    # 'pygments': ('https://pygments.org/docs/', None),
    # 'tqdm': ('https://tqdm.github.io/', None),
    # Requires that the repo have objects.inv
    'kwarray': ('https://kwarray.readthedocs.io/en/latest/', None),
    'kwimage': ('https://kwimage.readthedocs.io/en/latest/', None),
    # 'kwplot': ('https://kwplot.readthedocs.io/en/latest/', None),
    'ndsampler': ('https://ndsampler.readthedocs.io/en/latest/', None),
    'ubelt': ('https://ubelt.readthedocs.io/en/latest/', None),
    'xdoctest': ('https://xdoctest.readthedocs.io/en/latest/', None),
    'networkx': ('https://networkx.org/documentation/stable/', None),
    'scriptconfig': ('https://scriptconfig.readthedocs.io/en/latest/', None),
    'rich': ('https://rich.readthedocs.io/en/latest/', None),

    'numpy': ('https://numpy.org/doc/stable/', None),
    'sympy': ('https://docs.sympy.org/latest/', None),
    'scikit-learn': ('https://scikit-learn.org/stable/', None),
    'pandas': ('https://pandas.pydata.org/docs/', None),
    'matplotlib': ('https://matplotlib.org/stable/', None),

    'pytest': ('https://docs.pytest.org/en/latest/', None),
    'platformdirs': ('https://platformdirs.readthedocs.io/en/latest/', None),

    'timerit': ('https://timerit.readthedocs.io/en/latest/', None),
    'progiter': ('https://progiter.readthedocs.io/en/latest/', None),
    'dateutil': ('https://dateutil.readthedocs.io/en/latest/', None),
    # 'pytest._pytest.doctest': ('https://docs.pytest.org/en/latest/_modules/_pytest/doctest.html', None),
    # 'colorama': ('https://pypi.org/project/colorama/', None),
    # 'cv2' : ('http://docs.opencv.org/2.4/', None),
    # 'h5py' : ('http://docs.h5py.org/en/latest/', None)
}
__dev_note__ = """
python -m sphinx.ext.intersphinx https://docs.python.org/3/objects.inv
python -m sphinx.ext.intersphinx https://kwcoco.readthedocs.io/en/latest/objects.inv
python -m sphinx.ext.intersphinx https://networkx.org/documentation/stable/objects.inv
python -m sphinx.ext.intersphinx https://kwarray.readthedocs.io/en/latest/objects.inv
python -m sphinx.ext.intersphinx https://kwimage.readthedocs.io/en/latest/objects.inv
python -m sphinx.ext.intersphinx https://ubelt.readthedocs.io/en/latest/objects.inv
python -m sphinx.ext.intersphinx https://networkx.org/documentation/stable/objects.inv

sphobjinv suggest -t 90 -u https://readthedocs.org/projects/pytest/reference/objects.inv
"signal.convolve2d"

python -m sphinx.ext.intersphinx https://pygments-doc.readthedocs.io/en/latest/objects.inv
"""


# 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', '.md']

# The master 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 = []

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'


# -- 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 = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]

# 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 = {
    'collapse_navigation': False,
    'display_version': True,
    'navigation_depth': -1,
    # 'logo_only': True,
}
# html_logo = '.static/xdoctest.svg'
# html_favicon = '.static/xdoctest.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']

# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself.  Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}


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

# Output file base name for HTML help builder.
htmlhelp_basename = project + 'doc'


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

# References:
# https://tex.stackexchange.com/questions/546246/centos-8-the-font-freeserif-cannot-be-found

"""
# https://www.sphinx-doc.org/en/master/usage/builders/index.html#sphinx.builders.latex.LaTeXBuilder
# https://tex.stackexchange.com/a/570691/83399
sudo apt install fonts-freefont-otf texlive-luatex texlive-latex-extra texlive-fonts-recommended texlive-latex-recommended tex-gyre latexmk
make latexpdf LATEXMKOPTS="-shell-escape --synctex=-1 -src-specials -interaction=nonstopmode"
make latexpdf LATEXMKOPTS="-lualatex -interaction=nonstopmode"
make LATEXMKOPTS="-lualatex -interaction=nonstopmode"

"""
# latex_engine = 'lualatex'
# latex_engine = 'xelatex'

latex_elements = {
    # The paper size ('letterpaper' or 'a4paper').
    #
    # 'papersize': 'letterpaper',

    # The font size ('10pt', '11pt' or '12pt').
    #
    # 'pointsize': '10pt',

    # Additional stuff for the LaTeX preamble.
    #
    # 'preamble': '',

    # Latex figure (float) alignment
    #
    # 'figure_align': 'htbp',
}

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


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

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


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

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


# -- Extension configuration -------------------------------------------------
from sphinx.domains.python import PythonDomain  # NOQA
# from sphinx.application import Sphinx  # NOQA
from typing import Any, List  # NOQA


# HACK TO PREVENT EXCESSIVE TIME.
# TODO: FIXME FOR REAL
MAX_TIME_MINUTES = None
if MAX_TIME_MINUTES:
    import ubelt  # NOQA
    TIMER = ubelt.Timer()
    TIMER.tic()


class PatchedPythonDomain(PythonDomain):
    """
    References:
        https://github.com/sphinx-doc/sphinx/issues/3866
    """
    def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
        """
        Helps to resolves cross-references
        """
        if target.startswith('ub.'):
            target = 'ubelt.' + target[3]
        if target.startswith('xdoc.'):
            target = 'xdoctest.' + target[3]
        return_value = super(PatchedPythonDomain, self).resolve_xref(
            env, fromdocname, builder, typ, target, node, contnode)
        return return_value


class GoogleStyleDocstringProcessor:
    """
    A small extension that runs after napoleon and reformats erotemic-flavored
    google-style docstrings for sphinx.
    """

    def __init__(self, autobuild=1):
        self.debug = 0
        self.registry = {}
        if autobuild:
            self._register_builtins()

    def register_section(self, tag, alias=None):
        """
        Decorator that adds a custom processing function for a non-standard
        google style tag. The decorated function should accept a list of
        docstring lines, where the first one will be the google-style tag that
        likely needs to be replaced, and then return the appropriate sphinx
        format (TODO what is the name? Is it just RST?).
        """
        alias = [] if alias is None else alias
        alias = [alias] if not isinstance(alias, (list, tuple, set)) else alias
        alias.append(tag)
        alias = tuple(alias)
        # TODO: better tag patterns
        def _wrap(func):
            self.registry[tag] = {
                'tag': tag,
                'alias': alias,
                'func': func,
            }
            return func
        return _wrap

    def _register_builtins(self):
        """
        Adds definitions I like of CommandLine, TextArt, and Ignore
        """

        @self.register_section(tag='CommandLine')
        def commandline(lines):
            new_lines = []
            new_lines.append('.. rubric:: CommandLine')
            new_lines.append('')
            new_lines.append('.. code-block:: bash')
            new_lines.append('')
            new_lines.extend(lines[1:])
            return new_lines

        @self.register_section(tag='SpecialExample', alias=['Benchmark', 'Sympy', 'Doctest'])
        def benchmark(lines):
            import textwrap
            new_lines = []
            tag = lines[0].replace(':', '').strip()
            # new_lines.append(lines[0])  # TODO: it would be nice to change the tagline.
            # new_lines.append('')
            new_lines.append('.. rubric:: {}'.format(tag))
            new_lines.append('')
            new_text = textwrap.dedent('\n'.join(lines[1:]))
            redone = new_text.split('\n')
            new_lines.extend(redone)
            # import ubelt as ub
            # print('new_lines = {}'.format(ub.urepr(new_lines, nl=1)))
            # new_lines.append('')
            return new_lines

        @self.register_section(tag='TextArt', alias=['Ascii'])
        def text_art(lines):
            new_lines = []
            new_lines.append('.. rubric:: TextArt')
            new_lines.append('')
            new_lines.append('.. code-block:: bash')
            new_lines.append('')
            new_lines.extend(lines[1:])
            return new_lines

        # @self.register_section(tag='TODO', alias=['.. todo::'])
        # def todo_section(lines):
        #     """
        #     Fixup todo sections
        #     """
        #     import xdev
        #     xdev.embed()
        #     import ubelt as ub
        #     print('lines = {}'.format(ub.urepr(lines, nl=1)))
        #     return new_lines

        @self.register_section(tag='Ignore')
        def ignore(lines):
            return []

    def process(self, lines):
        """
        Example:
            >>> import ubelt as ub
            >>> self = GoogleStyleDocstringProcessor()
            >>> lines = ['Hello world',
            >>>              '',
            >>>              'CommandLine:',
            >>>              '    hi',
            >>>              '',
            >>>              'CommandLine:',
            >>>              '',
            >>>              '    bye',
            >>>              '',
            >>>              'TextArt:',
            >>>              '',
            >>>              '    1',
            >>>              '    2',
            >>>              '',
            >>>              '    345',
            >>>              '',
            >>>              'Foobar:',
            >>>              '',
            >>>              'TextArt:']
            >>> new_lines = self.process(lines[:])
            >>> print(chr(10).join(new_lines))
        """
        orig_lines = lines[:]
        new_lines = []
        curr_mode = '__doc__'
        accum = []

        def accept():
            """ called when we finish reading a section """
            if curr_mode == '__doc__':
                # Keep the lines as-is
                new_lines.extend(accum)
            else:
                # Process this section with the given function
                regitem = self.registry[curr_mode]
                func = regitem['func']
                fixed = func(accum)
                new_lines.extend(fixed)
            # Reset the accumulator for the next section
            accum[:] = []

        for line in orig_lines:

            found = None
            for regitem in self.registry.values():
                if line.startswith(regitem['alias']):
                    found = regitem['tag']
                    break
            if not found and line and not line.startswith(' '):
                # if the line startswith anything but a space, we are no longer
                # in the previous nested scope. NOTE: This assumption may not
                # be general, but it works for my code.
                found = '__doc__'

            if found:
                # New section is found, accept the previous one and start
                # accumulating the new one.
                accept()
                curr_mode = found

            accum.append(line)

        # Finalize the last section
        accept()

        lines[:] = new_lines
        # make sure there is a blank line at the end
        if lines and lines[-1]:
            lines.append('')

        return lines

    def process_docstring_callback(self, app, what_: str, name: str, obj: Any,
                                   options: Any, lines: List[str]) -> None:
        """
        Callback to be registered to autodoc-process-docstring

        Custom process to transform docstring lines Remove "Ignore" blocks

        Args:
            app (sphinx.application.Sphinx): the Sphinx application object

            what (str):
                the type of the object which the docstring belongs to (one of
                "module", "class", "exception", "function", "method", "attribute")

            name (str): the fully qualified name of the object

            obj: the object itself

            options: the options given to the directive: an object with
                attributes inherited_members, undoc_members, show_inheritance
                and noindex that are true if the flag option of same name was
                given to the auto directive

            lines (List[str]): the lines of the docstring, see above

        References:
            https://www.sphinx-doc.org/en/1.5.1/_modules/sphinx/ext/autodoc.html
            https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html
        """
        if self.debug:
            print(f'ProcessDocstring: name={name}, what_={what_}, num_lines={len(lines)}')

        # print('BEFORE:')
        # import ubelt as ub
        # print('lines = {}'.format(ub.urepr(lines, nl=1)))

        self.process(lines)

        # docstr = '\n'.join(lines)
        # if 'Convert the Mask' in docstr:
        #     import xdev
        #     xdev.embed()

        # if 'keys in this dictionary ' in docstr:
        #     import xdev
        #     xdev.embed()

        render_doc_images = 0

        if MAX_TIME_MINUTES and TIMER.toc() > (60 * MAX_TIME_MINUTES):
            render_doc_images = False  # FIXME too slow on RTD

        if render_doc_images:
            # DEVELOPING
            if any('REQUIRES(--show)' in line for line in lines):
                # import xdev
                # xdev.embed()
                create_doctest_figure(app, obj, name, lines)

        FIX_EXAMPLE_FORMATTING = 1
        if FIX_EXAMPLE_FORMATTING:
            for idx, line in enumerate(lines):
                if line == "Example:":
                    lines[idx] = "**Example:**"
                    lines.insert(idx + 1, "")

        REFORMAT_SECTIONS = 0
        if REFORMAT_SECTIONS:
            REFORMAT_RETURNS = 0
            REFORMAT_PARAMS = 0

            docstr = SphinxDocstring(lines)

            if REFORMAT_PARAMS:
                for found in docstr.find_tagged_lines('Parameters'):
                    print(found['text'])
                    edit_slice = found['edit_slice']

                    # TODO: figure out how to do this.

                    # # file = 'foo.rst'
                    # import rstparse
                    # rst = rstparse.Parser()
                    # import io
                    # rst.read(io.StringIO(found['text']))
                    # rst.parse()
                    # for line in rst.lines:
                    #     print(line)

                    # # found['text']
                    # import docutils

                    # settings = docutils.frontend.OptionParser(
                    #     components=(docutils.parsers.rst.Parser,)
                    #     ).get_default_values()
                    # document = docutils.utils.new_document('<tempdoc>', settings)
                    # from docutils.parsers import rst
                    # rst.Parser().parse(found['text'], document)

            if REFORMAT_RETURNS:
                for found in docstr.find_tagged_lines('returns'):
                    # FIXME: account for new slice with -2 offset
                    edit_slice = found['edit_slice']
                    text = found['text']
                    new_lines = []
                    for para in text.split('\n\n'):
                        indent = para[:len(para) - len(para.lstrip())]
                        new_paragraph = indent + paragraph(para)
                        new_lines.append(new_paragraph)
                        new_lines.append('')
                    new_lines = new_lines[:-1]
                    lines[edit_slice] = new_lines

        # print('AFTER:')
        # print('lines = {}'.format(ub.urepr(lines, nl=1)))

        # if name == 'kwimage.Affine.translate':
        #     import sys
        #     sys.exit(1)


class SphinxDocstring:
    """
    Helper to parse and modify sphinx docstrings
    """
    def __init__(docstr, lines):
        docstr.lines = lines

        # FORMAT THE RETURNS SECTION A BIT NICER
        import re
        tag_pat = re.compile(r'^:(\w*):')
        directive_pat = re.compile(r'^.. (\w*)::\s*(\w*)')

        # Split by sphinx types, mark the line offset where they start / stop
        sphinx_parts = []
        for idx, line in enumerate(lines):
            tag_match = tag_pat.search(line)
            directive_match = directive_pat.search(line)
            if tag_match:
                tag = tag_match.groups()[0]
                sphinx_parts.append({
                    'tag': tag, 'start_offset': idx,
                    'type': 'tag',
                })
            elif directive_match:
                tag = directive_match.groups()[0]
                sphinx_parts.append({
                    'tag': tag, 'start_offset': idx,
                    'type': 'directive',
                })

        prev_offset = len(lines)
        for part in sphinx_parts[::-1]:
            part['end_offset'] = prev_offset
            prev_offset = part['start_offset']

        docstr.sphinx_parts = sphinx_parts

        if 0:
            for line in lines:
                print(line)

    def find_tagged_lines(docstr, tag):
        for part in docstr.sphinx_parts[::-1]:
            if part['tag'] == tag:
                edit_slice = slice(part['start_offset'], part['end_offset'])
                return_section = docstr.lines[edit_slice]
                text = '\n'.join(return_section)
                found = {
                    'edit_slice': edit_slice,
                    'text': text,
                }
                yield found


def paragraph(text):
    r"""
    Wraps multi-line strings and restructures the text to remove all newlines,
    heading, trailing, and double spaces.

    Useful for writing log messages

    Args:
        text (str): typically a multiline string

    Returns:
        str: the reduced text block
    """
    import re
    out = re.sub(r'\s\s*', ' ', text).strip()
    return out


def create_doctest_figure(app, obj, name, lines):
    """
    The idea is that each doctest that produces a figure should generate that
    and then that figure should be part of the docs.
    """
    import xdoctest
    import sys
    import types
    if isinstance(obj, types.ModuleType):
        module = obj
    else:
        module = sys.modules[obj.__module__]
    # TODO: read settings from pyproject.toml?
    if '--show' not in sys.argv:
        sys.argv.append('--show')
    if '--nointeract' not in sys.argv:
        sys.argv.append('--nointeract')
    modpath = module.__file__

    # print(doctest.format_src())
    import pathlib
    # HACK: write to the srcdir
    doc_outdir = pathlib.Path(app.outdir)
    doc_srcdir = pathlib.Path(app.srcdir)
    doc_static_outdir = doc_outdir / '_static'
    doc_static_srcdir = doc_srcdir / '_static'
    src_fig_dpath = (doc_static_srcdir / 'images')
    src_fig_dpath.mkdir(exist_ok=True, parents=True)
    out_fig_dpath = (doc_static_outdir / 'images')
    out_fig_dpath.mkdir(exist_ok=True, parents=True)

    # fig_dpath = (doc_outdir / 'autofigs' / name).mkdir(exist_ok=True)

    fig_num = 1

    import kwplot
    kwplot.autompl(force='agg')
    plt = kwplot.autoplt()

    docstr = '\n'.join(lines)

    # TODO: The freeform parser does not work correctly here.
    # We need to parse out the sphinx (epdoc)? individual examples
    # so we can get different figures. But we can hack it for now.

    import re
    split_parts = re.split('({}\\s*\n)'.format(re.escape('.. rubric:: Example')), docstr)
    # split_parts = docstr.split('.. rubric:: Example')

    # import xdev
    # xdev.embed()

    def doctest_line_offsets(doctest):
        # Where the doctests starts and ends relative to the file
        start_line_offset = doctest.lineno - 1
        last_part = doctest._parts[-1]
        last_line_offset = start_line_offset + last_part.line_offset + last_part.n_lines - 1
        offsets = {
            'start': start_line_offset,
            'end': last_line_offset,
            'stop': last_line_offset + 1,
        }
        return offsets

    # from xdoctest import utils
    # part_lines = utils.add_line_numbers(docstr.split('\n'), n_digits=3, start=0)
    # print('\n'.join(part_lines))

    to_insert_fpaths = []
    curr_line_offset = 0
    for part in split_parts:
        num_lines = part.count('\n')

        doctests = list(xdoctest.core.parse_docstr_examples(
            part, modpath=modpath, callname=name,
            # style='google'
        ))
        # print(doctests)

        # doctests = list(xdoctest.core.parse_docstr_examples(
        #     docstr, modpath=modpath, callname=name))

        for doctest in doctests:
            if '--show' in part:
                ...
                # print('-- SHOW TEST---')/)
                # kwplot.close_figures()
                try:
                    import pytest  # NOQA
                except ImportError:
                    pass
                try:
                    from xdoctest.exceptions import Skipped
                except ImportError:  # nocover
                    # Define dummy skipped exception if pytest is not available
                    class Skipped(Exception):
                        pass
                try:
                    doctest.mode = 'native'
                    doctest.run(verbose=0, on_error='raise')
                    ...
                except Skipped:
                    print(f'Skip doctest={doctest}')
                except Exception as ex:
                    print(f'ex={ex}')
                    print(f'Error in doctest={doctest}')

                offsets = doctest_line_offsets(doctest)
                doctest_line_end = curr_line_offset + offsets['stop']
                insert_line_index = doctest_line_end

                figures = kwplot.all_figures()
                for fig in figures:
                    fig_num += 1
                    # path_name = path_sanatize(name)
                    path_name = (name).replace('.', '_')
                    fig_fpath = src_fig_dpath / f'fig_{path_name}_{fig_num:03d}.jpeg'
                    fig.savefig(fig_fpath)
                    print(f'Wrote figure: {fig_fpath}')
                    to_insert_fpaths.append({
                        'insert_line_index': insert_line_index,
                        'fpath': fig_fpath,
                    })

                for fig in figures:
                    plt.close(fig)
                # kwplot.close_figures(figures)

        curr_line_offset += (num_lines)

    # if len(doctests) > 1:
    #     doctests
    #     import xdev
    #     xdev.embed()

    INSERT_AT = 'end'
    INSERT_AT = 'inline'

    end_index = len(lines)
    # Reverse order for inserts
    import shutil
    for info in to_insert_fpaths[::-1]:
        src_abs_fpath = info['fpath']

        rel_to_static_fpath = src_abs_fpath.relative_to(doc_static_srcdir)
        # dst_abs_fpath = doc_static_outdir / rel_to_static_fpath
        # dst_abs_fpath.parent.mkdir(parents=True, exist_ok=True)

        rel_to_root_fpath = src_abs_fpath.relative_to(doc_srcdir)

        dst_abs_fpath1 = doc_outdir / rel_to_root_fpath
        dst_abs_fpath1.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy(src_abs_fpath, dst_abs_fpath1)

        dst_abs_fpath2 = doc_outdir / rel_to_static_fpath
        dst_abs_fpath2.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy(src_abs_fpath, dst_abs_fpath2)

        dst_abs_fpath3 = doc_srcdir / rel_to_static_fpath
        dst_abs_fpath3.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy(src_abs_fpath, dst_abs_fpath3)

        if INSERT_AT == 'inline':
            # Try to insert after test
            insert_index = info['insert_line_index']
        elif INSERT_AT == 'end':
            insert_index = end_index
        else:
            raise KeyError(INSERT_AT)
        lines.insert(insert_index, '.. image:: {}'.format('..' / rel_to_root_fpath))
        # lines.insert(insert_index, '.. image:: {}'.format(rel_to_root_fpath))
        # lines.insert(insert_index, '.. image:: {}'.format(rel_to_static_fpath))
        lines.insert(insert_index, '')


def postprocess_hyperlinks(app, doctree, docname):
    """
    Extension to fixup hyperlinks.
    This should be connected to the Sphinx application's
    "autodoc-process-docstring" event.
    """
    # Your hyperlink postprocessing logic here
    from docutils import nodes
    import pathlib
    for node in doctree.traverse(nodes.reference):
        if 'refuri' in node.attributes:
            refuri = node.attributes['refuri']
            if '.rst' in refuri:
                if 'source' in node.document:
                    fpath = pathlib.Path(node.document['source'])
                    parent_dpath = fpath.parent
                    if (parent_dpath / refuri).exists():
                        node.attributes['refuri'] = refuri.replace('.rst', '.html')
                else:
                    raise AssertionError


def fix_rst_todo_section(lines):
    new_lines = []
    for line in lines:
        ...
    ...


def setup(app):
    import sphinx
    app : sphinx.application.Sphinx = app
    app.add_domain(PatchedPythonDomain, override=True)

    app.connect("doctree-resolved", postprocess_hyperlinks)

    docstring_processor = GoogleStyleDocstringProcessor()
    # https://stackoverflow.com/questions/26534184/can-sphinx-ignore-certain-tags-in-python-docstrings
    app.connect('autodoc-process-docstring', docstring_processor.process_docstring_callback)

    def copy(src, dst):
        import shutil
        print(f'Copy {src} -> {dst}')
        assert src.exists()
        if not dst.parent.exists():
            dst.parent.mkdir()
        shutil.copy(src, dst)

    ### Hack for kwcoco: TODO: figure out a way for the user to configure this.
    HACK_FOR_KWCOCO = 0
    if HACK_FOR_KWCOCO:
        import pathlib
        doc_outdir = pathlib.Path(app.outdir) / 'auto'
        doc_srcdir = pathlib.Path(app.srcdir) / 'auto'

        mod_dpath = doc_srcdir / '../../../kwcoco'

        src_fpath = (mod_dpath / 'coco_schema.json')
        copy(src_fpath, doc_outdir / src_fpath.name)
        copy(src_fpath, doc_srcdir / src_fpath.name)

        src_fpath = (mod_dpath / 'coco_schema_informal.rst')
        copy(src_fpath, doc_outdir / src_fpath.name)
        copy(src_fpath, doc_srcdir / src_fpath.name)
    return app
