#!/usr/bin/env python3
"""Real standalone BO1/BO7 player choices, native wins, saves and cold resumes.

Private copied batteries only. A winning OT puck crosses the native goal line;
the game records its own result/password. No injected side/control words.
"""
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('--seed',type=Path,required=True);ap.add_argument('--case',help='e.g. 1-4');a=ap.parse_args()
here=Path(__file__).resolve().parent;out=a.output.resolve();out.mkdir(parents=True,exist_ok=True)
hook=out/'hook.py';hook.write_text('''from pathlib import Path
import json,sys,struct,hashlib
p=Path(outdir);spec=json.loads(os.environ['NATIVE_CASE']);phase='menu';started=None;injected=False;last_start=-1000;events=[]
def rw(a):return (wram[ram(a)]<<8)|wram[ram(a+1)]
def rb(a):return wram[ram(a)]
def ww(a,v):v&=65535;wram[ram(a)]=v>>8;wram[ram(a+1)]=v&255
def emit(f,event,**kw):events.append(dict(frame=f,event=event,**kw));(p/'events.json').write_text(json.dumps(events,indent=2)+'\\n')
def done(f):
 (p/'battery.srm').write_bytes(bytes(sram)[:65536]);(p/'result.json').write_text(json.dumps(events,indent=2)+'\\n');sys.exit(0)
def test_tick(f):
 global phase,started,injected,last_start
 b=bytes(sram)[1:65536:2];code=spec['count'];effective=2 if spec['device']==513 and code in [3,4] else code
 if f==spec['menu_check']:
  assert rw(0xf06c)==0 and rw(0xd048)==(1 if spec['resume'] else spec['format']),('not standalone playoffs',spec,rw(0xf06c),rw(0xd048))
  assert rw(0xf0da)==code and b[0x2e14:0x2e18]==b'NC'+bytes([code,code^255]),('wrong standalone preference',spec,rw(0xf0da),b[0x2e14:0x2e18].hex())
  emit(f,'menu-choice',count=code,players=rw(0xd04a));
 live=rw(0xc2e8)>8 and rw(0xc466)<=3 and 0<rw(0xc468)<300 and not rb(0xc2ec)&128 and not rb(0xc2f2)&4
 if phase=='menu' and f>spec['menu_check'] and live:
  side=rw(0xc328);sides=[rw(0xc328+2*i) for i in range(4)]
  expected=[side,3-side,0,0] if effective==5 else [side]*effective+[0]*(4-effective)
  assert side in [1,2] and sides==expected,('wrong active controls',spec,sides,expected)
  assert rw(0xf06c)==0 and rw(0xf0da)==code
  phase='game';started=f;held.clear();emit(f,'real-match',sides=sides,round=rw(0xceec),series=rw(0xceea))
  if spec['resume']:done(f)
 if phase=='game' and f>=started+200 and not injected and not rb(0xc2ea)&17:
  side=rw(0xc328);shooter=0 if side==1 else 6;other=11 if side==1 else 5;positive=bool(rb(0xb04a+shooter*128+0x62)&128)
  ww(0xb04a+other*128,120);ww(0xc466,3);ww(0xc468,600);ww(0xc46a,1);ww(0xbeda,shooter);ww(0xbee0,65535)
  for addr in [0xb74a,0xb74c,0xb760,0xb762,0xb764,0xb772,0xb776]:ww(addr,0)
  ww(0xb75e,260 if positive else -260);ww(0xb774,0x1000 if positive else -0x1000);ww(0xb7aa,65535);wram[ram(0xc2ea)]&=254
  injected=True;phase='finish';emit(f,'native-goal-positioned')
 if phase=='finish':
  ctx=b[0x2d20:0x2e14];ctxsum=sum(ctx[4:242])%256
  complete=ctx[:4]==b'NPO1' and ctx[242:244]==bytes([ctxsum,ctxsum^255]) and b[0x3fff]==sum(b[0x2000:0x3ffe])%256 and b[0x3ffe]==(b[0x3fff]^255)
  if complete and rw(0xf06c)==0 and rw(0xd048)==1 and rw(0xc466)==4 and b[0x1fff]==sum(b[:0x1ffe])%256:
   assert b[0x2e14:0x2e18]==b'NC'+bytes([code,code^255]);assert b[0x2d20:0x2d24]==b'NPO1'
   seed=(p/'seed.srm').read_bytes()[1::2]
   for o in [0,8192,16384,24576]:assert b[o+0xe20:o+0x1ea0]==seed[o+0xe20:o+0x1ea0],('standalone game changed season data',o)
   assert rw(0xceec)==(1 if spec['format']==2 else 0)
   emit(f,'native-result-saved',round=rw(0xceec),series=rw(0xceea),score=[rw(0xc6da),rw(0xca3e)]);done(f)
  if f-last_start>=120 and not live:
   held[(0,IDS['Start'])]=5;last_start=f
 if f>=16000:raise AssertionError(('native playoff timeout',spec,phase,rw(0xc466),rw(0xc468),rw(0xd048),rw(0xc2ec)))
''')
def put(p,b):
 for o in range(0,len(b),8192):b[o+0x1fff]=sum(b[o:o+0x1ffe])%256;b[o+0x1ffe]=b[o+0x1fff]^255
 raw=bytearray(len(b)*2);raw[1::2]=b;p.write_bytes(raw)
def execute(p,spec,lines):
 (p/'seed.srm').write_bytes((p/'battery.srm').read_bytes());(p/'commands.txt').write_text('\n'.join(lines)+'\n');env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};env.update(GPGX_DEV=str(spec['device']),GPGX_DEV2=str(spec['device']),GPGX_SRM=str(p/'battery.srm'),GPGX_HOOK=str(hook),NATIVE_CASE=json.dumps(spec))
 with (p/'log.txt').open('w') as log:subprocess.run(['python3',str(here/'test_core.py'),str(a.rom.resolve()),str(p/'commands.txt'),str(p)],env=env,check=True,stdout=log,stderr=log,timeout=30)
 return json.loads((p/'result.json').read_text())
def run(case):
 fmt,count=case;p=out/f'{fmt}-{count}';p.mkdir(exist_ok=True);b=bytearray(a.seed.read_bytes()[1::2]);assert len(b)==32768;b[0x2d20:0x2e20]=bytes(256);slot=int.from_bytes(b[0x1ea6:0x1ea8],'big');put(p/'battery.srm',b)
 lines=[f'{1000+i*200} press 1 Right 5' for i in range(4-slot+(fmt==3))]+['2000 press 1 Down 5']+[f'{2100+i*200} press 1 Right 5' for i in range(5 if count==1 else count-1)]+['3000 snap','3100 press 1 Up 5']+[f'{3300+i*600} press 1 Start 5' for i in range(12)]+['16001 exit']
 spec=dict(format=fmt,count=count,device=1281,resume=False,menu_check=3000);events=execute(p,spec,lines);source=p/'battery.srm';boots=[]
 for device in ([1281,513,1281] if count in [3,4] else [1281]):
  q=out/(p.name+'-resume'+str(len(boots)));q.mkdir(exist_ok=True);(q/'battery.srm').write_bytes(source.read_bytes());logical=source.read_bytes()[1::2];slot=int.from_bytes(logical[0x1ea6:0x1ea8],'big');steps=4-slot
  lines=[f'{1000+i*200} press 1 Right 5' for i in range(steps)]+['1900 snap']+[f'{2100+i*600} press 1 Start 5' for i in range(12)]+['16001 exit'];spec=dict(format=fmt,count=count,device=device,resume=True,menu_check=1900)
  boots.append(execute(q,spec,lines));source=q/'battery.srm'
 return dict(format=fmt,count=count,events=events,resumes=len(boots))
cases=[(fmt,count) for fmt in [2,3] for count in range(1,6) if not a.case or a.case==f'{fmt-1}-{count}']
with ThreadPoolExecutor(max_workers=3) as pool:results=list(pool.map(run,cases))
(out/'results.json').write_text(json.dumps(results,indent=2)+'\n');print('PASS',len(results),'standalone player choices/native results;',sum(r['resumes'] for r in results),'cold resumes; BO1/BO7 and head-to-head retained')
if (out/'2-4/battery.srm').exists():
 p=out/'namespace';p.mkdir(exist_ok=True);b=bytearray((out/'2-4/battery.srm').read_bytes()[1::2]);active=int.from_bytes(b[0x1ea6:0x1ea8],'big')
 for slot in [0,1,2,3]:
  count={1:1,2:2,3:3}[active if slot==0 else slot];b[slot*8192+0x1ef0:slot*8192+0x1ef4]=b'PC'+bytes([count,count^255])
 put(p/'battery.srm',b)
 h=p/'hook.py';h.write_text('''from pathlib import Path
import json,sys
checks=json.loads(os.environ['NAMESPACE_CHECKS'])
def rw(a):return (wram[ram(a)]<<8)|wram[ram(a+1)]
def test_tick(f):
 if str(f) not in checks:return
 view,slot,count=checks[str(f)]
 assert rw(0xf06c)==view and rw(0xf06a)==slot,('wrong namespace',f,rw(0xf06c),rw(0xf06a))
 assert rw(0xf0d0 if view else 0xf0da)==count,('count leaked across modes',f,count)
 b=bytes(sram)[1:65536:2];assert b[0x2e14:0x2e18]==b'NC\\x04\\xfb'
 if f==max(map(int,checks)):
  Path(outdir,'battery.srm').write_bytes(bytes(sram)[:65536]);Path(outdir,'result.json').write_text(json.dumps(checks));sys.exit(0)
''')
 # Walk from the current season through every season and the standalone context,
 # then repeat after another new core/process using the just-written battery.
 for boot in range(2):
  q=p/f'boot{boot}';q.mkdir(exist_ok=True);(q/'battery.srm').write_bytes((p/'battery.srm').read_bytes());current=active;lines=[];checks={1300:[1,current,current]};t=1400
  for _ in range(4-current):lines.append(f'{t} press 1 Right 5');current+=1;checks[t+100]=[0,3,4] if current==4 else [1,current,current];t+=200
  for key,expected in [('Left',[1,3,3]),('Left',[1,2,2]),('Left',[1,1,1]),('Right',[1,2,2]),('Right',[1,3,3]),('Right',[0,3,4])]:lines.append(f'{t} press 1 {key} 5');checks[t+100]=expected;t+=200
  (q/'commands.txt').write_text('\n'.join(lines+[f'{t+1} exit'])+'\n');env=dict(os.environ,GPGX_DEV='1281',GPGX_DEV2='1281',GPGX_SRM=str(q/'battery.srm'),GPGX_HOOK=str(h),NAMESPACE_CHECKS=json.dumps(checks))
  with (q/'log.txt').open('w') as log:subprocess.run(['python3',str(here/'test_core.py'),str(a.rom.resolve()),str(q/'commands.txt'),str(q)],env=env,stdout=log,stderr=log,check=True)
  (p/'battery.srm').write_bytes((q/'battery.srm').read_bytes());active=3
 print('PASS independent Full1/Light2/Mini3/native4 preferences through roundtrips and reboot')
