#!/usr/bin/env python3
"""Native point decision hooks: free forward option and unchanged fallback/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('--forward-limit',type=int,choices=[144,160],default=144);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']]
cases += [dict(site=entry,team=t,direction=d,depth=112,expected=1) for entry in ['pass','shot'] for t in [0,1] for d in [-1,1]]
# Point policy must preserve the native pass branch when no safe forward
# option exists, for each team/end. These cases call the actual E444 hook.
point_negative=[('difficulty',0),('difficulty',2),('flags',8),('flags',32),('unavailable',4),('whistle',1),('view',128),('offside',128),('penalty',4),('special',4),('wrong_owner',True),('receiver_role',-1),('receiver_boxed',4),('marked',True),('blocked',True),('breakaway',2),('depth',87),('depth',a.forward_limit+1),('carrier_x',129),('receiver_y',135)]
for team in [0,1]:
 for direction in [-1,1]:
  for key,value in point_negative:
   spec=dict(site='pass',team=team,direction=direction,depth=112,goalie_y=270,expected=2);spec[key]=value;cases.append(spec)
# Isolate point forwarding from the separate deep cross selector by keeping
# the keeper outside that selector's232..264 eligibility in point boundary cases.
for team in [0,1]:
 for direction in [-1,1]:
  for depth in [a.forward_limit,145]:
   cases.append(dict(site='pass',team=team,direction=direction,depth=depth,goalie_y=270,expected=1 if 88<=depth<=a.forward_limit else 2))
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'):
   b=0xffb04a+128*opp;body += [f' move.w #1,0x{b+0x34:X}',f' move.w #{rx if s.get("marked") else (rx+x)//2},0x{b:X}',f' move.w #{(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'),(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')
