#!/usr/bin/env bash
# Which button is focused in a screenshot of the real game?
#
#   tools/port/which-focus SHOT.png              # main_menu (5 buttons)
#   tools/port/which-focus SHOT.png extras       # extras (3 buttons)
#
# Renders the port's own screen with each button focused in turn and reports
# which one differs least from the shot. Answers a question the Decoder needs to
# drive the game -- `newgame_path.sh` assumed NEW GAME is focused at boot, drove
# on that, and landed in a tutorial mission, because HANDOFF Q5 measured focus as
# UNSTABLE across boots. Counting presses cannot substitute: up from the first
# item wraps to the last, so no fixed number of presses lands on a known item
# from an unknown start.
#
# ⚠️ IT RUNS ITS OWN CONTROL FIRST AND REFUSES TO ANSWER IF THE CONTROL FAILS.
# `docs/re/captures/title-builds/live-main-menu-options-focused.png` has the
# answer in its filename, so the method can be tested on every invocation rather
# than once when it was written. A brightness-per-row detector was tried for this
# job and picked NEW GAME on that capture; this method picks OPTIONS by 4.7x.
# A control that does not execute is not a control.
#
# 🔴 IT NEEDS GODOT AND THE PORT'S EXPORT TREE, so it does NOT run in the RE
# container -- no engine there, and rendering this project is outside that
# agent's role. It reads a capture, but it answers by RENDERING the candidates.
# `tools/re-capture/focus_from_capture.py` is the capture-only alternative; note
# that its offline controls are its own calibration inputs, which is
# self-consistency rather than validation, so it is the live transition test
# (NEW GAME -> down -> LOAD GAME, expected LOAD GAME) that validates it.
#
# WHAT IT IS NOT. It identifies the focus in ONE FRAME. It says nothing about
# what selects focus -- Q5's four boots gave TUTORIAL, TUTORIAL, NEW GAME, NEW
# GAME and that instability stands.
set -euo pipefail
cd "${PROJECT_DIR:-/work}"
export DISPLAY="${DISPLAY:-:97}"
shot="${1:?usage: which-focus SHOT.png [screen]}"
screen="${2:-main_menu}"
OUT="${OUT:-$(mktemp -d)}"; mkdir -p "$OUT"
CAPS=docs/re/captures/title-builds

# Buttons, in the order ui_down walks them.
case "$screen" in
  main_menu) BUTTONS=(ptbtn01:NEW_GAME ptbtn02:LOAD_GAME ptbtn03:TUTORIAL ptbtn04:OPTIONS ptbtn05:EXTRAS) ;;
  extras)    BUTTONS=(ptbtn11:MISSION_SELECT ptbtn12:MOVIE_THEATER ptbtn13:THIRD) ;;
  *) echo "which-focus: no button list for $screen" >&2; exit 2 ;;
esac
n=${#BUTTONS[@]}
downs=$(python3 -c "print(','.join(['down']*($n-1)))")

render_all() {  # render_all <tag>
  godot --path port --resolution 1280x720 -- "--menu=$screen" "--script=$downs" \
    "--shots=$OUT/$1" >"$OUT/$1.log" 2>&1 || true
}
# Normalise any input to the captures' 1279x675 top-left crop. A 1280x720 guest
# frame and a 1279x675 screenshot are the same pixels; the difference is the
# crop the screenshot tool applies, not a scale.
norm() { convert "$1" -crop 1279x675+0+0 +repage "$2"; }

score() {   # score <shot> ; prints "<idx> <label> <pixels>" per candidate
  local s="$1" i=0 f
  for f in "$OUT"/r_*.png; do
    [ -f "$f" ] || continue
    norm "$f" "$OUT/cand.png"
    local d
    d=$(convert "$OUT/cand.png" "$s" -compose difference -composite \
        -colorspace Gray -threshold 25% -format "%[fx:mean*w*h]" info:)
    echo "$i ${BUTTONS[$i]#*:} $d"
    i=$((i+1))
  done
}

render_all r
# Godot names the shots `<tag>_00_start.png`, `<tag>_01_down.png`, ... -- rename
# to a sortable form so the candidate order is the ui_down order and not glob luck.
i=0
for f in "$OUT"/r_0*.png; do mv "$f" "$OUT/r_$(printf '%02d' $i).png"; i=$((i+1)); done
[ "$i" = "$n" ] || { echo "which-focus: rendered $i of $n focus states -- see $OUT" >&2; exit 3; }

verdict() {  # verdict <shot> <expected-or-empty>
  local s="$1" expect="${2:-}"
  norm "$s" "$OUT/shot.png"
  mapfile -t rows < <(score "$OUT/shot.png" | sort -k3 -n)
  local best_lbl best_px second_px
  best_lbl=$(echo "${rows[0]}" | awk '{print $2}')
  best_px=$(echo "${rows[0]}" | awk '{print $3}')
  second_px=$(echo "${rows[1]}" | awk '{print $3}')
  local margin
  margin=$(python3 -c "print('%.1f' % ($second_px/max($best_px,1)))")
  for r in "${rows[@]}"; do printf '    %-16s %8s\n' "$(echo "$r"|awk '{print $2}')" "$(echo "$r"|awk '{print $3}')"; done
  echo "  -> $best_lbl, margin ${margin}x"
  if [ -n "$expect" ]; then
    if [ "$best_lbl" = "$expect" ]; then echo "  CONTROL PASSED (expected $expect)"; return 0
    else echo "  🔴 CONTROL FAILED: expected $expect, got $best_lbl"; return 1; fi
  fi
  # A thin margin means the frame does not decide it. 2x is below the 4.7x the
  # control achieves and well above 1.0; a shot that cannot beat it should be
  # re-taken rather than guessed at.
  python3 -c "import sys; sys.exit(0 if $margin >= 2.0 else 1)" || {
    echo "  ⚠️ margin under 2x -- this frame does not decide it. Do not act on this."; return 1; }
}

if [ "$screen" = main_menu ]; then
  echo "control -- $CAPS/live-main-menu-options-focused.png (answer is in the filename):"
  verdict "$CAPS/live-main-menu-options-focused.png" OPTIONS || {
    echo "refusing to report a result from a method that just failed its control." >&2; exit 1; }
  echo
fi
# The exit code must carry the refusal. An earlier version printed "do not act on
# this" and exited 0, so a caller scripting this -- which is the entire point,
# the Decoder runs it between drive steps -- would have read a refusal as an
# answer. That is the same defect as a checker claiming a check it skipped.
echo "$shot:"
rc=0
verdict "$shot" || rc=$?
echo "artifacts in $OUT"
exit $rc
