#!/usr/bin/env python3
"""Generate six original A4 Dance aids and one quiet original two-note cue."""

from __future__ import annotations

import html
import math
import struct
import textwrap
import wave
from pathlib import Path

ROOT = Path(__file__).resolve().parent
INK = "#173344"
TEAL = "#a34850"
SOFT = "#fff1ed"
LINE = "#6d8490"
FONT = "DejaVu Sans, sans-serif"


def esc(value: str) -> str:
    return html.escape(value, quote=True)


def label(x: float, y: float, value: str, size: float = 4.0, bold: bool = False) -> str:
    weight = ' font-weight="700"' if bold else ""
    return (
        f'<text x="{x}" y="{y}" font-family="{FONT}" font-size="{size}"'
        f'{weight} fill="{INK}">{esc(value)}</text>'
    )


def box(x: float, y: float, w: float, h: float, fill: str = "#fff") -> str:
    return (
        f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="2"'
        f' fill="{fill}" stroke="{LINE}" stroke-width=".7"/>'
    )


DIAGRAMS: dict[str, tuple[str, ...]] = {
    "invitation-score": (
        '<rect x="4" y="2" width="24" height="14" rx="2"/><path d="M9 7 H22 M9 11 H18"/>',
        '<path d="M5 14 Q14 2 27 2 M5 14 Q16 10 29 14"/><path d="M23 1 L28 2 L26 7 M25 10 L30 14 L25 17"/>',
        '<path d="M10 2 V16 M22 2 V16" stroke-width="3.5"/>',
        '<path d="M4 14 C9 5 18 4 27 9"/><path d="M22 5 L28 9 L22 12"/>',
        '<path d="M4 14 H27 M4 14 V7"/><path d="M21 10 L28 14 L21 17"/>',
        '<rect x="6" y="2" width="19" height="15" rx="1"/><path d="M11 7 H21 M11 11 H21"/>',
    ),
    "answer-choice": (
        '<path d="M3 13 Q12 2 24 10"/><path d="M20 5 L25 10 L19 13"/>',
        '<path d="M4 13 C13 4 18 4 26 8"/><path d="M21 4 L27 8 L21 11"/>',
        '<path d="M4 13 H13 V5 H26"/><path d="M20 1 L27 5 L20 9"/>',
        '<path d="M2 14 Q9 3 15 8 M19 14 H28 V5"/>',
        '<path d="M4 13 Q12 2 20 8 M23 3 V15"/>',
        '<rect x="5" y="2" width="20" height="15" rx="1"/><path d="M9 7 H21 M9 12 H18"/>',
    ),
    "choice-share": (
        '<path d="M4 3 H21 V13 H12 L7 17 V13 H4 Z"/>',
        '<rect x="3" y="3" width="8" height="11"/><rect x="13" y="3" width="8" height="11"/><rect x="23" y="3" width="8" height="11"/>',
        '<path d="M10 2 V16 M21 2 V16" stroke-width="3.5"/>',
        '<circle cx="7" cy="8" r="4"/><circle cx="25" cy="8" r="4"/><path d="M13 8 H19 M17 5 L20 8 L17 11"/>',
        '<rect x="7" y="3" width="18" height="13" rx="2"/>',
        '<circle cx="16" cy="9" r="7"/><circle cx="16" cy="9" r="2"/>',
    ),
    "smooth-change-pause": (
        '<path d="M3 14 C9 2 19 2 27 11"/><path d="M22 7 L28 11 L22 15"/>',
        '<path d="M4 14 H15 V4 H27"/><path d="M21 1 L28 4 L21 8"/>',
        '<path d="M10 2 V16 M22 2 V16" stroke-width="3.5"/>',
        '<path d="M4 13 Q12 3 21 10"/><path d="M20 10 H28"/>',
        '<rect x="5" y="6" width="21" height="11" rx="1"/><path d="M10 2 H30 V12"/>',
        '<path d="M4 3 H26 V13 H15 L10 17 V13 H4 Z"/>',
    ),
    "maker-audience": (
        '<rect x="5" y="3" width="21" height="13" rx="1"/><path d="M9 7 H22 M9 11 H17"/>',
        '<path d="M4 14 C10 4 18 4 26 12"/><path d="M11 17 H25"/>',
        '<rect x="5" y="3" width="21" height="13" rx="1"/><path d="M11 7 H21 M11 11 H21"/>',
        '<circle cx="8" cy="8" r="4"/><circle cx="25" cy="8" r="4"/><path d="M14 8 H19"/>',
        '<path d="M4 3 H26 V13 H14 L9 17 V13 H4 Z"/>',
        '<rect x="5" y="3" width="21" height="13" rx="1"/><path d="M10 8 H21"/>',
    ),
    "evidence-split": (
        '<rect x="5" y="3" width="21" height="13" rx="1"/><path d="M9 7 H22 M9 11 H17"/>',
        '<path d="M3 14 Q12 2 27 11"/><path d="M22 7 L28 11 L22 14"/>',
        '<path d="M3 13 H13 V4 H27"/><path d="M22 1 L28 4 L22 8"/>',
        '<rect x="5" y="3" width="21" height="13" rx="1"/><path d="M10 8 H21"/>',
        '<circle cx="8" cy="8" r="4"/><circle cx="25" cy="8" r="4"/><path d="M14 8 H19"/>',
        '<path d="M4 3 H26 V13 H14 L9 17 V13 H4 Z"/>',
    ),
}


def diagram(name: str, index: int, y: float) -> str:
    """Broad pictogram inside each field; ordered text supplies the exact meaning."""
    return (
        f'<g transform="translate(155 {y + 3})" fill="none" stroke="{INK}"'
        ' stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">'
        f"{DIAGRAMS[name][index]}</g>"
    )


def panel(name: str, y: float, number: int, words: str) -> str:
    lines = textwrap.wrap(words, width=35)
    text_lines = "\n".join(
        label(31, y + 8 + 5 * i, line, 3.75, True) for i, line in enumerate(lines)
    )
    return "\n".join(
        (
            box(14, y, 182, 33),
            box(18, y + 4, 9, 9, SOFT),
            label(20.6, y + 10.5, str(number), 4, True),
            text_lines,
            diagram(name, number - 1, y),
            f'<path d="M31 {y + 22} H189" stroke="{LINE}" stroke-width=".45"/>',
            label(31, y + 28, "Point, mark, dictate or leave blank.", 2.6),
        )
    )


def page(name: str, title: str, subtitle: str, fields: tuple[str, ...]) -> str:
    assert len(fields) == 6
    desc = "; ".join(fields)
    body = "\n".join(
        panel(name, 54 + i * 36, i + 1, word) for i, word in enumerate(fields)
    )
    if name in {"invitation-score", "smooth-change-pause"}:
        upper = 54 + (1 if name == "invitation-score" else 0) * 36 + 16
        lower = 54 + (3 if name == "invitation-score" else 2) * 36 + 16
        body += (
            f'\n<path d="M196 {upper} H202 V{lower} H196" fill="none"'
            f' stroke="{TEAL}" stroke-width="1.35"/>'
            f'<path d="M199 {lower - 4} L202 {lower} L205 {lower - 4}"'
            f' fill="none" stroke="{TEAL}" stroke-width="1.35"/>'
        )
    return f'''<svg xmlns="http://www.w3.org/2000/svg" width="210mm" height="297mm" viewBox="0 0 210 297" role="img" aria-labelledby="title desc">
<title id="title">{esc(title)} A4 aid</title><desc id="desc">{esc(desc)}. Six broad ordered fields with large line and shape diagrams at right. Diagrams are explained in MATERIALS.md and can be traced or read aloud; they are paper cues, not required body positions.</desc>
<rect width="210" height="297" fill="#ffffff"/>
{label(14, 16, "SubjectNest / Foundation Dance / Weeks 3–4", 4.0, True)}
{label(14, 29, title, 7.2, True)}
<path d="M14 36 H196" stroke="{TEAL}" stroke-width="1.3"/>
{label(14, 45, subtitle, 3.65)}
{body}
<path d="M14 276 H196" stroke="{LINE}" stroke-width=".6"/>
{label(14, 284, "Read aloud / point / sign / AAC / private response / no touch.", 3.1)}
{label(14, 292, "Original SubjectNest vector  •  © 2026 NeuroForgeIO Pty Ltd  •  CC BY 4.0", 2.9)}
</svg>
'''


PAGES: tuple[tuple[str, str, str, tuple[str, ...]], ...] = (
    (
        "invitation-score",
        "An idea can invite an answer",
        "OPEN to WAIT to chosen shape is a paper score, not a floor route.",
        (
            "MAKER'S OWN IDEA",
            "OPEN / OFFER",
            "WAIT / SPACE",
            "CHOSEN THIRD SHAPE",
            "WHAT I WANT TO COMMUNICATE",
            "PAPER / ACTUAL MOVEMENT / UNKNOWN",
        ),
    ),
    (
        "answer-choice",
        "Answer without copying exactly",
        "Put two choices side by side and name what changed.",
        (
            "SOURCE IDEA I NOTICED",
            "MY ANSWER MAY BE DIFFERENT",
            "CHANGE DIRECTION / QUALITY / PAUSE",
            "TWO IDEAS SIDE BY SIDE",
            "NO EXACT COPY REQUIRED",
            "WHAT IS OBSERVED / UNKNOWN",
        ),
    ),
    (
        "choice-share",
        "Choose whether to share",
        "A puppet-only scene is rehearsal, not a real audience.",
        (
            "ASK BEFORE SHOWING",
            "YES / NO / NOT NOW",
            "NO REASON NEEDED",
            "CHOOSE A WILLING PERSON OR PRIVATE ROUTE",
            "STOP OR CHANGE AT ANY TIME",
            "ACTUAL PERSON SAW / HEARD? YES / NO",
        ),
    ),
    (
        "smooth-change-pause",
        "Smooth, change, pause",
        "A scored curve is not a request to roll a body.",
        (
            "SMOOTH LINE ON PAPER",
            "CHANGE DIRECTION ON PAPER",
            "PAUSE MARK",
            "CHILD-CHOSEN SAFE ACTION IF ANY",
            "SOURCE: PAPER / ACTUAL / ADULT",
            "WHAT THE MAKER MEANT",
        ),
    ),
    (
        "maker-audience",
        "Who made it? Who actually saw it?",
        "Record events, not an imagined listener result.",
        (
            "WORK TITLE + MAKER",
            "ORIGINAL SCORE OR AUTHORIZED REAL WORK",
            "ACTUALLY PERFORMED? DATE / NO",
            "CHOSEN WILLING AUDIENCE OR PRIVATE",
            "ACTUAL RESPONSE OR NO RESPONSE",
            "COMMUNITY MEANING: SOURCE OR UNKNOWN",
        ),
    ),
    (
        "evidence-split",
        "Keep the Dance sources separate",
        "Private teacher record only; no child identifiers in public pages.",
        (
            "DATE + SCHOOL APPROVAL",
            "CHILD'S PAPER IDEA + SUPPORT",
            "CHILD'S ACTUAL MOVEMENT OR NOT OBSERVED",
            "ACTUAL WORK / MAKER SOURCE OR NOT ENCOUNTERED",
            "SHARED WITH REAL WILLING PERSON OR NOT",
            "ACTUAL AUDIENCE RESPONSE OR NONE",
        ),
    ),
)


def two_note_cue() -> None:
    """Quiet, deterministic PCM cue; equivalent numbered cards need no audio."""
    sample_rate = 22050
    seconds = 1.5
    notes = ((0.18, 0.23, 392.0), (0.86, 0.23, 523.25))
    pcm = bytearray()
    for index in range(round(seconds * sample_rate)):
        t = index / sample_rate
        value = 0.0
        for start, duration, hz in notes:
            relative = t - start
            if 0 <= relative < duration:
                envelope = min(1.0, relative / 0.025, (duration - relative) / 0.06)
                value += 0.055 * envelope * math.sin(2 * math.pi * hz * relative)
        pcm += struct.pack("<h", round(32767 * value))
    path = ROOT / "audio/two-note-cue.wav"
    path.parent.mkdir(exist_ok=True)
    with wave.open(str(path), "wb") as sound:
        sound.setnchannels(1)
        sound.setsampwidth(2)
        sound.setframerate(sample_rate)
        sound.writeframes(bytes(pcm))


def main() -> None:
    for name, title, subtitle, fields in PAGES:
        (ROOT / f"{name}.svg").write_text(
            page(name, title, subtitle, fields), encoding="utf-8"
        )
    two_note_cue()
    print(f"Generated {len(PAGES)} original A4 SVG aids and quiet PCM cue")


if __name__ == "__main__":
    main()
