#!/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);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=[dict(team=t,direction=d,active=True) for t in [0,1] for d in [-1,1]]
for key,value in [('difficulty',0),('difficulty',2),('flags',8),('flags',32),('boxed',4),('whistle',1),('view',128),('offside',128),('penalty',4),('special',1),('special',4),('role',0),('role',-1)]:cases.append(dict(active=False,**{key:value}))
# Manual expected refresh policy: attack3, defending real opponent1.
for team in [0,1]:
 for direction in [-1,1]:
  for state in ['enemy','own','pass','ownpass','cleared','missed','keeper','unavailable','invalid','enemy_locked']:
   cases.append(dict(team=team,direction=direction,state=state,active=True,interval=1 if state in ['enemy','pass','enemy_locked'] else 3))
for original in list(cases[:17]):
 if not original['active']:cases.append(dict(original,state='enemy'))
reports=[]
for batch in range((len(cases)+7)//8):
 ss=cases[batch*8:batch*8+8];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):
  actor=s.get('team',0)*6+2;ab=0xffb04a+128*actor
  body += [' lea 0xFFB04A,%a0',' move.w #383,%d0',f'reset{n}: clr.l (%a0)+',f' dbra %d0,reset{n}']
  for slot in range(12):body += [f' move.w #-1,0x{0xffb04a+128*slot+0x34:X}',f' move.w #{slot},0x{0xffb04a+128*slot+0x52:X}']
  body += [' move.w #-1,0xFFB7AA',' move.w #-1,0xFFBEE0',' move.w #-1,0xFFBEDA',' move.w #40,0xFFB74A',' move.w #200,0xFFB75E',' clr.w 0xFFB772',' clr.w 0xFFB774']
  state=s.get('state');own=6*s.get('team',0);opponent=6*(1-s.get('team',0));target=own+1 if state in ['own','ownpass'] else opponent+2;sender=own if state=='ownpass' else opponent
  if state:
   for slot,x,y,role in [(sender,100,200,3),(target,-40,220,0 if state=='keeper' else 4)]:
    b=0xffb04a+128*slot
    for off,val in [(0,x),(0x14,y),(0x34,role)]:body.append(f' move.w #{val},0x{b+off:X}')
   if state in ['pass','ownpass','cleared','missed']:
    body += [f' move.w #{0xff00|target if state=="cleared" else target},0xFFBEE0',f' move.w #{sender},0xFFBEDA',f' move.w #{512 if state=="missed" else -512},0xFFB772',' move.w #256,0xFFB774']
   else:body.append(f' move.w #{12 if state=="invalid" else target},0xFFB7AA')
   if state=='unavailable':body.append(f' move.b #4,0x{0xffb04a+128*target+0x63:X}')
   if state=='enemy_locked':body.append(f' move.b #32,0x{0xffb04a+128*target+0x62:X}')

  body += [f' lea 0x{ab:X},%a3',f' move.w #{s.get("difficulty",1)},0xFFF07A',f' move.w #{s.get("role",4)},0x34(%a3)',f' move.w #{actor},0x52(%a3)',f' move.b #{s.get("flags",0)+(64 if actor>=6 else 0)+(128 if s.get("direction",1)==1 else 0)},0x62(%a3)',f' move.b #{s.get("boxed",0)},0x63(%a3)']
  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}')
  body += [' clr.w 0x42(%a3)',' clr.w (%a3)',' clr.w 0x14(%a3)',' clr.w 0x54(%a3)',' clr.l 0xFF3FEC',' clr.l 0xFF3FF4',' moveq #23,%d6',f'loop{n}: move.w #80,%d0',' move.w #200,%d1',' moveq #1,%d7',' lea 0x2DBF00,%a0',' jsr 0x103C8',f' dbra %d6,loop{n}',f' move.l 0xFF3FEC,0x{0xff4800+n*4:X}',f' move.l 0xFF3FF4,0x{0xff4880+n*4:X}']
 body += [' move.l #0x5154524E,0xFF3FF0','end: bra.s end'];(p/'driver.s').write_text('\n'.join(body)+'\n');subprocess.run([str(site/'tools/m68kasm.sh'),str(p/'driver.s'),'2D9000',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[0x2d9000:0x2d9000+len(code)]==b'\xff'*len(code);data[0x2d9000:0x2d9000+len(code)]=code;data[0x10750:0x10758]=bytes.fromhex('52b900ff3ff44e75');data[0x2dbf00:0x2dbf08]=bytes.fromhex('52b900ff3fec4e75');struct.pack_into('>I',data,4,0x2d9000);(p/'driver.bin').write_bytes(data);(p/'commands.txt').write_text(f'500 dump ff3ff0 4 done\n500 dump ff4000 {64*len(ss)} regs\n500 dump ff4800 {4*len(ss)} timers\n500 dump ff4880 {4*len(ss)} acceleration_calls\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(site/'tools/experimental/test_core.py'),str(p/'driver.bin'),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=30)
 result={l.split()[1]:bytes.fromhex(' '.join(l.split()[2:])) for l in (p/'commands.out').read_text().splitlines()};assert result['done']==b'QTRN'
 for n,s in enumerate(ss):
  acceleration_calls=struct.unpack_from('>I',result['acceleration_calls'],4*n)[0];assert acceleration_calls==24,(s,'native acceleration call count',acceleration_calls)
  count=struct.unpack_from('>I',result['timers'],4*n)[0];assert count==(24//s.get('interval',3) if s['active'] else 2),(s,count);reports.append(dict(spec=s,actual_callback_updates_in24_native_calls=count,native_acceleration_calls=acceleration_calls))
(o/'results.json').write_text(json.dumps(reports,indent=2)+'\n');print('PASS',len(reports),'native steering frequency cases; exactly24 native acceleration calls in every context')
