"""Read the verified original logical input bindings; no calls, hooks or writes."""
import argparse, ctypes as c, ctypes.wintypes as w, hashlib, json, struct
from pathlib import Path

p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--pid',type=int,required=True)
p.add_argument('--components',type=Path,required=True)
p.add_argument('--output',type=Path,required=True)
p.add_argument('--all',action='store_true',help='Include all original logical binding names for read-only input ownership analysis')
a=p.parse_args()
snapshot=json.loads(a.components.read_text(encoding='utf-8-sig'))
if snapshot['pid']!=a.pid:raise ValueError('Component process differs')
image=Path(r'D:/SteamLibrary/steamapps/common/Elite Dangerous/Products/elite-dangerous-odyssey-64/EliteDangerous64.exe')
if hashlib.sha256(image.read_bytes()).hexdigest()!=snapshot['gameSha256']:raise ValueError('Build changed')
k=c.WinDLL('kernel32',use_last_error=True)
k.OpenProcess.argtypes=[w.DWORD,w.BOOL,w.DWORD];k.OpenProcess.restype=w.HANDLE
k.CloseHandle.argtypes=[w.HANDLE]
k.QueryFullProcessImageNameW.argtypes=[w.HANDLE,w.DWORD,w.LPWSTR,c.POINTER(w.DWORD)]
k.ReadProcessMemory.argtypes=[w.HANDLE,c.c_void_p,c.c_void_p,c.c_size_t,c.POINTER(c.c_size_t)]
handle=k.OpenProcess(0x410,False,a.pid)
if not handle:raise c.WinError(c.get_last_error())
def read(at,size):
    data=c.create_string_buffer(size);done=c.c_size_t()
    if not k.ReadProcessMemory(handle,at,data,size,c.byref(done)) or done.value!=size:raise ValueError('Read unavailable')
    return data.raw
def u64(at):return struct.unpack('<Q',read(at,8))[0]
try:
    process_name=c.create_unicode_buffer(32768);size=w.DWORD(len(process_name))
    if not k.QueryFullProcessImageNameW(handle,0,process_name,c.byref(size)) or Path(process_name.value).resolve()!=image.resolve():raise ValueError('Process image differs')
    base=int(snapshot['base'],16)
    entries=[x for x in snapshot['routeBindings'] if x.get('name')=='launchAssist' and x.get('owner')=='ship']
    if len(entries)!=1:raise ValueError('Original ship input owner is not unique')
    computer=int(entries[0]['address'],16);service=int(entries[0]['inputBindings']['address'],16)
    if u64(computer)!=base+0x5165288 or u64(computer+0x230)!=service or u64(service)!=base+0x512FD90 or u64(base+0x512FD90+0x40)!=base+0x8FEE20:raise ValueError('Original input interfaces changed')
    buckets=u64(service+0xC0);count=u64(service+0xC8)
    if not buckets or not 1<=count<=4096:raise ValueError('Binding map size outside bound')
    seen=set();rows=[]
    for i in range(count):
        node=u64(buckets+i*8)
        while not buckets<=node<buckets+count*8:
            if not node or node in seen:break
            if len(seen)>=4096:raise ValueError('Binding traversal exceeds bound')
            seen.add(node);binding=u64(node+0x10);header=read(binding,0x38);name_ptr=u64(node+8)
            if not name_ptr:
                node=u64(node);continue
            try:length=u64(name_ptr)
            except ValueError as error:raise ValueError(f'Unexpected binding node {node:#x}, words {struct.unpack("<3Q",read(node,24))}, header {header[:24].hex()}') from error
            if not 1<=length<=128:raise ValueError('Binding name size outside bound')
            name=read(name_ptr+0x14,length).decode('ascii')
            if not all(32<=ord(x)<127 for x in name):raise ValueError('Unexpected binding name')
            if a.all or name.startswith('UI_') or name in ('SelectTarget','StationServices','OpenCodex','GalaxyMapOpen','PlayerHUDModeToggle'):
                rows.append({'name':name,'address':hex(binding),'current':list(struct.unpack_from('<3i',header,0x18)),
                             'forced':bool(header[0x24]),'previous':list(struct.unpack_from('<3i',header,0x28)),'previousForced':bool(header[0x34])})
            node=u64(node)
    if u64(service+0xC0)!=buckets or u64(service+0xC8)!=count:raise ValueError('Binding map changed while reading')
    result={'pid':a.pid,'base':hex(base),'service':hex(service),'bindings':rows,'total':len(seen),'gameInputsSent':0,'processAccess':'QUERY_INFORMATION | VM_READ'}
    with a.output.open('x',encoding='utf-8') as out:json.dump(result,out,indent=2)
    print(json.dumps(result if not a.all else {key:value for key,value in result.items() if key!='bindings'}))
finally:k.CloseHandle(handle)
