"""Texinfo builder."""

from __future__ import annotations

import os.path
import warnings
from typing import TYPE_CHECKING

from docutils import nodes
from docutils.frontend import OptionParser
from docutils.io import FileOutput

from sphinx import addnodes, package_dir
from sphinx._cli.util.colour import darkgreen
from sphinx.builders import Builder
from sphinx.environment.adapters.asset import ImageAdapter
from sphinx.errors import NoUri
from sphinx.locale import _, __
from sphinx.util import logging
from sphinx.util.display import progress_message, status_iterator
from sphinx.util.docutils import new_document
from sphinx.util.nodes import inline_all_toctrees
from sphinx.util.osutil import SEP, copyfile, ensuredir, make_filename_from_project
from sphinx.writers.texinfo import TexinfoTranslator, TexinfoWriter

if TYPE_CHECKING:
    from collections.abc import Iterable, Set
    from typing import Any

    from docutils.nodes import Node

    from sphinx.application import Sphinx
    from sphinx.config import Config
    from sphinx.util.typing import ExtensionMetadata

logger = logging.getLogger(__name__)
template_dir = package_dir.joinpath('templates', 'texinfo')


class TexinfoBuilder(Builder):
    """Builds Texinfo output to create Info documentation."""

    name = 'texinfo'
    format = 'texinfo'
    epilog = __('The Texinfo files are in %(outdir)s.')
    if os.name == 'posix':
        epilog += __(
            "\nRun 'make' in that directory to run these through "
            'makeinfo\n'
            "(use 'make info' here to do that automatically)."
        )

    supported_image_types = ['image/png', 'image/jpeg', 'image/gif']
    default_translator_class = TexinfoTranslator

    def init(self) -> None:
        self.docnames: Iterable[str] = []
        self.document_data: list[tuple[str, str, str, str, str, str, str, bool]] = []

    def get_outdated_docs(self) -> str | list[str]:
        return 'all documents'  # for now

    def get_target_uri(self, docname: str, typ: str | None = None) -> str:
        if docname not in self.docnames:
            raise NoUri(docname, typ)
        return '%' + docname

    def get_relative_uri(self, from_: str, to: str, typ: str | None = None) -> str:
        # ignore source path
        return self.get_target_uri(to, typ)

    def prepare_writing(self, _docnames: Set[str]) -> None:
        preliminary_document_data = [list(x) for x in self.config.texinfo_documents]
        if not preliminary_document_data:
            logger.warning(
                __(
                    'no "texinfo_documents" config value found; no documents '
                    'will be written'
                )
            )
            return
        # assign subdirs to titles
        self.titles: list[tuple[str, str]] = []
        for entry in preliminary_document_data:
            docname = entry[0]
            if docname not in self.env.all_docs:
                logger.warning(
                    __(
                        '"texinfo_documents" config value references unknown '
                        'document %s'
                    ),
                    docname,
                )
                continue
            self.document_data.append(entry)  # type: ignore[arg-type]
            docname = docname.removesuffix(SEP + 'index')
            self.titles.append((docname, entry[2]))

    def write_documents(self, _docnames: Set[str]) -> None:
        for entry in self.document_data:
            docname, targetname, title, author = entry[:4]
            targetname += '.texi'
            direntry = description = category = ''
            if len(entry) > 6:
                direntry, description, category = entry[4:7]
            toctree_only = False
            if len(entry) > 7:
                toctree_only = entry[7]
            destination = FileOutput(
                destination_path=self.outdir / targetname,
                encoding='utf-8',
            )
            with progress_message(__('processing %s') % targetname, nonl=False):
                appendices = self.config.texinfo_appendices or []
                doctree = self.assemble_doctree(
                    docname, toctree_only, appendices=appendices
                )

            with progress_message(__('writing')):
                self.post_process_images(doctree)
                docwriter = TexinfoWriter(self)
                with warnings.catch_warnings():
                    warnings.filterwarnings('ignore', category=DeprecationWarning)
                    # DeprecationWarning: The frontend.OptionParser class will be replaced
                    # by a subclass of argparse.ArgumentParser in Docutils 0.21 or later.
                    settings: Any = OptionParser(
                        defaults=self.env.settings,
                        components=(docwriter,),
                        read_config_files=True,
                    ).get_default_values()
                settings.author = author
                settings.title = title
                settings.texinfo_filename = targetname[:-5] + '.info'
                settings.texinfo_elements = self.config.texinfo_elements
                settings.texinfo_dir_entry = direntry or ''
                settings.texinfo_dir_category = category or ''
                settings.texinfo_dir_description = description or ''
                settings.docname = docname
                doctree.settings = settings
                docwriter.write(doctree, destination)
                self.copy_image_files(targetname[:-5])

    def assemble_doctree(
        self,
        indexfile: str,
        toctree_only: bool,
        appendices: list[str],
    ) -> nodes.document:
        self.docnames = {indexfile, *appendices}
        logger.info(darkgreen(indexfile))
        tree = self.env.get_doctree(indexfile)
        tree['docname'] = indexfile
        if toctree_only:
            # extract toctree nodes from the tree and put them in a
            # fresh document
            new_tree = new_document('<texinfo output>')
            new_sect = nodes.section()
            new_sect += nodes.title('<Set title in conf.py>', '<Set title in conf.py>')
            new_tree += new_sect
            for node in tree.findall(addnodes.toctree):
                new_sect += node
            tree = new_tree
        largetree = inline_all_toctrees(
            self, self.docnames, indexfile, tree, darkgreen, [indexfile]
        )
        largetree['docname'] = indexfile
        for docname in appendices:
            appendix = self.env.get_doctree(docname)
            appendix['docname'] = docname
            largetree.append(appendix)
        logger.info('')
        logger.info(__('resolving references...'))
        self.env.resolve_references(largetree, indexfile, self)
        # TODO: add support for external :ref:s
        for pendingnode in largetree.findall(addnodes.pending_xref):
            docname = pendingnode['refdocname']
            sectname = pendingnode['refsectname']
            newnodes: list[Node] = [nodes.emphasis(sectname, sectname)]
            for subdir, title in self.titles:
                if docname.startswith(subdir):
                    newnodes.extend((
                        nodes.Text(_(' (in ')),
                        nodes.emphasis(title, title),
                        nodes.Text(')'),
                    ))
                    break
            pendingnode.replace_self(newnodes)
        return largetree

    def copy_assets(self) -> None:
        self.copy_support_files()

    def copy_image_files(self, targetname: str) -> None:
        if self.images:
            stringify_func = ImageAdapter(self.env).get_original_image_uri
            for src in status_iterator(
                self.images,
                __('copying images... '),
                'brown',
                len(self.images),
                self.app.verbosity,
                stringify_func=stringify_func,
            ):
                dest = self.images[src]
                try:
                    imagedir = self.outdir / f'{targetname}-figures'
                    ensuredir(imagedir)
                    copyfile(
                        self.srcdir / src,
                        imagedir / dest,
                        force=True,
                    )
                except Exception as err:
                    logger.warning(
                        __('cannot copy image file %r: %s'),
                        self.srcdir / src,
                        err,
                    )

    def copy_support_files(self) -> None:
        try:
            with progress_message(__('copying Texinfo support files')):
                logger.info('Makefile ', nonl=True)
                copyfile(
                    template_dir / 'Makefile',
                    self.outdir / 'Makefile',
                    force=True,
                )
        except OSError as err:
            logger.warning(__('error writing file Makefile: %s'), err)


def default_texinfo_documents(
    config: Config,
) -> list[tuple[str, str, str, str, str, str, str]]:
    """Better default texinfo_documents settings."""
    filename = make_filename_from_project(config.project)
    return [
        (
            config.root_doc,
            filename,
            config.project,
            config.author,
            filename,
            'One line description of project',
            'Miscellaneous',
        )
    ]


def setup(app: Sphinx) -> ExtensionMetadata:
    app.add_builder(TexinfoBuilder)

    app.add_config_value(
        'texinfo_documents',
        default_texinfo_documents,
        '',
        types=frozenset({list, tuple}),
    )
    app.add_config_value('texinfo_appendices', [], '', types=frozenset({list, tuple}))
    app.add_config_value('texinfo_elements', {}, '', types=frozenset({dict}))
    app.add_config_value(
        'texinfo_domain_indices',
        True,
        '',
        types=frozenset({frozenset, list, set, tuple}),
    )
    app.add_config_value('texinfo_show_urls', 'footnote', '', types=frozenset({str}))
    app.add_config_value('texinfo_no_detailmenu', False, '', types=frozenset({bool}))
    app.add_config_value('texinfo_cross_references', True, '', types=frozenset({bool}))

    return {
        'version': 'builtin',
        'parallel_read_safe': True,
        'parallel_write_safe': True,
    }
