#!/usr/bin/env python3
"""Verify original FM detection and AWE32 FM sends with Unicorn 2.1.4.

No-SB cases model an absent DSP and optional AdLib OPL timer status.
Delay routines and low-level OPL writes are intercepted; original decisions run.
The AWE check executes the original FM initialiser, intercepts its register
helpers, and supplies the hardware sync bit. It does not synthesise audio.
"""
import hashlib,json,struct
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
from trace_hidden_commands import Harness
from unpack_exe import IMAGE_SHA256
from unicorn import UC_HOOK_INSN
from unicorn.x86_const import UC_X86_INS_IN,UC_X86_INS_OUT
image=(ROOT/'analysis/unpacked/image.bin').read_bytes()
assert hashlib.sha256(image).hexdigest() == IMAGE_SHA256
class Probe(Harness):
 def __init__(self,opl):
  self.opl=opl;self.status=0;self.writes=[];self.ports=[]
  super().__init__(image)
  self.cpu.hook_add(UC_HOOK_INSN,self.input,None,1,0,UC_X86_INS_IN)
  self.cpu.hook_add(UC_HOOK_INSN,self.output,None,1,0,UC_X86_INS_OUT)
 def ret(self):
  sp=self.getreg('SP');self.setreg('IP',int.from_bytes(self.cpu.mem_read(self.STACK+sp,2),'little'));self.setreg('SP',sp+2)
 def hook(self,cpu,address,size,data):
  off=address-self.BASE
  if off in (0x185b,0xecc4):self.ret()
  elif off==0x1185:
   self.setreg('DX',0x388);ax=self.getreg('AX');r=ax&255;v=ax>>8;self.writes.append([r,v])
   if r==4:self.status=0xc0 if v==0x21 else 0
   self.ret()
 def input(self,cpu,port,size,data):
  return self.status if port==0x388 and self.opl else 255
 def output(self,cpu,port,size,value,data):self.ports.append([port,size,value])

class AWE(Harness):
 def __init__(self):
  self.writes=[];self.ports=[];self.poll=0
  super().__init__((ROOT/'analysis/resources/00_037.bin').read_bytes())
  self.write(0x1c,struct.pack('<H',0x620))
  self.cpu.hook_add(UC_HOOK_INSN,self.input,None,1,0,UC_X86_INS_IN)
  self.cpu.hook_add(UC_HOOK_INSN,self.output,None,1,0,UC_X86_INS_OUT)
 def hook(self,cpu,address,size,data):
  off=address-self.BASE
  if off not in (0x3218,0x32b2):return
  sp=self.getreg('SP');n=2 if off==0x3218 else 3
  words=struct.unpack('<'+'H'*(n+1),cpu.mem_read(self.STACK+sp,(n+1)*2))
  value=words[1] if n==2 else words[1]|words[2]<<16
  self.writes.append(dict(register=f'{words[n]:04x}',value=f'{value:08x}'))
  self.setreg('IP',words[0]);self.setreg('SP',sp+(n+1)*2)
 def input(self,cpu,port,size,data):
  assert port==0xe22
  self.poll^=0x1000;return self.poll
 def output(self,cpu,port,size,value,data):self.ports.append([port,size,value])

rows=[]
for opl in (False,True):
 for music in (255,0,1,2,3,4):
  p=Probe(opl);p.write(0xab4,bytes([music]));p.call(0xeae2)
  flag=p.read(0xe81)[0];n=len(p.writes);p.call(0xedb)
  rows.append(dict(opl=opl,music_index=music,effects_hardware_flag=flag,initialise_effects_register_writes=len(p.writes)-n,other_output_ports=sorted(set(x[0] for x in p.ports))))
  if not opl:
   p.call(0x33f1)
   assert len(p.writes)==n
  assert flag==int(opl)
  assert (len(p.writes)>n)==opl


p=AWE();p.call(0x28e0)
writes={int(x['register'],16):int(x['value'],16) for x in p.writes}
for channel in (30,31):
 assert (writes[0x1000+channel] >> 8) & 255 == 0, 'FM reverb send is nonzero'
 assert writes[0x7000+channel] >> 24 == 0, 'FM chorus send is nonzero'
out=ROOT/'analysis/audio/hardware-effects'
out.mkdir(exist_ok=True)
(out/'detection.json').write_text(json.dumps(rows,indent=2)+'\n')
(out/'awe32-fm-initialisation.json').write_text(json.dumps(dict(register_writes=p.writes,direct_port_writes=p.ports),indent=2)+'\n')
print('Verified 12 no-SB detection cases, disabled live effects, and zero AWE32 FM effect sends.')
