#!/usr/bin/env python3
"""Fly TO a capital ship and dump a draw capture at several ranges.

Why this exists: the 2026-07-31 Stage-02 capture correlated **zero** parts, and
inverting the match (`cargo run --example invert_capture`) showed why — at the
captured frames no capital-ship hull was drawn at all. The only large draw was
the player's own craft (`DeltaSaber_T:f001`, 10891 verts); of `f101`/`e105`/
`e106` only a handful of tiny far-LOD/effect pieces appeared. The ships were
simply too far away. Pressing F10 wherever the craft happens to be is therefore
not a capture strategy.

So: pick a capital ship, fly at it, and press F10 as each distance band is
crossed. That gives (a) frames where the full-detail hull is actually drawn —
what the correlator needs — and (b) as a by-product, the game's own **LOD
ladder**, because each capture is stamped with the range it was taken at.

Firing is disabled (the target is usually a friendly), and navigator.py's
closest-point-of-approach avoidance is inherited unchanged, so closing on a hull
does not end in a collision.

Usage: approach_capture.py <config.json> [seconds] [--target REGEX] [--dry]
Env:   SYLPH_CAPTURE_WIN  xdotool window id to send F10 to (unset = no capture)
       SYLPH_CAPTURE_OUT  where to write the band log and screenshots
"""
import json
import math
import os
import re
import subprocess
import sys
import time
from collections import Counter

import numpy as np

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import navigator  # noqa: E402
from navigator import Navigator, ang, norm  # noqa: E402
from flight_probe import Pad  # noqa: E402

# Ranges (guest units) at which to dump a capture, largest first. Chosen to
# straddle the plausible LOD switches: the far-LOD pieces seen in the 2026-07-31
# capture were drawn at whatever range the craft sat at, and the one validated
# capture (e106, Stage_S01) had the ship close.
BANDS = [8000.0, 6000.0, 4500.0, 3000.0, 2000.0, 1400.0, 900.0]

# A capital ship, not a fighter: the definition's own size radius says which.
CAPITAL_RADIUS = 150.0


class Approach(Navigator):
    # Never shoot: the approach target is usually the escorted asset, and a
    # negative cone makes the inherited fire gate unsatisfiable.
    FIRE_CONE = -1.0
    HOLD = 700.0          # stop closing inside this; the capture is already made

    def __init__(self, W, pad, target_re=None, dry=False, log=sys.stdout,
                 win=None, out=None):
        super().__init__(W, pad, dry=dry, log=log)
        self.target_re = re.compile(target_re, re.I) if target_re else None
        self.win = win
        self.out = out or "/sylph-home/re/shipcap"
        self.locked = None        # (off, name) — stay on one ship
        self.pending = list(BANDS)
        self.captures = []
        self.throttle = None

    # -------------------------------------------------------------- target
    def pick(self, me_p, me_v, fwd, ents, me_off):
        """The chosen capital ship — locked once, so the run is one approach."""
        cands = [e for e in ents
                 if e[0] != me_off and "Player" not in e[1] and e[4] >= CAPITAL_RADIUS
                 and (self.target_re is None or self.target_re.search(e[1]))]
        if not cands:
            return None
        if self.locked is not None:
            same = [e for e in cands if e[0] == self.locked]
            if same:
                e = same[0]
                return (e[0], e[1], e[2], e[2] - me_p, float(np.linalg.norm(e[2] - me_p)))
        # First lock: the biggest ship that is not absurdly far.
        cands.sort(key=lambda e: (-e[4], float(np.linalg.norm(e[2] - me_p))))
        e = cands[0]
        self.locked = e[0]
        print(f"LOCK {e[1]} radius={e[4]:.0f} d={np.linalg.norm(e[2]-me_p):.0f}",
              file=self.log, flush=True)
        return (e[0], e[1], e[2], e[2] - me_p, float(np.linalg.norm(e[2] - me_p)))

    # ------------------------------------------------------------- capture
    def capture(self, band, dist, name):
        idx = len(self.captures) + 1
        shot = f"{self.out}/approach-{idx:02d}.png"
        if self.win:
            subprocess.run(["screenshot", shot], capture_output=True)
            subprocess.run(["xdotool", "key", "--window", self.win, "F10"],
                           capture_output=True)
        rec = {"index": idx, "band": band, "distance": round(dist, 1),
               "target": name, "shot": shot, "t": round(time.time(), 3)}
        self.captures.append(rec)
        print(f"CAPTURE {idx:02d} band={band:.0f} d={dist:.0f} {name}",
              file=self.log, flush=True)
        with open(f"{self.out}/approach-bands.jsonl", "a") as f:
            f.write(json.dumps(rec) + "\n")

    # ---------------------------------------------------------------- loop
    def step(self, t, dt, prev_vhat):
        msg, vhat = super().step(t, dt, prev_vhat)
        # Distance to the locked ship drives both the throttle and the captures.
        ents = self.W.sample(t)
        me = next((e for e in ents if "Player" in e[1]), None)
        tgt = next((e for e in ents if e[0] == self.locked), None) if self.locked else None
        if me is None or tgt is None:
            return msg, vhat
        d = float(np.linalg.norm(tgt[2] - me[2]))

        # Throttle: RT to close, LT to hold off once we are as near as we want.
        want = 1 if d > self.HOLD * 2 else (-1 if d < self.HOLD else 0)
        if want != self.throttle and not self.dry:
            self.pad.trig("RT", 1.0 if want > 0 else 0.0)
            self.pad.trig("LT", 1.0 if want < 0 else 0.0)
            self.throttle = want

        while self.pending and d <= self.pending[0]:
            band = self.pending.pop(0)
            self.capture(band, d, tgt[1])
        return f"{msg} | d={d:7.0f} thr={want:+d} left={len(self.pending)}", vhat


def main():
    cfg = json.load(open(sys.argv[1]))
    secs = float(sys.argv[2]) if len(sys.argv) > 2 and not sys.argv[2].startswith("-") else 240.0
    target = None
    if "--target" in sys.argv:
        target = sys.argv[sys.argv.index("--target") + 1]
    W = navigator.World(cfg)
    a = Approach(W, Pad(), target_re=target, dry="--dry" in sys.argv,
                 win=os.environ.get("SYLPH_CAPTURE_WIN"),
                 out=os.environ.get("SYLPH_CAPTURE_OUT"))
    a.run(secs)
    print(f"CAPTURES {json.dumps(a.captures)}", flush=True)


if __name__ == "__main__":
    main()
