"""Read-only libretro video capture hook for test_core.py; native stereo is GPGX_WAV.
No game-memory, actor, puck, clock, score or RNG writes. Separate private instance.
"""
import atexit,subprocess,json,hashlib
from pathlib import Path
class DemoGeometry(C.Structure):
 _fields_=[('base_width',C.c_uint),('base_height',C.c_uint),('max_width',C.c_uint),('max_height',C.c_uint),('aspect',C.c_float)]
class DemoTiming(C.Structure):
 _fields_=[('fps',C.c_double),('sample_rate',C.c_double)]
class DemoAV(C.Structure):
 _fields_=[('geometry',DemoGeometry),('timing',DemoTiming)]
demo_av=DemoAV();core.retro_get_system_av_info(C.byref(demo_av))
demo_path=Path(os.environ['NHL94_VIDEO_FILE']);demo_start=int(os.environ.get('GPGX_AUDIO_FROM','100'));demo_count=0;demo_last=None;demo_encoder=None;demo_error=None
demo_geometry=[];demo_last_size=None
demo_log=demo_path.with_suffix('.encoder.log').open('wb')
def demo_vid(data,width,height,pitch):
 global demo_count,demo_last,demo_encoder,demo_error,demo_last_size
 try:
  if data:frame[0]=(C.string_at(data,pitch*height),width,height,pitch)
  if f<demo_start:return
  assert pixfmt[0] in[1,2],pixfmt[0]
  bpp=4 if pixfmt[0]==1 else 2
  if data:
   raw=C.string_at(data,pitch*height)
   if (width,height,pixfmt[0])!=demo_last_size:
    demo_last_size=(width,height,pixfmt[0]);demo_geometry.append(dict(frame=f,width=width,height=height,pixel_format=pixfmt[0]))
   # Native H32/H40 screens switch256/320 pixels. Normalize callback frames
   # before the fixed rawvideo pipe; preserve the complete visible native frame.
   if (width,height)!=(320,224):
    if pixfmt[0]==1:im=Image.frombuffer('RGBA',(width,height),raw,'raw','BGRA',pitch,1).convert('RGB')
    else:
     px=bytearray()
     for yy in range(height):
      for(v,)in struct.iter_unpack('<H',raw[yy*pitch:yy*pitch+width*2]):px+=bytes(((v>>11)<<3,((v>>5)&63)<<2,(v&31)<<3))
     im=Image.frombytes('RGB',(width,height),bytes(px))
    demo_last=im.resize((320,224),Image.Resampling.NEAREST).tobytes();demo_pipe_fmt='rgb24'
   else:
    # Always use one stable RGB24 pipe format across native geometry switches.
    if pixfmt[0]==1:im=Image.frombuffer('RGBA',(width,height),raw,'raw','BGRA',pitch,1).convert('RGB')
    else:
     im=Image.frombytes('RGB',(width,height),raw,'raw','BGR;16',pitch,1)
    demo_last=im.tobytes();demo_pipe_fmt='rgb24' 
  if demo_last is None:return
  if demo_encoder is None:
   fmt='rgb24'
   demo_encoder=subprocess.Popen(['ffmpeg','-v','error','-y','-f','rawvideo','-pixel_format',fmt,'-video_size','320x224','-framerate',str(demo_av.timing.fps),'-i','pipe:0','-an','-c:v','ffv1','-threads','2',str(demo_path)],stdin=subprocess.PIPE,stderr=demo_log)
  demo_encoder.stdin.write(demo_last);demo_count+=1
 except Exception as e:demo_error=repr(e)
demo_callback=VID(demo_vid);core.retro_set_video_refresh(demo_callback)
def demo_finish():
 if demo_encoder:
  demo_encoder.stdin.close();rc=demo_encoder.wait(timeout=60)
 else:rc=-1
 demo_log.close()
 import wave
 if os.environ.get('GPGX_WAV') and not Path(os.environ['GPGX_WAV']).exists():
  with wave.open(os.environ['GPGX_WAV'],'wb') as wav:
   wav.setnchannels(2);wav.setsampwidth(2);wav.setframerate(round(demo_av.timing.sample_rate));wav.writeframes(b''.join(audio_chunks))
 result=dict(status='passed'if rc==0 and not demo_error and demo_count else'failed',frames=demo_count,fps=demo_av.timing.fps,sample_rate=demo_av.timing.sample_rate,seconds=demo_count/demo_av.timing.fps,encoder_returncode=rc,error=demo_error,native_geometry=demo_geometry,ROM_sha256=hashlib.sha256(romdata).hexdigest(),core_sha256=hashlib.sha256(Path(CORE).read_bytes()).hexdigest(),scope='Actual libretro callbacks from the unmodified released ROM. Only physical pad inputs and authenticated savestate load; no scene or outcome memory writes. Native audio separately captures the same callback range.')
 demo_path.with_suffix('.capture.json').write_text(json.dumps(result,indent=2)+'\n')
atexit.register(demo_finish)
demo_input_hook=os.environ.get('NHL94_INPUT_HOOK')
if demo_input_hook:
 exec(compile(Path(demo_input_hook).read_text(),demo_input_hook,'exec'),globals())
 demo_input_tick=test_tick
else:demo_input_tick=lambda f:None
demo_inputs=[];demo_previous_inputs=None
def test_tick(f):
 global demo_previous_inputs
 demo_input_tick(f)
 current=sorted((port,button)for(port,button),remaining in held.items()if remaining>0)
 if current!=demo_previous_inputs:
  demo_inputs.append(dict(frame=f,held=current));demo_previous_inputs=current
  demo_path.with_suffix('.inputs.json').write_text(json.dumps(demo_inputs,indent=2)+'\n')

