#!/usr/bin/env python3
"""Native Hard attack target/receiver/shot guards. Private reset drivers only.
Both profiles call actual installed native hook sites. Physics tested separately.
"""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import argparse,json,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(parents=True,exist_ok=True)
def run(rom,name,s,kind):
 p=out/name;p.mkdir(exist_ok=True);team=s.get('team',0);direction=s.get('direction',1);role=s.get('role',3);actor=team*6+({5:0,3:1,4:2}.get(role,1));own=team*6;opp=(1-team)*6;carrier=own+2 if kind=='support' and actor!=own+2 else own;carrier=s.get('owner',carrier if kind=='support' else actor);flags=(64 if team else 0)+(128 if direction==1 else 0)+s.get('flags',0);px=s.get('px',110);depth=s.get('depth',220)
 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']
 def put(i,off,v):body.append(f' move.w #{v},0x{0xffb04a+128*i+off:X}')
 for i in range(12):put(i,0x34,-1);put(i,0x52,i)
 put(actor,0x34,role);put(actor,0x44,60 if kind=='support' else 0);put(actor,0x46,230);put(actor,0,px if kind!='support' else -90);put(actor,0x14,depth*direction if kind!='support' else 150*direction)
 body += [f' lea 0x{0xffb04a+128*actor:X},%a3',f' move.b #{flags},0x62(%a3)',f' move.b #{s.get("unavailable",0)},0x63(%a3)',f' move.b #{s.get("breakaway",0)},0x64(%a3)',f' move.w #{s.get("difficulty",1)},0xFFF07A',f' move.w #{carrier},0xFFB7AA',f' move.w #{px},0xFFB74A',f' move.w #{depth*direction},0xFFB75E']
 for addr,v in [(0xc2ea,s.get('whistle',0)),(0xc2ec,s.get('view',0)),(0xc2ee,s.get('offside',0)),(0xc2f2,s.get('penalty',0)),(0xc2fa,s.get('shootout',0))]:body.append(f' move.b #{v},0x{0xff0000+addr:X}')
 for n,t in enumerate(s.get('receivers',[])):
  i=own+t.get('local',n)
  for off,v in [(0,t['x']),(0x14,t['y']*direction),(0x34,t.get('role',3)),(0x52,i)]:put(i,off,v)
  body += [f' move.b #{t.get("unavailable",0)},0x{0xffb04a+128*i+0x63:X}',f' move.b #{t.get("flags",0)+(64 if team else 0)+(128 if direction==1 else 0)},0x{0xffb04a+128*i+0x62:X}']
 for n,t in enumerate(s.get('opponents',[])):
  i=opp+n
  for off,v in [(0,t['x']),(0x14,t['y']*direction),(0x34,t.get('role',1))]:put(i,off,v)
  body += [f' move.b #{t.get("unavailable",0)},0x{0xffb04a+128*i+0x63:X}']
 call={'support':0xd3aa if role==4 else 0xd1bc,'pass':0xe45e,'shot':0xe292}[kind]
 body+=[' move.l #0x11223344,%d2',' move.l #0x22334455,%d3',' move.l #0x33445566,%d4',' move.l #0x44556677,%d5',' move.l #0x55667788,%d6',' move.l #0x66778899,%d7',' movea.l #0xFF1234,%a1',f' lea 0x{0xffc6ce+team*0x364:X},%a2',' move.l #0x12345678,0xFFD066',f' jsr 0x{call:X}',' lea 0xFF4000,%a4',' move.w %d0,(%a4)+',' move.w %d1,(%a4)+',' movem.l %d2-%d7/%a0-%a3,(%a4)',' move.l #0x41494154,0xFF3FF0','end: bra.s end']
 (p/'driver.s').write_text('\n'.join(body)+'\n');subprocess.run([str(here.parent/'m68kasm.sh'),str(p/'driver.s'),'2DA000',str(p/'code.bin')],check=True,stdout=subprocess.DEVNULL)
 b=bytearray(rom.read_bytes());c=(p/'code.bin').read_bytes();assert b[0x2da000:0x2da000+len(c)]==b'\xff'*len(c);b[0x2da000:0x2da000+len(c)]=c;struct.pack_into('>I',b,4,0x2da000)
 b[0x103c8:0x103ca]=bytes.fromhex('4e75');b[0xe464:0xe466]=bytes.fromhex('4e75');b[0x11086:0x1108a]=bytes.fromhex('70034e75')
 for address in [0xe298,0xe2e4]:b[address:address+10]=bytes.fromhex('33fc0001ffff3fe04e75')
 (p/'driver.bin').write_bytes(b);(p/'commands.txt').write_text('500 dump ff3ff0 4 done\n500 dump ff4000 44 result\n500 dump ff3fe0 2 native\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(['nice','-n','19','python3',str(here/'test_core.py'),str(p/'driver.bin'),str(p/'commands.txt'),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 (p/'commands.out').read_text().splitlines()};assert rows['done']==b'AIAT';x,y=struct.unpack_from('>2h',rows['result']);regs=list(struct.unpack_from('>10I',rows['result'],4));assert regs[:6]==[0x11223344,0x22334455,0x33445566,0x44556677,0x55667788,0x66778899],(name,regs)
 return dict(x=x,y=y,native=int.from_bytes(rows['native'],'big'),registers=regs)

cases=[]
for team in [0,1]:
 for direction in [-1,1]:
  for role in [3,4,5]:
   for px in [-110,110]:
    side=(1 if role==5 else -1)*direction
    if role==4:expected=(-16 if px>0 else 16,208*direction)
    elif side*px>32:expected=(90*side,180*direction)
    else:expected=(44*side,236*direction)
    cases.append((f'support-{team}-{direction}-{role}-{px}','support',dict(team=team,direction=direction,role=role,px=px),expected))
  good=[dict(local=0,x=0,y=220)]
  for kind in ['pass','shot']:
   cases.append((f'{kind}-{team}-{direction}',''+kind,dict(team=team,direction=direction,role=4,px=110,depth=180,receivers=good),0))
for name,change in [('normal',dict(difficulty=0)),('easy',dict(difficulty=2)),('human',dict(flags=8)),('animation',dict(flags=32)),('unavailable',dict(unavailable=4)),('whistle',dict(whistle=1)),('replay',dict(view=128)),('offside',dict(offside=128)),('penalty',dict(penalty=4)),('shootout',dict(shootout=1)),('upice',dict(depth=111)),('outside',dict(depth=293))]:
 for kind in ['support','pass','shot']:
  cases.append((kind+'-'+name,kind,dict(role=4,px=110,depth=180,receivers=[dict(local=0,x=0,y=220)],**change) if not 'depth' in change else dict(role=4,px=110,receivers=[dict(local=0,x=0,y=220)],**(dict(depth=87) if name=='upice' and kind=='pass' else change)),None))
for name,extra in [('boxed',dict(receivers=[dict(local=0,x=0,y=220,role=-1)])),('marked',dict(opponents=[dict(x=0,y=220)])),('blocked',dict(opponents=[dict(x=55,y=200)])),('no-upgrade',dict(receivers=[dict(local=0,x=120,y=160)])),('far',dict(receivers=[dict(local=0,x=-120,y=220)]))]:
 s=dict(role=4,px=110,depth=180,receivers=[dict(local=0,x=0,y=220)]);s.update(extra);cases.append(('pass-'+name,'pass',s,3))
cases.append(('support-marked','support',dict(role=3,px=110,opponents=[dict(x=-44,y=236)]),(-68,208)))
cases.append(('shot-central','shot',dict(role=4,px=32,depth=180,receivers=[dict(local=0,x=0,y=220)]),1))
cases.append(('shot-no-receiver','shot',dict(role=4,px=110,depth=180),1))
def pair(item):
 name,kind,s,expected=item;old=run(a.baseline,'old-'+name,s,kind);new=run(a.rom,'new-'+name,s,kind)
 if expected is None:assert new==old,(name,old,new)
 elif kind=='support':assert (new['x'],new['y'])==expected,(name,old,new,expected)
 elif kind=='pass':assert new['x']==expected,(name,old,new,expected)
 else:assert new['native']==expected,(name,old,new,expected)
 return dict(case=name,kind=kind,spec=s,baseline=old,fixed=new)
with ThreadPoolExecutor(max_workers=3) as pool:reports=list(pool.map(pair,cases))
(out/'results.json').write_text(json.dumps(reports,indent=2)+'\n');print('PASS',len(reports),'paired native attack target/pass/shot cases')
