#!/usr/bin/env python3
"""Compare native camera-following crit flames with the preceding release.

Uses private live fixture/base.st plus fixture.json from verify_shot_balance.py.
The captured native EDM-CHI Mini fixture is documented in CRITICAL_TRAIL.md.
No ROM/save installation or visible emulator window.
"""
from pathlib import Path
import argparse,json,os,subprocess,statistics
here=Path(__file__).resolve().parent
ap=argparse.ArgumentParser(description=__doc__)
ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path)
ap.add_argument('--fixture',required=True,type=Path)
ap.add_argument('--baseline',type=Path);ap.add_argument('--shots',type=int,default=20)
a=ap.parse_args();out=a.output.resolve();out.mkdir(parents=True,exist_ok=True)
meta=json.loads((a.fixture/'fixture.json').read_text())
slot=next(x['slot'] for x in meta['actors'][:5] if x['role']==2)
reports=[]
for name,rom in ([('baseline',a.baseline)] if a.baseline else [])+[('fixed',a.rom)]:
 p=out/name;p.mkdir(exist_ok=True);(p/'shots.jsonl').unlink(missing_ok=True)
 # VBlank can land inside sprite/history construction. Snapshot at the actual
 # native draw boundary rather than pairing unrelated frontend RAM samples.
 probe = p/'projection-probe.s'
 probe.write_text("""    .text
    .globl _start
_start:
    clr.w 0xFF387C
    movem.l %d0/%a0-%a1,-(%sp)
    lea 0xFFF600,%a0
    lea 0xFF3800,%a1
    moveq #8,%d0
1:  move.l (%a0)+,(%a1)+
    dbra %d0,1b
    move.w 0xFFFFBD18,(%a1)+
    move.w 0xFFFFBD1C,(%a1)+
    move.w 0xFFF630,(%a1)+
    movem.l (%sp)+,%d0/%a0-%a1
    jsr 0x2E4004
    movem.l %d0/%a0-%a1,-(%sp)
    move.l %a6,%d0
    subi.w #0xC018,%d0
    lsr.w #3,%d0
    move.w %d0,0xFF387A
    lea 0xFFFFC018,%a0
    lea 0xFF382A,%a1
    moveq #11,%d0
2:  move.l (%a0)+,(%a1)+
    dbra %d0,2b
    addq.w #1,0xFF387E
    move.w #0x5052,0xFF387C
    movem.l (%sp)+,%d0/%a0-%a1
    rts
""")
 subprocess.run([str(here.parent/'m68kasm.sh'),str(probe),'2EF800',str(p/'projection-probe.bin')],check=True,stdout=subprocess.DEVNULL)
 data=bytearray(rom.read_bytes());code=(p/'projection-probe.bin').read_bytes()
 assert data[0x2ef800:0x2ef800+len(code)]==b'\xff'*len(code)
 data[0x2ef800:0x2ef800+len(code)]=code
 old=bytes.fromhex('4eb9002e4004');at=data.find(old,0,0x2ef800);assert at>=0 and data.find(old,at+1,0x2ef800)<0
 data[at:at+6]=bytes.fromhex('4eb9002ef800');private=p/'observed.bin';private.write_bytes(data)

 (p/'commands.txt').write_text('9999999 exit\n')
 spec=dict(fixture=str(a.fixture.resolve()),slot=slot,count=a.shots,distances=[180],charge_at=100)
 env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')}
 env.update(GPGX_DEV='513',GPGX_HOOK=str(here/'critical_trail_hook.py'),SHOT_CASE=json.dumps(spec))
 with (p/'log.txt').open('w') as log:
  subprocess.run(['python3',str(here/'test_core.py'),str(private.resolve()),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=60)
 ss=json.loads((p/'results.json').read_text())['shots']
 assert len(ss)==a.shots and all(s['crit'] and s['charge']==31 and s['animation']=='0x7fc' for s in ss)
 assert all(s['trail_measurement']['sprites']==6 and s['trail_measurement']['tiles_valid'] for s in ss)
 checks=[v for s in ss for v in s['projection_checks']]
 if name=='fixed':assert checks and all(checks),('trail did not follow its recorded world path',sum(checks),len(checks))
 report=dict(profile=name,shots=len(ss),projection_pass=sum(checks),projection_checks=len(checks),median_span_y=statistics.median(s['trail_measurement']['span_y'] for s in ss),settings=ss[0]['trail_measurement'])
 reports.append(report);print(name,len(ss),'crits; camera projection',sum(checks),'/',len(checks),'median span',report['median_span_y'],flush=True)
(out/'report.json').write_text(json.dumps(reports,indent=2)+'\n')
