#!/usr/bin/env python3
"""Native Hard decision contract, actual hooks and preserved registers/stack.
Uses an isolated reset driver; never reads or writes installed game saves.
"""
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 team in [0,1]:
 for direction in [-1,1]:
  for sign in [-1,1]:cases.append(dict(team=team,direction=direction,sign=sign,expected=team*6))
neg=[('difficulty',0),('difficulty',2),('flags',8),('flags',32),('unavailable',4),('whistle',1),('view',128),('offside',128),('penalty',4),('special',1),('special',4),('breakaway',2),('carrier_x',31),('carrier_x',113),('depth',159),('depth',281),('receiver_x',65),('receiver_y',183),('receiver_y',245),('goalie_x',41),('goalie_x',-41),('goalie_y',231),('goalie_y',265),('goalie_role',-1),('goalie_boxed',4),('receiver_role',-1),('receiver_boxed',4),('receiver_locked',32),('marked',True),('blocked',True),('wrong_owner',True),('goalie_x',0),('goalie_x',-20),('receiver_x',24)]
cases+=[dict(expected=-1,**{key:value}) for key,value in neg]
cases += [dict(expected=0,**kw) for kw in [dict(keeper_vx=0),dict(keeper_vx=-256),dict(goalie_x=29),dict(receiver_x=-49),dict(carrier_x=44,receiver_x=-44,equal_quality=True),dict(depth=264,carrier_x=112),dict(receiver_locked=8)]]
cases += [dict(team=t,direction=d,receiver_local=5,expected=t*6+5) for t in [0,1] for d in [-1,1]]
cases += [dict(expected=-1,depth=264)]  # actual keeper blocks the return pass from x64
# Wait for a keeper to commit to the carrier side; centered keepers retain native decisions.
for team in [0,1]:
 for direction in [-1,1]:
  for sign in [-1,1]:
   for gx in [0,7,-7]:cases.append(dict(team=team,direction=direction,sign=sign,carrier_x=80,receiver_x=-48,receiver_y=240,goalie_x=gx*sign,expected=-1))
   cases.append(dict(team=team,direction=direction,sign=sign,carrier_x=80,receiver_x=-48,receiver_y=240,goalie_x=-8*sign,expected=-1))
if a.allow_centered:
 cases += [dict(team=t,direction=d,sign=sg,carrier_x=64,depth=220,receiver_x=-48,receiver_y=240,goalie_x=0,equal_quality=True,expected=6*t) 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 abs(c.get('goalie_x',99))<8:c['expected']=6*c.get('team',0)
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+s.get('receiver_local',0));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("keeper_vx",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+=[' jsr 0x2D89B0',f' move.w %d0,0x{0xff4900+2*n:X}',' jsr 0x2DD808',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());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 ff4900 {2*len(ss)} old_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);assert (regs[0]&65535)==(s['expected']&65535),(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]);old_choice=struct.unpack_from('>h',rows['old_choices'],2*n)[0];assert not s.get('equal_quality') or old_choice==-1,(s,old_choice);reports.append(dict(spec=s,old_native_choice=old_choice,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),'native free cross-ice choice, equal-quality/deep/slot5/human opportunities, lane/keeper/register/stack cases')
