#!/usr/bin/env python3
"""Locate the player's flight object via its speed scalar, then its position.

The HUD prints the craft's speed, so it is a known quantity we can *change on
demand* — the classic two-state value scan, which is far more reliable than
hunting for a structure by shape:

  1. coast  -> RAM holds the cruise speed somewhere; collect every float ≈ it;
  2. boost  -> the real one rises; everything coincidental is filtered out;
  3. coast  -> it must come back down.

Whatever survives all three is the player's speed. Its object then contains the
position, which is confirmed independently: a position triple near that scalar
must move at exactly the speed the scalar reports.

Usage: findspeed.py <cruise> [boost_wait]
"""
import os
import struct
import sys
import time

import numpy as np

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import gmem  # noqa: E402

FIFO = "/tmp/sylph-vgamepad.fifo"


def pad(line):
    with open(FIFO, "w") as f:
        f.write(line + "\n")


def extents_arrays(fd, size):
    for a, b in gmem.extents(fd, size):
        n = (b - a) // 4 * 4
        if n >= 64:
            yield a, np.frombuffer(os.pread(fd, n, a), dtype=">f4")


def scan_equal(fd, size, val, tol):
    hits = []
    for a, arr in extents_arrays(fd, size):
        with np.errstate(invalid="ignore"):
            ok = np.isfinite(arr) & (np.abs(arr.astype(np.float64) - val) <= tol)
        for i in np.flatnonzero(ok):
            hits.append(a + int(i) * 4)
    return hits


def read_f(fd, off):
    b = os.pread(fd, 4, off)
    return struct.unpack(">f", b)[0] if len(b) == 4 else float("nan")


def main():
    cruise = float(sys.argv[1])
    wait = float(sys.argv[2]) if len(sys.argv) > 2 else 4.0
    fd = os.open(gmem.mem_path(), os.O_RDONLY)
    size = os.path.getsize(gmem.mem_path())

    pad("reset")
    time.sleep(2.0)
    c0 = scan_equal(fd, size, cruise, 2.0)
    print(f"# pass 1 (coast {cruise}): {len(c0)} candidates", flush=True)

    pad("trig RT 1.0")
    time.sleep(wait)
    c1 = [o for o in c0 if read_f(fd, o) > cruise + 40]
    print(f"# pass 2 (boost): {len(c1)} rose above {cruise+40:.0f}", flush=True)
    vals = {o: read_f(fd, o) for o in c1}

    pad("reset")
    time.sleep(wait + 2.0)
    c2 = [o for o in c1 if abs(read_f(fd, o) - cruise) <= 6.0]
    print(f"# pass 3 (coast again): {len(c2)} returned to ≈{cruise}", flush=True)
    for o in c2[:20]:
        print(f"   va {gmem.primary_va(o):#010x}  boost value was {vals[o]:.1f}")

    if not c2:
        print("# no survivors — is the craft actually accelerating?")
        return

    # position triple in the same object: must move at exactly that speed
    print("\n# looking for a position triple near each survivor", flush=True)
    RAD = 0x400
    for o in c2[:8]:
        lo = max(0, o - RAD)
        n = RAD * 2
        t0 = time.time()
        a0 = np.frombuffer(os.pread(fd, n, lo), dtype=">f4").astype(np.float64)
        time.sleep(0.4)
        t1 = time.time()
        a1 = np.frombuffer(os.pread(fd, n, lo), dtype=">f4").astype(np.float64)
        sp_now = read_f(fd, o)
        dt = t1 - t0
        best = []
        for i in range(len(a0) - 2):
            d = a1[i:i + 3] - a0[i:i + 3]
            if not np.all(np.isfinite(d)):
                continue
            v = float(np.linalg.norm(d)) / dt
            if abs(v - sp_now) <= max(8.0, 0.06 * sp_now):
                best.append((lo + i * 4, v, tuple(a0[i:i + 3])))
        print(f"  survivor va {gmem.primary_va(o):#010x} (speed {sp_now:.1f}): "
              f"{len(best)} matching triples")
        for off, v, p in best[:4]:
            print(f"     pos va {gmem.primary_va(off):#010x} delta-off "
                  f"{off - o:+#07x}  |v|={v:7.1f}  ({p[0]:+9.1f},{p[1]:+9.1f},{p[2]:+9.1f})")


if __name__ == "__main__":
    main()
