# 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 · W33–34", 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 star(x: float, y: float, r: float) -> str:
    """Draw an original five-point mark with no font or colour dependency."""
    import math

    pts = []
    for i in range(10):
        rr = r if i % 2 == 0 else r * 0.43
        angle = -math.pi / 2 + i * math.pi / 5
        pts.append(f"{x+rr*math.cos(angle)},{y+rr*math.sin(angle)}")
    return f'<polygon data-mark="star" points="{" ".join(pts)}" fill="white" stroke="{INK}" stroke-width="2"/>'


def marks(kind: str, which: str, x: float, y: float, h: float) -> str:
    """Draw only the exact count/type of marks supplied by the view source."""
    r = max(3, h * 0.034)
    if kind == "window":
        n = 3 if which == "first" else 2
        shape = "oval" if which == "first" else "narrow"
        out = ""
        for i in range(n):
            xx = x + (i - (n - 1) / 2) * h * 0.11
            yy = y - h * 0.24
            out += f'<ellipse data-mark="{shape}" cx="{xx}" cy="{yy}" rx="{h*(.045 if which=="first" else .019)}" ry="{h*(.024 if which=="first" else .056)}" fill="white" stroke="{INK}" stroke-width="2"/>'
            out += path(f"M{xx} {yy+h*.05} L{x} {y-h*.09}", 2)
        return out
    if kind == "cove":
        if which == "first":
            return star(x - h * 0.045, y, r) + star(x + h * 0.045, y, r)
        return f'<path data-mark="moon" d="M{x+r} {y-r} Q{x-r*1.8} {y-r} {x-r} {y+r} Q{x+r} {y+r*1.6} {x+r} {y-r} Q{x-r*.5} {y} {x+r} {y-r} Z" fill="white" stroke="{INK}" stroke-width="1.5"/>'
    if which == "first":
        r = max(2, h * 0.024)
        out = ""
        for i in range(3):
            xx = x + (i - 1) * h * 0.073
            out += f'<polygon data-mark="diamond" points="{xx},{y-r} {xx+r},{y} {xx},{y+r} {xx-r},{y}" fill="white" stroke="{INK}" stroke-width="1.5"/>'
        return out
    return "".join(
        f'<circle data-mark="dot" cx="{x+dx}" cy="{y}" r="{r*.67}" fill="{INK}"/>'
        for dx in (-h * 0.05, h * 0.05)
    )


def scene(kind: str, view: str, x: int, y: int, w: int, h: int) -> str:
    """Draw one limited source view, with explicit feature/mark metadata."""
    benchx = x + w * 0.18
    cy = y + h * 0.57
    firstx = x + w * 0.51
    secondx = x + w * 0.81
    side = h * 0.42
    out = f'<g data-scene-kind="{kind}" data-view="{view}">'
    bw = side * 1.35 if kind == "lantern" else side
    out += f'<rect data-feature="bench" x="{benchx-bw/2}" y="{cy-side/2}" width="{bw}" height="{side}" fill="white" stroke="{INK}" stroke-width="3"/>'
    for which, xx in (("first", firstx), ("second", secondx)):
        if which == "second" and view == "one":
            continue
        out += f'<g data-feature="{which}">'
        if kind == "lantern":
            rr = h * 0.17
            out += f'<polygon data-outline="holder" points="{xx},{cy-rr} {xx+rr},{cy+rr} {xx-rr},{cy+rr}" fill="white" stroke="{INK}" stroke-width="3"/>'
            out += marks(kind, which, xx, cy + h * 0.085, h)
        else:
            out += f'<circle data-outline="pot" cx="{xx}" cy="{cy}" r="{h*.12}" fill="white" stroke="{INK}" stroke-width="3"/>'
            out += marks(kind, which, xx, cy, h)
        out += "</g>"
    if view == "one":
        sx = x + w * 0.665
        out += f'<rect data-feature="screen" x="{sx}" y="{y+h*.09}" width="{w*.305}" height="{h*.82}" fill="{FILLS[4]}" stroke="{INK}" stroke-width="3"/>'
        out += path(f"M{sx+10} {y+h*.17} L{sx+w*.305-10} {y+h*.17}", 2)
    out += "</g>"
    return box(x, y, w, h, FILLS[0]) + out


def one() -> str:
    b = text(48, 193, "SOURCE A · SPOT ONE", 20, True)
    b += scene("window", "one", 68, 224, 658, 330)
    b += text(68, 600, "KEY: square = bench; circle = pot drawing;", 19)
    b += text(68, 632, "rectangle = screen. The right area is hidden.", 19)
    b += block(
        "A · EXACT SOURCE WORDS",
        "“Paper Window Corner, Spot One: a square bench and one round pot with three oval leaf outlines are shown. A rectangle screen hides the area to the right. This is an invented drawing.”",
        674,
        274,
        FILLS[1],
        21,
    )
    b += text(68, 987, "What could an actual approved view tell us?", 18)
    return page(
        "Window One · keep the hidden area",
        "An invented limited view; visible drawn features are not a real plant survey.",
        b,
    )


def two() -> str:
    b = text(48, 193, "SOURCE B · SPOT TWO", 20, True)
    b += scene("window", "two", 68, 224, 658, 330)
    b += text(68, 600, "KEY: square = bench; circles = pot drawings.", 19)
    b += text(68, 632, "Same invented scene; another view shows both pots.", 19)
    b += block(
        "B · EXACT SOURCE WORDS",
        "“Paper Window Corner, Spot Two: the square bench and two round pots are shown. The first pot has three oval leaf outlines; the second has two narrow leaf outlines. This is another view of the same invented scene.”",
        674,
        302,
        FILLS[2],
        21,
    )
    return page(
        "Window Two · another source view",
        "Different visibility is not a reported move, growth or actual observation.",
        b,
    )


def voices() -> str:
    b = block(
        "SOURCE C1 · NIA'S OWN REASON",
        "“This corner matters to me because I sit by the bench to draw my paper pictures. This is my own reason.”",
        190,
        222,
        FILLS[3],
        23,
    )
    b += path(
        "M91 491 Q91 457 134 457 L299 457 Q338 457 338 491 Q338 524 299 524 L145 524 L114 548 L121 522 Q91 514 91 491 Z",
        3,
    )
    b += path(
        "M459 491 Q459 457 502 457 L667 457 Q706 457 706 491 Q706 524 667 524 L513 524 L482 548 L489 522 Q459 514 459 491 Z",
        3,
    )
    b += text(151, 500, "NIA SAYS", 20, True) + text(519, 500, "JO SAYS", 20, True)
    b += block(
        "SOURCE C2 · JO'S OWN REASON",
        "“This corner matters to me because I talk about my paper pictures with a helper here. This is my own reason.”",
        583,
        222,
        FILLS[1],
        23,
    )
    b += block(
        "TWO PEOPLE · TWO OWN REASONS",
        "Keep each reason with its speaker. Others' reasons stay open; no winner is needed.",
        849,
        151,
        FILLS[4],
        20,
    )
    return page(
        "Two voices · keep both reasons",
        "Two source accounts can sit side by side without speaking for everyone.",
        b,
    )


def care() -> str:
    b = block(
        "SOURCE D · LEX'S CARE REQUEST",
        "“Leave picture pages on the bench shelf. Ask me before moving pots or the screen. This is a care step, not a use report.”",
        190,
        237,
        FILLS[2],
        23,
    )
    b += box(123, 500, 228, 170) + path(
        "M123 622 L351 622 M145 533 L218 533 L218 588 L145 588 Z", 3
    )
    b += circle(547, 587, 61) + box(638, 502, 70, 167, FILLS[4])
    b += text(123, 716, "PAGES ON BENCH SHELF", 17, True)
    b += text(479, 716, "ASK LEX FIRST", 17, True)
    b += block(
        "SOURCE / EXPLANATION / ACTION",
        "Who gives the step? How can this place be looked after? What actually happened? Leave action blank until observed.",
        772,
        226,
        FILLS[0],
        21,
    )
    return page(
        "A care step · ask the source",
        "A request, a child's explanation and an action record are distinct.",
        b,
    )


def trial_panels(y: int = 488) -> str:
    b = text(68, y - 20, "BEFORE · REPORTED MODEL", 17, True) + text(
        421, y - 20, "AFTER · ONE REPORTED TRY", 17, True
    )
    for phase, x in (("before", 72), ("after", 423)):
        b += f'<g data-model-phase="{phase}">'
        b += f'<rect data-feature="page" x="{x}" y="{y}" width="277" height="164" fill="white" stroke="{INK}" stroke-width="3"/>'
        b += path(
            f"M{x+28} {y+45} L{x+243} {y+45} M{x+28} {y+78} L{x+243} {y+78} M{x+28} {y+111} L{x+178} {y+111}",
            3,
        )
        if phase == "after":
            b += f'<rect data-feature="cover" x="{x+174}" y="{y-13}" width="115" height="89" fill="{FILLS[3]}" stroke="{INK}" stroke-width="3"/>'
        b += "</g>"
    return b


def report() -> str:
    b = block(
        "SOURCE E · ONE MODEL REPORT",
        "“Before my model try, the paper picture page was visible. In one try, a paper cover hid part of that page. I have not tried this in a real place.”",
        190,
        231,
        FILLS[1],
        23,
    )
    b += trial_panels()
    b += block(
        "KEEP THE REPORT SMALL",
        "One reported model try. Not a real place, every cover, plant result, use permission or a trial done by the learner.",
        709,
        188,
        FILLS[2],
        21,
    )
    b += text(68, 967, "What actual source or approved try would help next?", 18)
    return page(
        "One model report · keep its scope",
        "Diagrams illustrate the report; no material experiment is performed here.",
        b,
    )


def guide() -> str:
    b = block(
        "SOURCE F · HELPER'S GUIDE PLAN",
        "“I plan a two-window guide with each view named and Nia’s and Jo’s own words. I will ask Lex to check the care step before a private tour.”",
        190,
        210,
        FILLS[3],
        21,
    )
    rows = [
        ("1 · VIEW ONE / SOURCE", "Feature, spot/view and source/time; hidden area."),
        (
            "2 · VIEW TWO / SOURCE",
            "Another view or a recorded gap; preserve first window.",
        ),
        ("3 · PERSON SAYS", "Own reason and named source; others stay open."),
        (
            "4 · CARE / ASK FIRST / USE",
            "Source step, permission and exact action/help only if done.",
        ),
    ]
    for i, (label, value) in enumerate(rows):
        y = 422 + i * 150
        b += block(label, value, y, 133, FILLS[i], 18)
        b += path(f"M70 {y+111} L715 {y+111}", 2) + circle(708, y + 31, 11)
    return page(
        "Pocket guide · four useful panels",
        "Adult folds or four flat sheets; making and source reasoning have equal routes.",
        b,
    )


def actual() -> str:
    rows = [
        (
            "1 · APPROVED PLACE / TWO VIEWS",
            "Place/approval/connection or pass. Actual time/spot/view/method/source/help for each. Hidden areas/gaps.",
        ),
        (
            "2 · WILLING PERSON / OWN REASON",
            "Source ID/time/exact words/permission/correction. One genuine reason can suffice; second optional.",
        ),
        (
            "3 · LOCAL CARE / PLAN / ACTION",
            "Care step/source/date; child explanation; permission scope. Done only if observed: actor/time/action/help.",
        ),
        (
            "4 · READER / EDIT / K03 ELEMENTS",
            "Actual tour/use/reader words or pass; approved edit/help. Connection/features/meaning/care observed or open.",
        ),
    ]
    b = ""
    for i, (label, value) in enumerate(rows):
        y = 190 + i * 198
        b += block(label, value, y, 173, FILLS[i], 17)
        b += path(f"M70 {y+146} L715 {y+146}", 2) + circle(708, y + 31, 12)
    return page(
        "Actual place-trail record",
        "Blank local template only; educator preflight and actual sources are required.",
        b,
    )


def fresh_diagrams(kind: str, y: int = 198, h: int = 110) -> str:
    return (
        text(55, y - 17, "SOURCE 1 · SPOT ONE", 16, True)
        + text(412, y - 17, "SOURCE 2 · SPOT TWO", 16, True)
        + scene(kind, "one", 55, y, 329, h)
        + scene(kind, "two", 410, y, 329, h)
    )


def fresh_a() -> str:
    b = fresh_diagrams("cove")
    b += text(55, 336, "KEY: square = table; circles = boxes; rectangle = screen.", 17)
    rows = [
        (
            "SOURCE 1 · SPOT ONE",
            "“Paper Cove, Spot One: a square table and one round box with two star marks are shown. A screen hides the area to the right. This is an invented drawing.”",
            357,
            145,
        ),
        (
            "SOURCE 2 · SPOT TWO",
            "“Paper Cove, Spot Two: the square table and two round boxes are shown. The first has two star marks; the second has one moon mark. This is another view of the same invented scene.”",
            514,
            145,
        ),
        (
            "SOURCE 3 · TWO OWN REASONS",
            "“Rae says: The table matters to me for laying paper cards. Sami says: The round box matters to me for storing paper cards. Each gives their own reason.”",
            671,
            145,
        ),
        (
            "SOURCE 4 · PAZ'S REQUEST",
            "“Keep paper pages on the table. Ask me before moving boxes or the screen. This is a request, not a report of use.”",
            828,
            145,
        ),
    ]
    for i, (label, value, y, h) in enumerate(rows):
        b += block(label, value, y, h, FILLS[i], 17)
    return page(
        "Fresh Check A · Paper Cove",
        "Invented neutral sources; read learner prompts one short part at a time.",
        b,
    )


def fresh_b() -> str:
    b = fresh_diagrams("lantern", 188, 95)
    b += text(
        55, 311, "KEY: rectangle = pad; triangles = holders; tall box = screen.", 16
    )
    rows = [
        (
            "SOURCE 1 · SPOT ONE",
            "“Paper Lantern Bay, Spot One: a rectangle pad and one triangle holder with three diamond marks are shown. A screen hides the area to the right. This is an invented drawing.”",
            330,
            143,
        ),
        (
            "SOURCE 2 · SPOT TWO",
            "“Paper Lantern Bay, Spot Two: the rectangle pad and two triangle holders are shown. The first has three diamond marks; the second has two dot marks. This is another view of the same invented scene.”",
            480,
            143,
        ),
        (
            "SOURCE 3 · TWO OWN REASONS",
            "“Rin says: The pad matters to me for drawing paper patterns. Teo says: The holder matters to me for finding my paper patterns. Each gives their own reason.”",
            630,
            143,
        ),
        (
            "SOURCE 4 · ARI'S REQUEST",
            "“Keep pattern pages on the pad. Ask me before moving holders or the screen. This is a request, not a use report.”",
            780,
            117,
        ),
        (
            "SOURCE 5 · MIRO'S MODEL REPORT",
            "“Before one model try, my paper guide was visible. In that try, a paper cover hid its corner. I have not tried this in a real place.”",
            904,
            117,
        ),
    ]
    for i, (label, value, y, h) in enumerate(rows):
        b += block(label, value, y, h, FILLS[i], 17)
    return page(
        "Fresh Check B · Paper Lantern Bay",
        "Invented neutral sources; read learner prompts one short part at a time.",
        b,
    )


PAGES = {
    "window-one-view": one,
    "window-two-view": two,
    "two-place-voices": voices,
    "lex-care-step": care,
    "paper-cover-report": report,
    "pocket-guide-mat": guide,
    "actual-trail-record": actual,
    "check-a-paper-cove": fresh_a,
    "check-b-paper-lantern": fresh_b,
}
TACTILE = {
    "window-one-view": "A/Spot One shows a square bench left, one round pot centre with three oval leaf outlines above it, and tall rectangle SCREEN covering right area. No second pot is visible in this source drawing. Make raised bench/first-pot/three broad oval cues and a broad opaque screen over the right region, labelled A/Spot One; read exact words and let child direct adult without touching. Hidden area supplies no absence/total/growth fact.",
    "window-two-view": "B/Spot Two shows same square bench left, first round pot centre with three oval outlines above, second round pot right with two narrow outlines above; no covering rectangle is drawn in this view. Use broad raised features and exact source/view labels, preserve A separately. This represents another supplied view of same invented scene, not a physical move, growth or a real plant observation. Adult-directed exact reading equivalent.",
    "two-place-voices": "Two original empty speech-outline symbols labelled NIA SAYS and JO SAYS sit between their exact source panels. Keep two broad named flaps side by side; no person/identity/other viewpoint is pictured or implied. Read every word and let learner dictate/direct source-reason links. Different compatible own reasons need no winner; others stay open.",
    "lex-care-step": "Illustration shows a broad bench/shelf rectangle left with one outlined paper page, round pot cue centre-right and tall screen cue far right. Labels specify pages on bench shelf and ask Lex first. These illustrate D's requested step, not an action report. Use large raised outlines/page and ask-first tab or exact no-touch reading; source, child explanation and actual action fields stay separate.",
    "paper-cover-report": "Two labelled broad paper-page diagrams show BEFORE reported model and AFTER one reported try. Each page has three decorative text-like lines; after panel adds an opaque small rectangle over part of upper-right page. It illustrates part hidden, with no actual dimensions/performance facts. Use two broad page bases and opaque flap, read all source/scope words, label report illustration; adult movement is not original reported trial or real place evidence.",
    "pocket-guide-mat": "F's exact guide proposal above four broad numbered blank panels: first view/source, second view/source, person says, care/ask-first/use. Circles/lines are decorative blank-field markers, not facts. Use four flat raised-panel trays or adult-folded broad guide, read exact prompts and allow scribe/AAC/sign/directed placement. Plans/making/approval/tour/action remain distinct.",
    "actual-trail-record": "Four unfilled local panels prompt approved place/views, willing account, local care/action and reader/element evidence. Decorative circles/lines carry no actual fact. Read exact fields separately; use scribe/AAC/sign/large print/broad tray as chosen. No time/name/place/consent/action/result is pre-filled. Full educator preflight and expanded locally kept record required.",
    "check-a-paper-cove": "Two neutral diagrams: Spot One has square table left, first round box centre with two star marks, right region covered by tall screen; Spot Two has same table/first star box and second round box right with one moon mark, screen not drawn. Raised broad source bases/shape marks/view tabs or exact no-touch readings preserve these facts. Four numbered sources remain neutral; no added comparison/reason/permission answers.",
    "check-b-paper-lantern": "Two neutral diagrams: Spot One has rectangle pad left, first triangle holder centre with three diamond marks, right region covered by tall screen; Spot Two has same pad/first holder and second triangle holder right with two dot marks, screen not drawn. Use broad raised features/marks with two view/source tabs; read all five sources and prompts neutrally, adding no model-trial/meaning/care interpretation beyond source words.",
}


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")


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")
