"""Changelog builder."""

from __future__ import annotations

import html
from typing import TYPE_CHECKING

from sphinx import package_dir
from sphinx._cli.util.colour import bold
from sphinx.builders import Builder
from sphinx.locale import _, __
from sphinx.theming import HTMLThemeFactory
from sphinx.util import logging
from sphinx.util.fileutil import copy_asset_file

if TYPE_CHECKING:
    from collections.abc import Set

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

logger = logging.getLogger(__name__)


class ChangesBuilder(Builder):
    """Write a summary with all versionadded/changed/deprecated/removed directives."""

    name = 'changes'
    epilog = __('The overview file is in %(outdir)s.')

    def init(self) -> None:
        self.create_template_bridge()
        theme_factory = HTMLThemeFactory(self.app)
        self.theme = theme_factory.create('default')
        self.templates.init(self, self.theme)

    def get_outdated_docs(self) -> str:
        return str(self.outdir)

    typemap = {
        'versionadded': 'added',
        'versionchanged': 'changed',
        'deprecated': 'deprecated',
        'versionremoved': 'removed',
    }

    def write_documents(self, _docnames: Set[str]) -> None:
        version = self.config.version
        domain = self.env.domains.changeset_domain
        libchanges: dict[str, list[tuple[str, str, int]]] = {}
        apichanges: list[tuple[str, str, int]] = []
        otherchanges: dict[tuple[str, str], list[tuple[str, str, int]]] = {}

        changesets = domain.get_changesets_for(version)
        if not changesets:
            logger.info(bold(__('no changes in version %s.')), version)
            return
        logger.info(bold(__('writing summary file...')))
        for changeset in changesets:
            descname = changeset.descname
            ttext = self.typemap[changeset.type]
            context = changeset.content.replace('\n', ' ')
            if descname and changeset.docname.startswith('c-api'):
                if context:
                    entry = f'<b>{descname}</b>: <i>{ttext}:</i> {context}'
                else:
                    entry = f'<b>{descname}</b>: <i>{ttext}</i>.'
                apichanges.append((entry, changeset.docname, changeset.lineno))
            elif descname or changeset.module:
                module = changeset.module or _('Builtins')
                if not descname:
                    descname = _('Module level')
                if context:
                    entry = f'<b>{descname}</b>: <i>{ttext}:</i> {context}'
                else:
                    entry = f'<b>{descname}</b>: <i>{ttext}</i>.'
                libchanges.setdefault(module, []).append((
                    entry,
                    changeset.docname,
                    changeset.lineno,
                ))
            else:
                if not context:
                    continue
                entry = f'<i>{ttext.capitalize()}:</i> {context}'
                title = self.env.titles[changeset.docname].astext()
                otherchanges.setdefault((changeset.docname, title), []).append((
                    entry,
                    changeset.docname,
                    changeset.lineno,
                ))

        ctx = {
            'project': self.config.project,
            'version': version,
            'docstitle': self.config.html_title,
            'shorttitle': self.config.html_short_title,
            'libchanges': sorted(libchanges.items()),
            'apichanges': sorted(apichanges),
            'otherchanges': sorted(otherchanges.items()),
            'show_copyright': self.config.html_show_copyright,
            'show_sphinx': self.config.html_show_sphinx,
        }
        with open(self.outdir / 'index.html', 'w', encoding='utf8') as f:
            f.write(self.templates.render('changes/frameset.html', ctx))
        with open(self.outdir / 'changes.html', 'w', encoding='utf8') as f:
            f.write(self.templates.render('changes/versionchanges.html', ctx))

        hltext = [
            f'.. versionadded:: {version}',
            f'.. versionchanged:: {version}',
            f'.. deprecated:: {version}',
            f'.. versionremoved:: {version}',
        ]

        def hl(no: int, line: str) -> str:
            line = '<a name="L%s"> </a>' % no + html.escape(line)
            for x in hltext:
                if x in line:
                    line = '<span class="hl">%s</span>' % line
                    break
            return line

        logger.info(bold(__('copying source files...')))
        for docname in self.env.all_docs:
            with open(
                self.env.doc2path(docname), encoding=self.config.source_encoding
            ) as f:
                try:
                    lines = f.readlines()
                except UnicodeDecodeError:
                    logger.warning(
                        __('could not read %r for changelog creation'), docname
                    )
                    continue
            text = ''.join(hl(i + 1, line) for (i, line) in enumerate(lines))
            ctx = {
                'filename': str(self.env.doc2path(docname, False)),
                'text': text,
            }
            rendered = self.templates.render('changes/rstsource.html', ctx)
            targetfn = self.outdir / 'rst' / f'{docname}.html'
            targetfn.parent.mkdir(parents=True, exist_ok=True)
            with open(targetfn, 'w', encoding='utf-8') as f:
                f.write(rendered)
        themectx = {
            'theme_' + key: val for (key, val) in self.theme.get_options({}).items()
        }
        copy_asset_file(
            package_dir.joinpath('themes', 'default', 'static', 'default.css.jinja'),
            self.outdir,
            context=themectx,
            renderer=self.templates,
            force=True,
        )
        copy_asset_file(
            package_dir.joinpath('themes', 'basic', 'static', 'basic.css'),
            self.outdir / 'basic.css',
            force=True,
        )

    def hl(self, text: str, version: str) -> str:
        text = html.escape(text)
        for directive in (
            'versionchanged',
            'versionadded',
            'deprecated',
            'versionremoved',
        ):
            text = text.replace(
                f'.. {directive}:: {version}', f'<b>.. {directive}:: {version}</b>'
            )
        return text

    def finish(self) -> None:
        pass


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

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