#!/usr/bin/env python3
"""Actual native pass/shot dispatch, centered keepers, point carry guards and ABI."""
from pathlib import Path
import json,os,struct,subprocess
import argparse
ap=argparse.ArgumentParser(description=__doc__);ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path);ap.add_argument('--allow-centered',action='store_true');a=ap.parse_args()
site=Path(__file__).resolve().parents[2];h=site/'tools';o=a.output.resolve();o.mkdir(parents=True,exist_ok=True);cases=[]
for site in ['pass','shot']:
 for team in [0,1]:
  for direction in [-1,1]:
   for sign in [-1,1]:cases.append(dict(site=site,team=team,direction=direction,sign=sign,expected=1))
 for key,value in [('difficulty',0),('difficulty',2),('flags',8),('whistle',1),('offside',128),('goalie_x',0)]:cases.append(dict(site=site,expected=2 if site=='pass' else 4,**{key:value}))
cases += [dict(site=entry,carrier_x=44,receiver_x=-44,expected=1) for entry in ['pass','shot']]
# Actual point-shot deferral only when an advanced friend exists and there
# is no pressure. An immediately free forward pass still wins priority.
for team in [0,1]:
 for direction in [-1,1]:
  for sign in [-1,1]:
   for block in ['marked','blocked']:cases.append(dict(site='shot',team=team,direction=direction,sign=sign,depth=112,expected=0,**{block:True}))
   cases.append(dict(site='shot',team=team,direction=direction,sign=sign,depth=112,expected=1))
negative=[('difficulty',0),('difficulty',2),('flags',8),('flags',32),('unavailable',4),('whistle',1),('offside',128),('view',128),('penalty',4),('special',1),('special',4),('breakaway',2),('wrong_owner',True),('receiver_role',-1),('receiver_boxed',4),('receiver_y',135),('receiver_y',253),('carrier_x',101),('depth',87),('depth',161),('pressure',True)]
for team in [0,1]:
 for direction in [-1,1]:
  for key,value in negative:
   spec=dict(site='shot',team=team,direction=direction,depth=112,marked=True,expected=4);spec[key]=value;cases.append(spec)
# Keep16 centered-keeper fallback regression cases; this policy waits for commitment.
for entry in ['pass','shot']:
 for team in [0,1]:
  for direction in [-1,1]:
   for sign in [-1,1]:cases.append(dict(site=entry,team=team,direction=direction,sign=sign,carrier_x=80,receiver_x=-48,receiver_y=240,goalie_x=0,expected=2 if entry=='pass' else 4))
cases += [dict(site='shot',depth=dep,marked=True,expected=0) for dep in (145,160)]
if a.allow_centered:
 cases += [dict(site=entry,team=t,direction=d,sign=sg,carrier_x=64,depth=220,receiver_x=-48,receiver_y=240,goalie_x=0,expected=1) for entry in ('pass','shot') for t in (0,1) for d in (-1,1) for sg in (-1,1)]
 for c in cases:
  if c.get('carrier_x')==80 and c.get('receiver_x')==-48 and c.get('receiver_y')==240 and c.get('goalie_x')==0:c['expected']=1
reports=[]
for batch in range((len(cases)+11)//12):
 ss=cases[batch*12:batch*12+12];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):
  team=s.get('team',0);direction=s.get('direction',1);sign=s.get('sign',1);own=6*team;opp=6*(1-team);actor=own+2;ab=0xffb04a+128*actor;rb=0xffb04a+128*own;gb=0xffb04a+128*(opp+5);x=s.get('carrier_x',64)*sign;depth=s.get('depth',200);rx=s.get('receiver_x',0)*sign;ry=s.get('receiver_y',220)
  body+=[' lea 0xFFB04A,%a0',' move.w #383,%d0',f'clear{n}: clr.l (%a0)+',f' dbra %d0,clear{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 b,vals in [(ab,[(0,x),(0x14,depth*direction),(0x34,4)]),(rb,[(0,rx),(0x14,ry*direction),(0x34,s.get('receiver_role',5))]),(gb,[(0,s.get('goalie_x',20*sign)),(0x14,s.get('goalie_y',246)*direction),(0x34,s.get('goalie_role',0))])]:
   for off,v in vals:body.append(f' move.w #{v},0x{b+off:X}')
  body.append(f' move.w #{s.get("vx",0)},0x{ab+0x28:X}')
  body.append(f' move.w #{256*sign},0x{gb+0x28:X}')
  body += [f' lea 0x{ab:X},%a3',f' move.b #{(128 if direction==1 else 0)+(64 if team else 0)+s.get("flags",0)},0x62(%a3)',f' move.b #{s.get("unavailable",0)},0x63(%a3)',f' move.b #{s.get("breakaway",0)},0x64(%a3)',f' move.b #{s.get("receiver_boxed",0)},0x{rb+0x63:X}',f' move.b #{s.get("receiver_locked",0)+(128 if direction==1 else 0)+(64 if team else 0)},0x{rb+0x62:X}',f' move.b #{s.get("goalie_boxed",0)},0x{gb+0x63:X}',f' move.w #{s.get("difficulty",1)},0xFFF07A',f' move.w #{opp if s.get("wrong_owner") else actor},0xFFB7AA',f' move.w #{x},0xFFB74A',f' move.w #{depth*direction},0xFFB75E']
  for addr,key in [(0xc2ea,'whistle'),(0xc2ec,'view'),(0xc2ee,'offside'),(0xc2f2,'penalty'),(0xc2fa,'special')]:body.append(f' move.b #{s.get(key,0)},0x{0xff0000+addr:X}')
  if s.get('marked') or s.get('blocked') or s.get('pressure'):
   b=0xffb04a+128*opp;body += [f' move.w #1,0x{b+0x34:X}',f' move.w #{x if s.get("pressure") else rx if s.get("marked") else (rx+x)//2},0x{b:X}',f' move.w #{(depth+24 if s.get("pressure") else ry if s.get("marked") else (ry+depth)//2)*direction},0x{b+0x14:X}']
  for i in range(1,8):body.append(f' move.l #0x{0x11223300+i:08X},%d{i}')
  for i in [0,1,2,4,5,6]:body.append(f' movea.l #0x{0xff1000+256*i:X},%a{i}')
  body+=[' move.l #0x99880003,%d0',' clr.w 0xFF3FE0',f' bsr.w wrapper{n}',f' bra.s donewrapper{n}',f'wrapper{n}: jsr 0x{0xE444 if s["site"]=="pass" else 0xE292:X}',' rts',f'donewrapper{n}: move.w 0xFF3FE0,0x{0xff4800+2*n:X}',f' movem.l %d0-%d7/%a0-%a6,0x{0xff4000+n*64:X}',f' move.l %sp,0x{0xff4000+n*64+60:X}']
 body+=[' move.l #0x43524F53,0xFF3FF0','end: bra.s end'];(p/'driver.s').write_text('\n'.join(body)+'\n');subprocess.run([str(h/'m68kasm.sh'),str(p/'driver.s'),'2D5000',str(p/'code.bin')],check=True,stdout=subprocess.DEVNULL)
 data=bytearray(a.rom.read_bytes());
 for at,hexcode in [(0xe4a0,'33fc0001ffff3fe0584f4e75'),(0xe412,'33fc0004ffff3fe0584f4e75'),(0xe44c,'33fc0002ffff3fe04e75'),(0xe45e,'33fc0003ffff3fe04e75'),(0xe298,'33fc0004ffff3fe04e75'),(0xe2e4,'33fc0004ffff3fe04e75')]:data[at:at+len(bytes.fromhex(hexcode))]=bytes.fromhex(hexcode)
 code=(p/'code.bin').read_bytes();assert len(code)<0x3000 and data[0x2d5000:0x2d5000+len(code)]==b'\xff'*len(code);data[0x2d5000:0x2d5000+len(code)]=code;struct.pack_into('>I',data,4,0x2d5000);(p/'driver.bin').write_bytes(data);(p/'commands.txt').write_text(f'500 dump ff3ff0 4 done\n500 dump ff4000 {64*len(ss)} results\n500 dump ff4800 {2*len(ss)} choices\n501 exit\n')
 env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};env['GPGX_DEV']='513'
 with (p/'log.txt').open('w') as log:subprocess.run(['python3',str(h/'experimental/test_core.py'),str(p/'driver.bin'),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=30)
 rows={r.split()[1]:bytes.fromhex(' '.join(r.split()[2:])) for r in (p/'commands.out').read_text().splitlines()};assert rows['done']==b'CROS'
 for n,s in enumerate(ss):
  regs=struct.unpack_from('>16I',rows['results'],n*64);choice=struct.unpack_from('>H',rows['choices'],n*2)[0];assert choice==s['expected'],(s,choice);assert (regs[0]&65535)==(s.get('team',0)*6 if s['expected']==1 else 3),(s,regs[0]);assert list(regs[1:8])==[0x11223300+i for i in range(1,8)],(s,'D registers');expected=[0xff1000+256*i if i!=3 else 0xffb04a+128*(s.get('team',0)*6+2) for i in range(7)];assert list(regs[8:15])==expected,(s,'A registers',regs[8:15],expected);assert regs[15]==0xfff800,(s,'stack',regs[15]);reports.append(dict(spec=s,result=regs[0]&65535,registers_preserved=True,stack_preserved=True))
(o/'results.json').write_text(json.dumps(reports,indent=2)+'\n');print('PASS',len(reports),'actual native pass/shot hook dispatch, receiver slot, guards and exact stack/ABI cases')
