#!/usr/bin/env python3
"""Native Crit value rendering, cold On/Off, redraws and exact replay-column alignment."""
from pathlib import Path
import argparse,json,os,struct,subprocess

ap=argparse.ArgumentParser(description=__doc__)
ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path)
ap.add_argument('--seed',type=Path,required=True);ap.add_argument('--baseline',type=Path)
a=ap.parse_args();here=Path(__file__).resolve().parent;out=a.output.resolve();out.mkdir(exist_ok=True,parents=True)
source=''' .text
 .globl _start
_start:
 move.w %sr,-(%sp)
 movem.l %d0/%a0,-(%sp)
 cmpi.w #22,%d7
 bne.s replay
 addq.w #1,0xFFF080
 move.l %a1,0xFFF084
 move.w 0xFFB028,0xFFF08C
 bra.s done
replay:
 cmpi.w #20,%d7
 bne.s done
 addq.w #1,0xFFF082
 move.l %a1,0xFFF088
 move.w 0xFFB028,0xFFF08E
done:
 movem.l (%sp)+,%d0/%a0
 move.w (%sp)+,%sr
 jmp 0x11BA4
'''
(out/'probe.s').write_text(source)
subprocess.run([str(here.parent/'m68kasm.sh'),str(out/'probe.s'),'2FEA00',str(out/'probe.bin')],check=True,stdout=subprocess.DEVNULL)
code=(out/'probe.bin').read_bytes()
hook='''from pathlib import Path
import json,struct
p=Path(outdir);spec=json.loads((p/'spec.json').read_text());rows=[]
def rw(a):return (wram[ram(a)]<<8)|wram[ram(a+1)]
def pointer(a):return (rw(a)<<16)|rw(a+2)
def text(a):
 ptr=pointer(a)
 assert 0x2DE900<=ptr<0x2DEF00,('wrong value string pointer',hex(ptr))
 assert romdata[ptr:ptr+2]==b'\\x00\\x16',('wrong native string width',hex(ptr))
 return romdata[ptr+2:ptr+22].decode('ascii').rstrip()
def snapshot(name):
 raw,width,height,pitch=frame[0]
 if pixfmt[0]==1:im=Image.frombuffer('RGBA',(width,height),raw,'raw','BGRA',pitch,1).convert('RGB')
 else:
  assert pixfmt[0]==2
  pixels=bytearray()
  for y in range(height):
   for (v,) in struct.iter_unpack('<H',raw[y*pitch:y*pitch+width*2]):pixels+=bytes(((v>>11)<<3,((v>>5)&63)<<2,(v&31)<<3))
  im=Image.frombytes('RGB',(width,height),bytes(pixels))
 im.save(p/(name+'.png'))
def test_tick(f):
 if f not in (1850,2100,2300,2600):return
 wanted=(1-spec['initial']) if f==2100 and not spec.get('locked') else spec['initial']
 assert rw(0xf0ec)==wanted,('Crit preference incorrect',f,rw(0xf0ec),wanted)
 assert rw(0xf0de)==spec['replay'],('Replay preference changed',f)
 assert rw(0xf080)>0,('Crit value was never passed to native renderer',f)
 assert rw(0xf082)>0,('Replay value was never passed to native renderer',f)
 crit=text(0xf084);replay=text(0xf088)
 assert crit==('Off' if wanted else 'On'),('missing/wrong rendered Crit text',f,crit,wanted)
 assert replay==('Off' if spec['replay'] else 'On'),('wrong rendered Replay text',f,replay)
 assert rw(0xf08c)==rw(0xf08e)==17,('value columns differ',f,rw(0xf08c),rw(0xf08e))
 assert rw(0xd048)==0 and rw(0xf06c)==1
 rows.append(dict(frame=f,crit=crit,replay=replay,crit_calls=rw(0xf080),crit_column=rw(0xf08c),replay_column=rw(0xf08e)))
 snapshot('frame-'+str(f))
 if f==2600:(p/'results.json').write_text(json.dumps(rows,indent=2)+'\\n')
'''
(out/'hook.py').write_text(hook)

def battery(record,replay,locked=False):
    base=bytearray(a.seed.read_bytes()[1:16384:2]);base[0xe20:0x1ffe]=bytes(0x11de);banks=[]
    for slot,length in [(1,84),(2,31),(3,7)]:
        b=bytearray(base);struct.pack_into('>4s6H',b,0x1000,b'SESN',int(locked),11,int(locked),length,0,3);struct.pack_into('>4s4H',b,0x1ea0,b'S3SV',1,slot,7,1)
        b[0x1ef0:0x1ef6]=b'PC\x01\xfe\x00\xff';b[0x1ff6:0x1ffd]=bytes([0x54,0,0,2,2,0,0x5a]);banks.append(b)
    logical=bytearray(banks[0])+b''.join(banks)
    logical[0x2e18:0x2e1c]=b'RP'+bytes((replay,replay^255));logical[0x2e1c:0x2e20]=record
    for offset in range(0,len(logical),8192):
        logical[offset+0x1fff]=sum(logical[offset:offset+0x1ffe])%256;logical[offset+0x1ffe]=logical[offset+0x1fff]^255
    raw=bytearray(len(logical)*2);raw[1::2]=logical;return raw

def run(profile,rom,dev,name,record,initial,replay,locked=False):
    p=out/(profile+'-'+dev+'-'+name);p.mkdir(exist_ok=True)
    b=bytearray(rom.read_bytes());assert b[0xF7BF4:0xF7BFA]==bytes.fromhex('4ef900011ba4')
    assert b[0x2FEA00:0x2FEA00+len(code)]==b'\xff'*len(code)
    b[0x2FEA00:0x2FEA00+len(code)]=code;b[0xF7BF4:0xF7BFA]=bytes.fromhex('4ef9002fea00');(p/'probe-rom.bin').write_bytes(b)
    (p/'battery.srm').write_bytes(battery(record,replay,locked));(p/'spec.json').write_text(json.dumps(dict(initial=initial,replay=replay,locked=locked)))
    commands=[f'{900+80*i} press 1 Down 5' for i in range(11)]+['2000 press 1 Right 5','2200 press 1 Left 5','2400 press 1 Up 5','2500 press 1 Down 5','2701 exit']
    (p/'commands.txt').write_text('\n'.join(commands)+'\n')
    env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};env.update(GPGX_DEV=dev,GPGX_SRM=str(p/'battery.srm'),GPGX_HOOK=str(out/'hook.py'))
    with (p/'log.txt').open('w') as log:
        result=subprocess.run(['python3',str(here/'test_core.py'),str(p/'probe-rom.bin'),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,timeout=40)
    if profile=='baseline':
        assert result.returncode!=0 and 'Crit value was never passed to native renderer' in (p/'log.txt').read_text(),'Baseline did not reproduce the reported missing initial value'
        return dict(profile=profile,expected_failure=True,reason='Initial Crit value never rendered')
    assert result.returncode==0,(p/'log.txt').read_text()
    rows=json.loads((p/'results.json').read_text());assert len(rows)==4
    return dict(profile=profile,device=dev,case=name,observations=rows)

reports=[]
if a.baseline:reports.append(run('baseline',a.baseline,'513','absent-on',b'\0'*4,0,0))
for dev in ('513','1281'):
    for name,record,initial,replay in [('absent-off',b'\0'*4,1,0),('saved-on',b'CT\0\xff',0,1),('saved-off',b'CT\x01\xfe',1,0),('corrupt-default-off',b'CT\x09\xf6',1,1)]:
        reports.append(run('current',a.rom,dev,name,record,initial,replay))
for dev in ('513','1281'):
    for initial in (0,1):
        reports.append(run('current',a.rom,dev,'locked-'+str(initial),b'CT'+bytes([initial,initial^255]),initial,0,locked=True))
(out/'results.json').write_text(json.dumps(reports,indent=2)+'\n')
print('PASS native Crit On/Off strings, cold/default/corrupt/saved values, both adapter tables, Left/Right and full redraws, exact Replay column; cases',len(reports))
