#!/usr/bin/env python3
"""Generate original two-design Foundation Music print aids and PCM candidates."""

from __future__ import annotations

import argparse
import math
import os
import struct
import subprocess
import wave
import xml.etree.ElementTree as ET
from html import escape
from pathlib import Path

ROOT = Path(__file__).resolve().parent
INK = "#173a4b"
PALE = "#e7f1ee"
SAND = "#f6edcf"
BLUE = "#d8e7f4"
RATE = 22050
SECONDS = 1.9


def txt(x: int, y: int, value: str, size: int = 18, bold: bool = False) -> str:
    weight = "bold" if bold else "normal"
    return f'<text x="{x}" y="{y}" font-family="DejaVu Sans" font-size="{size}" font-weight="{weight}" fill="{INK}">{escape(value)}</text>'


def rect(x: int, y: int, w: int, h: int, fill: str = "white", radius: int = 12) -> str:
    return f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="{radius}" fill="{fill}" stroke="{INK}" stroke-width="2.5"/>'


def line(x1: int, y1: int, x2: int, y2: int, width: int = 3) -> str:
    return f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke="{INK}" stroke-width="{width}" stroke-linecap="round"/>'


def circle(cx: int, cy: int, r: int, fill: str = "white") -> str:
    return f'<circle cx="{cx}" cy="{cy}" r="{r}" fill="{fill}" stroke="{INK}" stroke-width="2.5"/>'


def pitch_icon(cx: int, cy: int, mark: str | None) -> list[str]:
    if mark is None:
        return [circle(cx, cy, 18), txt(cx - 6, cy + 7, "?", 19, True)]
    dy = 12 if mark == "L" else -12
    return [line(cx - 23, cy + 25, cx + 23, cy + 25, 2), circle(cx, cy + dy, 7, SAND)]


def level_icon(cx: int, cy: int, mark: str | None) -> list[str]:
    if mark is None:
        return [circle(cx, cy, 18), txt(cx - 6, cy + 7, "?", 19, True)]
    if mark == "V":
        return [circle(cx, cy, 7, INK)]
    return [circle(cx - 10, cy, 7, INK), circle(cx + 10, cy, 7, INK)]


def card(x: int, y: int, number: int, pitch: str | None, level: str | None) -> list[str]:
    parts = [rect(x, y, 186, 204, PALE), txt(x + 14, y + 31, f"EVENT {number}", 17, True)]
    parts += [rect(x + 12, y + 48, 162, 62, "white", 6), rect(x + 12, y + 120, 162, 62, "white", 6)]
    parts += [txt(x + 22, y + 86, f"PITCH: {pitch or '?'}", 17, True), txt(x + 22, y + 160, f"LEVEL: {level or '?'}", 17, True)]
    parts += pitch_icon(x + 145, y + 76, pitch) + level_icon(x + 145, y + 148, level)
    return parts


def row(pitch: tuple[str | None, ...], level: tuple[str | None, ...], y: int) -> list[str]:
    xs = (78, 304, 530)
    return [part for number, (x, p, l) in enumerate(zip(xs, pitch, level, strict=True), 1) for part in card(x, y, number, p, l)]


def panel(lines: tuple[str, ...], y: int = 877, height: int = 130) -> list[str]:
    parts = [rect(77, y, 641, height, PALE)]
    for index, value in enumerate(lines):
        parts.append(txt(98, y + 39 + 32 * index, value, 16, index == 0))
    return parts


def two_rows(ap: tuple[str, ...], al: tuple[str, ...], bp: tuple[str, ...], bl: tuple[str, ...], notes: tuple[str, ...]) -> list[str]:
    return [
        txt(79, 305, "DESIGN A · FIRST IDEA", 19, True),
        *row(ap, al, 323),
        txt(79, 587, "DESIGN B · ANOTHER IDEA", 19, True),
        *row(bp, bl, 606),
        *panel(notes, 866, 142),
    ]


def two_keys() -> list[str]:
    parts = [txt(80, 303, "TWO SEPARATE CHOICES AT EACH EVENT", 19, True)]
    for y, title, left, right, is_pitch in (
        (342, "PITCH", "L · LOW", "H · HIGH", True),
        (590, "PLANNED LEVEL", "V · VERY SOFT", "S · SOFT", False),
    ):
        parts += [rect(78, y, 638, 202, PALE), txt(102, y + 40, title, 20, True)]
        parts += [rect(103, y + 63, 276, 111, SAND), rect(414, y + 63, 276, 111, BLUE)]
        parts += [txt(123, y + 96, left, 19, True), txt(434, y + 96, right, 19, True)]
        parts += pitch_icon(320, y + 130, "L") + pitch_icon(633, y + 130, "H") if is_pitch else level_icon(320, y + 130, "V") + level_icon(633, y + 130, "S")
    parts += panel(("Pitch and level are different planned elements.", "A card cannot tell a person's actual feeling."), 870, 136)
    return parts


def three_events() -> list[str]:
    return [
        txt(79, 310, "ONE COMPLETE THREE-EVENT CUE", 19, True),
        *row(("L", "L", "H"), ("V", "V", "V"), 353),
        *panel(("At each numbered event, read pitch and level.", "One fixed invented source and event timing are assumed.", "The child may make another valid music idea."), 730, 204),
    ]


def two_designs() -> list[str]:
    return two_rows(("L", "L", "H"), ("V", "V", "V"), ("H", "L", "H"), ("V", "S", "V"), ("Same fictional paper-star moment; two plans.", "Pitch differs at 1; level differs at 2.", "A listener's feeling is not printed here."))


def maker_listener() -> list[str]:
    parts = [txt(79, 300, "KEEP THESE SOURCES OF MEANING APART", 18, True)]
    for y, heading, prompt in (
        (337, "1 · MAKER INTENDED", "Child's own words, if offered: ______________"),
        (523, "2 · QUESTION TO ASK", "Non-leading question: _______________________"),
        (709, "3 · LISTENER ACTUALLY SAID", "NOT ASKED / exact words after reply: _________"),
    ):
        parts += [rect(78, y, 638, 157, PALE), txt(100, y + 45, heading, 19, True), txt(100, y + 103, prompt, 16)]
    parts += panel(("A planned score cannot report a real listener.", "Never infer a child's feeling from music marks."), 894, 113)
    return parts


def evidence_cards() -> list[str]:
    parts = [txt(80, 304, "PUT EACH CLAIM WITH ITS REAL SOURCE", 18, True)]
    for y, heading, statement, fill in (
        (343, "SCORE SHOWS", "Which pitch/level marks differ?", PALE),
        (511, "MAKER SAID", "What intention did the maker state?", SAND),
        (679, "LISTENER ACTUALLY SAID", "Leave blank until a willing person replies.", BLUE),
    ):
        parts += [rect(78, y, 638, 143, fill), txt(102, y + 43, heading, 20, True), txt(102, y + 95, statement, 17)]
    parts += panel(("“Everyone feels worried” is not a score fact.", "Ask people; do not assign feelings from a cue."), 868, 140)
    return parts


def make_two() -> list[str]:
    return two_rows((None,)*3, (None,)*3, (None,)*3, (None,)*3, ("One fictional moment, two child-owned cues.", "My two intentional differences: _____________", "Actual sound/reader, if any: ________________"))


def check_one() -> list[str]:
    return two_rows(("L", "H", "L"), ("V", "V", "V"), ("L", "H", "H"), ("S", "V", "V"), ("Find one pitch and one level difference.", "Show a mark that stayed the same.", "Paper cannot prove a real person's feeling."))


def check_two() -> list[str]:
    return two_rows(("L", "H", "L"), ("V", "V", "S"), ("L", "H", "L"), ("V", "V", "S"), ("In B circle ONE old pitch and ONE old level mark.", "Write each new letter beside it; keep four others.", "Tell your own musical idea for the same moment."))


PAGES = {
    "print/two-keys": ("A · Pitch and level keys", "Low/high pitch and very-soft/soft planned level are two independent original mark keys.", "Prepare separate raised pitch and level lanes. L dot low/H dot high on a baseline; V one raised dot/S two raised dots. Read full words and let child choose one each.", two_keys),
    "print/three-events": ("B · One three-event cue", "Three numbered event cards with pitch L L H and planned level V V V, one pitch/level pair per event.", "Set three broad fixed event columns. Pitch cards L,L,H above level cards V,V,V; read each pair left to right, silently if desired.", three_events),
    "print/two-designs": ("C · Two ideas for one moment", "Design A pitch L L H, level V V V; Design B pitch H L H, level V S V. Same fictional moment, two planned differences.", "Build two fixed three-event raised strips with separate pitch and level slots. Compare corresponding cells; child identifies pitch at place 1 and level at place 2 in practice.", two_designs),
    "print/maker-listener": ("D · Maker and listener evidence", "Three separate empty fields for maker intention, non-leading listener question, and listener's actual words if any.", "Make three broad labelled fields. Child can fill intention/question; leave actual-listener field NOT ASKED unless a willing person replied.", maker_listener),
    "print/evidence-cards": ("E · What is the source?", "Three graphic panels distinguish SCORE SHOWS, MAKER SAID and LISTENER ACTUALLY SAID, with an unsupported universal-feeling reminder.", "Prepare three raised labelled evidence panels. Place a mark comparison under SCORE SHOWS, child's own stated intention under MAKER SAID, and a real willing reply only under LISTENER ACTUALLY SAID.", evidence_cards),
    "print/make-two": ("F · My two music ideas", "Two blank three-event pitch/level score rows with fields for two child-owned differences and actual sound/reader if any.", "Use two broad raised three-event strips with pitch and level lanes. Child directs both six-mark scores and identifies two differences; real-event fields stay blank until true.", make_two),
    "check/design-1": ("Fresh Design 1 · Find differences", "Public unfamiliar pair: A pitch L H L/level V V V; B pitch L H H/level S V V. Two changed cells are not identified on sheet.", "Fix two raised three-event score strips using the public rows. Read each cell neutrally; child locates one pitch and one level difference independently.", check_one),
    "check/design-2": ("Fresh Design 2 · Make another cue", "Public base Design A pitch L H L/level V V S and identical Design B copy; child changes one pitch and one level mark.", "Make two identical fixed raised strips from the public base; child replaces one pitch and one level card in B while holding four others.", check_two),
}


def svg(title: str, description: str, body: list[str]) -> str:
    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(description)}</desc>',
        rect(27, 24, 740, 1074),
        txt(57, 80, "SUBJECTNEST · FOUNDATION MUSIC", 22, True),
        txt(57, 125, "Two original ideas · same fictional moment · no-sound route", 15),
        txt(57, 184, title, 22, True),
        txt(76, 232, "PITCH L=LOW / H=HIGH; LEVEL V=VERY SOFT / S=SOFT", 15, True),
        txt(76, 260, "Line height and dot count are planned marks, not feelings.", 15),
        *body,
        txt(57, 1055, "Original resource · CC BY 4.0 · exact alternatives in pack", 13),
        "</svg>",
    ]) + "\n"


def alternatives() -> str:
    lines = [
        "# Exact text, tactile and no-print routes for eight A4 Music sheets", "",
        "Pitch L/H means low/high with a raised line-and-dot key; planned level V/S means very soft/soft with one/two raised dots. Each design has three fixed numbered event columns and one separate row per lane. Colour is never the only key. A neutral adult reads or moves broad cards only as directed. A no-sound card route meets score planning/comparison, not actual listening. Candidate PCM needs human listening/device-volume review. The fresh-check routes state only the public task.", "",
    ]
    for key, (title, description, tactile, draw) in PAGES.items():
        root = ET.fromstring(svg(title, description, draw()))
        printed = [node.text or "" for node in root.iter() if node.tag.rsplit("}", maxsplit=1)[-1] == "text"]
        lines.extend((f"## {title}", "", f"**File:** {key}.svg / {key}.pdf.", f"**Exact content:** {description}", "**Verbatim printed text in layout order:** " + " | ".join(printed) + ".", f"**Tactile/no-print alternative:** {tactile}", ""))
    lines.extend(("**Evidence limit:** Score marks show planned choices. Actual sound, comfort, listener words, community context and audience receipt require their own observations.", ""))
    return "\n".join(lines)


CUES: dict[str, tuple[tuple[str, ...], tuple[str, ...]]] = {
    "cue-idea-one": (("L", "L", "H"), ("V", "V", "V")),
    "cue-idea-two": (("H", "L", "H"), ("V", "S", "V")),
    "cue-another": (("H", "H", "L"), ("S", "V", "S")),
}
STARTS = (.10, .65, 1.20)


def pcm(pitches: tuple[str, ...], levels: tuple[str, ...]) -> bytes:
    events = [(start, .22, p, l) for start, p, l in zip(STARTS, pitches, levels, strict=True)]
    data = bytearray()
    for i in range(round(SECONDS * RATE)):
        instant = i / RATE
        value = 0.0
        for start, duration, pitch, level in events:
            local = instant - start
            if 0 <= local < duration:
                fade = min(1.0, local/.012, (duration-local)/.012)
                hz = 330 if pitch == "L" else 392
                amp = .018 if level == "V" else .050
                value += amp * fade * math.sin(2*math.pi*hz*local)
        data.extend(struct.pack("<h", round(value*32767)))
    return bytes(data)


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--write", action="store_true", help="intentionally regenerate owned assets")
    args = parser.parse_args()
    if not args.write:
        parser.error("use --write for intentional asset regeneration")
    for key, (title, description, _tactile, draw) in PAGES.items():
        path = ROOT / f"{key}.svg"
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(svg(title, description, draw()), encoding="utf-8")
        subprocess.run(["rsvg-convert", "--format", "pdf", "--output", str(path.with_suffix(".pdf")), str(path)], check=True, env=dict(os.environ, SOURCE_DATE_EPOCH="1790640000"))
    (ROOT / "print/TEXT-ALTERNATIVES.md").write_text(alternatives(), encoding="utf-8")
    for name, (pitches, levels) in CUES.items():
        target = ROOT / "audio" / f"{name}.wav"
        target.parent.mkdir(parents=True, exist_ok=True)
        with wave.open(str(target), "wb") as out:
            out.setnchannels(1); out.setsampwidth(2); out.setframerate(RATE)
            out.writeframes(pcm(pitches, levels))
    licence = Path("/usr/share/doc/fonts-dejavu-core/copyright").read_text(encoding="utf-8")
    (ROOT / "print/dejavu-font-copyright.txt").write_text("\n".join(line.rstrip() for line in licence.splitlines()) + "\n", encoding="utf-8")
    print(f"Wrote {len(PAGES)} A4 SVG/PDF pairs and {len(CUES)} PCM candidates.")


if __name__ == "__main__":
    main()
