"""Constructed MIDI 1.0 channel-state examples, not a DAW playback test.
Run in an empty directory with Python 3. Existing files with these names are replaced.
"""
import struct, json, csv, hashlib
from pathlib import Path

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

def write_midi(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):
    value=0
    while True:
        byte=data[pos];pos+=1;value=(value<<7)|(byte&127)
        if not byte&128:return value,pos

def inspect_midi(filename):
    data=Path(filename).read_bytes()
    assert data[:4]==b'MThd' and data[14:18]==b'MTrk'
    pos=22; tick=0; state=[8192]*16; notes=[]; trace=[]
    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, 'These fixtures deliberately do not use running status'
        a,b=data[pos:pos+2];pos+=2;channel=status&15
        if status&240==224:
            state[channel]=a+(b<<7)
            trace.append({'tick':tick,'channel':channel+1,'rawHex':f'{status:02X} {a:02X} {b:02X}','unsigned':state[channel],'signed':state[channel]-8192})
        elif status&240==144 and b:
            notes.append({'tick':tick,'channel':channel+1,'note':a,'velocity':b,'bendUnsigned':state[channel],'bendSigned':state[channel]-8192})
    return {'notes':notes,'bendEvents':trace,'finalChannel1':state[0],'sha256':hashlib.sha256(data).hexdigest(),'bytes':len(data)}

common=[(0,bytes([224,0,96])),(0,bytes([144,60,80])),(240,bytes([128,60,0]))]
tail=[(480,bytes([144,64,80])),(720,bytes([128,64,0]))]
variants={
 'no-reset':common+tail,
 'reset-before-next-note':common+[(480,bytes([224,0,64]))]+tail,
 'reset-wrong-channel':common+[(480,bytes([225,0,64]))]+tail,
 'reset-after-next-note':common+tail+[(960,bytes([224,0,64]))],
}
for name,events in variants.items():write_midi(name,events)
results={name:inspect_midi(name+'.mid') for name in variants}
assert [results[n]['notes'][1]['bendUnsigned'] for n in variants]==[12288,8192,12288,12288]
assert all([(r['notes'][0]['note'],r['notes'][1]['note'])==(60,64) for r in results.values()])
assert results['reset-after-next-note']['finalChannel1']==8192
Path('results.json').write_text(json.dumps({'type':'constructed event-state model; no DAW/audio playback','ppq':480,'tempoBpm':120,'initialState':8192,'hostAutoReset':False,'rule':'channel state changes only on an explicit pitch-bend message; note-off does not change it','variants':results},indent=2)+'\n',encoding='utf-8')
with Path('note-observations.csv').open('w',newline='',encoding='utf-8') as f:
    writer=csv.writer(f);writer.writerow(['fixture','tick','channel','note','velocity','bend_unsigned','bend_signed'])
    for name,r in results.items():
        for n in r['notes']:writer.writerow([name,n['tick'],n['channel'],n['note'],n['velocity'],n['bendUnsigned'],n['bendSigned']])
print(json.dumps({name:r['notes'][1]['bendUnsigned'] for name,r in results.items()}))
