#!/usr/bin/env python3
"""Where does the craft keep its hull and shield? Anchor on the definition.

The parsed unit definition already has solved fields (unit-struct-runtime.md):
`HP` at `+0x054`, shield `MaxValue` at `+0x238`, shield `ChargeSpeed` at
`+0x244`. A craft that has taken no damage is at full hull and full shield, so
its live entity object must *contain those very numbers*. That turns "find the
HP field" from a value-scan over 4 GB into: read two floats from the definition,
then look for them inside the entity object.

Then watch the candidates while the craft is under fire. The live field is the
one that falls; a copy of the definition value that never moves is not it.

Usage: own_state.py <config.json> [watch_seconds] [out.json]
"""
import json
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
import navigator  # noqa: E402

DEF_HP, DEF_SHIELD_MAX, DEF_SHIELD_CHG = 0x054, 0x238, 0x244
BACK, FWD = 0x800, 0x800


def near(a, v):
    return np.abs(a - v) <= max(1e-3, abs(v) * 1e-4)


def main():
    cfg = json.load(open(sys.argv[1]))
    secs = float(sys.argv[2]) if len(sys.argv) > 2 else 40.0
    out = sys.argv[3] if len(sys.argv) > 3 else None

    W = navigator.World(cfg)
    ents = W.scan()
    me = [(off, va) for off, va in ents if "Player" in W.defs[va]]
    if not me:
        sys.exit("player entity not found — not in flight?")
    me_off, def_va = me[0]
    print(f"# player {W.defs[def_va]} pos off {me_off:#x} def {def_va:#010x}")

    d = os.pread(W.fd, 0x300, gmem.va_to_off(def_va))
    want = {}
    for nm, o in (("HP", DEF_HP), ("Shield_MaxValue", DEF_SHIELD_MAX),
                  ("Shield_ChargeSpeed", DEF_SHIELD_CHG)):
        (v,) = struct.unpack_from(">f", d, o)
        want[nm] = v
        print(f"#   definition {nm:<18} = {v:g}")

    blob = os.pread(W.fd, BACK + FWD, me_off - BACK)
    arr = np.frombuffer(blob, dtype=">f4").astype(np.float64)
    cands = []          # (label, delta_from_position)
    for nm, v in want.items():
        if not np.isfinite(v) or v == 0.0:
            continue
        for i in np.flatnonzero(near(arr, v)):
            cands.append((nm, int(i) * 4 - BACK))
    print(f"# {len(cands)} candidate offsets inside the entity object:")
    for nm, dlt in cands:
        print(f"   pos{dlt:+#07x}  == definition {nm}")
    if not cands:
        print("#   none — the object does not carry the definition's own numbers"
              " at this offset window; widen BACK/FWD or the craft is damaged")

    # ---- watch them; the live field is the one that moves
    hist = {c: [] for c in cands}
    t0 = time.time()
    last_print = 0.0
    while time.time() - t0 < secs:
        p = W.pos(me_off)
        if p is None:
            print("# player object gone (death / stage change?)")
            break
        w = os.pread(W.fd, BACK + FWD, me_off - BACK)
        a = np.frombuffer(w, dtype=">f4").astype(np.float64)
        for c in cands:
            i = (c[1] + BACK) // 4
            hist[c].append(float(a[i]))
        t = time.time() - t0
        if t - last_print > 4.0:
            last_print = t
            cur = " ".join(f"{c[0][:4]}{c[1]:+#x}={hist[c][-1]:.1f}" for c in cands[:6])
            print(f"[{t:6.1f}] {cur}", flush=True)
        time.sleep(0.2)

    print("\n# offset          anchor              first     last      min   moved")
    moving = []
    for c in cands:
        h = np.array(hist[c])
        if not len(h):
            continue
        mv = float(h.max() - h.min())
        print(f"  pos{c[1]:+#07x}  {c[0]:<18} {h[0]:8.1f} {h[-1]:8.1f} "
              f"{h.min():8.1f} {mv:7.3f}")
        if mv > 1e-3:
            moving.append({"anchor": c[0], "delta": c[1],
                           "first": h[0], "last": float(h[-1]),
                           "min": float(h.min())})
    if not moving:
        print("# nothing moved — the craft took no damage during the window")
    if out:
        json.dump({"player": W.defs[def_va], "def_va": def_va,
                   "definition": want,
                   "candidates": [{"anchor": a, "delta": b} for a, b in cands],
                   "moved": moving}, open(out, "w"), indent=1)
        print(f"# wrote {out}")


if __name__ == "__main__":
    main()
