#!/usr/bin/env python3
"""Probe original flight arithmetic; no DOS I/O or full-game emulation.

Requires Unicorn and Capstone (PYTHONPATH=/tmp/darker-python locally).
Uses original startup table generation and its sine-table extension; map/craft states are fixtures.
"""
from pathlib import Path
import hashlib, json, random, struct
from trace_hidden_commands import Harness
from unpack_exe import IMAGE_SHA256
ROOT=Path(__file__).resolve().parents[1]
IMAGE=(ROOT/'analysis/unpacked/image.bin').read_bytes()
assert hashlib.sha256(IMAGE).hexdigest()==IMAGE_SHA256
OUT=ROOT/'analysis/flight-physics'
BP=0xD986
_init=Harness(IMAGE)
_init.setreg('ES',0x6000)
_init.cpu.emu_start(_init.BASE+0x396,_init.BASE+0x3AB,count=5000)
assert _init.getreg('IP')==0x3AB
LOOKUP=bytes(_init.cpu.mem_read(0x6A800,256))

def s16(x):
    return (x+32768)%65536-32768

def fresh():
    h=Harness(IMAGE);h.setreg('BP',BP)
    # 9D6E..9D79 sets up the copy, performed by REP MOVSW at 03DC.
    h.write(0x9C4A,IMAGE[0x944A:0x964A])
    return h

def put(h,off,v):
    h.cpu.mem_write(h.STACK+BP+off,struct.pack('<H',v&65535))

def word(h,off):
    return int.from_bytes(h.cpu.mem_read(h.STACK+BP+off,2),'little')

def smoothed(old,target,dt):
    # IMUL signed high word; CMP/ADC adds one only when that word is zero.
    delta=(s16(target-old)*s16(dt))>>16
    new=(old+delta+(delta==0))&65535
    midpoint=(old+(s16(new-old)>>1))&65535
    return new,s16(midpoint)

def tower_reference(x,y,height,fx,fy,state,kind=1):
    if height>=0x2D60 or kind!=1:return 0
    tx=LOOKUP[x//256];ty=LOOKUP[y//256]
    if tx>=128 or ty>=128:return 0
    rx=((x-tx*256)*16+fx//16)-2048
    ry=((y-ty*256)*16+fy//16)-2048
    rz=2*height-4800
    denominator=max(256,(rx*rx+ry*ry+rz*rz)>>14)+1024
    return (state*0x10101)//denominator

def tower_probe(x,y,height,fx=0,fy=0,state=255,kind=1):
    h=fresh();h.write(0xFDF0,struct.pack('<H',0x6000))
    lut=LOOKUP
    h.cpu.mem_write(0x6A800,lut)
    tx=lut[x//256];ty=lut[y//256]
    h.cpu.mem_write(0x60000+ty*256+tx*2,bytes([kind,state]))
    for r,v in [('DX',x),('BX',y),('SI',height),('AX',fx+256*fy)]:h.setreg(r,v)
    h.call(0x8450)
    result=h.getreg('AX')
    expected=tower_reference(x,y,height,fx,fy,state,kind)
    assert result==expected,(x,y,height,fx,fy,state,result,expected)
    return result

def reserve_probe(source,cheat=False,engine=True):
    h=fresh();h.write(0x4552,bytes([engine]));h.setreg('AX',source)
    # Apply the original engine gate and charge-indicator calculation.
    h.cpu.emu_start(h.BASE+0x7F38,h.BASE+0x7F50,count=100)
    effective=h.getreg('DI');indicator=h.read(0x4550)[0]
    h.setreg('CX',1028) # Caero's transformed tick for input CX=8
    for offset in [0x7F8F,0x7FA6,0x8540]:h.write(offset,b'\0\0')
    if cheat:h.write(0x7F7B,b'\x89\xca')
    h.cpu.emu_start(h.BASE+0x7F6C,h.BASE+0x7FC5,count=1000)
    assert h.getreg('IP')==0x7FC5
    gain=(1028//2 if cheat else ((max(effective-250,0)*1028)>>17)) if effective>250 else 0
    if gain:gain+=1028>>8
    incoming=(min(effective,250)*257*1028)>>18
    transfer=min(incoming,1028>>2) if effective>=256 else 0
    get=lambda p:int.from_bytes(h.read(p,2),'little')
    assert (get(0x7F8F),get(0x7FA6),get(0x8540))==(incoming-transfer,transfer,gain)
    return dict(source=source,engine=engine,cheat=cheat,indicator=indicator,buffer=get(0x7F8F),energy=get(0x7FA6),boost=get(0x8540))

def beacon_fade_probe(dimming):
    h=fresh();h.write(0xFDF0,struct.pack('<H',0x6000))
    h.write(0x0BF0,struct.pack('<H',1000))
    h.write(0xC526,bytes([255 if dimming else 0]))
    # Script handler C470: horizontal run, packed lattice coordinate + count.
    h.write(0xE800,bytes([0x44,2]));h.setreg('SI',0xE800)
    h.cpu.emu_start(h.BASE+0xC470,h.BASE+0xC452,count=500)
    assert h.getreg('IP')==0xC452
    locations=[0x60000+36*256+36*2+1,0x60000+36*256+45*2+1]
    for at in locations:h.cpu.mem_write(at,bytes([255 if dimming else 0]))
    values=[]
    for elapsed in [0,1,64,128,192,255,256,300]:
        h.write(0x0BF0,struct.pack('<H',1000+elapsed))
        h.cpu.ctl_remove_cache(h.BASE+0xC510,h.BASE+0xC569)
        h.call(0xC510)
        states=[h.cpu.mem_read(at,1)[0] for at in locations]
        expected=255-min(elapsed,255) if dimming else min(elapsed,255)
        assert states==[expected,expected],(elapsed,states,expected)
        values.append(dict(elapsed_timer_units=elapsed,state=states[0]))
    return dict(mode='dim to zero' if dimming else 'restore to full',samples=values)

def main():
    OUT.mkdir(exist_ok=True);rng=random.Random(1995)
    city=(ROOT/'analysis/resources/00_068.bin').read_bytes()
    towers={(i%128,i//128) for i,v in enumerate(city) if v==1}
    assert towers=={(x,y) for y in range(0,128,9) for x in range(0,128,9)}
    center=36*256+128
    examples=[]
    for dx,dy,height,state,kind in [(0,0,2400,255,1),(256,0,2400,255,1),(512,0,2400,255,1),(0,0,0,255,1),(0,0,11615,255,1),(0,0,11616,255,1),(0,0,2400,128,1),(0,0,2400,0,1),(0,0,2400,255,2)]:
        value=tower_probe(center+dx,center+dy,height,state=state,kind=kind)
        examples.append(dict(x_offset_cells=dx/256,y_offset_cells=dy/256,height=height,state=state,type=kind,energy_value=value))
    for _ in range(200):
        tower_probe(center+rng.randint(-800,800),center+rng.randint(-800,800),rng.randrange(12000),rng.randrange(256),rng.randrange(256),rng.randrange(256))
    fades=[beacon_fade_probe(dimming) for dimming in [True,False]]
    reserves=[reserve_probe(s,cheat,engine) for s in [0,250,255,256,771,3276,13107] for cheat in [False,True] for engine in [False,True]]
    movement=[]
    for _ in range(200):
        old=rng.randrange(-900,900);target=rng.randrange(-900,900);dt=rng.randrange(1,4096)
        z=rng.randrange(1000,10000);fraction=rng.randrange(256)
        h=fresh();put(h,0x40,old);put(h,0xC,z);h.cpu.mem_write(h.STACK+BP+6,bytes([fraction]))
        h.setreg('AX',target&65535);h.setreg('CX',dt);h.call(0x826E)
        new,mid=smoothed(old,target,dt)
        # Original uses twice the signed 32-bit product; only its high 24 bits
        # are accumulated into the 16-bit position plus 8-bit fraction.
        expected=((z<<8)+fraction+((mid*dt*2)>>8))&0xFFFFFF
        actual=(word(h,0xC)<<8)+h.cpu.mem_read(h.STACK+BP+6,1)[0]
        assert word(h,0x40)==new and actual==expected
    # Horizontal helper at pitch zero, headings 0/90/180/270 degrees.
    sine=struct.unpack_from('<1024h',IMAGE,0x944A)
    for heading in [0,256,512,768]:
        h=fresh();old=300;dt=1028;target=500
        put(h,0x42,old);put(h,8,10000);put(h,10,10000)
        h.setreg('AX',target);h.setreg('CX',dt);h.setreg('DI',0);h.setreg('SI',heading*2)
        h.call(0x8208)
        projected=(target*sine[256]*2)>>16
        new,mid=smoothed(old,projected,dt)
        travel=-((mid*dt)>>16)
        dx=(sine[heading]*travel)>>8;dy=(sine[(heading+256)%1024]*travel)>>8
        actualx=(word(h,8)<<8)+h.cpu.mem_read(h.STACK+BP+4,1)[0]
        actualy=(word(h,10)<<8)+h.cpu.mem_read(h.STACK+BP+5,1)[0]
        assert word(h,0x42)==new and actualx==(10000*256+dx)&0xFFFFFF and actualy==(10000*256+dy)&0xFFFFFF, (heading,word(h,0x42),new,actualx,actualy,dx,dy)
        movement.append(dict(heading_degrees=heading*360/1024,speed=s16(new),delta_x_24bit=dx,delta_y_24bit=dy))
    # Subtle CMP/ADC behavior: zero error still increments the velocity by one.
    h=fresh();put(h,0x40,100);h.setreg('AX',100);h.setreg('CX',8);h.call(0x826E)
    assert word(h,0x40)==101
    from capstone import Cs, CS_ARCH_X86, CS_MODE_16
    cs=Cs(CS_ARCH_X86,CS_MODE_16)
    ranges=[('tower coordinate lookup initialization',0x396,0x3AB),('keyboard steering',0x7AD6,0x7CA2),('craft callbacks and movement',0x7E49,0x8412),('tower energy',0x8450,0x84D0),('reserve accumulation',0x8514,0x854E),('beacon script handlers',0xC442,0xC454),('beacon run setup',0xC467,0xC475),('beacon fade and initial outage list',0xC510,0xC582)]
    listing=['; Canonical image offsets; selected reachable regions, not a full disassembly.', '; Self-modifying immediate operands retain their INITIAL values.', '; 98/99 in 16-bit mode are CBW/CWD (normalised below).']
    for label,a,z in ranges:
        listing.append('\n; '+label)
        for i in cs.disasm(IMAGE[a:z],a):
            mnemonic={b'\x98':'cbw',b'\x99':'cwd'}.get(bytes(i.bytes),i.mnemonic)
            listing.append(f'{i.address:04X}  {bytes(i.bytes).hex():14} {mnemonic:8} {i.op_str}')
    (OUT/'selected-disassembly.asm').write_text('\n'.join(listing)+'\n')
    report=dict(image_sha256=IMAGE_SHA256,scope='Original 0396, 8450, reserve accounting, 826E, 8208, C470 and C510; startup sine extension; synthetic map/craft state plus Delphi lattice check; not full callbacks or real-time calibration',delphi_beacons=len(towers),tower_coordinate_lookup=list(LOOKUP),tower_checks=209,vertical_checks=200,horizontal_checks=4,zero_error_velocity_probe={'before':100,'target':100,'after':101},tower_examples=examples,beacon_fades=fades,reserve_checks=len(reserves),reserve_examples=reserves,heading_examples=movement)
    (OUT/'verification.json').write_text(json.dumps(report,indent=2)+'\n')
    print(f'Passed: {report["tower_checks"]} tower, {len(reserves)} reserve, 200 vertical, 4 horizontal, 1 rounding, and 2 beacon fade timelines')
if __name__=='__main__':main()
