#!/usr/bin/env python3
"""Paired native weak-side target and exclusion checks, no user saves/windows.
The reset driver calls native D00C and stubs only skateto to observe its inputs.
"""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import argparse,copy,json,math,os,struct,subprocess
ap=argparse.ArgumentParser(description=__doc__);ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path);ap.add_argument('--baseline',type=Path,required=True);a=ap.parse_args();here=Path(__file__).resolve().parent;out=a.output.resolve();out.mkdir(exist_ok=True,parents=True)

def run(profile,rom,name,spec):
 p=out/(profile+'-'+name);p.mkdir(exist_ok=True);team=spec.get('team',0);orient=spec.get('orient',1);px=spec.get('px',110);py=spec.get('depth',-210)*orient;side=-1 if px>0 else 1;role=spec.get('role',5 if side==orient else 3);actor=team*6+(0 if role==5 else 1);opp=(1-team)*6;carrier=spec.get('owner',opp+2);flags=(128 if orient==1 else 0)+(64 if team else 0)+spec.get('flags',0)
 body=['    .text','    .globl _start','_start:','    move.w #0x2700,%sr','    movea.l #0xFFF800,%sp','    lea 0xFFB04A,%a0','    move.w #383,%d0','clear: clr.l (%a0)+','    dbra %d0,clear']
 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}']
 body += [f'    lea 0x{0xffb04a+128*actor:X},%a3',f'    move.w #{role},0x34(%a3)',f'    move.b #{flags},0x62(%a3)',f'    move.b #{spec.get("unavailable",0)},0x63(%a3)',f'    move.w #{spec.get("difficulty",1)},0xFFF07A',f'    move.w #{px},0xFFB74A',f'    move.w #{py},0xFFB75E',f'    move.w #{carrier},0xFFB7AA',f'    move.b #{spec.get("whistle",0)},0xFFC2EA',f'    move.b #{spec.get("view",0)},0xFFC2EC',f'    move.b #{spec.get("special",0)},0xFFC2FA']
 if 0<=carrier<12:
  o=0xffb04a+128*carrier
  body += [f'    move.w #4,0x{o+0x34:X}',f'    move.w #{px},0x{o:X}',f'    move.w #{py},0x{o+0x14:X}']
 for n,t in enumerate(spec.get('threats',[dict(x=side*48,y=-210)])):
  o=0xffb04a+128*(opp+n)
  for off,v in [(0,t.get('x',side*48)),(0x14,t.get('y',-210)*orient),(0x28,t.get('vx',0)),(0x2a,t.get('vy',0)*orient),(0x34,t.get('role',5)),(0x52,t.get('index',opp+n))]:body.append(f'    move.w #{v},0x{o+off:X}')
  body.append(f'    move.b #{t.get("unavailable",0)},0x{o+0x63:X}')
 body += [f'    lea 0x{0xffc6ce+team*0x364:X},%a2','    movea.l #0xFF1234,%a1','    move.l #0x12345678,0xFFD066','    move.l #0x11223344,%d3','    move.l #0x22334455,%d4','    move.l #0x33445566,%d5','    move.l #0x44556677,%d6','    move.l #0x1234,%d7','    jsr 0xD00C','    lea 0xFF4000,%a4','    move.w %d0,(%a4)+','    move.w %d1,(%a4)+','    move.w %d2,(%a4)+','    movem.l %d3-%d7/%a0-%a3,(%a4)','    move.l #0x41495447,0xFF3FF0','end: bra.s end']
 (p/'driver.s').write_text('\n'.join(body)+'\n');code=p/'code.bin';subprocess.run([str(here.parent/'m68kasm.sh'),str(p/'driver.s'),'2DA000',str(code)],check=True,stdout=subprocess.DEVNULL)
 data=bytearray(rom.read_bytes());c=code.read_bytes();assert len(c)<0x4000 and data[0x2da000:0x2da000+len(c)]==b'\xff'*len(c);data[0x2da000:0x2da000+len(c)]=c;struct.pack_into('>I',data,4,0x2da000);data[0x103c8:0x103ca]=bytes.fromhex('4e75');(p/'driver.bin').write_bytes(data)
 cmd=p/'commands.txt';cmd.write_text('1000 dump ff3ff0 4 done\n1000 dump ff4000 42 result\n1000 dump ffd066 4 random\n1001 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(['nice','-n','19','python3',str(here/'test_core.py'),str(p/'driver.bin'),str(cmd),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=30)
 rows={l.split()[1]:bytes.fromhex(' '.join(l.split()[2:])) for l in cmd.with_suffix('.out').read_text().splitlines()};assert rows['done']==b'AITG';assert rows['random']==bytes.fromhex('12345678')
 x,y,_=struct.unpack_from('>3h',rows['result']);regs=list(struct.unpack_from('>9I',rows['result'],6));assert regs==[0x11223344,0x22334455,0x33445566,0x44556677,0x1234,0xcf98,0xff1234,0xffc6ce+team*0x364,0xffb04a+128*actor],(profile,name,regs)
 return dict(x=x,y=y,raw=rows['result'].hex(),receiver_distance=round(math.hypot(x-side*48,y-(-210*orient)),3))

cases=[]
for team in [0,1]:
 for orient in [-1,1]:
  for px in [-110,110]:
   side=-1 if px>0 else 1;base=dict(team=team,orient=orient,px=px)
   cases.append((f'coverage-t{team}-o{orient}-x{px}',base,(side*36,-222*orient),'cover'))
   moving=dict(base,threats=[dict(x=side*48,y=-210,vx=-side*4096,vy=-2048)])
   cases.append((f'lead-t{team}-o{orient}-x{px}',moving,(side*30,-226*orient),'lead'))
for name,change in [('normal',dict(difficulty=0)),('easy',dict(difficulty=2)),('strong-side',dict(role=5)),('human',dict(flags=8)),('animation',dict(flags=32)),('unavailable',dict(unavailable=4)),('whistle',dict(whistle=1)),('menu-replay',dict(view=128)),('penalty-shootout',dict(special=4)),('defender',dict(role=1)),('center',dict(role=4)),('loose',dict(owner=65535)),('invalid-owner',dict(owner=12)),('teammate',dict(owner=0)),('puck-middle',dict(px=55)),('puck-upice',dict(depth=-159)),('puck-outside-x',dict(px=145)),('puck-outside-y',dict(depth=-297))]:cases.append((name,change,None,'unchanged'))
for name,threats in [('no-receiver',[]),('boxed',[dict(x=-48,y=-210,role=-1)]),('unavailable-receiver',[dict(x=-48,y=-210,unavailable=4)]),('outside-slot',[dict(x=-110,y=-210)]),('behind-goal',[dict(x=-48,y=-280)]),('carrier-not-receiver',[dict(x=-48,y=-210,index=8)])]:cases.append((name,dict(threats=threats),(-32,-216),'support'))
cases += [('dangerous-receiver',dict(threats=[dict(x=-80,y=-155),dict(x=-32,y=-224)]),(-24,-236),'cover'),('behind-own-goal',dict(depth=-275),(-36,-222),'cover'),('boundary-depth',dict(depth=-160),(-36,-222),'cover'),('boundary-width',dict(px=56),(-36,-222),'cover')]

def pair(item):
 name,spec,expected,kind=item;before=run('baseline',a.baseline,name,spec);after=run('fixed',a.rom,name,spec)
 if expected is None:assert after==before,(name,before,after)
 else:
  assert (after['x'],after['y'])==expected,(name,before,after,expected)
  if kind=='cover':assert after['receiver_distance']<before['receiver_distance'],(name,before,after)
 return dict(case=name,kind=kind,spec=spec,baseline=before,fixed=after)
with ThreadPoolExecutor(max_workers=3) as pool:results=list(pool.map(pair,cases))
(out/'results.json').write_text(json.dumps(results,indent=2)+'\n');print('PASS',len(results),'paired native AI target/guard/register/RNG cases')
