#!/bin/bash
# z4: drive the local game from a terminal pane while logview.py panes show the servers.
#   z4 play       (re)launch the local server + private client; the window goes back to the saved spot
#   z4 eu         (re)launch the client against the EU server (deploy/eu.host) instead; no local server
#   z4 stop       quit the game
#   z4 login      type the local login into the game window (keys go to the game process only)
#   z4 mark TEXT  marker line in the server log (shows in every log pane)
#   z4 shot       screenshot of the game window into logs/shots/ (path also goes into the log)
#   z4 place      remember where the game window is now
#   z4 half       toggle a half-size window (Wine's Retina mode; off by default) for the next launch
#   z4 test       toggle test mode (level 300, every Arcade enemy at 1 HP) for the next launch
#   z4 console    single-key menu for all of the above
#   z4 lan [stop] (re)start a second server for the Windows laptop on this Mac's Wi-Fi address; stop: stop it
#   z4 headless   (re)launch like play, but the game never takes the focus, comes in front or moves
#                 the cursor (tools/nofocus); drive it with post and see it with peek
#   z4 hlogin     log in as dev/dev on a headless game's login screen
#   z4 post CMD.. input as window messages (tools/nofocus/winpost.rs: c X Y click, D X Y double-click, r X Y right-click, t TEXT, k VK, s MS),
#                 at game pixels (1366 x 768)
#   z4 peek       screenshot of the game window without bringing it forward, into logs/shots/
#   z4 reload     rebuild z4-logic and swap it in; the gateway keeps the game connected
#   z4 dev        reload on every change under crates/ (Ctrl-C stops watching)
#   z4 panel      open the dev panel (items, stats, enemy HP, shortcuts) in the browser
ROOT="$(cd "$(dirname "$0")" && pwd)"
OFF="${Z4_PORT_OFFSET:-1000}"   # the local server's port shift (play.sh)
LAN_OFF=3000                    # z4 lan: the laptop's server, ports 32992-32998, own characters and log
WINE_BIN="$ROOT/wine/Wine Staging.app/Contents/Resources/wine/bin"
LOG="$ROOT/logs/server.log"
POS="$ROOT/window.pos"
HALF="$ROOT/window.half"     # exists: launch at half size (Wine's Retina mode)
WIN="$ROOT/tools/z4win"

stamp() { date +%H:%M:%S.000; }
game_pid() { pgrep -f '[\/]Zone4Local[.]exe' | head -1; }

# z4win: "<window id> <x> <y> <w> <h> <pid>" of the on-screen "Zone4 Online" window (compiled once).
win() {
  if [ ! -x "$WIN" ]; then
    cat > "$WIN.swift" <<'EOF'
import CoreGraphics
let l = CGWindowListCopyWindowInfo([.optionOnScreenOnly], kCGNullWindowID) as? [[String: Any]] ?? []
for w in l where (w["kCGWindowName"] as? String) == "Zone4 Online" {
    let b = w["kCGWindowBounds"] as! [String: Any]
    print(w["kCGWindowNumber"]!, b["X"]!, b["Y"]!, b["Width"]!, b["Height"]!, w["kCGWindowOwnerPID"]!)
    break
}
EOF
    swiftc -O -o "$WIN" "$WIN.swift" 2>/dev/null || return 1
  fi
  "$WIN"
}

stop_game() {
  pkill -f "$ROOT/play.sh" 2>/dev/null
  WINEPREFIX="$ROOT/prefix" "$WINE_BIN/wineserver" -k 2>/dev/null
  pkill -f 'Zone4Local[.]exe' 2>/dev/null
  for _ in 1 2 3 4 5 6 7 8; do [ -z "$(game_pid)" ] && break; sleep 0.5; done
}

# Move each new game window to the saved spot once (the launcher may retry a crashed startup, which
# makes a new window); after that the window is left wherever it is dragged.
place_restore() {
  [ -f "$POS" ] || return 0
  local x y done_pid="" tries=0 w
  read -r x y < "$POS"
  for _ in $(seq 1 150); do
    sleep 2
    w=$(win) || continue
    [ -z "$w" ] && continue
    set -- $w
    [ "$6" = "$done_pid" ] && continue
    if { [ "$2" = "$x" ] && [ "$3" = "$y" ]; } || [ $tries -ge 5 ]; then done_pid=$6; tries=0; continue; fi
    tries=$((tries + 1))
    osascript -e "tell application \"System Events\" to set position of window 1 of (first process whose unix id is $6) to {$x, $y}" > /dev/null 2>&1
  done
}

play() {
  stop_game
  : > "$ROOT/logs/launch.log"
  local retina=0; [ -f "$HALF" ] && retina=1
  (Z4_SERVER=local Z4_RETINA=$retina nohup "$ROOT/play.sh" >> "$ROOT/logs/launch.log" 2>&1 &)
  (place_restore > /dev/null 2>&1 &)
  echo " launching (login screen in ~20-90 s; the launcher retries Wine's startup crashes and hangs)"
}

eu() {
  local host; host=$(cat "$ROOT/deploy/eu.host" 2>/dev/null) || { echo " no deploy/eu.host"; return 1; }
  [ -f "$ROOT/game-589/.enemy-hp-backup/mode" ] && echo " note: the stage files still have the dev panel's enemy HP (set it back to normal there)"
  stop_game
  : > "$ROOT/logs/launch.log"
  local retina=0; [ -f "$HALF" ] && retina=1
  (Z4_REMOTE=$host Z4_RETINA=$retina nohup "$ROOT/play.sh" >> "$ROOT/logs/launch.log" 2>&1 &)
  (place_restore > /dev/null 2>&1 &)
  echo " launching against the EU server ($host); log in with your account there"
}

headless() {
  stop_game
  : > "$ROOT/logs/launch.log"
  local retina=0; [ -f "$HALF" ] && retina=1
  (Z4_SERVER=local Z4_RETINA=$retina Z4_NOFOCUS=1 nohup "$ROOT/play.sh" >> "$ROOT/logs/launch.log" 2>&1 &)
  echo " launching headless (login screen in ~20-90 s; z4 peek to look, z4 hlogin to log in)"
}

# The newest change under these crates (epoch seconds).
changed() {
  local d dirs=(); for d in "$@"; do dirs+=("$ROOT/crates/$d"); done
  find "${dirs[@]}" \( -name '*.rs' -o -name '*.html' -o -name Cargo.toml \) -exec stat -f %m {} + | sort -n | tail -1
}
# When a process started (epoch seconds), if it runs.
started() { kill -0 "$1" 2>/dev/null && LC_ALL=C date -j -f "%a %b %d %T %Y" "$(LC_ALL=C ps -o lstart= -p "$1" | sed "s/ *$//")" +%s; }
logic_pid() { cat "$ROOT/server.pid" 2>/dev/null; }
gateway_pid() { cat "$ROOT/run/gateway.pid" 2>/dev/null; }

# Build z4-logic and start it with play.sh's settings; it connects to the gateway, which hands it
# the running one's world (players, connections, rounds), so the game stays in.
reload() {
  kill -0 "$(gateway_pid)" 2>/dev/null || { echo " no server running (p starts one)"; return 1; }
  echo " building..."
  (cd "$ROOT" && set -o pipefail && PATH="$HOME/.cargo/bin:$PATH" cargo build -q -p z4-logic 2>&1 | tail -20) \
    || { echo " the build failed; the running server stays"; return 1; }
  local retina=0
  [ -f "$HALF" ] && retina=1
  Z4_SERVER=local Z4_PORT_OFFSET=$OFF Z4_RETINA=$retina Z4_GAME_DIR="$ROOT/game-589" Z4_OPCODES="$ROOT/opcodes-589.json" \
    nohup "$ROOT/target/debug/z4-logic" --log "$LOG" > "$ROOT/logs/server.out" 2>&1 &
  echo $! > "$ROOT/server.pid"
  local n0; n0=$(grep -c "took over the world" "$LOG")
  for _ in $(seq 1 20); do
    sleep 0.5
    [ "$(grep -c "took over the world" "$LOG")" -gt "$n0" ] && { echo " $(date +%H:%M:%S) reloaded: $(grep "took over the world" "$LOG" | tail -1 | cut -d' ' -f2-)"; return; }
  done
  echo " the new server didn't take over: see logs/server.out"
}

dev() {
  local last="" now
  kill -0 "$(gateway_pid)" 2>/dev/null || { echo " no server running (z4 play)"; return 1; }
  echo " watching crates/ for changes (Ctrl-C stops watching; the server keeps running)"
  while :; do
    now=$(changed z4-logic z4-proto)
    [ -n "$last" ] && [ "$now" != "$last" ] && reload
    last=$now
    sleep 1
  done
}

# "<window id>\t<title>" of the game's window, wherever it is (tools/gamewin.swift, compiled once).
gamewin() {
  local pid; pid=$(game_pid); [ -z "$pid" ] && return 1
  [ -x "$ROOT/tools/gamewin" ] || swiftc -O -o "$ROOT/tools/gamewin" "$ROOT/tools/gamewin.swift" 2>/dev/null || return 1
  "$ROOT/tools/gamewin" "$pid"
}

post() {
  local w exe="$ROOT/tools/nofocus/build/winpost.exe"
  w=$(gamewin) && [ -n "$w" ] || { echo " no game window"; return 1; }
  [ -f "$exe" ] || "$ROOT/tools/nofocus/build-winpost.sh" > /dev/null || return 1
  WINEPREFIX="$ROOT/prefix" WINEDEBUG=-all "$WINE_BIN/wine" "$exe" "${w#*$'\t'}" "$@"
}

# The login screen's boxes and Go at game pixels. Empty boxes only (a fresh login screen): after a
# run of posted Backspaces, the rest of the input had no effect.
hlogin() {
  post c 673 604 s 300 t dev s 300 c 673 645 s 300 t dev s 300 c 841 604 && echo " login posted"
}

peek() {
  local w f; w=$(gamewin) && [ -n "$w" ] || { echo " no game window"; return 1; }
  mkdir -p "$ROOT/logs/shots"
  f="$ROOT/logs/shots/$(date +%H%M%S).png"
  screencapture -x -o -l "${w%%$'\t'*}" "$f" && sips -Z 1000 "$f" --out "${f%.png}_s.png" > /dev/null
  echo " saved ${f%.png}_s.png"
}

place_save() {
  local w; w=$(win)
  [ -z "$w" ] && { echo " no game window on screen"; return 1; }
  set -- $w
  echo "$2 $3" > "$POS"
  echo " window spot saved ($2, $3); z4 play puts it back there"
}

login() {
  [ -x "$ROOT/tools/typein.sh" ] || { echo " needs $ROOT/tools/typein.sh"; return 1; }
  "$ROOT/tools/typein.sh" 0.50 0.818 dev && "$ROOT/tools/typein.sh" 0.50 0.862 dev &&
    "$ROOT/tools/t.sh" click 0.60 0.82 1 > /dev/null
  echo " login typed"
}

toggle_half() {
  if [ -f "$HALF" ]; then rm -f "$HALF"; echo " next launch: full size"; else : > "$HALF"; echo " next launch: half size"; fi
}

toggle_test() {
  local py="$ROOT/venv/bin/python"
  if [ "$("$py" "$ROOT/tools/testmode.py")" = "test mode on" ]; then "$py" "$ROOT/tools/testmode.py" off; else "$py" "$ROOT/tools/testmode.py" on; fi
}

lan() {
  local ip
  for f in "$ROOT/lan.pid" "$ROOT/run/lan-logic.pid"; do
    [ -f "$f" ] && kill "$(cat "$f")" 2>/dev/null
    rm -f "$f"
  done
  pkill -f "server.py --log $ROOT/logs/lan.log" 2>/dev/null   # a Python one from before
  [ "${1:-}" = "stop" ] && { echo " laptop server stopped"; return; }
  ip=$(ipconfig getifaddr en0 || ipconfig getifaddr en1) || { echo " no Wi-Fi address"; return 1; }
  sleep 0.5
  "$ROOT/tools/db.sh" start || { echo " PostgreSQL failed to start (logs/postgres.log)"; return 1; }
  (cd "$ROOT" && PATH="$HOME/.cargo/bin:$PATH" cargo build -q -p z4-gateway -p z4-logic) \
    || { echo " the Rust server didn't build"; return 1; }
  # Its own socket, database and log, so it runs next to z4 play's server. The macOS firewall
  # asks once per build whether z4-gateway may accept incoming connections: Allow.
  local env=(Z4_LAN_HOST="$ip" Z4_PORT_OFFSET=$LAN_OFF Z4_SOCKET="$ROOT/run/lan.sock"
    Z4_DATABASE_URL="host=$ROOT/pgdata dbname=zone4_lan user=zone4")
  env "${env[@]}" nohup "$ROOT/target/debug/z4-gateway" > "$ROOT/logs/lan-gateway.out" 2>&1 & echo $! > "$ROOT/lan.pid"
  env "${env[@]}" nohup "$ROOT/target/debug/z4-logic" --log "$ROOT/logs/lan.log" > "$ROOT/logs/lan.out" 2>&1 & echo $! > "$ROOT/run/lan-logic.pid"
  sleep 1
  kill -0 "$(cat "$ROOT/lan.pid")" 2>/dev/null || { echo " laptop server failed to start:"; cat "$ROOT/logs/lan-gateway.out"; return 1; }
  kill -0 "$(cat "$ROOT/run/lan-logic.pid")" 2>/dev/null || { echo " laptop server failed to start:"; cat "$ROOT/logs/lan.out"; return 1; }
  echo " laptop server on $ip, ports $((29992 + LAN_OFF))-$((29998 + LAN_OFF)) (the game's ports + $LAN_OFF); log: logs/lan.log"
}
mark() { echo "$(stamp) ==== MARK: $*" >> "$LOG"; }

shot() {
  local w f; w=$(win)
  [ -z "$w" ] && { echo " no game window on screen"; return 1; }
  mkdir -p "$ROOT/logs/shots"
  f="$ROOT/logs/shots/$(date +%H%M%S).png"
  screencapture -x -o -l "${w%% *}" "$f" && sips -Z 1000 "$f" --out "${f%.png}_s.png" > /dev/null
  echo "$(stamp) ==== SHOT: ${f%.png}_s.png" >> "$LOG"
  echo " saved ${f%.png}_s.png"
}

# The game is up once it reached the login server after the latest server start.
reached_login() { tail -n 3000 "$LOG" 2>/dev/null | awk -v p="[$((29996 + OFF))] connect" '/listening on/ {c = 0} index($0, p) {c = 1} END {exit !c}'; }

# The server's version is when its logic started: it is out of date when a change under crates/
# came after that. A logic change needs r; a gateway one p (the gateway holds the game's
# connections, so a new one means a relaunch).
server_status() {
  local lt gt at
  gt=$(started "$(gateway_pid)") || { printf "\033[2m○ server off\033[0m"; return; }
  lt=$(started "$(logic_pid)") || { printf "\033[31m● server down\033[0m (r)"; return; }
  at=$(date -r "$lt" +%H:%M)
  if [ "$(changed z4-gateway)" -gt "$gt" ]; then
    printf "\033[33m◆ gateway changed\033[0m (p relaunches)"
  elif [ "$(changed z4-logic z4-proto)" -gt "$lt" ]; then
    printf "\033[33m◆ server out of date\033[0m %s, changed %s (r)" "$at" "$(date -r "$(changed z4-logic z4-proto)" +%H:%M)"
  else
    printf "\033[32m● server\033[0m %s" "$at"
  fi
}

status() {
  local p t secs tries
  server_status
  p=$(game_pid)
  if [ -z "$p" ]; then printf "  \033[2m○ game off\033[0m"; return; fi
  t=$(ps -o etime= -p "$p" | tr -d ' ')
  if reached_login; then printf "  \033[32m● game up\033[0m %s" "$t"; return; fi
  secs=$(echo "$t" | awk -F: '{print (NF == 3) ? $1 * 3600 + $2 * 60 + $3 : $1 * 60 + $2}')
  tries=$(grep -c "startup crash" "$ROOT/logs/launch.log" 2>/dev/null)
  printf "  \033[33m◐ starting\033[0m %s try %d" "$t" $((tries + 1))
  [ "${secs:-0}" -ge 45 ] && printf " \033[33m(white? p)\033[0m"
}

menu() {
  local size=full test=""; [ -f "$HALF" ] && size=half; [ -d "$ROOT/game-589/arcade/THAI.orig" ] && test=", test mode"
  printf '\n\033[1m ZONE4 local game\033[0m \033[2m(%s size%s)\033[0m\n' "$size" "$test"
  printf ' \033[36mp\033[0m play/relaunch  \033[36mk\033[0m stop\n'
  printf ' \033[36ml\033[0m type login     \033[36mm\033[0m note\n'
  printf ' \033[36ms\033[0m screenshot     \033[36mw\033[0m save spot\n'
  printf ' \033[36mh\033[0m half/full size \033[36mt\033[0m test mode\n'
  printf ' \033[36mr\033[0m reload server  \033[36mq\033[0m quit\n\n'
}

console() {
  local key t
  menu
  while :; do
    printf '\r\033[K%s' "$(status)"
    read -rsn1 -t 2 key || continue
    printf '\r\033[K'
    case "$key" in
      p) play ;;
      r) reload ;;
      k) stop_game; echo " stopped" ;;
      l) login ;;
      m) read -rp " mark: " t; mark "$t"; echo " marked" ;;
      s) shot ;;
      w) place_save ;;
      h) toggle_half; menu ;;
      t) toggle_test; menu ;;
      q) echo; return ;;
      *) menu ;;
    esac
  done
}

case "${1:-console}" in
  play) play ;;
  eu) eu ;;
  stop) stop_game ;;
  login) login ;;
  mark) shift; mark "$*" ;;
  shot) shot ;;
  place) place_save ;;
  half) toggle_half ;;
  test) toggle_test ;;
  console) console ;;
  lan) shift; lan "$@" ;;
  headless) headless ;;
  hlogin) hlogin ;;
  post) shift; post "$@" ;;
  peek) peek ;;
  reload) reload ;;
  dev) dev ;;
  panel) open "http://127.0.0.1:$((29991 + OFF))/" ;;
  *) sed -n 2,22p "$0"; exit 1 ;;
esac
