# SPDX-License-Identifier: Apache-2.0
"""Reproduce nine original A4 SVG/PDF source aids and exact alternatives."""
from __future__ import annotations

import argparse
import hashlib
import os
import subprocess
import tempfile
import xml.etree.ElementTree as ET
from html import escape
from pathlib import Path

from PIL import ImageFont

HERE = Path(__file__).resolve().parent
INK = "#183743"
FILLS = ("#e7f2f6", "#edf4e8", "#fff0de", "#f0eafa", "#eef0f2")
FONT = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"


def text(x: float, y: float, value: str, size: int = 20, bold: bool = False) -> str:
    """Return visible selectable text with a shared dark ink colour."""
    return f'<text x="{x}" y="{y}" font-family="DejaVu Sans" font-size="{size}" font-weight="{700 if bold else 400}" fill="{INK}">{escape(value)}</text>'


def box(x: float, y: float, w: float, h: float, fill: str = "white") -> str:
    """Draw a clearly outlined panel; colour is supplementary."""
    return f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="12" fill="{fill}" stroke="{INK}" stroke-width="3"/>'


def path(d: str, width: int = 4) -> str:
    """Draw an original rounded line illustration."""
    return f'<path d="{d}" fill="none" stroke="{INK}" stroke-width="{width}" stroke-linecap="round" stroke-linejoin="round"/>'


def circle(x: float, y: float, r: float, fill: str = "white") -> str:
    """Draw an outlined or dark filled circular mark."""
    return f'<circle cx="{x}" cy="{y}" r="{r}" fill="{fill}" stroke="{INK}" stroke-width="3"/>'


def paragraph(
    value: str,
    x: int,
    y: int,
    width: int = 640,
    size: int = 20,
    leading: int = 30,
    bold: bool = False,
) -> tuple[str, int]:
    """Wrap measured text within a panel and return the next baseline."""
    font = ImageFont.truetype(BOLD if bold else FONT, size)
    rows: list[str] = []
    current = ""
    for word in value.split():
        candidate = f"{current} {word}".strip()
        if font.getlength(candidate) > width and current:
            rows.append(current)
            current = word
        else:
            current = candidate
    rows.append(current)
    return (
        "".join(
            text(x, y + i * leading, row, size, bold) for i, row in enumerate(rows)
        ),
        y + len(rows) * leading,
    )


def block(label: str, value: str, y: int, h: int, fill: str, size: int = 20) -> str:
    """Place an exact source quotation in an outlined labelled panel."""
    out = box(48, y, 698, h, fill) + text(68, y + 34, label, 18, True)
    body, end = paragraph(value, 68, y + 70, 655, size, 29 if size >= 19 else 23)
    if end > y + h + 5:
        raise ValueError(f"{label}: text exceeds panel")
    return out + body


def page(title: str, desc: str, body: str) -> str:
    """Wrap a semantic SVG with A4 geometry and original-material attribution."""
    return "\n".join(
        [
            '<svg xmlns="http://www.w3.org/2000/svg" width="210mm" height="297mm" viewBox="0 0 794 1123" role="img" aria-labelledby="title desc">',
            f'<title id="title">{escape(title)}</title>',
            f'<desc id="desc">{escape(desc)}</desc>',
            '<rect width="794" height="1123" fill="white"/>',
            text(42, 53, "SUBJECTNEST · FOUNDATION HASS · W35–36", 17, True),
            text(42, 95, title, 25, True),
            paragraph(desc, 42, 130, 705, 16, 23)[0],
            body,
            path("M42 1038 L752 1038", 2),
            text(
                42,
                1064,
                "Original teaching material · SubjectNest by NeuroForgeIO Pty Ltd",
                12,
            ),
            text(
                42,
                1085,
                "© 2026 · CC BY 4.0 · exact text/tactile alternatives in pack",
                12,
            ),
            "</svg>",
            "",
        ]
    )


SOURCES = {
    "A": "Paper Share Corner: a square board is on the left and a round picture table is on the right. This is an invented place drawing.",
    "B": "I plan two layouts for the same board-page and table-page: side by side, or board-page above table-page. A planned layout is not a use report.",
    "C1": "The board matters to me because I show my paper drawings there. This is my own reason.",
    "C2": "The table matters to me because I talk about picture pages there. This is my own reason.",
    "D": "Look after this corner by returning picture pages to the broad folder after use. Ask me before putting anything on the board. This is a care request.",
    "E": "During one paper try, I looked at the side-by-side layout. I pointed to the board-page when asked. I have not reported anyone else’s use.",
    "F": "During one paper try, I looked at the stacked layout. I asked what the lower page meant. I have not reported later use or everyone’s view.",
}
FRESH = {
    "arch": (
        "Paper Arch: a rectangle picture wall is on the left and an oval reading pad is on the right. This is an invented place drawing.",
        "I plan the same wall-page and pad-page beside each other, or wall-page below pad-page. This is a plan, not a use report.",
        "The wall matters to me because I show my paper story pages there. This is my own reason.",
        "Return picture pages to the broad pocket after use. Ask me before putting a page on the wall. This is a care request.",
        "During one paper try, I looked at the beside layout. I pointed to the pad-page when asked. I have not reported other people’s use.",
    ),
    "sail": (
        "Paper Sail: a triangle picture frame is on the left and a square page tray is on the right. This is an invented place drawing.",
        "I plan the same frame-page and tray-page beside each other, or tray-page above frame-page. This is a plan, not a use report.",
        "The tray matters to me because I keep my paper story pages there. This is my own reason.",
        "Return picture pages to the broad sleeve after use. Ask me before putting a page in the frame. This is a care request.",
        "I looked at the beside plan and chose pass before the paper try. I did not use the display. I have not reported other people’s views.",
    ),
}
KINDS = {
    "main": (("board", "square"), ("table", "round")),
    "arch": (("wall", "rectangle"), ("pad", "oval")),
    "sail": (("frame", "triangle"), ("tray", "square")),
}


def icon(shape: str, x: float, y: float, r: float) -> str:
    """Draw the exact source feature shape, with machine-readable geometry."""
    if shape in ("square", "rectangle"):
        w = r * (2.8 if shape == "rectangle" else 2)
        return f'<rect data-shape="{shape}" x="{x-w/2}" y="{y-r}" width="{w}" height="{r*2}" fill="white" stroke="{INK}" stroke-width="3"/>'
    if shape == "round":
        return f'<circle data-shape="round" cx="{x}" cy="{y}" r="{r}" fill="white" stroke="{INK}" stroke-width="3"/>'
    if shape == "oval":
        return f'<ellipse data-shape="oval" cx="{x}" cy="{y}" rx="{r*1.5}" ry="{r*.8}" fill="white" stroke="{INK}" stroke-width="3"/>'
    return f'<polygon data-shape="triangle" points="{x},{y-r} {x+r},{y+r} {x-r},{y+r}" fill="white" stroke="{INK}" stroke-width="3"/>'


def diagram(kind: str, mode: str, x: int, y: int, w: int, h: int) -> str:
    """Draw place features or the same feature pages in two distinct planned layouts."""
    small = w < 300
    out = box(x, y, w, h, FILLS[0])
    label = (
        "PLACE DRAWING"
        if mode == "place"
        else "PLAN · " + ("BESIDE" if mode == "beside" else "TOP / BOTTOM")
    )
    out += text(x + 10, y + 23, label, 12 if small else 18, True)
    out += f'<g data-kind="{kind}" data-layout="{mode}">'
    order = list(KINDS[kind])
    if mode == "stacked" and kind in ("arch", "sail"):
        order.reverse()
    for i, (feature, shape) in enumerate(order):
        out += f'<g data-feature="{feature}">'
        if mode == "place":
            xx, yy = x + w * (0.27 if i == 0 else 0.73), y + h * 0.54
            out += icon(shape, xx, yy, h * 0.18)
            out += text(
                xx - (len(feature) * 3.8 if small else len(feature) * 6),
                y + h * 0.9,
                feature.upper(),
                12 if small else 18,
                True,
            )
        else:
            if mode == "beside":
                pw, ph = w * 0.40, h * 0.35
                px, py = x + w * (0.06 if i == 0 else 0.54), y + h * 0.43
            else:
                pw, ph = w * 0.40, h * 0.35
                px, py = x + w * 0.30, y + h * (0.20 if i == 0 else 0.61)
            out += f'<rect data-page="true" x="{px}" y="{py}" width="{pw}" height="{ph}" rx="5" fill="white" stroke="{INK}" stroke-width="2"/>'
            xx, yy = px + pw / 2, py + ph * 0.37
            out += icon(shape, xx, yy, min(ph * 0.23, pw * 0.2))
            size = 11 if small else 16
            out += text(px + 8, py + ph * 0.85, feature.upper() + " PAGE", size, True)
        out += "</g>"
    return out + "</g>"


def quote(key: str) -> str:
    return "“" + SOURCES[key] + "”"


def place_a() -> str:
    body = diagram("main", "place", 48, 195, 698, 255)
    body += block("A · PLACE DRAWING", quote("A"), 475, 211, FILLS[0])
    body += block(
        "MAKE A PLACE KIT",
        "Make or direct two broad feature pages. Keep the source tab. Ask what a real view could show.",
        712,
        162,
        FILLS[1],
    )
    body += path("M88 940 L180 940 M135 915 L135 965", 3) + circle(710, 940, 18)
    body += text(202, 948, "FICTION · actual place fields stay blank", 16, True)
    return page(
        "Paper Share Corner · A",
        "Invented place source. Feature shapes/labels carry meaning; colour is supplementary.",
        body,
    )


def plans_b() -> str:
    body = block("B · TWO LAYOUTS PLANNED", quote("B"), 192, 212, FILLS[0])
    body += diagram("main", "beside", 48, 436, 338, 328)
    body += diagram("main", "stacked", 408, 436, 338, 328)
    body += block(
        "SAME SOURCED FEATURE PAGES",
        "Keep place meaning and care flaps with both plans. A reader may try or pass. Equal paper space is not agreement or fairness.",
        794,
        205,
        FILLS[1],
        19,
    )
    return page(
        "Two feature-page plans · B",
        "Proposals only. Neither arrangement is an actual use result or a claim about all people.",
        body,
    )


def voices() -> str:
    body = block("C1 · EDDA SAYS", quote("C1"), 192, 178, FILLS[1])
    body += block("C2 · FINN SAYS", quote("C2"), 394, 178, FILLS[3])
    body += box(48, 609, 698, 214, FILLS[0])
    body += icon("square", 192, 690, 37) + icon("round", 600, 690, 37)
    body += path("M252 686 Q282 646 340 666 L366 650 L360 681 Q353 717 275 720 Z", 3)
    body += path("M422 686 Q452 646 510 666 L536 650 L530 681 Q523 717 445 720 Z", 3)
    body += text(113, 782, "BOARD · EDDA", 17, True) + text(
        516, 782, "TABLE · FINN", 17, True
    )
    body += block(
        "MAKE TWO SOURCE FLAPS",
        "One person's reason stays their own. Other people's reasons stay open. No personal disclosure is required.",
        852,
        157,
        FILLS[4],
        19,
    )
    return page(
        "Two own place reasons · C",
        "Invented particular-person meanings. Keep each speaker attached to their feature and words.",
        body,
    )


def care() -> str:
    body = block("D · DARA ASKS", quote("D"), 194, 241, FILLS[2])
    body += box(48, 471, 698, 314, FILLS[1])
    body += icon("square", 210, 565, 49) + text(99, 661, "ASK BEFORE BOARD", 16, True)
    body += path("M374 545 L429 545 L429 520 L466 560 L429 600 L429 575 L374 575 Z", 3)
    body += path("M498 531 L592 531 L617 557 L700 557 L700 651 L498 651 Z", 3)
    body += path("M517 574 L678 574 M517 598 L678 598", 2)
    body += text(520, 699, "BROAD FOLDER", 16, True)
    body += text(100, 756, "REQUEST · permission and done stay separate", 17, True)
    body += block(
        "CARE FLAP",
        "Tell the sourced step. Ask before action. Leave done blank unless actually observed under permission.",
        820,
        173,
        FILLS[4],
        19,
    )
    return page(
        "Ask first and return · D",
        "Requested care is not permission for a real action or a report that someone has done it.",
        body,
    )


def reports() -> str:
    body = block("E · JORI'S ONE TRY ACCOUNT", quote("E"), 192, 204, FILLS[0], 19)
    body += block("F · MELL'S ONE TRY ACCOUNT", quote("F"), 416, 204, FILLS[3], 19)
    body += box(48, 644, 698, 151, FILLS[4])
    body += circle(99, 702, 25) + path(
        "M131 702 L193 702 M174 686 L193 702 L174 718", 3
    )
    body += text(219, 708, "Separate reader · layout · reported response", 17, True)
    body += text(88, 762, "Exact original question / consent / later use: OPEN", 16)
    body += block(
        "LOG EACH ACCOUNT SEPARATELY",
        "One point or question is one reported response. Different people and conditions do not show a better layout for everyone.",
        824,
        180,
        FILLS[1],
        19,
    )
    return page(
        "One try at a time · E / F",
        "Fictional accounts, not observed school events or a controlled comparison. Do not fill source gaps.",
        body,
    )


def blank_panel(n: int, label: str, value: str, y: int, h: int) -> str:
    body = box(48, y, 698, h, FILLS[(n - 1) % 5]) + circle(80, y + 35, 18)
    body += text(74, y + 41, str(n), 17, True) + text(112, y + 42, label, 18, True)
    wrapped, end = paragraph(value, 72, y + 82, 648, 17, 26)
    body += wrapped + path(f"M74 {y+h-22} L716 {y+h-22}", 2)
    if end > y + h - 20:
        raise ValueError(label + ": prompt exceeds blank panel")
    return body


def logbook() -> str:
    body = blank_panel(
        1,
        "FIRST PLAN / DIRECTION",
        "Place feature/source; person says; care step; planned layout; first exact direction. No response pre-filled.",
        194,
        188,
    )
    body += blank_panel(
        2,
        "ONE TRY / REPORTED ACCOUNT",
        "Reported or actual? Actor/source; time/view/layout; exact question and response/help, pass or unobserved.",
        399,
        188,
    )
    body += blank_panel(
        3,
        "ONE REVISED STEP",
        "Keep first words. Revised words/source reason; renewed willingness/permission; exact retry response or OPEN.",
        604,
        188,
    )
    body += blank_panel(
        4,
        "CARE HANDOVER",
        "Feature/meaning/care source; approved return or owner-review kit; actual action/help or not done; later use OPEN.",
        809,
        188,
    )
    return page(
        "Make a try logbook",
        "Four broad flat panels or adult-folded pages. Plans, source accounts and actual actions stay separate.",
        body,
    )


def actual() -> str:
    body = blank_panel(
        1,
        "APPROVED FAMILIAR PLACE",
        "Local ID; connection/belonging source; feature and actual date/time/view/method; permission scope; gaps.",
        194,
        188,
    )
    body += blank_panel(
        2,
        "PARTICULAR PERSON / LOCAL CARE",
        "Willing source ID; exact own meaning and time; quote/audience permission; local care procedure/source.",
        399,
        188,
    )
    body += blank_panel(
        3,
        "APPROVED TRY / RESPONSE",
        "Owner scope; renewed willing reader; actual time/layout/view; first/revised direction and exact response/help or pass.",
        604,
        188,
    )
    body += blank_panel(
        4,
        "HANDOVER / K03 ELEMENTS",
        "Care plan versus done/not done; actor/time/help; feature/connection/meaning/care evidence and gaps; retention/withdrawal.",
        809,
        188,
    )
    return page(
        "Actual display record · blank",
        "Use full educator preflight and expanded local record. Keep actual data locally. No source/consent/result supplied.",
        body,
    )


def fresh(kind: str) -> str:
    label = "A" if kind == "arch" else "B"
    body = diagram(kind, "place", 48, 194, 218, 131)
    body += diagram(kind, "beside", 288, 194, 218, 131)
    body += diagram(kind, "stacked", 528, 194, 218, 131)
    titles = (
        "DRAWING",
        "PLAN",
        "UMA SAYS" if kind == "arch" else "WYNN SAYS",
        "OREN ASKS" if kind == "arch" else "TALI ASKS",
        "VES REPORTS" if kind == "arch" else "NELA REPORTS",
    )
    y = 346
    for i, (name, words) in enumerate(zip(titles, FRESH[kind]), 1):
        # Exact full sources fit at a measured 17 pt, with no worked answer.
        h = 127 if i != 4 else 140
        body += block(
            f"{label}{i} · {name}", "“" + words + "”", y, h, FILLS[(i - 1) % 5], 17
        )
        y += h + 7
    return page(
        "Fresh " + label + " · Paper " + kind.title(),
        "Hold until Day "
        + ("175" if kind == "arch" else "180")
        + ". Read neutrally; use separate learner prompts after release.",
        body,
    )


def fresh_a() -> str:
    return fresh("arch")


def fresh_b() -> str:
    return fresh("sail")


PAGES = {
    "share-corner-place": place_a,
    "two-feature-layouts": plans_b,
    "two-place-reasons": voices,
    "dara-care-step": care,
    "one-try-notes": reports,
    "try-logbook": logbook,
    "actual-display-record": actual,
    "check-a-paper-arch": fresh_a,
    "check-b-paper-sail": fresh_b,
}
TACTILE = {
    "share-corner-place": "A's original diagram has square board left of round table. Shape/word labels identify source features, not people. Make broad raised square and round cues on a left/right base, labelled A/FICTION; child may direct adult placement or hear exact no-touch relations. Two-feature pages link to same source; no actual site, permission, people or belonging pictured.",
    "two-feature-layouts": "Two original outlined PLAN bases show same broad BOARD PAGE and TABLE PAGE cards. Beside base has board-page left/table-page right; top/bottom base has board-page above/table-page. Each card contains its source feature square/round icon and label. Use two broad flat/raised arrangement bases and same large cues or exact no-touch description. Equal paper area isn't fairness/consent/access; no user result is drawn.",
    "two-place-reasons": "Two separate exact source panels give Edda's board reason and Finn's table reason. Square board with EDDA label left and round table with FINN label right sit with original empty speech outlines. Use large named source flaps attached to their features or exact read/dictation; no face/identity, other people's reason or consensus depicted.",
    "dara-care-step": "Original square board cue left carries ASK BEFORE BOARD; broad outlined folder right has paper-like lines and BROAD FOLDER label. Arrow links the requested care steps only, not an observed event or permission. Use broad raised board/folder flap and ASK source tab, or exact no-touch reading/directed placement. Actual approval, care explanation and done must remain separate.",
    "one-try-notes": "Separate Jori/E and Mell/F exact account panels, plus original circle/arrow mark and blank-gap words, illustrate separate reader/layout/response records. No reader face, original question, consent or action is drawn. Read exact reports and let child direct broad named panels or dictate separate records. Do not reenact illustration as original evidence or infer causal benefit.",
    "try-logbook": "Four broad blank panels in order FIRST PLAN/DIRECTION, ONE TRY/REPORTED ACCOUNT, ONE REVISED STEP, CARE HANDOVER. Number circles and writing lines are field markers only. Use adult-prepared broad folded pages, four flat raised panels, scribe or exact no-touch reading. First words, source account, actual reply/pass and care status are separate; nothing is pre-filled.",
    "actual-display-record": "Four broad empty local panels prompt familiar-place feature/connection, particular-person meaning/care, permitted willing try and response, and handover/K03 element gaps. Circles/writing lines contain no data. Exact no-touch reading, sign/AAC/scribe/directed broad panels preserve every field. Actual time/source/permission/consent/action/knowledge are blank; expanded preflight and local record required.",
    "check-a-paper-arch": "Three neutral original diagrams: place rectangle wall left/oval pad right; beside PLAN shows wall-page left/pad-page right; top/bottom PLAN shows pad-page above wall-page. Same broad source cards carry rectangle/oval icons. Read all five exact sources neutrally and direct large raised/flat source cues without touch as needed. No worked conclusion, real permission, original question, consent or action shown.",
    "check-b-paper-sail": "Three neutral original diagrams: place triangle frame left/square tray right; beside PLAN frame-page left/tray-page right; top/bottom PLAN tray-page above frame-page. Same source cards carry triangle/square icons. Read all five sources neutrally, use broad raised/flat cues or exact no-touch description. Nela's pass is in source words; no completed use, reason for pass, consent or actual care pictured.",
}


def write() -> None:
    """Write only nine named A4 pairs and full exact alternatives."""
    readings = [
        "# Exact linear readings and tactile/no-print routes",
        "",
        "Semantic SVG metadata and selectable PDF text support access. PDFs are not tagged; no PDF/UA or classroom accessibility certification is claimed. Each exact reading includes every visible text item in SVG/PDF order, including attribution. Read [source script](../MATERIALS.md), [held fresh prompts](../STUDENT-CHECKS.md) or [actual pathway](../ACTUAL-SOURCE-PATHWAY.md) neutrally. Shape/word labels carry meaning; colour supplementary. Raised construction needs local individual review and has not been physically learner-tested. No worked check answers appended.",
        "",
    ]
    ns = {"s": "http://www.w3.org/2000/svg"}
    for stem, fn in PAGES.items():
        content = fn()
        svg, pdf = HERE / f"{stem}.svg", HERE / f"{stem}.pdf"
        svg.write_text(content, encoding="utf-8")
        with pdf.open("wb") as output:
            subprocess.run(
                ["rsvg-convert", "--format", "pdf", str(svg)],
                check=True,
                stdout=output,
                env=dict(os.environ, SOURCE_DATE_EPOCH="1780000000"),
            )
        top = ET.fromstring(content)
        exact = " ".join("".join(n.itertext()) for n in top.findall(".//s:text", ns))
        readings += [
            f"## {stem}.svg / {stem}.pdf",
            "",
            "**Exact visible text:** " + exact,
            "",
            "**Illustration and tactile/no-print:** " + TACTILE[stem],
            "",
        ]
    (HERE / "TEXT-ALTERNATIVES.md").write_text("\n".join(readings), encoding="utf-8")


def check_reproduction() -> None:
    """Generate only in a temporary directory; preserve every pack file."""
    global HERE
    original = HERE
    pack = HERE.parent

    def state():
        return {
            p.relative_to(pack).as_posix(): (
                hashlib.sha256(p.read_bytes()).hexdigest(),
                p.stat().st_size,
                p.stat().st_mtime_ns,
            )
            for p in pack.rglob("*")
            if p.is_file()
        }

    before = state()
    names = [f"{stem}.{ext}" for stem in PAGES for ext in ("svg", "pdf")] + [
        "TEXT-ALTERNATIVES.md"
    ]
    try:
        with tempfile.TemporaryDirectory(
            prefix="subjectnest-hass35-repro-"
        ) as directory:
            HERE = Path(directory)
            write()
            for name in names:
                if (HERE / name).read_bytes() != (original / name).read_bytes():
                    raise AssertionError("reproduction drift: " + name)
    finally:
        HERE = original
    if before != state():
        raise AssertionError("reproduction changed pack bytes/size/mtime")
    print(
        "PASS: 19/19 print artifacts reproduced in temporary storage; all pack bytes, sizes and modification times preserved"
    )


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument(
        "--regenerate",
        action="store_true",
        help="Deliberately rewrite only named print assets/alternatives",
    )
    group.add_argument(
        "--check-reproduction",
        action="store_true",
        help="Generate in temporary storage and compare; pack stays byte/mtime unchanged",
    )
    args = parser.parse_args()
    if args.check_reproduction:
        check_reproduction()
    else:
        write()
        print("Generated nine original A4 SVG/PDF pairs and exact alternatives")
