# SPDX-FileCopyrightText: 2023 James R. Barlow
# SPDX-License-Identifier: MPL-2.0

"""OCRmyPDF page processing pipeline functions."""

from __future__ import annotations

import logging
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from pikepdf import Dictionary, Name, Pdf
from pikepdf import __version__ as PIKEPDF_VERSION
from pikepdf.models.metadata import PdfMetadata, encode_pdf_date

from ocrmypdf._annots import remove_broken_goto_annotations
from ocrmypdf._defaults import PROGRAM_NAME
from ocrmypdf._jobcontext import PdfContext
from ocrmypdf._version import __version__ as OCRMYPF_VERSION
from ocrmypdf.languages import iso_639_2_from_3

log = logging.getLogger(__name__)


def get_docinfo(base_pdf: Pdf, context: PdfContext) -> dict[str, str]:
    """Read the document info and store it in a dictionary."""
    options = context.options

    def from_document_info(key):
        try:
            s = base_pdf.docinfo[key]
            return str(s)
        except (KeyError, TypeError):
            return ''

    pdfmark = {
        k: from_document_info(k)
        for k in ('/Title', '/Author', '/Keywords', '/Subject', '/CreationDate')
    }
    if options.title:
        pdfmark['/Title'] = options.title
    if options.author:
        pdfmark['/Author'] = options.author
    if options.keywords:
        pdfmark['/Keywords'] = options.keywords
    if options.subject:
        pdfmark['/Subject'] = options.subject

    creator_tag = context.plugin_manager.hook.get_ocr_engine().creator_tag(options)

    pdfmark['/Creator'] = f'{PROGRAM_NAME} {OCRMYPF_VERSION} / {creator_tag}'
    pdfmark['/Producer'] = f'pikepdf {PIKEPDF_VERSION}'
    pdfmark['/ModDate'] = encode_pdf_date(datetime.now(timezone.utc))
    return pdfmark


def report_on_metadata(options, missing):
    if not missing:
        return
    if options.output_type.startswith('pdfa'):
        log.warning(
            "Some input metadata could not be copied because it is not "
            "permitted in PDF/A. You may wish to examine the output "
            "PDF's XMP metadata."
        )
        log.debug("The following metadata fields were not copied: %r", missing)
    else:
        log.error(
            "Some input metadata could not be copied."
            "You may wish to examine the output PDF's XMP metadata."
        )
        log.info("The following metadata fields were not copied: %r", missing)


def repair_docinfo_nuls(pdf):
    """If the DocumentInfo block contains NUL characters, remove them.

    If the DocumentInfo block is malformed, log an error and continue.
    """
    modified = False
    try:
        if not isinstance(pdf.docinfo, Dictionary):
            raise TypeError("DocumentInfo is not a dictionary")
        for k, v in pdf.docinfo.items():
            if isinstance(v, str) and b'\x00' in bytes(v):
                pdf.docinfo[k] = bytes(v).replace(b'\x00', b'')
                modified = True
    except TypeError:
        # TypeError can also be raised if dictionary items are unexpected types
        log.error("File contains a malformed DocumentInfo block - continuing anyway.")
    return modified


def should_linearize(working_file: Path, context: PdfContext) -> bool:
    """Determine whether the PDF should be linearized.

    For smaller files, linearization is not worth the effort.
    """
    filesize = os.stat(working_file).st_size
    if filesize > (context.options.fast_web_view * 1_000_000):
        return True
    return False


def _fix_metadata(meta_original: PdfMetadata, meta_pdf: PdfMetadata):
    # If xmp:CreateDate is missing, set it to the modify date to
    # ensure consistency with Ghostscript.
    if 'xmp:CreateDate' not in meta_pdf:
        meta_pdf['xmp:CreateDate'] = meta_pdf.get('xmp:ModifyDate', '')
    if meta_pdf.get('dc:title') == 'Untitled':
        # Ghostscript likes to set title to Untitled if omitted from input.
        # Reverse this, because PDF/A TechNote 0003:Metadata in PDF/A-1
        # and the XMP Spec do not make this recommendation.
        if 'dc:title' not in meta_original:
            del meta_pdf['dc:title']


def _unset_empty_metadata(meta: PdfMetadata, options):
    """Unset metadata fields that were explicitly set to empty strings.

    If the user explicitly specified an empty string for any of the
    following, they should be unset and not reported as missing in
    the output pdf. Note that some metadata fields use differing names
    between PDF/A and PDF.
    """
    if options.title == '' and 'dc:title' in meta:
        del meta['dc:title']  # PDF/A and PDF
    if options.author == '':
        if 'dc:creator' in meta:
            del meta['dc:creator']  # PDF/A (Not xmp:CreatorTool)
        if 'pdf:Author' in meta:
            del meta['pdf:Author']  # PDF
    if options.subject == '':
        if 'dc:description' in meta:
            del meta['dc:description']  # PDF/A
        if 'dc:subject' in meta:
            del meta['dc:subject']  # PDF
    if options.keywords == '' and 'pdf:Keywords' in meta:
        del meta['pdf:Keywords']  # PDF/A and PDF


def _set_language(pdf: Pdf, languages: list[str]):
    """Set the language of the PDF."""
    if Name.Lang in pdf.Root or not languages:
        return  # Already set or can't change
    primary_language_iso639_3 = languages[0]
    if not primary_language_iso639_3:
        return
    iso639_2 = iso_639_2_from_3(primary_language_iso639_3)
    if not iso639_2:
        return
    pdf.Root.Lang = iso639_2


class MetadataProgress:
    def __init__(self, progressbar_class, enable: bool = True):
        self.progressbar_class = progressbar_class
        self.progressbar = self.progressbar_class(
            total=100, desc="Linearizing", unit='%', disable=not enable
        )

    def __enter__(self):
        self.progressbar.__enter__()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        return self.progressbar.__exit__(exc_type, exc_value, traceback)

    def __call__(self, percent: int):
        if not self.progressbar_class:
            return
        self.progressbar.update(completed=percent)


def metadata_fixup(
    working_file: Path, context: PdfContext, pdf_save_settings: dict[str, Any]
) -> Path:
    """Fix certain metadata fields whether PDF or PDF/A.

    Override some of Ghostscript's metadata choices.

    Also report on metadata in the input file that was not retained during
    conversion.
    """
    output_file = context.get_path('metafix.pdf')
    options = context.options

    pbar_class = context.plugin_manager.hook.get_progressbar_class()
    with (
        Pdf.open(context.origin) as original,
        Pdf.open(working_file) as pdf,
        MetadataProgress(pbar_class, options.progress_bar) as pbar,
    ):
        docinfo = get_docinfo(original, context)
        with (
            original.open_metadata(
                set_pikepdf_as_editor=False, update_docinfo=False, strict=False
            ) as meta_original,
            pdf.open_metadata() as meta_pdf,
        ):
            meta_pdf.load_from_docinfo(
                docinfo, delete_missing=False, raise_failure=False
            )
            _fix_metadata(meta_original, meta_pdf)
            _unset_empty_metadata(meta_original, options)
            _unset_empty_metadata(meta_pdf, options)
            meta_missing = set(meta_original.keys()) - set(meta_pdf.keys())
            report_on_metadata(options, meta_missing)

        _set_language(pdf, options.languages)
        pdf.save(output_file, progress=pbar, **pdf_save_settings)

    return output_file
