#!/usr/bin/env python3
"""Native goal-net release thresholds, both ends and skaters/keepers.

Private reset drivers call the unchanged native HV/geometry/speed/release helper.
No speed-comparison result, HV read, net velocity or release outcome is injected.
"""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import argparse,json,hashlib,subprocess,struct,os
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);ap.add_argument('--matched-baseline',type=Path,help='current image with only original net thresholds restored; retain exact r13 historical reference');ap.add_argument('--state',type=Path,required=True);ap.add_argument('--fixtures-root',type=Path,required=True);a=ap.parse_args();h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(parents=True,exist_ok=True);sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest();assert sha(a.baseline)=='3070c2bbac4ec9fe6cf578c7fd0a3260869371a232b05ed76f33e44ca71ea899'
historical_reference_sha256=sha(a.baseline);historical_reference=a.baseline
if a.matched_baseline:
 before=a.matched_baseline.read_bytes();after=a.rom.read_bytes();diff=[i for i,(x,y) in enumerate(zip(before,after)) if x!=y];body=[i for i in diff if not 0x18e<=i<0x190]
 assert body==[0x14b6c,0x14b74,0x14b7c,0x14b84],('matched net control must differ only in four threshold bytes/checksum',body)
 assert [before[i] for i in body]==[0x20,0xe0,0x20,0xe0] and [after[i] for i in body]==[0x30,0xd0,0x30,0xd0]
 (o/'matched-baseline-scope.json').write_text(json.dumps(dict(current_sha256=sha(a.rom),matched_sha256=sha(a.matched_baseline),historical_r13_sha256=historical_reference_sha256,only_four_threshold_bytes_and_checksum_differ=True),indent=2)+'\n')
 a.baseline=a.matched_baseline
cases=[dict(net=n,actor=i,axis=axis,sign=sign,speed=s,puck_dy=0) for n in [12,13] for i in [0,5,6,11] for axis in [0,1] for sign in [-1,1] for s in [8191,8192,8193,12287,12288,12289,13868,16383,16384,16385,24576]]
cases += [dict(net=n,actor=5,axis=1,sign=sgn,speed=24576,puck_dy=dy) for n in [12,13] for sgn in [-1,1] for dy in [-41,-40,40,41]]

def run(item):
 profile,batch=item;rom=(a.baseline if profile=='baseline' else a.rom).read_bytes();ss=cases[batch*12:batch*12+12];p=o/f'{profile}-{batch:02}';p.mkdir(exist_ok=True);b=[' .text',' .globl _start','_start:',' move.w #0x2700,%sr',' movea.l #0xfffff900,%sp']
 for j,s in enumerate(ss):
  actor=0xffffb04a+128*s['actor'];net=0xffffb04a+128*s['net'];vy=268 if s['net']==12 else -268;v=[0,0];v[s['axis']]=s['sign']*s['speed'];save=0xffff4800+24*j
  b += [f' lea 0x{actor:x},%a3',f' lea 0x{net:x},%a2',f' move.w #{vy},0x14(%a2)',f' move.w #{vy+s["puck_dy"]},0xffffb75e',' clr.l 0x28(%a2)',f' move.w #{v[0]},0x28(%a3)',f' move.w #{v[1]},0x2a(%a3)',' move.b #1,0xffffc2ea',' clr.b 0xffffc2ec',' move.w #1023,%d5',f'loop{j}: move.l #0x11223344,%d0',' move.l #0x55667788,%d1',f' pea got{j}(%pc)',' jsr 0x14b38',' addq.l #4,%sp',f'got{j}: btst #6,0xffffc2ec',f' bne.w done{j}',f' dbra %d5,loop{j}',f'done{j}: move.w 0x28(%a2),0x{save:x}',f' move.w 0x2a(%a2),0x{save+2:x}',f' move.l %sp,0x{save+4:x}',f' move.w 0x28(%a3),0x{save+8:x}',f' move.w 0x2a(%a3),0x{save+10:x}',f' move.l %d0,0x{save+12:x}',f' move.l %d1,0x{save+16:x}',f' move.w %d5,0x{save+20:x}',f' move.b 0xffffc2ec,0x{save+22:x}']
 b+=[' move.l #0x4e455453,0xffff4f00','end: bra.s end'];(p/'driver.s').write_text('\n'.join(b)+'\n');subprocess.run([str(h.parent/'m68kasm.sh'),str(p/'driver.s'),'2d5000',str(p/'code.bin')],check=True,stdout=subprocess.DEVNULL);code=(p/'code.bin').read_bytes();assert len(code)<0x3000;r=bytearray(rom);assert set(r[0x2d5000:0x2d5000+len(code)])=={255};r[0x2d5000:0x2d5000+len(code)]=code;struct.pack_into('>I',r,4,0x2d5000);(p/'rom.bin').write_bytes(r);(p/'commands.txt').write_text(f'2000 dump ff4f00 4 done\n2000 dump ff4800 {24*len(ss)} results\n2001 exit\n');env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')}
 with(p/'log').open('w') as log:subprocess.run(['nice','-n','19','python3',str(h/'test_core.py'),str(p/'rom.bin'),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=60)
 rows={l.split()[1]:bytes.fromhex(' '.join(l.split()[2:])) for l in (p/'commands.out').read_text().splitlines()};assert rows['done']==b'NETS',('native return stack failed',profile,batch);result=[]
 for j,s in enumerate(ss):
  vx,vy,sp,px,py,d0,d1,tries,flags=struct.unpack_from('>hhIhhIIHB',rows['results'],24*j);threshold=8192 if profile=='baseline' else 12288;expected=s['speed']>threshold and abs(s['puck_dy'])<=40;released=bool(flags&64);assert released==expected,(profile,s,released,tries);assert sp==0xfffff900,(profile,s,'stack',sp)
  v=[0,0];v[s['axis']]=s['sign']*s['speed'];assert [vx,vy]==([q//4 for q in v] if released else [0,0]),('net velocity',profile,s,vx,vy)
  assert [px,py]==([0,0] if released else v),('collider velocity',profile,s,px,py)
  if not released:assert (d0,d1)==(0x11223344,0x55667788),('native saved registers',s,d0,d1)
  result.append(dict(spec=s,release=released,remaining_attempts=tries,net_velocity=[vx,vy],stack=sp))
 return dict(profile=profile,batch=batch,cases=result)
with ThreadPoolExecutor(max_workers=3) as pool:results=list(pool.map(run,[(profile,b) for profile in ['baseline','current'] for b in range((len(cases)+11)//12)]))
# Paired genuine gameplay: no actor/puck geometry, clock or physics writes.
def gameplay(profile,rom,kind,state,device,end):
 p=o/(profile+'-'+kind);p.mkdir(exist_ok=True);(p/'commands.txt').write_text(f'100 load {state.resolve()}\n{end+1} exit\n');env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};env.update(GPGX_DEV=str(device),GPGX_HOOK=str(h/'testcases'/('net_contact_gameplay_hook.py' if kind=='cpu' else 'net_contact_burst_hook.py')))
 with(p/'log').open('w') as log:subprocess.run(['nice','-n','19','python3',str(h/'test_core.py'),str(rom.resolve()),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=60)
 return json.loads((p/'result.json').read_text())
played={}
for profile,rom in [('baseline',a.baseline),('current',a.rom)]:
 cpu=gameplay(profile,rom,'cpu',a.state,1281,30100);burst=gameplay(profile,rom,'burst',a.fixtures_root/'goalie-outlet-20261024-173340/capture/gk5-clear.state',513,399)
 played[profile]=dict(moving_net_frames=len(cpu['net_movement_observations']),maximum_velocity=cpu['maxima'],burst_max_component=max(max(map(abs,r['velocity'])) for r in burst),clock=cpu['clock'])
# Extra unrelated instructions can shift the native HV random gate. Preserve the
# original unforced positive reproduction on exact r13 and an isolated four-byte
# threshold-only r13 control, rather than selecting another favorable trajectory.
# The current matched pair is still executed and recorded, with strict current
# no-displacement and human-motion assertions; no missing event is counted as a
# positive correction in that pair.
r=bytearray(historical_reference.read_bytes());offsets=[0x14b6c,0x14b74,0x14b7c,0x14b84]
for at in offsets:
 assert r[at:at+2] in [bytes.fromhex('2000'),bytes.fromhex('e000')]
 r[at:at+2]=(a.rom.read_bytes())[at:at+2]
r[0x18e:0x190]=(sum(int.from_bytes(r[i:i+2],'big') for i in range(0x200,len(r),2))&65535).to_bytes(2,'big')
isolated=o/'r13-threshold-only.bin';isolated.write_bytes(r)
diff=[i for i,(b,n) in enumerate(zip(historical_reference.read_bytes(),r)) if b!=n];assert [i for i in diff if not 0x18e<=i<0x190]==offsets,'isolated reference changed unrelated code'
causal={}
for label,rom in [('historical-r13',historical_reference),('isolated-r13-thresholds',isolated)]:
 cpu=gameplay(label,rom,'cpu',a.state,1281,30100);causal[label]=dict(rom_sha256=sha(rom),moving_net_frames=len(cpu['net_movement_observations']),clock=cpu['clock'])
assert causal['historical-r13']['moving_net_frames']>0 and causal['isolated-r13-thresholds']['moving_net_frames']==0,('original unforced positive reproduction lost',causal)
assert played['current']['moving_net_frames']==0,('current native CPU sequence dislodged a net',played)

assert (o/'baseline-burst/result.json').read_bytes()==(o/'current-burst/result.json').read_bytes(),'ordinary burst or motion changed'
assert played['current']['burst_max_component']>12288,'native human burst cannot exceed the new release threshold'
(o/'results.json').write_text(json.dumps(dict(rom_sha256=sha(a.rom),baseline_sha256=sha(a.baseline),historical_r13_sha256=historical_reference_sha256,paired_contexts=len(cases),scope=__doc__,native_gameplay=played,isolated_positive_native_control=causal,isolated_threshold_only_changed_addresses=offsets,matched_pair_positive_event=played['baseline']['moving_net_frames']>0,results=results),indent=2)+'\n');print('PASS',len(cases),'paired native goal-net contexts; both ends, skaters/keepers, boundaries, native HV retries, velocity, ABI; original unforced r13 net event corrected by isolated four-byte thresholds; current matched sequence recorded, real human burst retained')
