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

import argparse
import os
import subprocess
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 26)
    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 · W31–32", 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>",
            "",
        ]
    )


def folder(x: float, y: float, ident: str, place: str) -> str:
    """Draw one original broad folder icon with an explicit accessible identity."""
    return (
        f'<g data-item="{ident}" data-place="{place}">'
        + path(
            f"M{x-26} {y-20} L{x-26} {y+18} L{x+26} {y+18} L{x+26} {y-14} L{x-4} {y-14} L{x-11} {y-20} Z",
            2,
        )
        + text(x - (12 if len(ident) > 1 else 6), y + 8, ident, 18, True)
        + "</g>"
    )


def desk(kind: str, phase: str, x: int, y: int, w: int, h: int) -> str:
    """Draw exact source-backed item locations; shape and text carry each cue."""
    ids = {
        "kit": ["1", "2", "3", "4"],
        "drawing": ["5", "6", "7"],
        "puzzle": ["8", "9", "10"],
    }[kind]
    shelf = {
        ("kit", "earlier"): ids,
        ("kit", "later"): ids[:2],
        ("drawing", "earlier"): ids[:2],
        ("drawing", "later"): ids[:1],
        ("puzzle", "earlier"): ids,
        ("puzzle", "later"): ids[:1],
    }[(kind, phase)]
    tray = [v for v in ids if v not in shelf]
    lx, rx, cy = x + w * 0.25, x + w * 0.75, y + h * 0.47
    sw, sh = w * 0.42, h * 0.7
    radius = min(w * 0.235, h * 0.46)
    out = f'<g data-desk-kind="{kind}" data-phase="{phase}">'
    out += f'<rect data-feature="shelf" x="{lx-sw/2}" y="{cy-sh/2}" width="{sw}" height="{sh}" fill="white" stroke="{INK}" stroke-width="3"/>'
    out += f'<circle data-feature="tray" cx="{rx}" cy="{cy}" r="{radius}" fill="white" stroke="{INK}" stroke-width="3"/>'
    for items, centre, place in ((shelf, lx, "shelf"), (tray, rx, "tray")):
        for i, ident in enumerate(items):
            if len(items) > 2:
                dx = (-1 if i % 2 == 0 else 1) * min(42, w * 0.09)
                dy = (-1 if i < 2 else 1) * min(36, h * 0.18)
            elif len(items) == 2:
                dx, dy = (-1 if i == 0 else 1) * min(40, w * 0.09), 0
            else:
                dx, dy = 0, 0
            out += folder(centre + dx, cy + dy, ident, place)
    out += "</g>"
    return box(x, y, w, h, FILLS[0]) + out


def earlier() -> str:
    body = text(48, 194, "SOURCE A · EARLIER", 20, True)
    body += desk("kit", "earlier", 68, 224, 658, 334)
    body += text(68, 604, "KEY: square = shelf; round = return tray.", 20)
    body += text(68, 637, "Each folder has its own mark: 1, 2, 3, 4.", 20)
    body += block(
        "A · EXACT SOURCE WORDS",
        "“Paper Kit Nook, earlier: folder 1, folder 2, folder 3 and folder 4 are on the square shelf. The round return tray is empty.”",
        686,
        229,
        FILLS[1],
        22,
    )
    body += text(68, 976, "Keep this record when you make a later model.", 18)
    return page(
        "Paper Kit Nook · earlier record",
        "Invented source. Match each folder mark; no actual inventory is supplied.",
        body,
    )


def later() -> str:
    body = block(
        "SOURCE B · UMA'S LATER RECORD",
        "“Paper Kit Nook, later: folder 1 and folder 2 are on the square shelf. Folder 3 and folder 4 are in the round return tray. I did not record who put them there or why.”",
        190,
        258,
        FILLS[1],
        22,
    )
    body += desk("kit", "later", 68, 492, 658, 319)
    body += text(68, 854, "KEY: square = shelf; round = return tray.", 20)
    body += block(
        "ASK A SOURCE",
        "Where is each mark recorded? Who can tell who put it there or why?",
        883,
        134,
        FILLS[2],
        19,
    )
    return page(
        "Uma · later location record",
        "Same four folder marks, two recorded places; mover and cause are open.",
        body,
    )


def request() -> str:
    body = block(
        "SOURCE C · SOL'S CARE REQUEST",
        "“Put a used folder in the round return tray. Ask me before putting it back on the square shelf. This is a request, not a report that anyone did it.”",
        190,
        258,
        FILLS[2],
        23,
    )
    body += circle(180, 569, 78) + folder(180, 565, "3", "tray")
    body += box(493, 491, 156, 156)
    body += path("M450 514 L465 502 L474 514 L486 502", 3)
    body += text(94, 689, "ROUND RETURN TRAY", 17, True)
    body += text(493, 689, "SQUARE SHELF", 17, True)
    body += block(
        "1 · STEP TO FOLLOW",
        "Show the tray step. Ask Sol before the shelf step.",
        741,
        126,
        FILLS[0],
        20,
    )
    body += block(
        "2 · ACTION RECORD",
        "What actually happened? Leave blank until observed.",
        885,
        131,
        FILLS[4],
        20,
    )
    return page(
        "Sol · a care-step request",
        "A requested step and a reported action are different source records.",
        body,
    )


def meaning() -> str:
    body = block(
        "SOURCE D · PIA'S OWN ACCOUNT",
        "“Paper Kit Nook is special to me because I find my picture folders on its shelf. I have not spoken for anyone else.”",
        190,
        248,
        FILLS[3],
        23,
    )
    body += box(270, 510, 260, 190, FILLS[0])
    body += folder(348, 602, "1", "shelf") + folder(456, 602, "2", "shelf")
    body += path(
        "M105 535 L113 515 Q87 509 90 486 Q93 461 122 461 L196 461 Q230 461 230 487 Q231 514 203 514 L137 514 Z",
        3,
    )
    body += text(68, 764, "Whose reason is this?", 23, True)
    body += block(
        "OTHER PEOPLE",
        "Keep other people's reasons open. Ask a willing source; do not guess from identity.",
        801,
        184,
        FILLS[4],
        21,
    )
    return page(
        "Pia · why a place matters",
        "One person's reason; the illustration adds no other user or cause.",
        body,
    )


def ledger() -> str:
    rows = [
        (
            "1 · FIRST SOURCE / VIEW",
            "Record a feature or item mark, its place and source/time.",
        ),
        (
            "2 · SECOND SOURCE / VIEW",
            "Keep the first record. Match only supported identity cues.",
        ),
        (
            "3 · REQUEST / ONE PERSON SAYS",
            "Name the source of the care step and the personal reason.",
        ),
        (
            "4 · MADE / DONE / STILL OPEN",
            "Record actor, help and time only if it happened. What source next?",
        ),
    ]
    body = ""
    for i, (label, value) in enumerate(rows):
        y = 190 + i * 198
        body += block(label, value, y, 173, FILLS[i], 19)
        body += path(f"M70 {y+140} L715 {y+140}", 2) + circle(709, y + 31, 12)
    return page(
        "Desk ledger · keep the source",
        "Blank source fields for paper play or separately approved local records.",
        body,
    )


def message() -> str:
    body = block(
        "SOURCE E · HELPER'S PLAN",
        "“I plan a picture message showing the square shelf and round return tray, using Sol’s words. I will ask Sol to check it.”",
        190,
        197,
        FILLS[3],
        20,
    )
    body += block(
        "SOURCE F · SOL'S MESSAGE CHECK",
        "“I checked the helper’s paper message. It matches my request. I have not reported how a visitor used it.”",
        406,
        175,
        FILLS[1],
        20,
    )
    rows = [
        ("MY PLANNED CUE", "Feature, step, source and ask-first question."),
        (
            "SOURCE CHECK / MY REVISION",
            "Keep first version; record check scope or gap.",
        ),
        (
            "USE OR ACTION",
            "What actually happened? Do not infer use from a wording check.",
        ),
    ]
    for i, (label, value) in enumerate(rows):
        y = 604 + i * 138
        body += block(label, value, y, 121, FILLS[i], 17)
        body += path(f"M70 {y+101} L715 {y+101}", 2) + circle(709, y + 31, 10)
    return page(
        "Care message · plan and check",
        "Make a useful paper cue; check words against the source before permitted use.",
        body,
    )


def actual_record() -> str:
    rows = [
        (
            "1 · APPROVED PLACE / FIRST VIEW",
            "Place ID/approval. Familiar connection or pass. Features. Actual date/time/view/method; child words/help.",
        ),
        (
            "2 · WILLING ACCOUNT / CARE SOURCE",
            "Speaker/source ID/time/exact words/permission/correction. Whose reason? Local care step/source/date.",
        ),
        (
            "3 · PLAN / PERMISSION / ACTION",
            "Child care explanation. Task/cue. Approval scope. Done only if observed: actor/time/action/help.",
        ),
        (
            "4 · LATER VIEW / OPEN ELEMENTS",
            "Actual time/view/limits; reader response or pass. Child revision. K03 features/meaning/care/connection observed or open.",
        ),
    ]
    body = ""
    for i, (label, value) in enumerate(rows):
        y = 190 + i * 198
        body += block(label, value, y, 173, FILLS[i], 17)
        body += path(f"M70 {y+146} L715 {y+146}", 2) + circle(709, y + 31, 12)
    return page(
        "Actual shared-place care record",
        "Blank local template. Full educator preflight and actual sources are required.",
        body,
    )


def fresh_a() -> str:
    body = text(55, 190, "SOURCE 1 · EARLIER", 17, True) + text(
        416, 190, "SOURCE 2 · LATER", 17, True
    )
    body += desk("drawing", "earlier", 55, 208, 329, 148) + desk(
        "drawing", "later", 410, 208, 329, 148
    )
    body += text(55, 383, "KEY: square = ledge; round = tray; marks = folders.", 17)
    rows = [
        (
            "SOURCE 1 · EARLIER RECORD",
            "“Drawing Bay, earlier: folder 5 and folder 6 are on the square ledge. Folder 7 is in the round tray.”",
            402,
            132,
        ),
        (
            "SOURCE 2 · LATER RECORD",
            "“Drawing Bay, later: folder 5 is on the square ledge. Folder 6 and folder 7 are in the round tray. Who put them there and why are not recorded.”",
            546,
            158,
        ),
        (
            "SOURCE 3 · NELL'S REQUEST",
            "“Put a used folder in the round tray. Ask me before putting it on the square ledge. This is a request, not a report of what anyone did.”",
            716,
            158,
        ),
        (
            "SOURCE 4 · OLI'S ACCOUNT",
            "“Drawing Bay matters to me because its ledge keeps my paper pictures easy to find. I have not given anyone else’s reason.”",
            886,
            132,
        ),
    ]
    for i, (label, value, y, h) in enumerate(rows):
        body += block(label, value, y, h, FILLS[i], 17)
    return page(
        "Fresh Check A · Drawing Bay",
        "Invented sources. Read the learner prompts neutrally, one part at a time.",
        body,
    )


def fresh_b() -> str:
    body = text(55, 169, "SOURCE 1 · EARLIER", 17, True) + text(
        416, 169, "SOURCE 2 · LATER", 17, True
    )
    body += desk("puzzle", "earlier", 55, 187, 329, 134) + desk(
        "puzzle", "later", 410, 187, 329, 134
    )
    body += text(
        55, 348, "KEY: square = cabinet; round = basket; marks = envelopes.", 17
    )
    rows = [
        (
            "SOURCE 1 · EARLIER RECORD",
            "“Puzzle Point, earlier: envelope 8, envelope 9 and envelope 10 are on the square cabinet. The round basket is empty.”",
            363,
            127,
        ),
        (
            "SOURCE 2 · LATER RECORD",
            "“Puzzle Point, later: envelope 8 is on the square cabinet. Envelope 9 and envelope 10 are in the round basket.”",
            500,
            127,
        ),
        (
            "SOURCE 3 · DEV'S ACCOUNT",
            "“I put envelope 9 in the basket. I have not said who put envelope 10 there or why either was moved.”",
            637,
            117,
        ),
        (
            "SOURCE 4 · KEEPER'S REQUEST",
            "“Put a used envelope in the round basket. Ask the keeper before putting it on the square cabinet. This is a request, not a use report.”",
            764,
            127,
        ),
        (
            "SOURCE 5 · IVO'S ACCOUNT",
            "“Puzzle Point is special to me because I find my puzzle pictures at its cabinet. I have not spoken for other people.”",
            901,
            117,
        ),
    ]
    for i, (label, value, y, h) in enumerate(rows):
        body += block(label, value, y, h, FILLS[i], 17)
    return page(
        "Fresh Check B · Puzzle Point",
        "Invented sources. Read the learner prompts neutrally, one part at a time.",
        body,
    )


PAGES = {
    "kit-nook-earlier": earlier,
    "uma-later-record": later,
    "sol-care-request": request,
    "pia-place-account": meaning,
    "desk-ledger": ledger,
    "care-message-plan": message,
    "actual-care-record": actual_record,
    "check-a-drawing-bay": fresh_a,
    "check-b-puzzle-point": fresh_b,
}
TACTILE = {
    "kit-nook-earlier": "Source A: broad base with square shelf left and round tray right. Four folder icons marked 1, 2, 3, 4 are all inside the square; circle is empty. Make large raised square/circle and individually distinguishable broad folder marks in those exact places. Read exact words; child may direct adult without touching. Numerals are identities, not a counting test.",
    "uma-later-record": "Source B: square shelf left contains folder marks 1/2; round return tray right contains 3/4. Same four identities as A, all represented. Make raised outlines/broad marks in those places on a separate base, preserving A. Read Uma's limits exactly; a model does not supply actor/cause. Adult-directed exact oral placement is equivalent.",
    "sol-care-request": "The diagram illustrates the requested step only: round tray left on this instruction page contains a broad example folder 3; square shelf right is empty. A small speech/question zigzag beside shelf signals ask-first, not an action report. Use two raised shapes and an ask-first tab, read every word and keep request/action fields separate. No arrow implies real movement or compliance.",
    "pia-place-account": "A broad outlined shelf-like panel holds example folder marks 1/2. An empty speech outline beside it points to Pia's attributed account; it depicts no other person's reason. Use a raised broad shelf rectangle and two identity cues or exact no-touch description, attach PIA SAYS, read all words and leave other viewpoints open. Diagram is illustrative, not another inventory.",
    "desk-ledger": "Four broad numbered blank panels: first source/view, second source/view, request/one-person account, and made/done/open fields. Circular marks and lines are decorative writing markers with no added data. Read exact prompts one at a time; child may dictate, use AAC/sign or direct a scribe. Keep actual records separate from paper play.",
    "care-message-plan": "Exact E/F source panels above three blank fields for planned cue, source check/revision and use/action. Circular marks/lines decorate blank fields and add no approval or action. Read all words; offer three broad trays or adult-directed scribe. Keep first version, source check and actual use separate; F concerns helper's message only.",
    "actual-care-record": "Four blank local-record panels with approval/view, willing-account/care source, plan/permission/action and later-view/open-element prompts. Decorative circles/lines add no facts. Read exact text, follow full preflight and offer scribe/AAC/sign/large print or broad trays. No real place, time, speaker, action, consent or K03 evidence is pre-filled.",
    "check-a-drawing-bay": "Two separate neutral source diagrams: Source 1 square ledge left has folder marks 5/6, round tray right has 7; Source 2 square ledge left has 5, round tray right has 6/7. Make two broad raised bases with exact shape/identity key, keep numbered source cards. Read source/prompts neutrally without extra conclusions or worked answer. Adult follows child's placement only.",
    "check-b-puzzle-point": "Two neutral source diagrams: Source 1 square cabinet left has envelope marks 8/9/10 and round basket right empty; Source 2 square cabinet left has 8, round basket right has 9/10. Make separate broad raised bases/identity cues with exact key; five numbered source cards stay separate. Read all words/prompts neutrally; no actor/cause/approval answer is added beyond the source.",
}


def write() -> None:
    """Write only the nine named original print pairs and 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 reading transcribes every visible text item in SVG/PDF reading order, including attribution. Read the [source script](../MATERIALS.md), [fresh prompts](../STUDENT-CHECKS.md) or [actual route](../ACTUAL-SOURCE-PATHWAY.md) neutrally. Shape and words carry meaning; colour is supplementary. Tactile constructions need local individual review and have not been physically learner-tested. No worked check answers are 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")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--regenerate",
        action="store_true",
        help="Deliberately rewrite only named print assets/alternatives",
    )
    args = parser.parse_args()
    if not args.regenerate:
        parser.error("Explicit --regenerate required; normal import performs no writes")
    write()
    print("Generated nine original A4 SVG/PDF pairs and exact alternatives")
