#!/usr/bin/env bash
# Attribute the webcam demo's frame-rate problem by measurement, one variable at a
# time. You drive the browser; this handles the checkouts, the rebuilds and the
# bookkeeping, and puts everything back when it exits — including on Ctrl-C.
#
#   ./tools/bisect_perf.sh              run every step
#   ./tools/bisect_perf.sh 1 2 3        run only those steps
#   ./tools/bisect_perf.sh --list       show the steps and stop
#
# It never switches branch and never commits. It only does
#   git checkout <ref> -- <paths>
# and restores with `git checkout HEAD -- .`, so a dirty tree would be lost — it
# refuses to start unless the tree is clean.
set -uo pipefail
cd "$(dirname "$0")/.."

DEMO_URL="http://localhost:8080/demos/webcam/"
RESULTS="$(mktemp -t bisect_perf.XXXXXX.tsv)"
CFG="demos/webcam/config.js"

bold() { printf '\033[1m%s\033[0m\n' "$*"; }
dim()  { printf '\033[2m%s\033[0m\n' "$*"; }

restore() {
  echo
  dim "restoring the working tree..."
  git checkout HEAD -- src/ dist/ "$CFG" demos/webcam/index.html 2>/dev/null
  npm run build >/dev/null 2>&1
  bold "restored to HEAD ($(git rev-parse --short HEAD)); tree: $(git status --porcelain | wc -l | tr -d ' ') modified file(s)"
  if [ -s "$RESULTS" ]; then
    echo
    bold "=== RESULTS ==="
    printf '%-26s %-7s %-9s %-9s %-9s %s\n' STEP FPS CALLS PROGRAMS TEXTURES NOTE
    while IFS=$'\t' read -r s f c p t n; do
      printf '%-26s %-7s %-9s %-9s %-9s %s\n' "$s" "$f" "$c" "$p" "$t" "$n"
    done < "$RESULTS"
    echo
    dim "results file: $RESULTS"
  fi
}
trap restore EXIT INT TERM

# ---- step table: id | label | apply-command ----
step_label() { case "$1" in
  0) echo "baseline (HEAD as-is)";;
  1) echo "CONTROL: main src+config";;
  2) echo "probe A: transmission 0";;
  3) echo "probe B: autoLightIntensity 0";;
  4) echo "probe C: no eyeliner layer";;
  5) echo "probe D: willReadFrequently";;
  6) echo "src @ 868713f~1 (clearcoat)";;
  7) echo "src @ 866b4d5~1 (color)";;
  8) echo "src @ 16584a4~1 (opacity+pmrem)";;
  9) echo "src @ 6707df1~1 (retouch dispose)";;
 10) echo "src @ f679124~1 (geometry.ts)";;
 11) echo "probe E: camera capped 1280x720";;
 12) echo "probe F: no probe readback";;
 13) echo "probe G: panel + innerHTML off";;
 14) echo "probe H: E+F+G together";;
esac; }

step_apply() { case "$1" in
  0) : ;;
  1) git checkout main -- src/ "$CFG" ;;
  2) python3 - <<'PY'
import re,io
p='demos/webcam/config.js'; s=open(p).read()
s=s.replace('    transmission: 0.9,','    transmission: 0.0,',1)
open(p,'w').write(s)
PY
     ;;
  3) python3 - <<'PY'
p='demos/webcam/config.js'; s=open(p).read()
s=s.replace('  gaussianSobelSmoothing:','  autoLightIntensity: false,\n  gaussianSobelSmoothing:',1)
open(p,'w').write(s)
PY
     ;;
  4) python3 - <<'PY'
p='demos/webcam/config.js'; lines=open(p).read().split('\n')
i=next(k for k,l in enumerate(lines) if "name: 'eyeliner'" in l)
a=i
while not lines[a].strip().startswith('{'): a-=1
b=i
while lines[b].strip() not in ('},','}'): b+=1
lines[a:b+1]=['  // [bisect probe C] eyeliner layer removed']+['  // '+l for l in lines[a:b+1]]
open(p,'w').write('\n'.join(lines))
PY
     ;;
  5) python3 - <<'PY'
p='src/mtr_facemesh.ts'; s=open(p).read()
old="const contextVideo = this.canvasVideo.getContext('2d');"
assert old in s, 'contextVideo getContext line not found'
s=s.replace(old,
  "const contextVideo = this.canvasVideo.getContext('2d', {\n"
  "      willReadFrequently: true,\n"
  "    } as CanvasRenderingContext2DSettings);",1)
open(p,'w').write(s)
PY
     ;;
  6) git checkout 868713f~1 -- src/ ;;
  7) git checkout 866b4d5~1 -- src/ ;;
  8) git checkout 16584a4~1 -- src/ ;;
  9) git checkout 6707df1~1 -- src/ ;;
 10) git checkout f679124~1 -- src/ ;;
 11) probe_resolution ;;
 12) probe_no_readback ;;
 13) probe_no_panel ;;
 14) probe_resolution; probe_no_readback; probe_no_panel ;;
esac; }

# The webcam demo hard-requires min 1920x1080 in index.html, which steps 0-10 never
# touched — so it is identical in every one of them. MediaPipe inference, the video
# texture upload, the blur targets, drawImage and luminance all scale with it.
probe_resolution() { python3 - <<'PY'
# `min:` is a FLOOR, not a cap — replacing min:1920 with min:1280 lets the browser
# hand back 1920 anyway, which is why the first version of this probe did nothing.
# `max:` is the only form that actually caps it.
p='demos/webcam/index.html'; s=open(p).read()
n=s.count('1920')
s=s.replace('min: 1920','max: 1280').replace('ideal: 1920','max: 1280')
s=s.replace('min: 1080','max: 720').replace('ideal: 1080','max: 720')
open(p,'w').write(s)
assert n>0, 'no 1920 constraint found'
PY
}

# The frame light probe reads back a 96x96 canvas every 3 frames — a sync stall ~20
# times a second. Never probed by steps 0-10.
probe_no_readback() { python3 - <<'PY'
p='demos/webcam/index.html'; s=open(p).read()
old='window.faceMesh.requestPermissions({'
assert old in s
s=s.replace(old, 'window.faceMesh.frameLightProbeEnabled = false;\n          '+old, 1)
open(p,'w').write(s)
PY
}

# showFPSPanel/showScreenLogs are true here on BOTH branches, and the innerHTML
# rebuild runs every frame regardless of the flag. Turn the flags off AND gate the
# rebuild, so this measures the whole panel cost.
probe_no_panel() { python3 - <<'PY'
p='demos/webcam/config.js'; s=open(p).read()
s=s.replace('  showFPSPanel: true,','  showFPSPanel: false,',1)
s=s.replace('  showScreenLogs: true,','  showScreenLogs: false,',1)
open(p,'w').write(s)
q='src/mtr_facemesh.ts'; t=open(q).read()
old='this.fpsContainer.innerHTML = this.screenLogs.log();'
assert old in t, 'innerHTML line not found'
t=t.replace(old, 'if (this.showScreenLogs) {\n        '+old+'\n      }',1)
open(q,'w').write(t)
PY
}

if [ "${1:-}" = "--list" ]; then
  for i in $(seq 0 14); do printf '  %2s  %s\n' "$i" "$(step_label "$i")"; done
  trap - EXIT INT TERM; exit 0
fi

if [ -n "$(git status --porcelain)" ]; then
  bold "working tree is dirty — commit or stash first, this script overwrites src/, $CFG and demos/webcam/index.html"
  git status --short
  trap - EXIT INT TERM; exit 1
fi

STEPS=("$@"); [ ${#STEPS[@]} -eq 0 ] && STEPS=($(seq 0 14))

bold "Serve the repo ROOT in another terminal:  python3 -m http.server 8080"
bold "Then open $DEMO_URL and keep the console open."
echo
dim "At each step: hard-reload, let it settle ~10 s, read the fps panel, and paste"
dim "this in the console for the counters:"
echo
dim "  (i=>JSON.stringify({calls:i.render.calls,programs:i.programs.length,textures:i.memory.textures}))(faceMesh.getRenderer().info)"
echo
read -rp "ready? [enter] "

for s in "${STEPS[@]}"; do
  label="$(step_label "$s")"
  [ -z "$label" ] && { echo "unknown step $s"; continue; }
  echo
  bold "─── step $s: $label ───"
  git checkout HEAD -- src/ "$CFG" demos/webcam/index.html 2>/dev/null
  if ! step_apply "$s"; then
    echo "  apply failed, skipping"; continue
  fi
  printf '  building... '
  if npm run build >/dev/null 2>&1; then echo done; else echo FAILED; continue; fi
  echo "  hard-reload $DEMO_URL, let it settle, then:"
  read -rp "    fps (steady, after any drop)? " fps
  read -rp "    render.calls? " calls
  read -rp "    programs.length? " progs
  read -rp "    memory.textures? " texs
  read -rp "    note (optional)? " note
  printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$s $label" "${fps:--}" "${calls:--}" "${progs:--}" "${texs:--}" "${note:-}" >> "$RESULTS"
done
