#!/usr/bin/env python3
"""Create original DjVu text-layer teaching fixtures; never touches source books."""

import argparse
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path
import platform
import shutil
import subprocess
import tempfile

from PIL import Image, ImageChops, ImageDraw, ImageFont, __version__ as pillow_version


PAGES = [
    ["Carnet de lecture", "Le café ferme à seize heures.", "Repère : boîte 18, dossier B-204."],
    ["Reading notebook", "The parcel contains 18 notebooks.", "Reference B-204; shelf 7."],
]


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--djvu-bin", type=Path, required=True)
    parser.add_argument("--font", type=Path, required=True, help="Unmodified NotoSans-Regular.ttf")
    parser.add_argument("--output", type=Path, required=True, help="New directory only")
    args = parser.parse_args()
    tools = args.djvu_bin.resolve()
    out = args.output.resolve()
    if out.exists():
        parser.error("Output already exists; choose a new directory.")
    for name in ("ddjvu", "djvutxt", "djvused", "djvm", "cjb2"):
        if not (tools / name).is_file():
            parser.error(f"Missing DjVuLibre executable: {name}")
    for name in ("pdftotext", "pdfinfo", "pdfimages"):
        if not shutil.which(name):
            parser.error(f"Poppler {name} must be on PATH.")
    if not args.font.is_file():
        parser.error("The supplied font does not exist.")
    out.mkdir(parents=True)
    commands = []

    with tempfile.TemporaryDirectory(prefix="djvu-original-fixtures-") as temp:
        work = Path(temp)

        def run(name, *argv, expected=0):
            executable = str(tools / name) if (tools / name).is_file() else name
            command = [executable, *map(str, argv)]
            result = subprocess.run(command, capture_output=True, timeout=90, check=False)
            def portable(value):
                return value.replace(str(work), "WORK").replace(str(out), "OUTPUT").replace(str(tools), "DJVU_BIN")
            commands.append({
                "command": [portable(value) for value in command],
                "exit": result.returncode,
                "stdout": portable(result.stdout.decode("utf-8", errors="replace")),
                "stderr": portable(result.stderr.decode("utf-8", errors="replace")),
            })
            if result.returncode != expected:
                raise RuntimeError(f"Unexpected exit from {name}: {result.returncode}; {result.stderr!r}")
            return result.stdout

        version_output = run("ddjvu", "--help", expected=1).decode() or commands[-1]["stderr"]
        version = version_output.splitlines()[0]
        poppler_version = run("pdftotext", "-v").decode().strip() or commands[-1]["stderr"].splitlines()[0]
        pages = []
        for number, lines in enumerate(PAGES, 1):
            page = Image.new("1", (1200, 1600), 1)
            draw = ImageDraw.Draw(page)
            font = ImageFont.truetype(str(args.font), size=40)
            for line, y in zip(lines, (160, 280, 400)):
                draw.text((100, y), line, font=font, fill=0)
            page.save(out / f"source-page-{number}.png")
            page.save(work / f"page-{number}.pbm")
            target = work / f"page-{number}.djvu"
            run("cjb2", "-dpi", "300", work / f"page-{number}.pbm", target)
            pages.append(target)

        run("djvm", "-c", out / "image-only.djvu", *pages)
        for variant in ("correct-text", "wrong-text"):
            target = out / f"{variant}.djvu"
            shutil.copyfile(out / "image-only.djvu", target)
            edits = []
            for number, lines in enumerate(PAGES, 1):
                values = lines if variant == "correct-text" else [line.replace("18", "13").replace("B-204", "B-2O4") for line in lines]
                # DjVu coordinates originate at bottom left. These are authored
                # page-level text zones, not output produced by OCR.
                text = json.dumps("\n".join(values), ensure_ascii=False)
                edits.append(f"select {number}\nset-txt\n(page 0 0 1200 1600 {text})\n.\n")
            editor_script = work / f"{variant}.dsed"
            editor_script.write_text("\n".join(edits), encoding="utf-8")
            run("djvused", target, "-f", editor_script, "-s")

        expected_text = "\n\f".join("\n".join(lines) for lines in PAGES) + "\n\f"
        tests = []
        for variant in ("image-only", "correct-text", "wrong-text"):
            target = out / f"{variant}.djvu"
            count = int(run("djvused", target, "-e", "n").strip())
            assert count == 2
            text = run("djvutxt", target).decode("utf-8")
            (out / f"{variant}.txt").write_text(text, encoding="utf-8")
            equality = []
            for number in (1, 2):
                rendered = work / f"{variant}-{number}.pbm"
                run("ddjvu", "-format=pbm", f"-page={number}", target, rendered)
                with Image.open(rendered) as actual, Image.open(out / f"source-page-{number}.png") as source:
                    equal = actual.size == source.size and ImageChops.difference(actual.convert("L"), source.convert("L")).getbbox() is None
                    assert equal
                    equality.append(equal)
            if variant == "image-only":
                assert text == ""
            elif variant == "correct-text":
                assert text == expected_text
            else:
                assert "13 notebooks" in text and "B-2O4" in text and text != expected_text
            tests.append({"file": target.name, "pages": count, "extracted_text": text, "render_pixels_equal_source": equality})

        run("ddjvu", "-format=pdf", out / "correct-text.djvu", out / "converted-image-pages.pdf")
        pdf_text = run("pdftotext", out / "converted-image-pages.pdf", "-").decode("utf-8")
        (out / "converted-pdf.txt").write_text(pdf_text, encoding="utf-8")
        assert not pdf_text.strip(), repr(pdf_text)
        pdf_info = run("pdfinfo", out / "converted-image-pages.pdf").decode("utf-8")
        pdf_pages = int(next(line.split(":", 1)[1] for line in pdf_info.splitlines() if line.startswith("Pages:")))
        assert pdf_pages == 2
        run("pdfimages", "-png", out / "converted-image-pages.pdf", work / "pdf-image")
        pdf_equality = []
        for number in (1, 2):
            with Image.open(work / f"pdf-image-{number - 1:03d}.png") as actual, Image.open(out / f"source-page-{number}.png") as source:
                equal = actual.size == source.size and ImageChops.difference(actual.convert("L"), source.convert("L")).getbbox() is None
                assert equal
                pdf_equality.append(equal)
        tests.append({"file": "converted-image-pages.pdf", "source": "correct-text.djvu", "pages": pdf_pages, "embedded_image_pixels_equal_source": pdf_equality, "pdftotext_raw": pdf_text, "non_whitespace_characters": len(pdf_text.strip())})
        # A deliberately incomplete copy is a decoder error, unlike an intact
        # document that has no hidden text. Never modify a user's source file.
        damaged = work / "deliberately-truncated.djvu"
        damaged.write_bytes((out / "correct-text.djvu").read_bytes()[:64])
        bad = subprocess.run([str(tools / "ddjvu"), "-format=pbm", "-page=1", str(damaged), str(work / "bad.pbm")], capture_output=True, timeout=90)
        assert bad.returncode != 0
        tests.append({"file": "private deliberately truncated copy", "exit": bad.returncode, "stderr": bad.stderr.decode("utf-8", errors="replace").replace(str(work), "WORK")})

        hashes = {path.name: {"bytes": path.stat().st_size, "sha256": hashlib.sha256(path.read_bytes()).hexdigest()} for path in sorted(out.iterdir()) if path.is_file()}
        results = {
            "date": datetime.now(timezone.utc).date().isoformat(), "author": "BookTranslator Team",
            "environment": {"system": platform.platform(), "python": platform.python_version(), "Pillow": pillow_version, "DjVuLibre_reported": version, "Poppler": poppler_version},
            "provenance": "Two original authored pages; manually inserted correct and deliberately wrong hidden text. No OCR engine, user files, GUI reader or translation service tested.",
            "djvulibre_archive": {"url": "https://downloads.sourceforge.net/djvu/djvulibre-3.5.30.tar.gz", "sha256": "ee5e457d4cfebe566f94b99e5e3d3cc7f5c79ddb741c2ac2ba2e456f00329644", "note": "Official download labelled 3.5.30; compiled executables and configure report 3.5.29."},
            "settings": {"image_pixels": [1200, 1600], "dpi": 300, "font_sha256": hashlib.sha256(args.font.read_bytes()).hexdigest(), "font": "NotoSans-Regular.ttf, 40 px", "encoding": "cjb2 lossless default; no clean or lossy options", "hidden_text": "authored page-level zones", "pdf": "ddjvu -format=pdf default native resolution"},
            "ground_truth": PAGES, "tests": tests, "files": hashes, "commands": commands,
            "limits": ["Two simple pages do not measure OCR quality or complex layouts.", "The PDF check used pdftotext, not a screen reader or GUI.", "Incorrect text was inserted by this script, not produced by OCR.", "No DjVu support or translation outcome is claimed for BookTranslator."],
        }
        (out / "results.json").write_text(json.dumps(results, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
        print(json.dumps({"output": str(out), "tests": len(tests), "commands": len(commands), "status": "PASS"}))


if __name__ == "__main__":
    main()
