#!/usr/bin/env python3
"""Native keeper catch handoff ABI, all pad/team ownerships, guard/object/stack checks."""
from pathlib import Path
import argparse,json,subprocess,struct,os,hashlib
ap=argparse.ArgumentParser();ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path);a=ap.parse_args();h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(parents=True,exist_ok=True)
cases=[]
for team in (0,1):
 for count in range(1,5):
  for pad in range(count):cases.append(dict(team=team,count=count,pad=pad,kind='normal',active=True))
 for kind in ('no-free','pass-flag','orphan','cpu','wrong-owner','skater','auto-option','opposing','shootout','penalty-shot','whistle','highlight'):
  cases.append(dict(team=team,count=4,pad=3,kind=kind,active=kind in ('no-free','pass-flag')))
reports=[]
for batch in range((len(cases)+3)//4):
 ss=cases[batch*4:batch*4+4];p=o/f'abi-{batch}';p.mkdir(exist_ok=True);body=[' .text',' .globl _start','_start:',' move.w #0x2700,%sr',' movea.l #0xfff800,%sp'];expected=[]
 for n,s in enumerate(ss):
  k=s['kind'];own=s['team']*6;g=own+5;gb=0xffb04a+128*g;flags=64*s['team']+8;actor=bytearray(128*12);controls=[65535]*4;sides=[0]*4
  for i in range(12):
   b=128*i;struct.pack_into('>H',actor,b,20*(i%6));struct.pack_into('>H',actor,b+0x14,30*(i%6));struct.pack_into('>H',actor,b+0x34,0 if i%6==5 else 4);struct.pack_into('>H',actor,b+0x52,i);actor[b+0x62]=64*(i//6)
  for pad in range(s['count']):
   sides[pad]=s['team']+1;controls[pad]=g if pad==s['pad'] else own+pad
   actor[controls[pad]*128+0x62]|=8
  if k=='cpu':actor[g*128+0x62]&=~8
  if k=='orphan':controls[s['pad']]=own+4;actor[(own+4)*128+0x62]|=8
  if k=='skater':struct.pack_into('>H',actor,g*128+0x34,4)
  if k=='opposing':sides[0]=2-s['team']
  if k=='no-free':
   for i in range(own,own+5):actor[i*128+0x63]|=4
  if k=='pass-flag':actor[g*128+0x63]|=8
  original=bytes(actor)
  body += [' lea 0xffb04a,%a0',' lea fixture'+str(n)+'(%pc),%a1',' move.w #383,%d0',f'copyin{n}: move.l (%a1)+,(%a0)+',f' dbra %d0,copyin{n}']
  for pad in range(4):body += [f' move.w #{controls[pad]},0x{0xffc320+2*pad:x}',f' move.w #{sides[pad]},0x{0xffc328+2*pad:x}']
  body += [f' move.w #{g if k!="wrong-owner" else own},0xffb7aa',f' move.w #{int(k=="auto-option")},0xffd052',f' move.b #{int(k=="shootout")},0xffc2fa',f' move.w #{int(k=="penalty-shot")},0xffd412',f' move.b #{1 if k=="whistle" else 16 if k=="highlight" else 0},0xffc2ea']
  regs=[0x11223300+i for i in range(8)];addresses=[0xff1000+i*256 for i in range(7)];addresses[2]=gb
  for i,v in enumerate(regs):body.append(f' move.l #0x{v:x},%d{i}')
  for i,v in enumerate(addresses):body.append(f' movea.l #0x{v:x},%a{i}')
  at=0xff4000+1728*n;body += [' move.w #0x271f,%sr',' jsr 0x2dbc00',f' movem.l %d0-%d7/%a0-%a6,0x{at:x}',f' move.w %sr,0x{at+60:x}',f' move.l %sp,0x{at+64:x}',' lea 0xffc320,%a0',f' lea 0x{at+68:x},%a1',' moveq #3,%d0',f'copycontrol{n}: move.l (%a0)+,(%a1)+',f' dbra %d0,copycontrol{n}',' lea 0xffb04a,%a0',f' lea 0x{at+96:x},%a1',' move.w #383,%d0',f'copyout{n}: move.l (%a0)+,(%a1)+',f' dbra %d0,copyout{n}']
  if s['active']:
   free=[i for i in range(own,own+5) if not actor[i*128+0x62]&8 and not actor[i*128+0x63]&4];target=min(free,key=lambda i:((20*(i%6)-100)**2+(30*(i%6)-150)**2,-i)) if free else -1
   actor[g*128+0x62]=(actor[g*128+0x62]&~8)|2
   if target>=0:actor[target*128+0x62]|=8
   controls[s['pad']]=target&65535
  expected.append((regs+addresses,bytes(actor),controls+sides))
  s['initial_object_sha256']=hashlib.sha256(original).hexdigest()
  s['_fixture']=original
 body += [' move.l #0x47434149,0xff3ff0','done: bra.s done']
 for n,s in enumerate(ss):body += ['fixture'+str(n)+':']+[' .byte '+','.join(str(v) for v in s['_fixture'][i:i+32]) for i in range(0,1536,32)]
 (p/'driver.s').write_text('\n'.join(body)+'\n');subprocess.run([str(h.parent/'m68kasm.sh'),str(p/'driver.s'),'2D5000',str(p/'code.bin')],check=True,stdout=subprocess.DEVNULL);code=(p/'code.bin').read_bytes();assert len(code)<0x2000
 r=bytearray(a.rom.read_bytes());assert set(r[0x2d5000:0x2d5000+len(code)])=={255};r[0x2d5000:0x2d5000+len(code)]=code;struct.pack_into('>I',r,4,0x2d5000);(p/'driver.bin').write_bytes(r);(p/'commands.txt').write_text(f'100 dump ff3ff0 4 done\n100 dump ff4000 {1728*len(ss)} result\n101 exit\n');env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')}
 with (p/'log.txt').open('w') as log:subprocess.run(['python3',str(h/'test_core.py'),str(p/'driver.bin'),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=30)
 data={line.split()[1]:bytes.fromhex(' '.join(line.split()[2:])) for line in (p/'commands.out').read_text().splitlines()};assert data['done']==b'GCAI'
 for n,(s,(regs,actor,controls)) in enumerate(zip(ss,expected)):
  raw=data['result'][n*1728:(n+1)*1728];assert list(struct.unpack_from('>15I',raw))==regs,('registers',s);assert int.from_bytes(raw[60:62],'big')==0x271f and int.from_bytes(raw[64:68],'big')==0xfff800,('CCR/stack',s);assert list(struct.unpack_from('>8H',raw,68))==controls,('pad/side ownership',s,list(struct.unpack_from('>8H',raw,68)),controls);assert raw[96:1632]==actor,('whole native actor objects',s)
  s.pop('_fixture');reports.append(s)
(o/'results.json').write_text(json.dumps(dict(rom_sha256=hashlib.sha256(a.rom.read_bytes()).hexdigest(),cases=reports,scope='Synthetic native C0F8 handoff ABI: every register/CCR/stack, all12 whole objects and all pad/side ownership words. No forced successful result.'),indent=2)+'\n');print('PASS',len(reports),'native keeper catch handoff/guard/full-object cases')
