"""Execute the unchanged glider rotation branch with private finite inputs.

The PE remains read/execute-only Unicorn data. No game process is opened.
Only private fixture fields change. The scratch ActorRotation uses the original
FLT_MAX fast path so the produced quaternion reaches CurrentTransform unchanged.
This does not assert the live player's template uses that speed or mode.
"""
import importlib.util
import json
import math
import struct
import sys
from pathlib import Path

sys.path.insert(0, "src/loader/runtime/profile-tools/dev/tools/enshrouded")
from discovery.pe import PE
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
from unicorn import UC_HOOK_CODE
from unicorn.x86_const import *

directory = Path(__file__).parent
pe = PE(r"C:\Program Files (x86)\Steam\steamapps\common\Enshrouded\enshrouded.exe")
assert pe.sha256 == "4d6d43ea4a91e71c82d60b045dde932179c99e32e40657efe374688b2933589a"
spec = importlib.util.spec_from_file_location("attribute_oracle",
    "src/loader/runtime/profile-tools/dev/tools/enshrouded/verify-attribute-vm.py")
vm = importlib.util.module_from_spec(spec)
spec.loader.exec_module(vm)
oracle = vm.Oracle(pe)
u = oracle.uc
frame, stack = oracle.STACK + 0x6000, oracle.STACK + 0x1000
transform, player, actor_input = (oracle.SCRATCH + n for n in (0x1000, 0x2000, 0x3000))
item, actor, rotation = (oracle.SCRATCH + n for n in (0x4000, 0x6000, 0x7000))
observed = {}


def write(address, fmt, *values):
    u.mem_write(address, struct.pack(fmt, *values))


def vector(value):
    return int.from_bytes(struct.pack("<4f", value, 0, 0, 0), "little")


def scalar(register):
    return struct.unpack("<f", u.reg_read(register).to_bytes(16, "little")[:4])[0]


def pitch_degrees(q):
    x, y, z, w = q
    return math.degrees(math.asin(max(-1, min(1, 2 * (w*x - y*z)))))


def observe(_u, address, _size, _data):
    if address == pe.base + 0x26ae36:
        observed.update(clampedTarget=scalar(UC_X86_REG_XMM6),
                        originalCurrent=scalar(UC_X86_REG_XMM14),
                        inputTarget=scalar(UC_X86_REG_XMM8))
    elif address == pe.base + 0x26b06a:
        observed["finalMatrixPitch"] = scalar(UC_X86_REG_XMM6)


u.hook_add(UC_HOOK_CODE, observe, begin=pe.base + 0x26ae36, end=pe.base + 0x26ae36)
u.hook_add(UC_HOOK_CODE, observe, begin=pe.base + 0x26b06a, end=pe.base + 0x26b06a)


def step(quaternion, rate, input_z, input_mode=0, rotation_speed=None, interpolation_mode=1, dt=1/60):
    observed.clear()
    u.mem_write(oracle.SCRATCH, bytes(0x10000))
    u.mem_write(oracle.STACK, bytes(0x10000))
    for offset, value in ((0x50, actor), (-0x60, actor), (-0x38, rotation),
                          (0x80, transform), (-0x28, actor_input), (0x10, player)):
        write(frame + offset, "<Q", value)
    write(stack + 0x60, "<Q", transform)
    write(frame + 0x1688, "<f", dt)
    write(transform + 0x18, "<4f", *quaternion)
    write(actor_input + 0x18c, "<3f", 0, 0, input_z)
    write(player + 0x2c8, "<B", input_mode)
    write(item + 0x4d0, "<3f", 55, rate, 145)
    if rotation_speed is None:
        rotation_speed = struct.unpack("<f", bytes.fromhex("ffff7f7f"))[0]
    write(rotation + 0x50, "<fB", rotation_speed, interpolation_mode)
    for register, value in ((UC_X86_REG_RDI, item), (UC_X86_REG_RBP, frame),
                            (UC_X86_REG_RSP, stack), (UC_X86_REG_XMM15, vector(1)),
                            (UC_X86_REG_XMM12, 0),
                            (UC_X86_REG_XMM13, int.from_bytes(struct.pack("<4I", *[0x80000000] * 4), "little"))):
        u.reg_write(register, value)
    u.emu_start(pe.base + 0x26a845, pe.base + 0x26b236, timeout=100000, count=100000)
    assert u.reg_read(UC_X86_REG_RIP) == pe.base + 0x26b236
    target = struct.unpack("<4f", u.mem_read(actor + 0xec4, 16))
    if rotation_speed == 0:
        # Original early exit goes straight to next entity. Stop before its
        # inaccessible scheduler fixture, with the transform left unchanged.
        end = 0x26f711
    elif rotation_speed == struct.unpack("<f", bytes.fromhex("ffff7f7f"))[0]:
        end = 0x26a2da
    else:
        end = 0x26eb71
    u.emu_start(pe.base + 0x26a245, pe.base + end, timeout=100000, count=100000)
    assert u.reg_read(UC_X86_REG_RIP) == pe.base + end
    final = struct.unpack("<4f", u.mem_read(transform + 0x18, 16))
    result = dict(observed, rate=rate, inputZ=input_z, inputMode=input_mode,
                  targetQuaternion=target, currentTransformQuaternion=final,
                  targetPitchDegrees=pitch_degrees(target),
                  finalPitchDegrees=pitch_degrees(final))
    if rotation_speed == struct.unpack("<f", bytes.fromhex("ffff7f7f"))[0]:
        assert final == target
    assert abs(result["targetPitchDegrees"] - math.degrees(result["finalMatrixPitch"])) < 0.002
    return result


def quaternion(pitch_degrees):
    angle = math.radians(pitch_degrees) / 2
    return (math.sin(angle), 0, 0, math.cos(angle))


cases = []
for current in (-75, -30, 0, 5, 20, 80):
    for rate in (-70, 0, 70):
        for input_z in (-1, 0, 1):
            for mode in (0, 1):
                result = step(quaternion(current), rate, input_z, mode)
                cases.append(dict(initialPitchDegrees=current, **result))
                assert math.degrees(result["clampedTarget"]) >= 4.99999
                assert math.degrees(result["clampedTarget"]) <= 80.00001

trajectory = []
q = quaternion(5)
for phase, rate, input_z, count in (("up", -70, -1, 60), ("hold", 0, 0, 20),
                                    ("down", 70, 1, 40)):
    for tick in range(count):
        result = step(q, rate, input_z)
        q = result["currentTransformQuaternion"]
        trajectory.append(dict(phase=phase, tick=tick, **result))
assert -66 < trajectory[59]["finalPitchDegrees"] < -64
assert abs(trajectory[79]["finalPitchDegrees"] - trajectory[59]["finalPitchDegrees"]) < 0.01
assert trajectory[-1]["finalPitchDegrees"] > trajectory[79]["finalPitchDegrees"] + 46

interpolation = []
for mode in (0, 1, 2, 3):
    for speed in (-70, 0, 70):
        interpolation.append(dict(rotationSpeed=speed, interpolationMode=mode,
            **step(quaternion(0), 70, 0, rotation_speed=speed, interpolation_mode=mode)))

# Use the independently captured real player's template, not the FLT_MAX fixture.
capture = json.loads((directory / "player-glider-rotation-template-live-20261016.json").read_text())
config = capture["players"][0]["configs"][0]
assert capture["image"]["sha256"] == pe.sha256
assert config["gliderRotationSpeed"] == 200 and config["gliderInterpolationMode"] == 1
sampled = []
q, time, next_update, rate, tick = quaternion(5), 0.0, 0.0, 70, 0
while time < 3.0:
    dt = (1/30, 1/60, 1/144, 1/90)[tick % 4]
    input_z = -1 if time < 2 else (0 if time < 2.3 else 1)
    if time >= next_update:
        pitch = pitch_degrees(q)
        rate = -min(70 * -input_z, max(0, (pitch + 85) / 0.25)) if input_z < 0 else (0 if input_z == 0 else 70)
        next_update = time + 0.016
    result = step(q, rate, input_z, rotation_speed=config["gliderRotationSpeed"],
                  interpolation_mode=config["gliderInterpolationMode"], dt=dt)
    q = result["currentTransformQuaternion"]
    sampled.append(dict(time=time, dt=dt, **result))
    time, tick = time + dt, tick + 1
assert min(frame["finalPitchDegrees"] for frame in sampled) >= -85.01
assert min(frame["finalPitchDegrees"] for frame in sampled) < -80
held = [frame["finalPitchDegrees"] for frame in sampled if 2.05 < frame["time"] < 2.3]
assert max(held) - min(held) < 0.01
assert sampled[-1]["finalPitchDegrees"] > min(held) + 40

c = Cs(CS_ARCH_X86, CS_MODE_64)
def disassembly(begin, end):
    return [dict(rva=hex(i.address), mnemonic=i.mnemonic, operands=i.op_str, bytes=i.bytes.hex())
            for i in c.disasm(pe.read(begin, end-begin), begin)]

output = dict(image=pe.identity(), processOpened=False, processWrites=False,
    nativeGameCalls=False, environment="Unchanged read/execute-only PE under Unicorn, private scratch data",
    fixture=dict(entry="0x26a845, after HangGliding/action/item gating",
                 output="Actor.rotationInterpolation.targetOrientation, then CurrentTransform.transform.rotation",
                 gravityDirection="Original null-component default (0,-1,0)",
                 actorRotation="Private FLT_MAX rotationSpeed/ExponentialLerp for direct-output cases",
                 timestepSeconds=1/60, optionalDynamicLocomotion=False, impulseComponent=False),
    originalCode=dict(inputAndClamp=disassembly(0x26abba,0x26ac6c),
                      clamp=disassembly(0x26ade8,0x26ae36),
                      postClampStep=disassembly(0x26aeae,0x26af01),
                      finalPitch=disassembly(0x26b05e,0x26b0be),
                      directOutput=disassembly(0x26a264,0x26a2df),
                      exponentialInterpolation=disassembly(0x26f0a6,0x26f104)),
    gridCases=cases, controlledPrivateTrajectory=trajectory, interpolationCases=interpolation,
    capturedPlayerSampledTrajectory=sampled,
    conclusions=dict(fixedIntermediateTargetClampUnchanged=True,
                     negativeFinitePitchRateCanProduceUpwardFinalOrientation=True,
                     zeroRateHoldsPitch=True,
                     negativeConstantRateIsNotAStableOrBidirectionallyControllableUnlock=True,
                     rateSignControlProducesPrivateUpHoldDownTrajectory=True,
                     oneToOneToolkitReplacementProven=False,
                     liveGameplayEffectProven=False),
    limitations=["Rate is stored in shared loaded ItemInfo, not per-player ECS state",
                 "Rate-sign steering is a new controller, not the original toolkit clamp behavior",
                 "No pitch-limit field is added to reflection by this finding",
                 "The sampled trajectory uses captured player rotation speed200, interpolation mode1, modifier1; its frame timing and callbacks remain private fixtures",
                 "No scheduler, gravity, collision, animation or flight-physics execution is covered",
                 "No game process or executable code was changed"])
out = directory / "glider-post-clamp-original-20261016.json"
out.write_text(json.dumps(output, indent=2), encoding="utf-8")
print(json.dumps(dict(gridCases=len(cases), trajectoryFrames=len(trajectory),
    interpolationCases=len(interpolation), capturedPlayerFrames=len(sampled), sampledMinimum=min(frame["finalPitchDegrees"] for frame in sampled), passed=True, upDegrees=trajectory[59]["finalPitchDegrees"],
    holdDegrees=trajectory[79]["finalPitchDegrees"], downDegrees=trajectory[-1]["finalPitchDegrees"], path=str(out))))
