"""Constructed binary CC64/key-state model, not audio or DAW playback.
Python3, standard library only. Run in an empty directory; named outputs replaced.
"""
import struct,json,csv,hashlib
from pathlib import Path

def vlq(value):
    out=[value&127]
    while value>>7:
        value>>=7;out.insert(0,(value&127)|128)
    return bytes(out)

def save(name,events):
    track=bytearray(b'\x00\xff\x51\x03\x07\xa1\x20');previous=0
    for tick,message in events:
        track.extend(vlq(tick-previous));track.extend(message);previous=tick
    track.extend(b'\x00\xff\x2f\x00')
    Path(name+'.mid').write_bytes(b'MThd'+struct.pack('>IHHH',6,0,1,480)+b'MTrk'+struct.pack('>I',len(track))+track)

def read_vlq(data,pos):
    result=0
    while True:
        byte=data[pos];pos+=1;result=(result<<7)|(byte&127)
        if not byte&128:return result,pos

def inspect(name):
    data=Path(name+'.mid').read_bytes();assert data[:4]==b'MThd'and data[14:18]==b'MTrk'
    pos=22;tick=0;pedal=[False]*16;pressed=set();deferred=set();observations=[];release=None;keyoff=None
    while pos<len(data):
        delta,pos=read_vlq(data,pos);tick+=delta;status=data[pos];pos+=1
        if status==255:
            kind=data[pos];pos+=1;size,pos=read_vlq(data,pos);pos+=size
            if kind==47:break
            continue
        assert status&128,'Fixtures use explicit status for every event'
        a,b=data[pos:pos+2];pos+=2;ch=status&15;kind=status&240;key=(ch,a)
        if kind==144 and b:
            pressed.add(key)
        elif kind==128 or(kind==144 and b==0):
            pressed.discard(key)
            if key==(0,60):keyoff=tick
            if pedal[ch]:deferred.add(key)
            elif key==(0,60):release=tick
        elif kind==176 and a==64:
            pedal[ch]=b>=64
            if not pedal[ch]:
                for k in list(deferred):
                    if k[0]==ch and k not in pressed:
                        deferred.remove(k)
                        if k==(0,60):release=tick
        observations.append({'tick':tick,'messageHex':f'{status:02X} {a:02X} {b:02X}','channel':ch+1,'key60Down':(0,60)in pressed,'channel1PedalOn':pedal[0],'key60Deferred':(0,60)in deferred,'releaseEligibleAt':release})
    assert not pressed and not deferred,'Every fixture explicitly releases its only note'
    return {'keyOffTick':keyoff,'releaseEligibleTick':release,'keyOffSeconds':keyoff/960,'releaseEligibleSeconds':release/960,'trace':observations,'bytes':len(data),'sha256':hashlib.sha256(data).hexdigest()}

def cc(channel,value):return bytes([176+channel,64,value])
on=bytes([144,60,80]);off=bytes([128,60,0])
cases={
 'pedal-63':[(0,cc(0,63)),(0,on),(240,off),(480,cc(0,0))],
 'pedal-64':[(0,cc(0,64)),(0,on),(240,off),(480,cc(0,0))],
 'pedal-127':[(0,cc(0,127)),(0,on),(240,off),(480,cc(0,0))],
 'wrong-channel-up':[(0,cc(0,127)),(0,on),(240,off),(480,cc(1,0)),(720,cc(0,0))],
 'key-still-held':[(0,cc(0,127)),(0,on),(480,cc(0,0)),(720,off)],
}
for name,events in cases.items():save(name,events)
results={name:inspect(name)for name in cases}
assert [v['releaseEligibleTick']for v in results.values()]==[240,480,480,720,720]
assert [v['keyOffTick']for v in results.values()]==[240,240,240,240,720]
Path('results.json').write_text(json.dumps({'type':'constructed binary MIDI1 event-state inspection; no audio rendered','ppq':480,'tempoBpm':120,'ticksPerSecond':960,'scope':'single non-repeated note per file, independent channel CC64 states, values0..63 off and64..127 on; no automatic host resets, half-pedaling, envelopes or voice stealing','cases':results},indent=2)+'\n',encoding='utf-8')
with Path('observations.csv').open('w',newline='',encoding='utf-8')as f:
    w=csv.writer(f);w.writerow(['fixture','key_off_tick','release_eligible_tick','key_off_seconds','release_eligible_seconds','sha256'])
    for n,r in results.items():w.writerow([n,r['keyOffTick'],r['releaseEligibleTick'],r['keyOffSeconds'],r['releaseEligibleSeconds'],r['sha256']])
print(json.dumps({n:r['releaseEligibleTick']for n,r in results.items()}))
