"""Recover the original GalaxyMapControls graph without running the client.

The frozen pointer tables are decoded, rather than treating adjacent strings as
bindings. Native InputChord 913450/8D70E0 independently proves the required-on
and required-off lists; GalaxyMapCamera 3B36F10/3B507D0 proves their consumers.
"""
from pathlib import Path
import argparse
import hashlib
import json
import struct

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--verify', action='store_true')
args = parser.parse_args()
directory = Path('artifacts/galaxy-query-research/palette-resources')
key = '86ec03fba621526059fc6812faca6f10adcb63cb'
f = (directory / (key + '.fres')).read_bytes()
b = (directory / (key + '.indexed1')).read_bytes()
np = struct.unpack_from('<I', f, 0x34)[0]
nb = struct.unpack_from('<I', f, 0x38)[0] - struct.unpack_from('<I', f, 0x40)[0]
nr = struct.unpack_from('<I', f, 0x20)[0]
cursor = total = 0
while total < np:
    kind, count = struct.unpack_from('<HH', b, cursor)
    if kind not in (2, 3, 4):
        raise ValueError('Unexpected original frozen pool group')
    cursor += 4
    total += count
if total != np:
    raise ValueError('Invalid original pool count')
pools = [struct.unpack_from('<8I', b, cursor + i * 32) for i in range(np)]
cursor += np * 32
buffers = [struct.unpack_from('<IIHHIQQ', b, cursor + i * 32) for i in range(nb)]
cursor += nb * 32 + sum(x[3] for x in buffers) * 8
roots = [struct.unpack_from('<4I', b, cursor + i * 16) for i in range(nr)]
cursor += nr * 16
refs = {}
while cursor + 16 <= len(b):
    p, o, q, v = struct.unpack_from('<4I', b, cursor)
    if p >= np or q >= np or o >= pools[p][2] or v >= pools[q][2]:
        break
    if (p, o) in refs:
        raise ValueError('Duplicate original frozen reference')
    refs[p, o] = q, v
    cursor += 16
if b[cursor:cursor + 16] != bytes.fromhex('0000000000000000ffbf7f3f08040201'):
    raise ValueError('Original frozen delimiter changed')
start = cursor + 16


def at(pointer):
    p, o = pointer
    if p >= np or o < 0 or o >= pools[p][2]:
        raise ValueError('Original pointer exceeds pool')
    return start + pools[p][3] + o


def label(pointer):
    pos = at(pointer)
    return b[pos:b.index(0, pos, pos + 256)].decode('ascii')


name_pos = f.index(b'galaxymapcontrols\0')
name_entry = f.find(struct.pack('<I', name_pos - 144), 43000, 50000)
if name_entry < 0:
    raise ValueError('Original named resource entry not found')
resource_id, flags = struct.unpack_from('<II', f, name_entry + 4)
if (resource_id, flags) != (0xE800A1BD, 0x10004):
    raise ValueError('Original GalaxyMapControls resource binding changed')
root = next(row for row in roots if row[:2] == (resource_id, 0x42B3EE19))
rp, ro = root[2:]
count = struct.unpack_from('<I', b, at((rp, ro)) + 8)[0]
if count != 100:
    raise ValueError('Original control graph node count changed')
table = refs[rp, ro]
nodes = []
for index in range(count):
    p, o = refs[table[0], table[1] + index * 8]
    # Final semaphore entries use a different layout and are not mouse axes.
    if (p, o + 16) not in refs:
        continue
    name = label(refs[p, o + 16])
    if not (name.startswith('Mouse') or name in (
        'CamPitch', 'CamYaw', 'CamCrane', 'CameraCraning', 'TranslateX', 'TranslateY')):
        continue
    row = dict(index=index, pointer=[p, o], name=name,
               serializedKind=struct.unpack_from('<I', b, at((p, o)) + 8)[0])
    if (p, o + 24) in refs:
        row['source'] = label(refs[p, o + 24])
    if p == 494:
        for offset, field in ((32, 'requiredOn'), (48, 'requiredOff')):
            length = struct.unpack_from('<I', b, at((p, o)) + offset + 8)[0]
            ptr = refs.get((p, o + offset))
            if length > 8 or (length and ptr is None):
                raise ValueError('Invalid original InputChord conditions')
            row[field] = [label(refs[ptr[0], ptr[1] + i * 8]) for i in range(length)]
    if name in ('MouseXAxisScaled', 'MouseYAxisScaled', 'MouseZAxisScaled'):
        row['factor'] = struct.unpack_from('<f', b, at((p, o)) + 36)[0]
    nodes.append(row)


def require(name, source, on=None, off=None):
    matches = [x for x in nodes if x['name'] == name and x.get('source') == source
               and (on is None or x.get('requiredOn') == on)
               and (off is None or x.get('requiredOff') == off)]
    if len(matches) != 1:
        raise ValueError('Original mouse binding changed: ' + name)


require('MouseLeft', 'UI_Mouse_Button0')
require('MouseCrane', 'UI_Mouse_Button1')
require('MouseRotate', 'MouseLeft', [], ['MouseIsOverGUI'])
require('MouseTranslate', 'MouseRotate', ['MouseCrane'], ['IsInSystemMap'])
require('MouseCameraCraning', 'MouseCrane', [], ['MouseRotate'])
require('MouseCamPitch', 'MouseYAxisScaled', ['MouseRotate'], ['MouseTranslate'])
require('MouseCamYaw', 'MouseXAxisScaled', ['MouseRotate'], ['MouseTranslate', 'MouseIsOverGUI'])
require('MouseCamYaw', 'MouseXAxisScaled', ['MouseCrane'], ['MouseTranslate', 'MouseIsOverGUI'])
require('MouseCamCrane', 'MouseYAxisScaled', ['MouseCrane'], ['MouseTranslate', 'MouseIsOverGUI'])
require('MouseTranslateX', 'MouseXAxisInverted', ['MouseTranslate'], [])
require('MouseTranslateY', 'MouseYAxisScaled', ['MouseTranslate'], [])
require('MouseTranslate', 'UI_Mouse_Button0', ['IsInSystemMap'], ['PlanetaryViewOn', 'MouseIsOverGUI'])
result = dict(
    sourceResource=key,
    sourceSha256='29a90189d0bb238c88d09dcc0f15ff751ea29aa208b1a90894ef3b47f8942e5f',
    frozenSha256=hashlib.sha256(b).hexdigest(), fresSha256=hashlib.sha256(f).hexdigest(),
    pinnedGameSha256='6891f70a021fa9f3a0e00458c284f48d1226e68ae7fc02ad48081aed47857e93',
    gameProcessAccess=False, gameCalls=0, networkRequests=0,
    resourceName='galaxymapcontrols', resourceId=hex(resource_id),
    root=list(root), count=count, nodes=nodes,
    nativeEvidence={
        'inputChordBind': '913450: config+10 output, +18 source, +20/+28 required-on, +30/+38 required-off',
        'inputChordEvaluate': '8D70E0: AND all +24 active bits of on inputs and AND !active of off inputs before forwarding source',
        'inputChordCsmBinding': '87800 -> 5F32440 = 512CBF8 -> 8D70E0; init 913450 uses 5F32448',
        'invert': '87890 -> 5F326E0 = 512CBE0 -> 8D6F20: source float XOR sign bit',
        'cameraBindings': '3B36F10 component base+8 -> CamPitch/CamYaw/CameraCraning/CamCrane/CamDolly/TranslateX/TranslateY',
        'cameraUpdate': '3B507D0 camera base+70: +410 pitch, +418 yaw, +428 crane, +438 translateX, +440 translateY',
    },
    interpretation={
        'leftOnly': {'x': 'yaw', 'y': 'pitch'},
        'rightOnly': {'x': 'yaw', 'y': 'world height/crane'},
        'leftAndRight': {'x': 'horizontal-plane translation from inverted mouse X', 'y': 'horizontal-plane translation from mouse Y'},
        'systemMap': 'separate Button0 translation branch; must not replace Galaxy rules',
        'scope': 'original frozen control graph plus pinned original native consumers; no live cursor/acceleration validation',
    })
output = Path('artifacts/galaxy-query-research/power-outer-sources/original-galaxy-mouse-controls.json')
encoded = json.dumps(result, ensure_ascii=False, indent=2) + '\n'
if args.verify:
    if output.read_text(encoding='utf-8') != encoded:
        raise ValueError('Recorded original mouse evidence does not match current local resource')
else:
    output.write_text(encoded, encoding='utf-8')
print(json.dumps(dict(verified=args.verify, nodes=len(nodes), count=count,
                     gameProcessAccess=False, output=str(output))))
