#!/usr/bin/env python3
"""Reproduce original one/pair rhythm illustrations and quiet PCM candidates."""
from __future__ import annotations
import argparse
from html import escape
import math
import os
from pathlib import Path
import struct
import subprocess
import wave
import xml.etree.ElementTree as ET

ROOT=Path(__file__).resolve().parent
INK='#173a4b'; PALE='#e7f1ee'; SAND='#f6edcf'; BLUE='#d8e7f4'
RATE=22050; DURATION=2.7
BEATS=(.2,.8,1.4,2.0)
CUES={'cue-one':(1,1,1,1),'cue-pair':(2,2,2,2),'cue-mixed':(1,1,2,2)}


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


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


def circle(x: int,y: int,r: int=14,fill: str=SAND) -> str:
    """Return a short sound-event mark."""
    return f'<circle cx="{x}" cy="{y}" r="{r}" fill="{fill}" stroke="{INK}" stroke-width="2.5"/>'


def score(y: int,label: str,recipe: tuple[int|None,...]) -> list[str]:
    """Draw four equal beats, each with explicit first/middle event positions."""
    out=[text(75,y,label,19,True),text(75,y+30,'FOUR BEAT STARTS',14,True)]
    for i,value in enumerate(recipe):
        x=84+i*154
        out += [text(x+32,y+50,str(i+1),18,True),circle(x+36,y+70,8,BLUE),rect(x,y+105,144,80,PALE)]
        out.append(f'<line x1="{x+72}" y1="{y+107}" x2="{x+72}" y2="{y+183}" stroke="{INK}" stroke-width="1.5" stroke-dasharray="5 5"/>')
        if value is None:
            out += [text(x+57,y+153,'?',26,True),text(x+16,y+221,'CHOOSE',18,True)]
        else:
            out += [circle(x+36,y+145),circle(x+108,y+145) if value==2 else text(x+97,y+153,'—',22,True),text(x+45 if value==1 else x+40,y+221,'ONE' if value==1 else 'PAIR',18,True)]
    return out


def panel(y: int,lines: tuple[str,...]) -> list[str]:
    """Draw a short readable prompt panel."""
    return [rect(75,y,644,34+len(lines)*32,PALE),*[text(94,y+32+i*32,s,16,i==0) for i,s in enumerate(lines)]]


def one(recipe: tuple[int,...],notes: tuple[str,...]) -> list[str]:
    """Make a practice score with generous space."""
    return score(350,'MY FOUR-BEAT RHYTHM',recipe)+panel(705,notes)


def two(a: tuple[int|None,...],b: tuple[int|None,...],labels: tuple[str,str],notes: tuple[str,...]) -> list[str]:
    """Make two equal-width score versions."""
    return score(327,labels[0],a)+score(612,labels[1],b)+panel(897,notes)


def key() -> list[str]:
    """Show one versus sequential pair with original graphic illustrations."""
    out=[text(76,322,'ONE BEAT · TWO CHOICES',19,True)]
    for y,value in ((357,1),(575,2)):
        out += [rect(75,y,644,176,PALE),text(97,y+37,'ONE' if value==1 else 'PAIR',22,True)]
        out += [circle(123,y+91,18),circle(227,y+91,18) if value==2 else text(213,y+99,'—',26,True)]
        out += [text(320,y+82,'one short sound' if value==1 else 'two short sounds',19),text(320,y+119,'at the beat start' if value==1 else 'start, then middle',18)]
    out += panel(800,('The beat stays the same length.','PAIR sounds follow each other; not together.','Small dash = no second sound in this beat.','Choose a tiny opening or ending of your own.'))
    return out


PAGES={
 'print/one-pair-key':('A · One beat, one or two sounds','ONE has one sound at beat start and no second event. PAIR has two equal short sounds in sequence: one at start, one halfway through the same beat. Beat length holds.','Use one broad beat card divided into equal halves. ONE has one raised disc in first half, flat dash in second; PAIR has one disc in each half. Trace the sequence first then middle, never both together. Make words available without touch.',key),
 'print/steady-one':('B · Four beats, four sounds','Four even beat-start dots. Rhythm ONE ONE ONE ONE: one first-half sound per beat, no second-half sounds; four separate events.','Four broad equal beat cards with raised first/middle divider. One disc in first half and dash in second of each. Four beat-start tags remain separate from the rhythm tokens.',lambda:one((1,1,1,1),('Four beats continue at one steady pace.','ONE / ONE / ONE / ONE.','One short sound starts each beat.','A paper score is a plan; sound is optional.'))),
 'print/steady-pair':('C · Four beats, eight sounds','Four even beat-start dots. Rhythm PAIR PAIR PAIR PAIR: first/middle short event in each beat; eight sequential events in four beats.','Four broad equal beat cards. Place two discs sequentially in each card, first and middle. Count four beat-start tags independently from eight rhythm discs.',lambda:one((2,2,2,2),('Four beats continue at the SAME steady pace.','PAIR / PAIR / PAIR / PAIR.','Two short sounds fit inside each beat.','More sounds does not mean faster beats.'))),
 'print/mixed-piece':('D · A tiny rhythm of my own','Four even beat-start dots. Rhythm ONE ONE PAIR PAIR: six events in four beats. The last two beats each contain two short sounds.','Four equal beat cards with separate fixed beat-start tags. First two ONE cards have first disc and middle dash; last two PAIR cards have first/middle discs. Child may choose a different plan.',lambda:one((1,1,2,2),('ONE / ONE / PAIR / PAIR.','A maker may choose a tiny opening or ending.','Try with a quiet approved sound, or use cards.','Ask the maker what their idea means.'))),
 'print/move-pair':('E · Move the pair, keep the beat','First ONE ONE ONE PAIR; next PAIR ONE ONE ONE. Four even beats and five events in each. Beats 1 and 4 change; middle two hold.','Make two four-beat strips. First 1/1/1/2 discs; second 2/1/1/1. Pair moves from beat 4 to beat 1; preserve equal beat cards and the five-event count.',lambda:two((1,1,1,2),(2,1,1,1),('FIRST RHYTHM','ANOTHER RHYTHM'),('Same four beats; same five sound events.','The pair moves from beat 4 to beat 1.','Maker choice / actual listener words stay separate.'))),
 'print/my-piece':('F · My tiny opening or ending','Two blank four-beat rhythm plans with fixed beat-start dots and question-mark cells. Learner chooses ONE or PAIR in each beat. Actual sound/audience fields unset.','Two four-beat strips, each beat with first/middle divider and fixed beat-start tags. Blank cards are undecided, not rests. Learner directs ONE/PAIR discs and middle dashes; save first plan before another version.',lambda:two((None,)*4,(None,)*4,('FIRST IDEA · CHILD CHOOSES','ANOTHER IDEA · CHILD CHOOSES'),('Actual sound: NOT TRIED / maker: ______','Actual audience: NOT SHARED / words: ______','Paper records a rhythm plan, not performance.'))),
 'check/subdivide-1':('Fresh Split 1 · Compare new rhythms','Plan P PAIR ONE PAIR ONE; plan Q PAIR PAIR PAIR ONE. Both have four even beat starts. Public questions give no totals or changed-beat answer.','Raised P discs per beat 2/1/2/1; Q 2/2/2/1; middle dash for every ONE. Fixed beat-start tags in both. Read all choices neutrally; do not count or classify for the learner.',lambda:two((2,1,2,1),(2,2,2,1),('PLAN P','PLAN Q'),('Which beat changes? Show its old and new choice.','How many beats? How many sounds in each?','Do beats speed up? What can paper not prove?'))),
 'check/subdivide-2':('Fresh Split 2 · Finish with a pair','Given ONE PAIR PAIR ONE. Response holds ONE at beat 1 and PAIR at beat 2; beats 3/4 are blank. Keep six total events in four steady beats and finish with PAIR. Public response leaves final choices undecided.','Given raised disc counts 1/2/2/1. Response first two fixed 1/2; final two broad cards blank. Child chooses ONE/PAIR for 3/4 under public rules; adult gives no solution or cue.',lambda:two((1,2,2,1),(1,2,None,None),('GIVEN RHYTHM','YOUR RHYTHM · KEEP BEATS 1 AND 2'),('Keep beats 1 and 2. Finish with PAIR.','Still six sounds in four steady beats.','Show which two beats change, and what holds.'))),
}


def svg(title: str,description: str,body: list[str]) -> str:
    """Return accessible standalone A4 SVG."""
    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),text(57,80,'SUBJECTNEST · FOUNDATION MUSIC',22,True),text(57,124,'One steady beat can hold one or two short sounds',16),text(57,185,title,22,True),text(76,239,'ONE = ONE SOUND / PAIR = TWO INSIDE ONE BEAT',16,True),text(76,268,'Dots above the rhythm mark the four even beat starts.',15),*body,text(57,1055,'Original resource · CC BY 4.0 · exact alternatives in pack',13),'</svg>'])+'\n'


def alternatives() -> str:
    """Provide exact text/structural and no-sound/tactile equivalents."""
    out=['# Exact linear, raised-card and no-sound alternatives','', 'Read page top to bottom and each four-beat score left to right. The small upper dots are four fixed beat starts; do not count them as extra rhythm sounds. Each broad rhythm card has first/middle halves. ONE has first sound, middle dash/no second event; PAIR has two sequential equal short events within one beat. A question mark is an undecided card. Colour is redundant. An adult moves broad fixed tokens only as directed. Silent routes meet rhythm planning; hearing/steady performed beat and comfortable making require separate actual accessible observations.','']
    for path,(title,desc,tactile,draw) in PAGES.items():
        root=ET.fromstring(svg(title,desc,draw()));printed=[n.text or '' for n in root.iter() if n.tag.rsplit('}',1)[-1]=='text']
        out += [f'## {title}','',f'**File:** {path}.svg / {path}.pdf.',f'**Exact linear content:** {desc}','**Verbatim printed text in layout order:** '+' | '.join(printed)+'.',f'**Raised/no-print/no-sound:** {tactile}','']
    return '\n'.join(out)


def starts(recipe: tuple[int,...]) -> tuple[float,...]:
    """Return one or two equally subdivided onsets in each fixed beat."""
    return tuple(start+offset for start,n in zip(BEATS,recipe) for offset in ((0.,) if n==1 else (0.,.3)))


def pcm(recipe: tuple[int,...]) -> bytes:
    """Synthesize identical short quiet original 330Hz sine events."""
    onsets=starts(recipe);data=bytearray()
    for i in range(round(DURATION*RATE)):
        t=i/RATE;value=0.0
        for onset in onsets:
            local=t-onset
            if 0 <= local < .12:
                fade=min(1.0,local/.012,(.12-local)/.012)
                value += .018*fade*math.sin(2*math.pi*330*local)
        data += struct.pack('<h',round(value*32767))
    return bytes(data)


def main() -> None:
    """Regenerate this pack only on explicit --write."""
    parser=argparse.ArgumentParser();parser.add_argument('--write',action='store_true');args=parser.parse_args()
    if not args.write:parser.error('use --write for intentional asset regeneration')
    for path,(title,desc,_tactile,draw) in PAGES.items():
        target=ROOT/f'{path}.svg';target.parent.mkdir(exist_ok=True);target.write_text(svg(title,desc,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,recipe in CUES.items():
        with wave.open(str(ROOT/'audio'/f'{name}.wav'),'wb') as out:
            out.setnchannels(1);out.setsampwidth(2);out.setframerate(RATE);out.writeframes(pcm(recipe))

if __name__=='__main__':main()
