#!/usr/bin/env python3
"""Run complete original tape-input routing, including post-read control policy.
Original input/stdio and mouse-queue insertion are explicit host boundaries.
The player world, modal ownership, and mouse callback execution are not supplied
by this packet-only comparison.
"""
import json,struct,random
from pathlib import Path
from native_snapshot import NativeSnapshot
from native_reference import ROOT
from unicorn import UC_HOOK_CODE
from unicorn.x86_const import UC_X86_REG_ESP
def run(output):
 rows=[];records=[]
 for demo in range(1,5):
  n=NativeSnapshot();raw=b'';position=0;mouse=[]
  def boundary(cpu,a,size,u):
   nonlocal position
   sp=cpu.reg_read(UC_X86_REG_ESP)
   if a==0x60a10:
    dest,width,count,handle=n.get(sp+4,'4I');amount=width*count
    value=raw[position:position+amount];assert len(value)==amount
    cpu.mem_write(dest,value);position+=amount;n.return_value(count,0)
   elif a==0x44830:
    time,ptr=n.get(sp+4,'2I');mouse.append(n.get(ptr,'iiHI'));n.return_value(0,2)
  for a in [0x60a10,0x44830]:n.cpu.hook_add(UC_HOOK_CODE,boundary,begin=a,end=a)
  data=(ROOT/f'work/research/tapes/demo{demo}/prefs.cfg').read_bytes();n.cpu.mem_write(0xbd562,data)
  n.put(0x95819,'B',data[8]);n.put(0xb5b76,'h',1);n.put(0xb5b78,'h',0);n.put(0x958be,'B',0);n.put(0xb5bd0,'h',0)
  n.put(0xb5b90,'h',0);n.put(0xb5bc6,'h',0);n.put(0x958bc,'4B',0,0,0,0);n.put(0x9580c,'3I',0,0,1);n.put(0xbd634,'8B',*([0]*8))
  frames=json.loads((ROOT/f'work/research/tapes/demo{demo}/frames.json').read_text());source=(ROOT/f'extracted/cd_1994_10/TAPES/DEMO{demo}.TAP').read_bytes()
  rec=bytearray(struct.pack('<BI',demo,len(frames)))
  notes=[]
  for i,f in enumerate(frames):
   end=frames[i+1]['offset']if i+1<len(frames)else len(source);raw=source[f['offset']:end];position=0;mouse=[]
   out=n.call(0x4cb14)
   assert position==len(raw)
   packet=bytes(n.cpu.mem_read(out,29));alias=n.get(out+29,'I')[0]-out;mode=n.get(0x95819,'B')[0]
   rec+=packet+struct.pack('<HBB',n.get(out+41,'H')[0],alias,mode)
   if any(packet[2:13]):notes.append(dict(frame=i,pointer=packet[2:13].hex()))
  records.append(rec);rows.append(dict(demo=demo,frames=len(frames),nonzero_pointer_packets=notes,initial_preferences=list(data)))
 # Exercise the post-read branches independently of the four tapes' defaults.
 rng=random.Random(0x4cb14);cases=bytearray();count=2048
 for i in range(count):
  mode=i%3;wcs=bool(i&4);external=bool(i&8);missile=bool(i&16)
  held=bool(i&32);back=bool(i&64);suppress=bool(i&128);throttle=rng.randrange(-256,256)
  n.put(0x95819,'B',mode);n.put(0xbd56c,'B',wcs);n.put(0xb5b90,'h',external);n.put(0xb5bc6,'h',missile)
  n.put(0x958bc,'B',held);n.put(0x95810,'I',back);n.put(0xbd634,'8B',suppress,0,0,0,suppress,0,0,0)
  n.put(0x95896,'h',throttle)
  packet=struct.pack('<H4hHH',rng.randrange(65536),*[rng.randrange(-32768,32768)for _ in range(4)],rng.randrange(65536),rng.randrange(4096))
  raw=packet+bytes(4);position=0;mouse=[];out=n.call(0x4cb14);assert position==len(raw)
  cases+=struct.pack('<7Bh',mode,wcs,external,missile,held,back,suppress,throttle)+packet
  cases+=bytes(n.cpu.mem_read(out,29))+struct.pack('<HBBB',n.get(out+41,'H')[0],n.get(out+29,'I')[0]-out,n.get(0x95819,'B')[0],n.get(0x958bc,'B')[0])
 output.with_name('tape_routing_cases.bin').write_bytes(b'ESTPCR01'+struct.pack('<I',count)+cases)
 output.write_bytes(b'ESTPPK01'+struct.pack('<I',4)+b''.join(records))
 (ROOT/'work/research/tapes/packet-routing.json').write_text(json.dumps(dict(scope=__doc__,tapes=rows,post_read_context_cases=count),indent=2)+'\n')
 print(sum(r['frames']for r in rows),'complete original tape packets;',sum(len(r['nonzero_pointer_packets'])for r in rows),'nonzero direct-pointer fields')
if __name__=='__main__':
 import argparse
 p=argparse.ArgumentParser();p.add_argument('--output',type=Path,required=True);run(p.parse_args().output)
