#!/usr/bin/env python3
"""Generate the redistributable PDF quality corpus using only Python's stdlib."""
from __future__ import annotations

import hashlib
import json
from pathlib import Path

ROOT = Path(__file__).resolve().parent
FIXTURES = ROOT / "fixtures"
GENERATOR_VERSION = "2"


def pdf(objects: list[bytes], root: int = 1) -> bytes:
    out = bytearray(b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n")
    offsets = [0]
    for number, body in enumerate(objects, 1):
        offsets.append(len(out))
        out += f"{number} 0 obj\n".encode() + body + b"\nendobj\n"
    xref = len(out)
    out += f"xref\n0 {len(objects)+1}\n0000000000 65535 f \n".encode()
    out += b"".join(f"{offset:010d} 00000 n \n".encode() for offset in offsets[1:])
    out += f"trailer\n<< /Size {len(objects)+1} /Root {root} 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode()
    return bytes(out)


def stream(data: bytes, extra: bytes = b"") -> bytes:
    return b"<< /Length " + str(len(data)).encode() + (b" " + extra if extra else b"") + b" >>\nstream\n" + data + b"\nendstream"


def one_page(content: bytes, *, media: str = "0 0 612 792", extra_page: bytes = b"") -> bytes:
    return pdf([
        b"<< /Type /Catalog /Pages 2 0 R >>",
        b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
        f"<< /Type /Page /Parent 2 0 R /MediaBox [{media}] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R {extra_page.decode()} >>".encode(),
        b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
        stream(content),
    ])


def digital_text() -> bytes:
    c = b"BT /F1 24 Tf 72 720 Td (Deterministic Digital Text) Tj 0 -34 Td /F1 12 Tf (Searchable paragraph with punctuation: alpha, beta, 123.) Tj 0 -20 Td (Second line preserves reading order.) Tj ET"
    return one_page(c)


def links() -> bytes:
    c = b"BT /F1 16 Tf 72 700 Td (Safe HTTPS link) Tj 0 -40 Td (Internal destination) Tj ET"
    return pdf([
        b"<< /Type /Catalog /Pages 2 0 R >>",
        b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
        b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R /Annots [6 0 R 7 0 R] >>",
        b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", stream(c),
        b"<< /Type /Annot /Subtype /Link /Rect [70 695 220 720] /A << /S /URI /URI (https://example.com/pdf2html-fixture) >> >>",
        b"<< /Type /Annot /Subtype /Link /Rect [70 650 220 680] /Dest [3 0 R /Fit] >>",
    ])


def tables_forms() -> bytes:
    c = b"0.5 w 70 650 360 90 re S 190 650 m 190 740 l S 310 650 m 310 740 l S 70 680 m 430 680 l S 70 710 m 430 710 l S BT /F1 12 Tf 78 718 Td (Name) Tj 120 0 Td (Quantity) Tj 120 0 Td (Price) Tj ET"
    return pdf([
        b"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [6 0 R] /NeedAppearances true >> >>",
        b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
        b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R /Annots [6 0 R] >>",
        b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", stream(c),
        b"<< /Type /Annot /Subtype /Widget /FT /Tx /T (customer_name) /Rect [75 610 300 635] /V (Ada) /P 3 0 R >>",
    ])


def vectors() -> bytes:
    c = b"q /GS1 gs 1 0 0 rg 80 590 220 140 re f 0 0 1 rg 190 650 220 140 re f Q 0 0 0 RG 4 w 80 540 m 180 620 280 500 380 580 c S"
    return pdf([
        b"<< /Type /Catalog /Pages 2 0 R >>", b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
        b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /ExtGState << /GS1 6 0 R >> >> /Contents 5 0 R >>",
        b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", stream(c), b"<< /Type /ExtGState /ca 0.55 /CA 0.75 >>",
    ])


def actual_text_fixture(text_hex: str, label: str) -> bytes:
    c = f"BT /F1 15 Tf 72 720 Td ({label}) Tj ET /Span << /ActualText <FEFF{text_hex}> >> BDC 0 0 0 rg 90 650 20 28 re f 116 650 20 28 re f 142 650 20 28 re f EMC".encode()
    return one_page(c)


def rtl_actualtext() -> bytes:
    return actual_text_fixture("05E905DC05D505DD", "RTL ActualText")


def cjk_actualtext() -> bytes:
    return actual_text_fixture("4F60597D4E16754C", "CJK ActualText")


def catalog_layout() -> bytes:
    # Dense, asymmetric product grid with repeated image, price, and description regions.
    content = b"""0.92 g 42 430 250 315 re f 320 430 250 315 re f 42 72 528 330 re f
0 G 1 w 42 430 250 315 re S 320 430 250 315 re S 42 72 528 330 re S
0.75 g 58 548 218 170 re f 336 548 218 170 re f 58 112 190 250 re f
0 g BT /F1 22 Tf 58 758 Td (AUTUMN CATALOG) Tj /F1 15 Tf 0 -48 Td (Canvas Weekender) Tj 278 0 Td (Studio Lamp) Tj
/F1 11 Tf -278 -198 Td (Durable travel essential) Tj 0 -18 Td ($129) Tj 278 18 Td (Warm directional light) Tj 0 -18 Td ($89) Tj
-278 -386 Td /F1 18 Tf (FIELD NOTES) Tj /F1 11 Tf 210 0 Td (Layered editorial copy beside a tall product image.) Tj
0 -18 Td (A catalog spread tests columns, hierarchy, alignment,) Tj 0 -18 Td (repeated cards, whitespace, and mixed visual density.) Tj ET"""
    return one_page(content, media="0 0 612 820")


def scanned() -> bytes:
    # A tiny image-only page: deliberately no text operators, suitable for OCR-path checks.
    pixels = bytes([255,255,255, 0,0,0, 255,255,255, 0,0,0] * 4)
    c = b"q 360 0 0 180 100 500 cm /Im1 Do Q"
    return pdf([
        b"<< /Type /Catalog /Pages 2 0 R >>", b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
        b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /XObject << /Im1 4 0 R >> >> /Contents 5 0 R >>",
        stream(pixels, b"/Type /XObject /Subtype /Image /Width 4 /Height 4 /ColorSpace /DeviceRGB /BitsPerComponent 8"), stream(c),
    ])


def rotated_mixed() -> bytes:
    return pdf([
        b"<< /Type /Catalog /Pages 2 0 R >>", b"<< /Type /Pages /Kids [3 0 R 6 0 R] /Count 2 >>",
        b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Rotate 90 /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>",
        b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", stream(b"BT /F1 16 Tf 72 720 Td (Rotated portrait page) Tj ET"),
        b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 842 595] /Resources << /Font << /F1 4 0 R >> >> /Contents 7 0 R >>",
        stream(b"BT /F1 16 Tf 72 520 Td (Landscape mixed-size page) Tj ET"),
    ])


def malformed() -> bytes:
    # Parseable by tolerant readers but intentionally lacks a cross-reference table.
    return b"%PDF-1.4\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Count 0 /Kids [] >> endobj\ntrailer << /Root 1 0 R >>\n%%EOF\n"


def hostile_bounded() -> bytes:
    # Structural stress only: finite nesting/references, tiny payload, and no active action.
    nested = b"[" * 32 + b"0" + b"]" * 32
    return pdf([
        b"<< /Type /Catalog /Pages 2 0 R /Names << /EmbeddedFiles 4 0 R >> >>",
        b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
        b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << >> /Contents 5 0 R >>",
        b"<< /Names [(bounded-nesting) 6 0 R] >>", stream(b""), b"<< /Type /Filespec /F (bounded.txt) /Desc " + nested + b" >>",
    ])


GENERATORS = {
    "digital-text.pdf": digital_text,
    "links-annotations.pdf": links,
    "tables-forms.pdf": tables_forms,
    "vectors-transparency.pdf": vectors,
    "rtl-actualtext.pdf": rtl_actualtext,
    "cjk-actualtext.pdf": cjk_actualtext,
    "catalog-layout.pdf": catalog_layout,
    "ocr-scanned.pdf": scanned,
    "rotated-mixed-size.pdf": rotated_mixed,
    "malformed-xref.pdf": malformed,
    "hostile-bounded.pdf": hostile_bounded,
}


def main() -> None:
    FIXTURES.mkdir(parents=True, exist_ok=True)
    hashes = {}
    for name, factory in GENERATORS.items():
        data = factory()
        (FIXTURES / name).write_bytes(data)
        hashes[name] = hashlib.sha256(data).hexdigest()
    print(json.dumps(hashes, indent=2, sort_keys=True))


if __name__ == "__main__":
    main()
