#!/bin/zsh
# widget-screen-coord — resolve the absolute screen coordinate of a DOM node
# inside a Chrome/Edge tab, accounting for window position, page scroll, and
# stale-tab gotchas. Designed to feed clicker-vnc / clicker-vhid coordinates
# that *actually land* on the intended widget.
#
# Usage:
#   widget-screen-coord --url-contains URL --selector CSS_OR_XPATH
#                       [--browser chrome|edge]
#                       [--scroll-into-view] [--pin x,y,w,h] [--pretty]
#
# Output (one line JSON):
#   {"screen": {"x": INT, "y": INT},
#    "viewport": {"x": INT, "y": INT},
#    "rect": {"x": INT, "y": INT, "w": INT, "h": INT},
#    "window": {"screen_x": INT, "screen_y": INT, "chrome_bar": INT,
#               "scroll_x": INT, "scroll_y": INT}}
#
# Why this exists:
#   bin/clicker-vnc clicks at SCREEN coordinates. Computing those right
#   requires:
#     1. The tab is the *active* tab in *its* window (otherwise getBoundingRect
#        returns stale numbers from another tab).
#     2. The widget is in viewport (not below the fold) — otherwise
#        rect.top is positive but huge.
#     3. window.screenX/Y is current (window may have been moved).
#     4. (window.outerHeight - innerHeight) is the chrome-bar offset.
#   This wrapper does all four robustly: optionally pins the window
#   (--pin), set-actives the tab, optionally scrolls the widget into view,
#   then reads everything in one eval round-trip.
set -euo pipefail
BIN_DIR=${0:A:h}

usage() {
  awk 'NR == 1 { next } /^set -/ { exit } { sub(/^# ?/, ""); print }' "${(%):-%x}"
}

url=""
selector=""
browser="Google Chrome"
scroll=0
pin=""
pretty=0

normalize_browser() {
  local raw="${1:-Google Chrome}"
  local key="${raw:l}"
  key="${key// /-}"
  case "$key" in
    chrome|google-chrome|googlechrome) echo "Google Chrome" ;;
    edge|microsoft-edge|microsoftedge|msedge) echo "Microsoft Edge" ;;
    *) echo "$raw" ;;
  esac
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --url-contains)     url="$2"; shift 2;;
    --selector)         selector="$2"; shift 2;;
    --browser)          browser="$(normalize_browser "$2")"; shift 2;;
    --scroll-into-view) scroll=1; shift;;
    --pin)              pin="$2"; shift 2;;
    --pretty)           pretty=1; shift;;
    -h|--help)          usage; exit 0;;
    *) echo "widget-screen-coord: unknown arg: $1" >&2; exit 2;;
  esac
done

if [[ -z "$url" || -z "$selector" ]]; then
  echo 'widget-screen-coord: --url-contains and --selector are required' >&2
  exit 2
fi

# 1. Optional pin to known bounds for repeatability
if [[ -n "$pin" ]]; then
  IFS=',' read -r px py pw ph <<< "$pin"
  "$BIN_DIR/chrome-pin-window" --browser "$browser" --url-contains "$url" \
    --x "$px" --y "$py" --w "$pw" --h "$ph" --no-activate >/dev/null 2>&1 || true
fi

# 2. Make the matching tab active inside its window (no browser activate)
/usr/bin/osascript >/dev/null 2>&1 <<OSA
tell application "$browser"
  repeat with theWindow in windows
    set tIdx to 0
    repeat with theTab in tabs of theWindow
      set tIdx to tIdx + 1
      if URL of theTab contains "$url" then
        set active tab index of theWindow to tIdx
        return
      end if
    end repeat
  end repeat
end tell
OSA

# 3. Get window bounds from AppleScript (reliable even for background windows,
#    unlike window.screenX/Y/outerHeight which return 0 when Chrome is not frontmost)
WIN_BOUNDS=$(/usr/bin/osascript 2>/dev/null <<OSA
tell application "$browser"
  repeat with theWindow in windows
    repeat with theTab in tabs of theWindow
      if URL of theTab contains "$url" then
        set b to bounds of theWindow
        return "" & (item 1 of b) & "," & (item 2 of b) & "," & (item 3 of b) & "," & (item 4 of b)
      end if
    end repeat
  end repeat
  return ""
end tell
OSA
)
WIN_LEFT=0; WIN_TOP=0; WIN_RIGHT=0; WIN_BOTTOM=0
if [[ -n "$WIN_BOUNDS" ]]; then
  IFS=',' read -r WIN_LEFT WIN_TOP WIN_RIGHT WIN_BOTTOM <<< "$WIN_BOUNDS"
fi

# 4. Build the JS payload (selector embedded as JSON-quoted string for safety)
SELECTOR_JSON=$(SEL="$selector" /usr/bin/python3 -c 'import json,os; print(json.dumps(os.environ["SEL"]))')

JS_FILE=$(/usr/bin/mktemp -t widgetcoord.XXXXXX.js)
trap "rm -f $JS_FILE" EXIT

/bin/cat > "$JS_FILE" <<JS
(() => {
  const sel = ${SELECTOR_JSON};
  const isXPath = sel.startsWith("/") || sel.startsWith("(/") || sel.startsWith("./");
  let el = null;
  if (isXPath) {
    const r = document.evaluate(sel, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
    el = r.singleNodeValue;
  } else {
    el = document.querySelector(sel);
  }
  if (!el) return {error: "selector matched nothing", selector: sel};
  if (${scroll}) el.scrollIntoView({block: "center", inline: "center"});
  const r = el.getBoundingClientRect();
  if (r.width === 0 || r.height === 0) return {error: "element has zero-sized bounds", selector: sel, rect: {x: r.left, y: r.top, w: r.width, h: r.height}};
  // Use AppleScript bounds (passed from shell) for reliable screen coords —
  // window.screenX/Y/outerHeight return 0 when Chrome is a background app on macOS.
  const winLeft = ${WIN_LEFT};
  const winTop = ${WIN_TOP};
  const winHeight = ${WIN_BOTTOM} - ${WIN_TOP};
  const chromeBar = Math.max(0, winHeight - window.innerHeight);
  const sxOffset = winLeft;
  const syOffset = winTop + chromeBar;
  return {
    screen:   {x: Math.round(r.left + r.width / 2 + sxOffset), y: Math.round(r.top + r.height / 2 + syOffset)},
    viewport: {x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2)},
    rect:     {x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height)},
    window:   {screen_x: winLeft, screen_y: winTop, chrome_bar: chromeBar, scroll_x: Math.round(window.scrollX), scroll_y: Math.round(window.scrollY)},
  };
})()
JS

# 4. Run via background-web-control eval, parse out the value
RAW=$("$BIN_DIR/background-web-control" --browser "$browser" eval --url-contains "$url" --js-file "$JS_FILE" 2>&1 || true)

if [[ $pretty -eq 1 ]]; then
  echo "$RAW" | /usr/bin/python3 -c '
import sys, json, re
text = sys.stdin.read()
m = re.search(r"\"value\":\s*(\{.+?\})\s*\}\s*\}\s*$", text, re.S)
if m:
    try:
        print(json.dumps(json.loads(m.group(1)), indent=2, ensure_ascii=False))
        sys.exit(0)
    except Exception:
        pass
print(text)
'
else
  echo "$RAW" | /usr/bin/python3 -c '
import sys, json, re
text = sys.stdin.read()
m = re.search(r"\"value\":\s*(\{.+?\})\s*\}\s*\}\s*$", text, re.S)
if m:
    try:
        print(json.dumps(json.loads(m.group(1)), ensure_ascii=False))
        sys.exit(0)
    except Exception:
        pass
print("{\"error\": \"eval output unparseable\", \"raw\": " + json.dumps(text[:500]) + "}")
'
fi
