#!/usr/bin/env python3
"""Find the SELECTED TARGET inside the player object, and the input that changes it.

The OPTIONS key-config screen lists `Change Target` and `Padlock Mode Toggle` as
real bindable actions (docs/re/flight-controls-runtime.md), but a button sweep
that watched the *ammo counters* could not see either — neither action fires a
weapon. Screenshots of the reticle were no better: the view keeps moving, so
"did the selection change" is not legible frame to frame.

Guest memory is legible. If the craft holds a selected target, it holds a
**pointer to that target's object**, and every live entity's address is already
known from the entity scan. So:

  1. enumerate live entities and their addresses;
  2. read a window of the player object and keep every word that points at one
     of them (allowing a small fixed delta, since a pointer to an object's base
     is not a pointer to its transform);
  3. tap each candidate input and see which of those words switches to a
     *different* entity.

The word that follows the button is the selection, and the button that moves it
is `Change Target`. Both answers come out of the same run, and neither depends
on reading pixels.

Usage: target_probe.py <config.json> [seconds_per_button]
"""
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
from flight_probe import Pad  # noqa: E402

BACK, FWD = 0x400, 0x1000
MAX_DELTA = 0x400          # how far below its transform an object's base may sit
BUTTONS = ["LB", "X", "B", "A", "LS", "RS", "BACK", "START", "Y", "RB"]


def main():
    cfg = json.load(open(sys.argv[1]))
    dwell = float(sys.argv[2]) if len(sys.argv) > 2 else 1.2
    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 = me[0][0]
    print(f"# player object at {gmem.primary_va(me_off):#010x}, "
          f"{len(ents)} live entities")

    # every address that could be a pointer to some entity
    ptr_map = {}
    for off, va in ents:
        pos_va = gmem.primary_va(off)
        if pos_va is None:
            continue
        for d in range(0, MAX_DELTA, 4):
            ptr_map.setdefault(pos_va - d, (W.defs[va], d, off))

    def window():
        b = os.pread(W.fd, BACK + FWD, me_off - BACK)
        return np.frombuffer(b, dtype=">u4").copy()

    # ---- global mode: the selection need not live in the player object at all.
    # Scan every mapped extent for words that hold an entity pointer, then see
    # which of THOSE follow a button. A word that is an entity pointer before
    # AND after, pointing at a different entity, is a selection by construction.
    keys = np.array(sorted(ptr_map), dtype=np.uint32)

    def global_ptrs():
        out = {}
        for a0, b0 in gmem.extents(W.fd, W.size):
            n = (b0 - a0) // 4 * 4
            if n < 64:
                continue
            arr = np.frombuffer(os.pread(W.fd, n, a0), dtype=">u4").astype(np.uint32)
            idx = np.flatnonzero(np.isin(arr, keys))
            for k in idx:
                out[a0 + int(k) * 4] = int(arr[k])
        return out

    # ---- delta mode: WHERE does an entity keep its target?
    # The AI ships clearly hold pointers to other entities, so the field is a
    # fixed offset from the transform — exactly the situation entities2.py
    # solved for the definition pointer. Tally, over many entities, the delta at
    # which a word points at *another* entity; the offset that repeats is the
    # field, and reading it on the PLAYER gives our selected target.
    if "--delta" in sys.argv:
        from collections import Counter
        votes, examples = Counter(), {}
        for off, va in ents:
            blob = os.pread(W.fd, 0x1000, max(0, off - 0x800))
            arr = np.frombuffer(blob[:len(blob) // 4 * 4], dtype=">u4")
            for i, w in enumerate(arr):
                hit = ptr_map.get(int(w))
                if hit and hit[2] != off:
                    d = i * 4 - 0x800
                    votes[d] += 1
                    examples.setdefault(d, (W.defs[va], hit[0]))
        print(f"# target-pointer delta candidates over {len(ents)} entities:")
        for d, n in votes.most_common(10):
            src, dst = examples[d]
            print(f"   pos{d:+#07x}  seen {n:4d}   e.g. {src[:26]:<26} -> {dst}")
        best = votes.most_common(1)
        if best:
            d = best[0][0]
            pw = struct.unpack(">I", os.pread(W.fd, 4, me_off + d))[0]
            hit = ptr_map.get(pw)
            print(f"\n# PLAYER at that delta: {pw:#010x} -> "
                  f"{hit[0] if hit else 'not an entity pointer'}")
        return

    a = window()
    cands = []
    for i, w in enumerate(a):
        hit = ptr_map.get(int(w))
        if hit and hit[2] != me_off:      # a self-pointer is not a target
            cands.append((i, hit[0], hit[1]))
    print(f"# {len(cands)} word(s) in the player object point at a live entity")
    for i, nm, d in cands[:30]:
        print(f"   pos{i * 4 - BACK:+#07x}  -> {nm}  (entity_va - {d:#x})")
    if not cands:
        print("#   none — widen BACK/FWD or MAX_DELTA, or nothing is selected")

    # ---- which input moves the selection?
    pad = Pad()
    if "--global" in sys.argv:
        print("\n# GLOBAL scan: every word in RAM that holds an entity pointer")
        g0 = global_ptrs()
        print(f"#   {len(g0)} entity-pointer words in RAM")
        for btn in BUTTONS:
            pad.f.write(f"tap {btn} 200\n")
            time.sleep(dwell)
            g1 = global_ptrs()
            moved = [(o, g0[o], g1[o]) for o in g0
                     if o in g1 and g1[o] != g0[o]]
            named = []
            for o, v0, v1 in moved[:4]:
                n0 = ptr_map.get(v0, ("?",))[0]
                n1 = ptr_map.get(v1, ("?",))[0]
                named.append(f"{gmem.primary_va(o):#010x} {n0[:16]}->{n1[:16]}")
            print(f"  [{btn:<5}] {len(moved):4d} switched   " + "  ".join(named),
                  flush=True)
            g0 = g1
        pad.reset()
        return
    print("\n# tapping each input; a word that switches to a DIFFERENT entity is"
          " the selection")
    for btn in BUTTONS:
        before = window()
        pad.f.write(f"tap {btn} 200\n")
        time.sleep(dwell)
        after = window()
        moved = []
        for i, nm, d in cands:
            if before[i] == after[i]:
                continue
            hit = ptr_map.get(int(after[i]))
            moved.append((i, nm, hit[0] if hit else f"{int(after[i]):#010x}"))
        tag = "  ".join(f"pos{i * 4 - BACK:+#x} {o[:18]}->{n[:18]}"
                        for i, o, n in moved[:3])
        print(f"  [{btn:<5}] {len(moved):2d} changed  {tag}", flush=True)
    pad.reset()


if __name__ == "__main__":
    main()
