"""Texinfo builder."""

from __future__ import annotations

import os
import warnings
from os import path
from typing import TYPE_CHECKING, Any

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

from sphinx import addnodes, package_dir
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.console import darkgreen
from sphinx.util.display import progress_message, status_iterator
from sphinx.util.docutils import new_document
from sphinx.util.fileutil import copy_asset_file
from sphinx.util.nodes import inline_all_toctrees
from sphinx.util.osutil import SEP, ensuredir, make_filename_from_project
from sphinx.writers.texinfo import TexinfoTranslator, TexinfoWriter

if TYPE_CHECKING:
    from collections.abc import Iterable

    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 = os.path.join(package_dir, '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 init_document_data(self) -> 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]
            if docname.endswith(SEP + 'index'):
                docname = docname[:-5]
            self.titles.append((docname, entry[2]))

    def write(self, *ignored: Any) -> None:
        self.init_document_data()
        self.copy_assets()
        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=path.join(self.outdir, targetname),
                encoding='utf-8')
            with progress_message(__("processing %s") % targetname):
                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) + " ", nonl=True)
        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
            else:
                pass
            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.app.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 = path.join(self.outdir, targetname + '-figures')
                    ensuredir(imagedir)
                    copy_asset_file(path.join(self.srcdir, src),
                                    path.join(imagedir, dest))
                except Exception as err:
                    logger.warning(__('cannot copy image file %r: %s'),
                                   path.join(self.srcdir, src), err)

    def copy_support_files(self) -> None:
        try:
            with progress_message(__('copying Texinfo support files')):
                logger.info('Makefile ', nonl=True)
                copy_asset_file(os.path.join(template_dir, 'Makefile'), self.outdir)
        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, '')
    app.add_config_value('texinfo_appendices', [], '')
    app.add_config_value('texinfo_elements', {}, '')
    app.add_config_value('texinfo_domain_indices', True, '', list)
    app.add_config_value('texinfo_show_urls', 'footnote', '')
    app.add_config_value('texinfo_no_detailmenu', False, '')
    app.add_config_value('texinfo_cross_references', True, '')

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