"""Exact new-layout VRAM/font/clock observer; legacy layout stays separate."""
def vram():
 global offset
 size=core.retro_serialize_size();buf=C.create_string_buffer(size);assert core.retro_serialize(buf,size);raw=buf.raw;offset=74789
 assert raw.startswith(b'GENPLUS-GX 1.7.6'),'unverified serialized VRAM ABI'
 return raw[offset:offset+65536]
def words(v,row,col,count):return list(struct.unpack_from('<'+str(count)+'H',v,0xe000+row*128+2*col))
def cells(v,row):return words(v,18+row,0,8)
def check_panel(f):
 global checks,hud_font
 v=vram();layout=rw(0xf64c)
 if spec.get('penalty'):
  snap('pp-diagnostic-'+str(f));(p/'pp-state.json').write_text(json.dumps(dict(frame=f,layout=layout,held=rw(0xf64e),show=rw(0xf646),flags=[rw(0xc2ea),rw(0xc2ec)],home=[rw(0xc6ce+i)for i in range(0,48,2)],away=[rw(0xca32+i)for i in range(0,48,2)],badge25=words(v,25,1,7),badge26=words(v,26,1,7)),indent=2)+'\n')
 assert layout in [1,2],('no new panel',f,layout)
 if not rw(0xf64e) or romdata[0x2ec000:0x2ec002]==bytes.fromhex('6000'):
  assert words(v,23,0,9)==[0xa3ff]+[0xa400]*7+[0xa401],('clock frame changed',f,words(v,23,0,9))
  assert words(v,27,0,9)==[0xa405]+[0xa406]*7+[0xa407],('clock bottom changed',f)
  assert words(v,24,0,1)==[0xa402] and words(v,24,8,1)==[0xa404]
 else:assert words(v,23,0,9)==[0x7ff]*9 and words(v,27,0,9)==[0x7ff]*9,('native replay clock clearing',f)
 assert words(v,18,32,8)!=[0xa3ff]+[0xa400]*6+[0xa401],('old upper right shot panel still present',f)
 if layout==1:
  refined=romdata[0x258000:0x258002]==bytes.fromhex('6000')
  expected_map=[0x87f4,0x87f5,0x87f6,0xa403]+([0xa403]*3 if refined else [0x87f7,0x87f8,0x87f9])
  assert words(v,26,1,7)==expected_map,('compact speed map',f,words(v,26,1,7))
  from shot_panel_left import tiles
  selected=[(0xfe80,rw(0xf652),'MPH')] if refined and rw(0xf8d8)==1 else [(0xfe80,rw(0xf650),'KMH')] if refined else [(0xfe80,rw(0xf650),'KMH'),(0xfee0,rw(0xf652),'MPH')]
  for addr,value,unit in selected:
   raw=tiles(f'{min(value,999):3d}{unit}');expected=bytes(n for i in range(0,len(raw),2) for n in [raw[i+1],raw[i]])
   assert v[addr:addr+96]==expected,('compact glyph actual VRAM differs',f,unit,v[addr:addr+16].hex(),expected[:16].hex())
 else:
  assert cells(v,0)==[0xa3ff]+[0xa400]*6+[0xa401]
  refined=romdata[0x258000:0x258002]==bytes.fromhex('6000')
  mph=refined and rw(0xf8d8)==1
  assert cells(v,2)[1:-1]==digits(rw(0xf652 if mph else 0xf650))+([0x843a,0x843c,0x8435]if mph else[0x8438,0x843a,0x8435])
  assert cells(v,3)[1:-1]==([0xa403]*6 if refined else digits(rw(0xf652))+[0x843a,0x843c,0x8435])
 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):
 relocated=romdata[0x12af0]in[20,21]
 framed=romdata[0x12af0]==20
 pp=relocated and bool(wram[ram(0xc2ee)]&32)and rw(0xc6f2)!=rw(0xca56)
 for row in range(5):
  # New native PP captions own only cols1..7 of21/22. Every other old
  # fallback cell must still retire; the separate real-minor oracle checks
  # the actual club/timer text and its full native end lifetime.
  owned=(row in[1,2,3,4]if framed else row in[3,4])and pp
  assert all(x==0x7ff for i,x in enumerate(cells(v,row))if not(owned and(framed or i>=1))),('fallback panel not cleared outside native PP ownership',row,cells(v,row))
 if pp and framed and not rb(0xc2ec)&0x81 and frame[0]is not None and sum(x!=0 for x in frame[0][0])>10000:
  assert words(v,19,0,9)==[0xa3ff]+[0xa400]*7+[0xa401],'native PP top border damaged'
  assert words(v,22,0,9)==[0xa405]+[0xa406]*7+[0xa407],'native PP bottom border damaged'
  for row in[20,21]:assert words(v,row,0,1)==[0xa402]and words(v,row,8,1)==[0xa404],'native PP side borders damaged'
 expected=[0xa403]*7
 refined=romdata[0x258000:0x258002]==bytes.fromhex('6000')
 if refined and romdata[0x12c7c]!=22 and rw(0xf84a)==0 and rw(0xf8de)&2:
  # These two cells are the separately enabled Momentum widget, which stays
  # present after the shot expires. No other old shot cell is licensed.
  expected[5:7]=[0xa7e5,0xa7e6]
 assert words(v,25,1,7)==expected and words(v,26,1,7)==[0xa403]*7,('compact shot panel/logo not cleared outside Momentum ownership',words(v,25,1,7),words(v,26,1,7),expected)

# R7's old observer checked speed patterns but omitted REPLAY's overlapping bank.
# The regression compares the actual full native title, frozen clock/frame map,
# bar pixels and both complete speed strings, through the real automatic replay.
_replay_hud_original_tick=test_tick
_replay_hud_before=None
_replay_hud_checks=0
_replay_hud_fixed=romdata[0x2ec000:0x2ec002]==bytes.fromhex('6000')
import re as _re
_title_source=(Path(__file__).resolve().parent/'replaytitle.s').read_text().split('\ntitle:\n')[1].split('| end of title')[0]
_title_values=[int(x,16) for x in _re.findall('0x[0-9A-Fa-f]+',_title_source)]
_title_golden=b''.join(struct.pack('<I',x) for x in _title_values)
# Serialized VRAM stores 16-bit words in host order; reverse each16-bit pair,
# not each32-bit long, when comparing ROM's tile pixel stream.
_title_golden=b''.join(struct.pack('>I',x) for x in _title_values)
_title_golden=b''.join(_title_golden[i:i+2][::-1] for i in range(0,len(_title_golden),2))
def _fallback_words(v):return b''.join(v[0xe000+row*128:0xe000+row*128+16]for row in range(18,23))if rw(0xf64c)==2 else b''
_PP_snapshot=romdata[0x2ec008:0x2ec00c]==b'PPFR'
def _panel_words(v):return b''.join(v[0xe000+row*128:0xe000+row*128+18] for row in range(18 if _PP_snapshot else 23,28))
def test_tick(f):
 global _replay_hud_before,_replay_hud_checks
 if released is not None and not 0xa501<=rw(0xf738)<=0xa506 and not rb(0xc2ec)&128 and rw(0xf64c):
  v=vram();_replay_hud_before=(_panel_words(v),v[0xfc20:0xfca0],v[0xfe80:0xff40],_fallback_words(v))
  if rw(0xf73c)==1:snap('before-replay')
 if (_replay_hud_fixed or spec.get('replay_visual')) and replay is not None and rw(0xf738)==0xa502 and rw(0xf73a)>=8:
  v=vram();base=rw(0xb026)+14
  issues=[]
  if v[base*32:base*32+864]!=_title_golden:issues.append('REPLAY pixels overwritten or not in allocated VCR bank')
  title_map=[]
  for row in range(3):title_map+=list(struct.unpack_from('<9H',v,rw(0xb00c)+row*(2<<rw(0xb00e))+4))
  if title_map!=[0x8000+base+i for i in range(27)]:issues.append('REPLAY tile map does not address its own title patterns')
  if _replay_hud_before is None:issues.append('missing genuine pre-replay panel snapshot')
  else:
   words_,bar_,fonts_,fallback_=_replay_hud_before
   now=_panel_words(v)
   if now[-90:]!=words_[-90:]:issues.append('lower-left frame/clock/map differs from before replay')
   if _PP_snapshot and now[:-90]!=words_[:-90]:issues.append('above-clock power-play/fallback frame differs from before replay')
   if fallback_ and _fallback_words(v)!=fallback_:issues.append('power-play fallback frame/bar/text map changed')
   if v[0xfc20:0xfca0]!=bar_:issues.append('recorded coloured charge bar changed during replay')
   if v[0xfe80:0xff40]!=fonts_:issues.append('recorded dual-unit speed pixels changed')
  if issues:
   snap('regression-failure');(p/'replay-visual-failure.json').write_text(json.dumps(dict(frame=f,issues=issues,vcr_base=rw(0xb026),title_base=base,golden_sha256=__import__('hashlib').sha256(_title_golden).hexdigest(),actual_head=v[base*32:base*32+32].hex(),expected_head=_title_golden[:32].hex(),first_difference=next((i for i,(x,y)in enumerate(zip(v[base*32:base*32+864],_title_golden))if x!=y),None),actual_golden_location=v.find(_title_golden)),indent=2)+'\n');raise AssertionError(('screenshot55 replay HUD regression',f,issues))
  _replay_hud_checks+=1
 _replay_hud_original_tick(f)

_replay_hud_original_done=done
_return_transition_saved=False
_return_palette_settled=None
def done(f):
 global _return_transition_saved,_return_palette_settled
 if _PP_snapshot and exit_at is not None:
  visible=frame[0]is not None and sum(x!=0 for x in frame[0][0])>10000
  if visible and not rb(0xc2ec)&0x81 and rw(0xbd26)&0x8000:
   if _return_palette_settled is None:_return_palette_settled=f
  else:_return_palette_settled=None
  # The video callback can precede the CPU's final fade step. Require the next
  # actual picture after the native fade counter has completed; preserve the
  # original15frame HUD-retirement assertions and unchanged overall deadline.
  if _return_palette_settled is None or f<=_return_palette_settled:
   if not _return_transition_saved:snap('native-replay-exit-transition');_return_transition_saved=True
   assert f<exit_at+60,'native replay return did not become visible within60frames'
   return
 if _replay_hud_checks:
  (p/'replay-visual.json').write_text(json.dumps(dict(frames_checked=_replay_hud_checks,full_chrome_title=True,clock_frame_map_exact=True,coloured_bar_pixels_exact=True,dual_speed_pixels_exact=True))+'\n')
 _replay_hud_original_done(f)

if spec.get('penalty'):
 _penalty_replay_tick=test_tick
 def test_tick(f):
  if f==180:
   ww(0xc3a4,0x1680);ww(0xc3a6,0);ww(0xc3e6,0);wram[ram(0xc2ea)]|=5
  if f==1000:
   caption_row=romdata[0x12af0]if romdata[0x12af0]in[20,21]else 25
   assert words(vram(),caption_row,1,7)!=[0xa403]*7,'actual native PP caption missing'
   snap('native-power-play')
  if f>=1100:_penalty_replay_tick(f-1000)
