#!/usr/bin/env python3
"""Generate original opening/contrast/return Music 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 = 2.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 icon(cx: int, cy: int, mark: str, key: str) -> list[str]:
    if mark == "?":
        return [circle(cx, cy, 26, "white"), txt(cx - 8, cy + 9, "?", 27, True)]
    if key == "pitch":
        dot_y = cy + 10 if mark == "L" else cy - 10
        return [line(cx - 24, cy + 22, cx + 24, cy + 22, 2), circle(cx, dot_y, 8, SAND)]
    if mark == "A":
        return [circle(cx, cy, 25, BLUE)]
    return [rect(cx - 25, cy - 25, 50, 50, BLUE, 3), *[line(cx + d, cy - 22, cx + d, cy + 22, 2) for d in (-15, -5, 5, 15)]]


def event(x: int, y: int, number: int, mark: str | None, key: str, *, small: bool = False) -> list[str]:
    mark = mark or "?"
    w, h = (85, 112) if small else (91, 207)
    parts = [rect(x, y, w, h, "white", 8), txt(x + 10, y + 28, f"{number}", 19, True)]
    parts += icon(x + w // 2, y + (69 if small else 102), mark, key)
    parts.append(txt(x + w // 2 - 9, y + (104 if small else 179), mark, 24, True))
    return parts


SECTIONS = ("OPENING", "DIFFERENT", "RETURN")


def section(x: int, y: int, index: int, values: tuple[str | None, str | None], key: str, *, small: bool = False) -> list[str]:
    w, h = (205, 192) if small else (205, 385)
    parts = [rect(x, y, w, h, PALE if index != 1 else SAND)]
    parts.append(txt(x + 12, y + 33, SECTIONS[index], 18 if not small else 16, True))
    parts.append(txt(x + 13, y + 58, f"PLACES {2*index+1}–{2*index+2}", 14))
    ey = y + (69 if small else 93)
    for offset, (number, value) in enumerate(zip((2*index+1, 2*index+2), values, strict=True)):
        parts += event(x + 9 + 98 * offset, ey, number, value, key, small=small)
    return parts


def score(values: tuple[str | None, ...], y: int, key: str, *, small: bool = False) -> list[str]:
    xs = (76, 294, 512)
    return [part for i, x in enumerate(xs) for part in section(x, y, i, values[2*i:2*i+2], key, small=small)]


def panel(lines: tuple[str, ...], y: int = 828, height: int = 180) -> list[str]:
    parts = [rect(76, y, 642, height, PALE)]
    for i, value in enumerate(lines):
        parts.append(txt(97, y + 43 + 37*i, value, 16, i == 0))
    return parts


def single(values: tuple[str | None, ...], key: str, lines: tuple[str, ...]) -> list[str]:
    return [*score(values, 316, key), *panel(lines, 827, 178)]


def section_road() -> list[str]:
    return single(("L", "H", "H", "H", None, None), "pitch", ("Three sections, two event places each.", "Opening: L H. Different: H H.", "Return is waiting for the opening to come back."))


def exact_return() -> list[str]:
    return single(("L", "H", None, None, "L", "H"), "pitch", ("First opening mark matches first return mark.", "Second opening mark matches second return mark.", "A reversed H L pair would be a different ending."))


def contrast_return() -> list[str]:
    return single(("L", "H", "L", "L", "L", "H"), "pitch", ("The middle differs at its second mark.", "The final L H copies opening L H in order.", "This is a paper score, not an actual sound report."))


def compare_endings() -> list[str]:
    return [
        txt(81, 290, "ROW 1 · RETURN AFTER CONTRAST", 18, True),
        *score(("L", "H", "H", "H", "L", "H"), 312, "pitch", small=True),
        txt(81, 552, "ROW 2 · A DIFFERENT ENDING", 18, True),
        *score(("L", "H", "H", "H", "H", "L"), 575, "pitch", small=True),
        *panel(("Compare first-to-first and second-to-second.", "Row 1 ends L H; row 2 ends H L.", "A different ending can be an intentional choice."), 832, 177),
    ]


def plan_actual() -> list[str]:
    parts = [txt(80, 293, "KEEP A SCORE PLAN AND REAL EVENT SEPARATE", 18, True)]
    for y, title, prompt in (
        (330, "1 · PLANNED THREE-SECTION SCORE", "Six marks and key: __________________________"),
        (519, "2 · ACTUALLY HEARD OR MADE", "NOT TRIED / child's exact words: ____________"),
        (708, "3 · COMFORT AND CHOICE", "NOT TRIED / child's exact words: ____________"),
    ):
        parts += [rect(76, y, 642, 163, PALE), txt(99, y + 46, title, 19, True), txt(99, y + 110, prompt, 16)]
    parts += panel(("A written score cannot fill an actual observation.", "No-sound is a full route for structure planning."), 908, 101)
    return parts


def transfer_key() -> list[str]:
    return single(("B", "A", "B", "B", "B", "A"), "source", ("A is a circle; B is a striped square.", "The opening B A returns exactly after B B.", "These are invented source labels, not instruments."))


def make_share() -> list[str]:
    return single((None,)*6, "pitch", ("Choose one key: pitch L/H or source A/B.", "My intended idea: __________________________", "Actually tried: ______  Real reader, if any: ______"))


def check_one() -> list[str]:
    return single(("H", "H", "L", "H", "H", "H"), "pitch", ("Which section repeats opening exactly in order?", "Which middle mark differs?", "What can paper not prove about real sound?"))


def check_two() -> list[str]:
    return single(("A", "B", "B", "B", None, None), "source", ("A = circle; B = striped square.", "Fill two RETURN places to copy opening in order.", "Find a middle difference; tell your own idea."))


PAGES = {
    "print/section-road": ("A · Three musical sections", "Opening L H; different H H; return two blank slots. Six numbered places grouped into three musical sections.", "Make three wide fixed raised panels, two places each. Set L,H in opening and H,H in middle; leave return blank. Full section words are tactile labels, not map directions.", section_road),
    "print/exact-return": ("B · Bring the opening back", "Opening L H, middle blank, return L H. The two return marks copy opening in the same order.", "Place L,H in opening and L,H in return on three raised panels. Match opening first to return first and opening second to return second.", exact_return),
    "print/contrast-return": ("C · Different middle, exact return", "Six pitch events L H / L L / L H; middle differs at second event, final two copy opening.", "Fix three two-slot raised panels and the six stated pitch cards. Child checks middle contrast and ordered final match.", contrast_return),
    "print/compare-endings": ("D · Compare two endings", "Row 1 L H / H H / L H has exact return; row 2 L H / H H / H L has a reversed ending.", "Build two separate three-section raised strips. Compare both final pairs to their opening first-to-first and second-to-second; child may choose either musical idea.", compare_endings),
    "print/plan-actual": ("E · Plan and actual sound", "Separate blank fields for planned score, actually heard or made, and comfort/choice; NOT TRIED allowed.", "Set three broad labelled boxes. Put a planned card score in the first only. Keep actual and comfort NOT TRIED unless safe events happened.", plan_actual),
    "print/transfer-key": ("F · Return in a source key", "Invented electronic source A circle/B striped square; six-place B A / B B / B A with exact return.", "Use large fixed three-section panels, round A and striped B raised cards. Set B,A / B,B / B,A and check the same form rule without sound.", transfer_key),
    "print/make-share": ("G · My three-section score", "Six blank numbered two-event section slots; child chooses one consistent pitch or source key and an idea, actual trial/reader fields optional.", "Offer three raised two-slot panels and a broad chosen key. Child directs opening, different middle and matching return; leave trial/recipient blank until true.", make_share),
    "check/return-1": ("Fresh Return 1 · Spot the match", "Unfamiliar six-place pitch score H H / L H / H H with the exact-return question; no worked answer printed.", "Fix H,H / L,H / H,H raised pitch cards in three sections. Read the public question neutrally; child points to match and middle difference independently.", check_one),
    "check/return-2": ("Fresh Return 2 · Complete the form", "Source-key opening A B, middle B B, final two slots blank; child places the exact return independently.", "Fix A,B / B,B / blank,blank in three raised panels. Offer circle A and striped B cards; adult does not choose or place final cards without child's direction.", 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, 81, "SUBJECTNEST · FOUNDATION MUSIC", 22, True),
        txt(57, 127, "Original three-section music score · no-sound route", 15),
        txt(57, 186, title, 23, True),
        txt(76, 232, "OPENING · DIFFERENT · RETURN = three musical sections", 16, True),
        txt(76, 263, "PITCH L=LOW / H=HIGH; SOURCE A=CIRCLE / B=STRIPED", 15),
        *body,
        txt(57, 1056, "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 nine A4 Music sheets", "",
        "OPENING covers places 1–2; DIFFERENT places 3–4; RETURN places 5–6. Each two-slot section has a broad border and full word label. Pitch L/H is low/high with a raised baseline and dot low/high; invented source A/B is circle/striped square. These are score keys, not real route directions, instruments or sound observations. Use large fixed raised panels and neutral adult reading/placement only as the child directs. Sound is optional and candidate files need human listening/device-volume review. Fresh-check alternatives state the public task, not a worked answer.", "",
    ]
    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:** Written/raised scores show planned sections and return. Actual hearing, sound production, comfort, community context and willing audience require separate observations.", ""))
    return "\n".join(lines)


CUES: dict[str, tuple[str, ...]] = {
    "cue-return-lh": ("L", "H", "H", "H", "L", "H"),
    "cue-return-hl": ("H", "L", "L", "L", "H", "L"),
    "cue-end-different": ("L", "H", "H", "H", "H", "L"),
}
STARTS = (0.10, 0.45, 1.05, 1.40, 2.00, 2.35)


def pcm(values: tuple[str, ...]) -> bytes:
    events = [(start, 0.16, key) for start, key in zip(STARTS, values, strict=True)]
    data = bytearray()
    for i in range(round(SECONDS * RATE)):
        instant = i / RATE
        value = 0.0
        for start, duration, key in events:
            local = instant - start
            if 0 <= local < duration:
                fade = min(1.0, local / 0.012, (duration - local) / 0.012)
                hz = 330 if key == "L" else 392
                value += 0.025 * 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 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():
        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, 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)} original A4 SVG/PDF pairs and {len(CUES)} PCM candidates.")


if __name__ == "__main__":
    main()
