"""Private real-controller shot/goal/replay HUD fixture for verify_shot_panel.py."""
from pathlib import Path
import json,math,struct,sys
p=Path(outdir);spec=json.loads(os.environ['SHOT_PANEL_CASE']);pad=spec['pad'];shooter=pad-1
released=None;replay=None;exit_at=None;frozen=None;offset=None;hud_font=None;checks=0;events=[];skipped=False;paused=None;resumed=False
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 sw(a):v=rw(a);return v-65536 if v&32768 else v
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 snap(name):
 raw,width,height,pitch=frame[0]
 if pixfmt[0]==1:im=Image.frombuffer('RGBA',(width,height),raw,'raw','BGRA',pitch,1).convert('RGB')
 else:
  px=bytearray()
  for y in range(height):
   for (v,) in struct.iter_unpack('<H',raw[y*pitch:y*pitch+width*2]):px+=bytes(((v>>11)<<3,((v>>5)&63)<<2,(v&31)<<3))
  im=Image.frombytes('RGB',(width,height),bytes(px))
 im.save(p/(name+'.png'))
def vram():
 global offset
 size=core.retro_serialize_size();buf=C.create_string_buffer(size);assert core.retro_serialize(buf,size);raw=buf.raw
 if offset is None:
  pattern=struct.pack('<8H',0xa3ff,*([0xa400]*6),0xa401);pos=raw.find(pattern);assert pos>=0,'new panel top frame missing';offset=pos-(0xe000+18*128+32*2)
 return raw[offset:offset+65536]
def cells(v,row):return list(struct.unpack_from('<8H',v,0xe000+(18+row)*128+64))
def digits(value):return [0xa403 if i<2 and value<10**(2-i) else 0x841e+int(f'{value:03}'[i]) for i in range(3)]
def check_panel(f):
 global checks,hud_font
 v=vram();assert cells(v,0)==[0xa3ff]+[0xa400]*6+[0xa401]
 assert cells(v,4)==[0xa405]+[0xa406]*6+[0xa407]
 for row in [1,2,3]:assert cells(v,row)[0]==0xa402 and cells(v,row)[-1]==0xa404
 assert cells(v,2)[1:-1]==digits(rw(0xf650))+[0x8438,0x843a,0x8435],('km/h text',f,cells(v,2))
 assert cells(v,3)[1:-1]==digits(rw(0xf652))+[0x843a,0x843c,0x8435],('mph text',f,cells(v,3))
 font=v[0x3ff*32:0x450*32]
 if hud_font is not None:assert font==hud_font,'native HUD font/frame patterns changed'
 hud_font=font;checks+=1
def check_erased(v):
 assert all(x==0x7ff for row in range(5) for x in cells(v,row))
def done(f):
 report=dict(case=spec,events=events,checks=checks,box_cells=([7,2] if left_panel else [8,5]),box_screen=([8,200,56,16] if left_panel else [256,144,64,40]),fallback_screen=([0,144,64,40] if left_panel else None),sprite_limit=True)
 (p/'result.json').write_text(json.dumps(report,indent=2)+'\n');print('PASS',spec,checks,'panel checks',flush=True);sys.exit(0)
def test_tick(f):
 global released,replay,exit_at,frozen,skipped,paused,resumed
 if f<100:return
 base=0xb04a+shooter*128
 if f==100:
  for addr in [0xf640,0xf738,0xf73c,0xf73e,0xf700]:ww(addr,0)
  ww(0xf07a,2)
  for i in range(12):
   b=0xb04a+128*i;ww(b,130);ww(b+0x14,0);ww(b+0x28,0);ww(b+0x2a,0)
  # Isolate the HUD/replay from save/goal randomness: park both keepers
  # off ice, unavailable and non-colliding; shots still score natively.
  for i in [5,11]:
   b=0xb04a+128*i;ww(b,-180);ww(b+0x14,80);ww(b+0x34,-1);wram[ram(b+0x63)]|=4;wram[ram(b+0x62)]|=4
  for i in range(4):ww(0xc320+2*i,i);ww(0xc328+2*i,1);wram[ram(0xb04a+128*i+0x62)]|=8
  # Keep this UI/replay test close to the open net: the old 140-unit
  # setup could hit the crossbar when native random shot lift changes with
  # CPU timing. Do not force a score or alter puck/shot physics.
  ww(base,0);ww(base+0x14,225 if rb(base+0x62)&128 else -225);ww(base+0x54,0)
  ww(0xb7aa,shooter);ww(0xb74a,0);ww(0xb75e,sw(base+0x14))
  for a in [0xb74c,0xb760,0xb762,0xb772,0xb774,0xb776]:ww(a,0)
  wram[ram(0xc2ea)]&=238;wram[ram(0xc2f2)]&=251
 if f==148:ww(base+0x54,0)
 if f==150:held[(pad-1,IDS['C'])]=spec.get('duration',40)
 if released is None and rw(0xf646)>0:
  released=f;frozen=[rw(a) for a in [0xf650,0xf652,0xf644]]
  assert frozen[0]>0 and frozen[1]>0
  if spec['kind'] in ['critical','skip']:assert rb(0xf0b0) and rw(base+0x58)==0x7fc and frozen[2]==31,('not a fully charged critical forehand',f,frozen,rw(base+0x58),rb(0xf0b0))
  emit(f,'native-shot',kmh=frozen[0],mph=frozen[1],charge=frozen[2],crit=bool(rb(0xf0b0)),animation=hex(rw(base+0x58)))
 if released is None:
  if f>=800:raise AssertionError('native shot did not release')
  return
 if spec['kind'] in ['timeout','pause'] and f>=released:
  ww(0xb74a,100);ww(0xb75e,0);ww(0xb772,0);ww(0xb774,0) # isolated no-goal display lifetime fixture
 if f==released+6:check_panel(f);snap('released')
 assert rw(0xc2e8)<=0x280,('sprite list overflow',f,rw(0xc2e8))
 if spec['kind']=='pause':
  if f==released+20:held[(0,IDS['Start'])]=5
  if f>released+20 and rb(0xc2ec)&128 and paused is None:paused=f
  if paused and f==paused+60:
   assert rw(0xf646)==rw(0xf64c)==0;emit(f,'pause-cleared-panel');held[(0,IDS['C'])]=5;resumed=True
  if resumed and f>=paused+180:assert not rb(0xc2ec)&128;assert rw(0xf64c)==0;done(f)
 elif spec['kind']=='timeout':
  if f>=released+400:
   assert rw(0xf646)==rw(0xf64c)==0;v=vram();check_erased(v);emit(f,'timeout-cleared-panel');done(f)
 else:
  state=rw(0xf738)
  if 0xa502<=state<=0xa506 and rw(0xf73a)>=4:
   if replay is None:replay=f;emit(f,'replay-start')
   if f>=replay+4:
    assert rw(0xf64e)==1 and rw(0xf646)>0,('replay panel lost',f,replay,rw(0xf646),rw(0xf64e),hex(rw(0xf640)),hex(state),hex(rw(0xc2ec)))
    assert [rw(a) for a in [0xf650,0xf652,0xf644]]==frozen,'replay overwrote the recorded shot'
    if (f-replay)%25==0:check_panel(f)
    if f==replay+50:snap('replay')
    if spec['kind']=='skip' and not skipped and f>=replay+70:
     held[(0,IDS['A'])]=8;held[(0,IDS['C'])]=8;skipped=True;emit(f,'skip-pressed')
  elif replay is not None and state==0xa500 and not rb(0xc2ec)&128:
   if exit_at is None:exit_at=f;emit(f,'replay-ended')
   if f>=exit_at+15:
    assert not rb(0xc2f6)&2,('automatic replay left the menu redraw latch set',f)
    assert rw(0xf646)==rw(0xf64c)==rw(0xf64e)==0
    v=vram();check_erased(v),('panel left behind after replay',f)
    assert rw(0xc6da)+rw(0xca3e)==1;check_font=v[0x3ff*32:0x450*32];assert check_font==hud_font
    snap('after-replay');done(f)
 if f>=1800:raise AssertionError(('HUD/replay test timeout',spec,rw(0xf738),rw(0xf646),rw(0xc2ec)))

# Fixed export +C exists only for the new lower-left renderer.
left_panel=romdata[0x2ed00c:0x2ed00e]==bytes.fromhex('6000')
if left_panel:
 exec(compile((Path(__file__).resolve().parent/'shot_panel_left_hook.py').read_text(),str(Path(__file__).resolve().parent/'shot_panel_left_hook.py'),'exec'),globals())
