#!/usr/bin/env python3
"""Generate original V/S dynamic-score A4 aids and optional 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 = 2.4


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" '
        f'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 = 14) -> str:
    return (
        f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="{radius}" '
        f'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}" '
        f'stroke="{INK}" stroke-width="{width}" stroke-linecap="round"/>'
    )


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


def mark(cx: int, cy: int, key: str | None, *, large: bool = False) -> list[str]:
    """Draw level as one/two dots on same baseline; halo is expressive, never pitch."""
    radius = 52 if large else 33
    parts = [circle(cx, cy, radius, SAND if key == "V" else BLUE)]
    if key == "V":
        parts.append(circle(cx, cy, 8 if large else 6, INK, 0))
    elif key == "S":
        gap = 13 if large else 10
        dot = 8 if large else 6
        parts += [circle(cx - gap, cy, dot, INK, 0), circle(cx + gap, cy, dot, INK, 0)]
    else:
        parts.append(txt(cx - 8, cy + 8, "?", 24, True))
    return parts


def event_card(x: int, y: int, width: int, number: int, key: str | None) -> list[str]:
    parts = [rect(x, y, width, 164, PALE), txt(x + 12, y + 30, f"PLACE {number}", 15, True)]
    parts += mark(x + width // 2, y + 87, key)
    label = {"V": "V", "S": "S", None: "V OR S"}[key]
    parts.append(txt(x + width // 2 - (7 if key else 29), y + 147, label, 18 if key else 15, True))
    return parts


def row(values: tuple[str | None, ...], y: int, *, width: int = 133) -> list[str]:
    n = len(values)
    xs = (74, 246, 418, 590) if n == 4 else (67, 209, 351, 493, 635)
    if n == 5:
        width = 103
    parts = []
    for number, (x, value) in enumerate(zip(xs, values, strict=True), 1):
        parts.extend(event_card(x, y, width, number, value))
    return parts


def panel(lines: tuple[str, ...], y: int, height: int = 186) -> list[str]:
    parts = [rect(75, y, 645, height, PALE)]
    for index, value in enumerate(lines):
        parts.append(txt(96, y + 42 + 38 * index, value, 16, index == 0))
    return parts


def legend() -> list[str]:
    parts = [rect(75, 295, 295, 405, SAND), rect(424, 295, 295, 405, BLUE)]
    parts += [txt(114, 340, "V · VERY SOFT", 23, True), txt(479, 340, "S · SOFT", 23, True)]
    parts += mark(222, 466, "V", large=True) + mark(571, 466, "S", large=True)
    parts += [txt(143, 603, "ONE DOT", 20, True), txt(495, 603, "TWO DOTS", 20, True)]
    parts += panel(
        (
            "Both marks stay in a planned soft range.",
            "Same note, source, event time and duration.",
            "A mark cannot tell how loud a real device was.",
        ),
        772,
        180,
    )
    return parts


def lanes() -> list[str]:
    return [
        txt(81, 291, "LIFT · VERY SOFT TO SOFT", 18, True),
        *row(("V", "V", "S", "S"), 314),
        txt(81, 571, "RELEASE · SOFT TO VERY SOFT", 18, True),
        *row(("S", "S", "V", "V"), 593),
        *panel(("Point to the gap where each level changes.", "Every card is one planned sound event.", "No event is silent; actual sound needs a trial."), 831, 172),
    ]


def compare() -> list[str]:
    return [
        txt(81, 290, "ROW A · ONE LIFT", 18, True),
        *row(("V", "V", "S", "S"), 314),
        txt(81, 569, "ROW B · THREE CHANGES", 18, True),
        *row(("V", "S", "V", "S"), 592),
        *panel(("Both rows start V and end S.", "The first differing card is at place 2.", "Choose a shape for your own musical idea."), 828, 174),
    ]


def trial() -> list[str]:
    parts = [txt(80, 290, "KEEP THESE THREE KINDS OF INFORMATION APART", 17, True)]
    for y, heading, prompt in (
        (326, "1 · PLANNED MARK", "V / S / your own score: ___________________"),
        (509, "2 · ACTUALLY HEARD OR MADE", "Not tried / child's exact words: _______________"),
        (692, "3 · COMFORT AND CHOICE", "Not tried / child's exact words: _______________"),
    ):
        parts += [rect(76, y, 642, 148, PALE), txt(98, y + 43, heading, 19, True), txt(98, y + 100, prompt, 16)]
    parts += panel(("A planned mark does not fill an actual observation.", "No-sound is a complete route for score planning."), 883, 119)
    return parts


def revise() -> list[str]:
    return [
        txt(81, 287, "FIRST SCORE · KEEP IT VISIBLE", 18, True),
        *row(("V", "V", "S", "V"), 311),
        txt(81, 566, "REVISED SCORE · CHANGE ONLY PLACE 2", 18, True),
        *row(("V", "S", "S", "V"), 590),
        *panel(("The lift moves earlier: after 2 to after 1.", "The release remains after place 3.", "Same note, source, spacing and duration."), 829, 174),
    ]


def make() -> list[str]:
    return [
        txt(82, 301, "MY FOUR PLANNED LEVELS", 19, True),
        *row((None, None, None, None), 331),
        txt(84, 578, "KEY: V = ONE DOT · S = TWO DOTS", 16, True),
        *panel(
            (
                "My musical idea: ____________________________",
                "Actually tested / what happened: ______________",
                "Received by / medium, if chosen: ______________",
                "It is okay to keep this private or silent.",
            ),
            711,
            262,
        ),
    ]


def fresh_w() -> list[str]:
    parts = [txt(79, 303, "NEW FIVE-PLACE LEVEL PATH", 19, True)]
    parts += row(("S", "V", "V", "S", "V"), 346)
    parts.append(txt(80, 601, "MARK EVERY GAP WHERE LEVEL CHANGES", 17, True))
    for x in (174, 316, 458, 600):
        parts += [rect(x, 634, 32, 80, "white", 6), txt(x + 8, 684, "?", 18, True)]
    parts += panel(("V = VERY SOFT / ONE DOT; S = SOFT / TWO DOTS.", "Read all five places, then inspect four gaps.", "What can the paper not prove about real sound?"), 813, 191)
    return parts


def fresh_x() -> list[str]:
    return [
        txt(81, 297, "MAKE YOUR OWN FOUR-PLACE LEVEL PATH", 18, True),
        *row((None, None, None, None), 341),
        txt(82, 604, "KEY: V = ONE DOT · S = TWO DOTS", 16, True),
        *panel(("Use exactly ONE S and THREE V marks.", "You choose the S place; show the whole order.", "Tell one musical idea for your choice.", "What can paper not tell us about real sound?"), 729, 240),
    ]


PAGES = {
    "print/level-legend": ("A · Two nearby dynamic levels", "V is one dot/very soft; S is two dots/soft. Both use a planned soft range and a fixed note/source/timing.", "Use one broad raised-dot V card and one broad two-raised-dot S card. Name the full words, and let the child choose one for a fixed sound event.", legend),
    "print/level-lanes": ("B · Lift and release lanes", "Two four-place rows: V V S S is a gentle lift; S S V V is a gentle release. Every place has a planned event.", "Fix two four-slot raised strips. First set one, one, two, two raised-dot cards; second set two, two, one, one. Child marks the change gap on each.", lanes),
    "print/shape-compare": ("C · Two dynamic shapes", "Four-place V V S S and V S V S rows share first and last marks but differ first at place 2.", "Use two fixed tactile strips and compare corresponding slots; the child chooses a score for their own idea.", compare),
    "print/actual-trial": ("D · Plan, actual event, comfort", "Three separate empty fields for planned mark, actual heard or made event, and comfort or choice; not tried is allowed.", "Make three broad labelled fields. A plan token goes only in the first. Leave the other fields empty or use a NOT TRIED card unless a safe event occurs.", trial),
    "print/move-lift": ("E · Revise one level mark", "First row V V S V; revised row V S S V. Only place 2 changes, bringing the lift earlier while the final release stays.", "Keep the original raised strip visible. On a second strip replace only place 2 one-dot V with two-dot S; compare all four corresponding slots.", revise),
    "print/make-share": ("F · My soft dynamic score", "Four blank numbered V/S slots and separate fields for own idea, actual test, and actual recipient if chosen.", "Offer four fixed blank numbered slots and broad V/S dot cards. Child directs the order; actual-trial and recipient fields stay blank until true.", make),
    "check/fresh-w": ("Fresh W · Read an unfamiliar path", "New five-place S V V S V row with four unmarked change-gap boxes and a paper-evidence question.", "Fix five raised cards: two, one, one, two, one dots. Leave all four gaps empty for independent child markers; read the paper-limit question neutrally.", fresh_w),
    "check/fresh-x": ("Fresh X · One soft highlight", "Blank four-place V/S score with public brief: exactly one S and three V, child chooses the S position.", "Offer four blank fixed slots, one two-dot S and three one-dot V cards. Do not place any card or supply a model answer; the child directs all placements.", fresh_x),
}


def svg(title: str, description: str, body: list[str]) -> str:
    parts = [
        '<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, 81, "SUBJECTNEST · FOUNDATION MUSIC", 22, True),
        txt(57, 127, "Original V/S dynamic key · no-sound route", 15),
        txt(57, 187, title, 23, True),
        txt(75, 235, "Dots show planned level, never pitch or measured volume.", 16),
        txt(75, 260, "V = VERY SOFT / ONE DOT; S = SOFT / TWO DOTS", 15, True),
        *body,
        txt(57, 1056, "Original resource · CC BY 4.0 · exact alternatives in pack", 13),
        "</svg>",
    ]
    return "\n".join(parts) + "\n"


def alternatives() -> str:
    lines = [
        "# Exact text and tactile routes for eight A4 dynamic-score sheets",
        "",
        "The invented key is **V = VERY SOFT = one dot** and **S = SOFT = two dots**. Dots are tactile/visual labels for *relative planned dynamics*, never a number of sounds, pitch height or measured room level. Use broad fixed numbered slots and large raised-dot cards; an adult moves only as directed. The score-planning goal is available without sound, screen or printer. Fresh-check routes disclose the public task but no worked answer. Candidate WAV playback has a separate human-listening/volume hold.",
        "",
    ]
    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 alternative:** {tactile}",
                "",
            )
        )
    lines.extend(("**Evidence limit:** Cards and printed scores show intended levels. Actual hearing, production, comfort, audience receipt and source-context inquiry need separate observations.", ""))
    return "\n".join(lines)


CUES: dict[str, tuple[str, str, str, str]] = {
    "cue-lift": ("V", "V", "S", "S"),
    "cue-release": ("S", "S", "V", "V"),
    "cue-return": ("V", "S", "S", "V"),
}


def pcm(values: tuple[str, str, str, str]) -> bytes:
    events = [(0.1 + index * 0.5, 0.20, value) for index, value in enumerate(values)]
    data = bytearray()
    fade = 0.012
    for index in range(round(SECONDS * RATE)):
        instant = index / RATE
        value = 0.0
        for start, duration, key in events:
            local = instant - start
            if 0 <= local < duration:
                envelope = min(1.0, local / fade, (duration - local) / fade)
                level = 0.018 if key == "V" else 0.050
                value += level * envelope * math.sin(2 * math.pi * 330 * 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 media")
    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():
        target = ROOT / f"{key}.svg"
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text(svg(title, description, draw()), encoding="utf-8")
        subprocess.run(
            ["rsvg-convert", "--format", "pdf", "--output", str(target.with_suffix(".pdf")), str(target)],
            check=True,
            env=dict(os.environ, SOURCE_DATE_EPOCH="1790640000"),
        )
    (ROOT / "print/TEXT-ALTERNATIVES.md").write_text(alternatives(), encoding="utf-8")
    for name, values in CUES.items():
        target = ROOT / "audio" / f"{name}.wav"
        target.parent.mkdir(parents=True, exist_ok=True)
        with wave.open(str(target), "wb") as output:
            output.setnchannels(1)
            output.setsampwidth(2)
            output.setframerate(RATE)
            output.writeframes(pcm(values))
    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)} illustrated A4 SVG/PDF pairs and {len(CUES)} PCM candidates.")


if __name__ == "__main__":
    main()
