"""Private native controller lifecycle hook for verify_four_lifecycle.py."""
from pathlib import Path
import json,sys,struct
p=Path(outdir);spec=json.loads(os.environ['LIFECYCLE']);events=[];phase='boot';live_at=None;menu_at=None;last_start=-1000;last_gp=0;injected=False;gk_seen=None;last_request=-1000;requests=0;passes=0;received=0;pending_target=None;last_owner=None;game_index=0
def rw(a):return (wram[ram(a)]<<8)|wram[ram(a+1)]
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 press(pad,button,duration=5):held[(pad-1,IDS[button])]=duration
def snap(name):
 raw,wd,ht,pitch=frame[0]
 if pixfmt[0]==1:im=Image.frombuffer('RGBA',(wd,ht),raw,'raw','BGRA',pitch,1).convert('RGB')
 else:
  pixels=bytearray()
  for y in range(ht):
   for (v,) in struct.iter_unpack('<H',raw[y*pitch:y*pitch+wd*2]):pixels+=bytes(((v>>11)<<3,((v>>5)&63)<<2,(v&31)<<3))
  im=Image.frombytes('RGB',(wd,ht),bytes(pixels))
 im.save(p/name)
def goal(f):
 shooter=0 if rw(0xc328)==1 else 6;other=11 if shooter==0 else 5;positive=bool(wram[ram(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 a in [0xb74a,0xb74c,0xb760,0xb762,0xb764,0xb772,0xb776]:ww(a,0)
 ww(0xb75e,260 if positive else -260);ww(0xb774,0x1000 if positive else -0x1000);ww(0xb7aa,65535);wram[ram(0xc2ea)]&=254
 emit(f,'native-OT-goal-positioned',shooter=shooter)
def test_tick(f):
 global phase,live_at,menu_at,last_start,last_gp,injected,gk_seen,last_request,requests,passes,received,pending_target,last_owner,game_index
 b=bytes(sram)[1:65536:2];h=struct.unpack_from('>6H',b,0x1004);gp=h[2]
 if f==1450 and spec['run']==1:
  assert rw(0xd046) and rw(0xd04a)=={3:5,4:6}[spec['count']] and rw(0xf07c)==spec['count'];emit(f,'players-selected',players=rw(0xd04a),count=rw(0xf07c));snap('players.png')
 if spec['run']==2 and f==1300:
  assert gp==2 and rw(0xd046) and rw(0xe3f6)==2;emit(f,'cold-reboot-resumes',header=h,players=rw(0xd04a),adapter=rw(0xd046));snap('reboot.png')
 live=rw(0xc468)>0 and rw(0xc466)<=3 and rw(0xc2e8)>8 and not(wram[ram(0xc2ec)]&128)
 if phase=='boot' and f>2200 and live and rw(0xc468)<300 and not wram[ram(0xc2f2)]&4:
  count=spec['count'];assert rw(0xd046) and [rw(0xc328+i*2) for i in range(4)]==[1]*count+[0]*(4-count)
  assert wram[ram(0xc331)]!=wram[ram(0xc333)];phase='game';live_at=f;injected=False;game_index+=1;emit(f,'native-game-start',game=game_index,count=count,clubs=[wram[ram(0xc331)],wram[ram(0xc333)]]);snap('game-'+str(game_index)+'.png')
 if phase=='game':
  if not rw(0xd046):raise AssertionError('adapter lost during game')
  if live and rw(0xf738) in [0,0xa500] and not(wram[ram(0xc2ea)]&17):
   if spec['fast'] and not injected and f>=live_at+180:goal(f);injected=True
   elif not spec['fast']:
    owner=rw(0xb7aa);gk=5 if rw(0xc328)==1 else 11
    if owner==gk:
     if gk_seen is None:gk_seen=f
     if f-gk_seen>=6 and f-last_request>60:
      requestpad=1+(requests%spec['count']);press(requestpad,'B',5);last_request=f;requests+=1;emit(f,'keeper-request',pad=requestpad)
    else:gk_seen=None
    if owner!=gk and f%120==0:
     for i in range(spec['count']):press(i+1,['Up','Right','Down','Left'][(f//120+i)%4],20)
     press(1+((f//120)%spec['count']),'C',3)
    if owner!=gk and rw(0xbeda)==gk and last_owner==gk:
     passes+=1;pending_target=rw(0xbee0);emit(f,'keeper-native-pass',target=pending_target)
    if pending_target is not None and owner==pending_target:
     received+=1;emit(f,'keeper-pass-received',receiver=owner);pending_target=None
    last_owner=owner
  if gp!=last_gp and gp>0 and b[0x1fff]==sum(b[:0x1ffe])%256:
   assert h[1]>0 and h[3]==spec['length'] and h[4]==0;assert b[0x1fff]==sum(b[:0x1ffe])%256;emit(f,'native-game-recorded',header=h,score=[rw(0xc6da),rw(0xca3e)]);last_gp=gp
  if gp>=game_index and rw(0xe3f6)==gp and rw(0xe3f2)==1 and rw(0xe3f0)==1:
   phase='menu';menu_at=f;held.clear();emit(f,'back-at-next-fixture',gp=gp,count=rw(0xf07c),adapter=rw(0xd046));snap('after-'+str(game_index)+'.png')
  elif not live and f-last_start>=120 and (rw(0xf738) in [0,0xa500] or (rw(0xf738)==0xa507 and wram[ram(0xc2ea)]&128)):
   press(1,'C' if rw(0xc466)<=2 and rw(0xc468)>0 else 'Start');last_start=f
 if phase=='menu':
  if game_index==2 or spec['run']==2:
   if f>=menu_at+120:
    assert b[0x1fff]==sum(b[:0x1ffe])%256
    size=core.retro_serialize_size();buf=C.create_string_buffer(size);assert core.retro_serialize(buf,size);(p/'main.state').write_bytes(buf.raw);(p/'battery.srm').write_bytes(bytes(sram)[:65536]);(p/'result.json').write_text(json.dumps(dict(header=h,games=game_index,requests=requests,passes=passes,received=received),indent=2)+'\n');print('DONE',spec,h,flush=True);sys.exit(0)
  else:
   if f==menu_at+150:press(1,'Down')
   if f==menu_at+250:press(1,'Right' if spec['count']==3 else 'Left');spec['count']=7-spec['count']
   if f==menu_at+350:press(1,'Up')
   if f==menu_at+500:press(1,'Start')
   if f==menu_at+1100:press(1,'C');phase='boot'
 if f%5000==0:(p/'progress.json').write_text(json.dumps(dict(frame=f,phase=phase,header=h,period=rw(0xc466),clock=rw(0xc468),requests=requests,passes=passes,received=received),indent=2))
 if f>=180000:raise AssertionError(('lifecycle timeout',spec,phase,h,rw(0xc466),rw(0xc468)))
