#!/usr/bin/env python3
"""Native accepted-pass tail and actual CPU frame: guards, ownership and expiry."""
from pathlib import Path
import argparse,json,os,struct,subprocess
ap=argparse.ArgumentParser();ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path);ap.add_argument('--mode',choices=['open','cross'],default='open');ap.add_argument('--ttl',type=int,default=64);ap.add_argument('--opening-gap',type=int,choices=[0,16,24],default=0);a=ap.parse_args();h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(parents=True,exist_ok=True)
cases=[dict(team=t,direction=d,sign=x,stage='catch',armed=True,shot=True) for t in (0,1) for d in (-1,1) for x in (-1,1)]
for key,val in [('difficulty',0),('difficulty',2),('flags',8),('flags',32),('unavailable',4),('whistle',1),('highlight',16),('view',128),('offside',128),('penalty',4),('special',1),('special',4),('receiver_role',0),('receiver_role',-1),('receiver_human',8),('receiver_boxed',4),('ry',159),('ry',253),('rx',93),('marked',True),('blocked',True),('enemy_target',True)]:cases.append(dict(stage='catch',armed=False,shot=False,**{key:val}))
for key,val in [('gx',0),('gx',7),('gx',-20),('gy',231),('gy',265),('ry',183),('ry',245),('rx',65),('keeper_role',-1)]:cases.append(dict(stage='catch',armed=a.mode=='open',shot=a.mode=='open',**{key:val}))
for stage in ['inflight','stopped','receding','cleared','enemy_owner','expired','catch_blocked','catch_marked','catch_human','catch_locked','highlight']:
 for team in (0,1):cases.append(dict(team=team,stage=stage,armed=True,shot=False))
# Native catch-time opening intentionally replaces unconditional open-mode release.
# Default pose with keeperX20 gains a far-post opening29 after smart aim.
if a.opening_gap:
 for c in cases:
  if c.get('stage')=='catch' and c.get('shot'):
   if c.get('gx')==0:c['shot']=False
   if a.opening_gap==24 and (c.get('gx')==7 or c.get('gy')==231 or c.get('ry')==245 or c.get('rx')==65):c['shot']=False
rows=[]
for batch in range((len(cases)+7)//8):
 ss=cases[batch*8:batch*8+8];p=o/str(batch);p.mkdir(exist_ok=True);body=[' .text',' .globl _start','_start:',' move.w #0x2700,%sr',' movea.l #0xfff800,%sp']
 for n,s in enumerate(ss):
  t=s.get('team',0);d=s.get('direction',1);sign=s.get('sign',1);own=6*t;opp=6*(1-t);actor=own+2;target=opp if s.get('enemy_target') else own;ab=0xffb04a+128*actor;rb=0xffb04a+128*target;gb=0xffb04a+128*(opp+5);cx=64*sign;cy=s.get('depth',200)*d;rx=s.get('rx',0)*sign;ry=s.get('ry',220)*d
  body+=[' clr.l 0xfff086',' clr.w 0xfff08a',' clr.l 0xfff08c',' clr.w 0xff3fe0',' lea 0xffb04a,%a0',' move.w #383,%d0',f'zero{n}: clr.l (%a0)+',f' dbra %d0,zero{n}']
  for i in range(12):body += [f' move.w #-1,0x{0xffb04a+128*i+0x34:x}',f' move.w #{i},0x{0xffb04a+128*i+0x52:x}']
  for base,vals in [(ab,[(0,cx),(0x14,cy),(0x34,4)]),(rb,[(0,rx),(0x14,ry),(0x34,s.get('receiver_role',5))]),(gb,[(0,s.get('gx',20*sign)),(0x14,s.get('gy',246)*d),(0x34,s.get('keeper_role',0))])]:
   for off,v in vals:body.append(f' move.w #{v},0x{base+off:x}')
  body += [f' lea 0x{ab:x},%a3',f' move.b #{(128 if d==1 else 0)+(64 if t else 0)+s.get("flags",0)},0x62(%a3)',f' move.b #{s.get("unavailable",0)},0x63(%a3)',f' move.b #{(128 if d==1 else 0)+(64 if target>=6 else 0)+s.get("receiver_human",0)},0x{rb+0x62:x}',f' move.b #{s.get("receiver_boxed",0)},0x{rb+0x63:x}',f' move.b #4,0x{rb+0x66:x}',f' move.w #{s.get("difficulty",1)},0xfff07a',f' move.w #{actor},0xffb7aa',f' move.w #{cx},0xffb74a',f' move.w #{cy},0xffb75e',f' move.w #{(rx-cx)*8},0xffb772',f' move.w #{target},0xffbee0',f' move.w #{actor},0xffbeda']
  for addr,key in [(0xc2ea,'whistle'),(0xc2ec,'view'),(0xc2ee,'offside'),(0xc2f2,'penalty'),(0xc2fa,'special')]:body.append(f' move.b #{s.get(key,0)+(s.get("highlight",0) if addr==0xc2ea else 0)},0x{0xff0000+addr:x}')
  if s.get('marked') or s.get('blocked'):
   ob=0xffb04a+128*opp;body += [f' move.w #1,0x{ob+0x34:x}',f' move.w #{rx+10 if s.get("marked") else rx//2},0x{ob:x}',f' move.w #{ry-10*d if s.get("marked") else (s.get("ry",220)+264)//2*d},0x{ob+0x14:x}']
  for i in range(8):body.append(f' move.l #0x{0x11223300+i:08x},%d{i}')
  vel=(ry-cy)*8;d1=0x11220000|(vel&65535);body.append(f' move.l #0x{d1:08x},%d1')
  for i in [0,1,2,4,5,6]:body.append(f' movea.l #0x{rb if i==0 else 0xff1000+256*i:x},%a{i}')
  body += [' move.w #0x2715,%sr',' jsr 0x2df800',f' move.w %sr,0x{0xff4c00+2*n:x}',f' movem.l %d0-%d7/%a0-%a6,0x{0xff4000+64*n:x}',f' move.l %sp,0x{0xff4000+64*n+60:x}',f' move.w 0xfff08c,0x{0xff4800+2*n:x}',f' move.w 0xfff08e,0x{0xff4900+2*n:x}']
  stage=s['stage'];owner=target if stage.startswith('catch') else opp if stage=='enemy_owner' else -1
  body += [f' move.w #{owner},0xffb7aa']
  if owner==target:body += [f' move.w #{rx},0xffb74a',f' move.w #{ry},0xffb75e']
  if stage in ['stopped','receding']:
   body += [f' move.w #{0 if stage=="stopped" else -(rx-cx)*8},0xffb772',f' move.w #{0 if stage=="stopped" else -vel},0xffb774']
  if stage=='cleared':body.append(f' move.w #{0xff00|target},0xffbee0')
  if stage=='expired':body.append(' clr.w 0xfff08e')
  if stage=='highlight':body.append(' move.b #16,0xffc2ea')
  if stage=='catch_human':body.append(f' bset #3,0x{rb+0x62:x}')
  if stage=='catch_locked':body.append(f' bset #5,0x{rb+0x62:x}')
  if stage in ['catch_blocked','catch_marked']:
   ob=0xffb04a+128*opp;body += [f' move.w #1,0x{ob+0x34:x}',f' move.w #{rx if stage=="catch_blocked" else rx+10},0x{ob:x}',f' move.w #{(s.get("ry",220)+264)//2*d if stage=="catch_blocked" else ry-10*d},0x{ob+0x14:x}']
  body += [' jsr 0x2dd010',f' move.w 0xff3fe0,0x{0xff4a00+2*n:x}',f' move.l 0xfff086,0x{0xff4b00+4*n:x}']
 body += [' move.l #0x504c414e,0xff3ff0','end: bra.s end'];(p/'driver.s').write_text('\n'.join(body)+'\n');subprocess.run([str(h.parent/'m68kasm.sh'),str(p/'driver.s'),'2d9000',str(p/'code.bin')],check=True,stdout=subprocess.DEVNULL)
 data=bytearray(a.rom.read_bytes());code=(p/'code.bin').read_bytes();assert len(code)<0x3000 and data[0x2d9000:0x2d9000+len(code)]==b'\xff'*len(code);data[0x2d9000:0x2d9000+len(code)]=code;struct.pack_into('>I',data,4,0x2d9000);data[0xc2f2:0xc2fc]=bytes.fromhex('33fc0001ffff3fe04e75');data[0x1073a:0x1073c]=bytes.fromhex('4e75');(p/'driver.bin').write_bytes(data)
 (p/'commands.txt').write_text('500 dump ff3ff0 4 done\n'+''.join(f'500 dump {addr:x} {length} {key}\n' for addr,length,key in [(0xff4000,64*len(ss),'regs'),(0xff4800,2*len(ss),'pending'),(0xff4900,2*len(ss),'ttl'),(0xff4a00,2*len(ss),'shots'),(0xff4b00,4*len(ss),'buffers'),(0xff4c00,2*len(ss),'sr')])+'501 exit\n');env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};env['GPGX_AUTO_CREDITS']='0'
 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)
 out={x.split()[1]:bytes.fromhex(' '.join(x.split()[2:])) for x in (p/'commands.out').read_text().splitlines()};assert out['done']==b'PLAN'
 for n,s in enumerate(ss):
  t=s.get('team',0);target=6*(1-t) if s.get('enemy_target') else 6*t;rb=0xffb04a+128*target;ry=s.get('ry',220)*s.get('direction',1);cy=s.get('depth',200)*s.get('direction',1);d1=0x11220000|(((ry-cy)*8)&65535);regs=list(struct.unpack_from('>16I',out['regs'],64*n));expected=[0x11223300+i for i in range(8)];expected[1]=d1;expected += [rb if i==0 else 0xffb04a+128*(6*t+2) if i==3 else 0xff1000+256*i for i in range(7)]+[0xfff800];assert regs==expected,(s,regs,expected)
  pending=struct.unpack_from('>H',out['pending'],2*n)[0];ttl=struct.unpack_from('>H',out['ttl'],2*n)[0];shots=struct.unpack_from('>H',out['shots'],2*n)[0];assert pending==(target+1 if s['armed'] else 0),(s,pending);assert ttl==(a.ttl if s['armed'] else 0),(s,ttl);assert shots==int(s['shot']),(s,shots)
  sr=struct.unpack_from('>H',out['sr'],2*n)[0];assert sr&31==(0x10|(8 if ry-cy<0 else 4 if ry==cy else 0)),(s,'displaced MOVE.W CCR',hex(sr))
  rows.append(dict(spec=s,pending=pending,ttl=ttl,actual_native_frame_release=shots,all_registers_stack_CCR_preserved=True))
(o/'results.json').write_text(json.dumps(rows,indent=2)+'\n');print('PASS',len(rows),'accepted-pass planning / native actual-catch release, guard, stale/blocked/human/expiry and full ABI cases')
