from __future__ import annotations

import csv
import hashlib
import json
from pathlib import Path

import pymupdf
from PIL import Image, ImageDraw, ImageFont


PACKAGE = Path(__file__).resolve().parents[1]
INPUT_DIR = PACKAGE / "samples" / "input"
OUTPUT_DIR = PACKAGE / "samples" / "output"
RENDER_DIR = PACKAGE / "evidence" / "rendered-pages"
SCREENSHOT_DIR = PACKAGE / "screenshots"

PAIRS = (
    ("text-heavy-handout.pdf", "text-heavy-handout_compressed.pdf", "Text-heavy handout"),
    ("image-rich-lookbook.pdf", "image-rich-lookbook_compressed.pdf", "Image-rich lookbook"),
    (
        "already-optimized-handout.pdf",
        "already-optimized-handout_compressed.pdf",
        "Already optimized",
    ),
)


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest().upper()


def render_pdf(path: Path, destination: Path) -> list[Path]:
    destination.mkdir(parents=True, exist_ok=True)
    rendered: list[Path] = []
    with pymupdf.open(path) as document:
        for page_index, page in enumerate(document, start=1):
            pixmap = page.get_pixmap(matrix=pymupdf.Matrix(2, 2), alpha=False)
            output = destination / f"page-{page_index:02d}.png"
            pixmap.save(output)
            rendered.append(output)
    return rendered


def fit_image(image: Image.Image, size: tuple[int, int]) -> Image.Image:
    copy = image.convert("RGB")
    copy.thumbnail(size, Image.Resampling.LANCZOS)
    canvas = Image.new("RGB", size, "#f4f1ee")
    x = (size[0] - copy.width) // 2
    y = (size[1] - copy.height) // 2
    canvas.paste(copy, (x, y))
    return canvas


def make_contact_sheet(rows: list[dict[str, object]]) -> None:
    font = ImageFont.load_default(size=22)
    small_font = ImageFont.load_default(size=18)
    sheet = Image.new("RGB", (1600, 1380), "#16051a")
    draw = ImageDraw.Draw(sheet)
    draw.text((60, 35), "Local PDF compression: before and after", fill="#fff7f1", font=font)
    draw.text(
        (60, 72),
        "Self-authored fixtures - Balanced preset - page 1 previews",
        fill="#d4becf",
        font=small_font,
    )
    draw.text((280, 118), "Before", fill="#ff8b71", font=font)
    draw.text((1040, 118), "After", fill="#ff8b71", font=font)

    for index, row in enumerate(rows):
        top = 165 + index * 395
        before_image = fit_image(Image.open(row["input_render"]), (660, 310))
        after_image = fit_image(Image.open(row["output_render"]), (660, 310))
        sheet.paste(before_image, (60, top))
        sheet.paste(after_image, (880, top))
        label = str(row["label"])
        before_bytes = int(row["source_bytes"])
        output_bytes = int(row["output_bytes"])
        reduction = float(row["reduction_percent"])
        draw.text((60, top + 320), label, fill="#fff7f1", font=font)
        draw.text(
            (60, top + 352),
            f"{before_bytes:,} bytes -> {output_bytes:,} bytes - {reduction:.1f}% smaller",
            fill="#d4becf",
            font=small_font,
        )

    sheet.save(SCREENSHOT_DIR / "06-before-after.jpg", quality=92, optimize=True)


def make_all_pages_sheet(rendered_paths: list[tuple[str, str, Path]]) -> None:
    thumb_size = (270, 200)
    columns = 4
    cell_width = 320
    cell_height = 255
    rows = (len(rendered_paths) + columns - 1) // columns
    sheet = Image.new("RGB", (columns * cell_width, rows * cell_height), "#ece8e4")
    draw = ImageDraw.Draw(sheet)
    font = ImageFont.load_default(size=15)
    for index, (kind, filename, path) in enumerate(rendered_paths):
        x = (index % columns) * cell_width + 25
        y = (index // columns) * cell_height + 15
        thumb = fit_image(Image.open(path), thumb_size)
        sheet.paste(thumb, (x, y))
        draw.text((x, y + 207), f"{kind}: {filename}", fill="#241f20", font=font)
        draw.text((x, y + 226), path.stem, fill="#5a5153", font=font)
    sheet.save(PACKAGE / "evidence" / "all-pages-contact-sheet.jpg", quality=90, optimize=True)


def main() -> None:
    records: list[dict[str, object]] = []
    all_rendered: list[tuple[str, str, Path]] = []

    for input_name, output_name, label in PAIRS:
        input_path = INPUT_DIR / input_name
        output_path = OUTPUT_DIR / output_name
        if not input_path.exists() or not output_path.exists():
            raise FileNotFoundError(f"Missing pair: {input_path} / {output_path}")

        input_renders = render_pdf(input_path, RENDER_DIR / "input" / input_path.stem)
        output_renders = render_pdf(output_path, RENDER_DIR / "output" / output_path.stem)
        if len(input_renders) != len(output_renders):
            raise RuntimeError(f"Page-count mismatch for {input_name}")

        source_bytes = input_path.stat().st_size
        output_bytes = output_path.stat().st_size
        reduction_bytes = source_bytes - output_bytes
        reduction_percent = reduction_bytes / source_bytes * 100
        input_hash = sha256(input_path)
        output_hash = sha256(output_path)

        record: dict[str, object] = {
            "label": label,
            "input_file": input_name,
            "output_file": output_name,
            "source_bytes": source_bytes,
            "output_bytes": output_bytes,
            "reduction_bytes": reduction_bytes,
            "reduction_percent": round(reduction_percent, 3),
            "page_count": len(input_renders),
            "input_sha256": input_hash,
            "output_sha256": output_hash,
            "byte_identical": input_hash == output_hash,
            "input_render": input_renders[1] if label == "Image-rich lookbook" else input_renders[0],
            "output_render": output_renders[1] if label == "Image-rich lookbook" else output_renders[0],
        }
        records.append(record)

        for render in input_renders:
            all_rendered.append(("input", input_name, render))
        for render in output_renders:
            all_rendered.append(("output", output_name, render))

    if not records[2]["byte_identical"]:
        raise RuntimeError("The already-optimized output was not an exact copy of its source")

    with (PACKAGE / "evidence" / "measurements.csv").open("w", newline="", encoding="utf-8") as handle:
        fields = (
            "label",
            "input_file",
            "output_file",
            "source_bytes",
            "output_bytes",
            "reduction_bytes",
            "reduction_percent",
            "page_count",
            "input_sha256",
            "output_sha256",
            "byte_identical",
        )
        writer = csv.DictWriter(handle, fieldnames=fields)
        writer.writeheader()
        for record in records:
            writer.writerow({field: record[field] for field in fields})

    serializable = [
        {key: value for key, value in record.items() if key not in {"input_render", "output_render"}}
        for record in records
    ]
    (PACKAGE / "evidence" / "verification.json").write_text(
        json.dumps(serializable, indent=2) + "\n",
        encoding="utf-8",
    )
    make_contact_sheet(records)
    make_all_pages_sheet(all_rendered)


if __name__ == "__main__":
    main()
