// offscreen.js — head-tracking host backed by Tracky Mouse (MIT, by Isaiah Odhner).
// Runs in a hidden offscreen document so the service worker can hold a camera stream.
// We use Tracky Mouse only as the head-tracking engine here; the dwell clicker
// runs separately in each tab's content script.

const VIRTUAL_W = 1920;
const VIRTUAL_H = 1080;

function log(...args) {
  console.log("[Offscreen]", ...args);
  chrome.runtime.sendMessage({ type: "HEADTRACK_LOG", level: "info", text: args.join(" ") }).catch(() => {});
}
function errlog(...args) {
  console.error("[Offscreen]", ...args);
  chrome.runtime.sendMessage({ type: "HEADTRACK_LOG", level: "error", text: args.join(" ") }).catch(() => {});
}

const SCROLL_ZONE_SIZE = 0.15; // top/bottom 15% of viewport triggers scroll
const SCROLL_ZONE_SPEED = 20;  // max px per frame at the very edge of the zone

function postCursor(nx, ny) {
  chrome.runtime.sendMessage({ type: "CURSOR_UPDATE", x: nx, y: ny }).catch(() => {});
  if (ny < SCROLL_ZONE_SIZE) {
    const depth = (SCROLL_ZONE_SIZE - ny) / SCROLL_ZONE_SIZE;
    chrome.runtime.sendMessage({ type: "SCROLL_BY", deltaY: -depth * SCROLL_ZONE_SPEED }).catch(() => {});
  } else if (ny > 1 - SCROLL_ZONE_SIZE) {
    const depth = (ny - (1 - SCROLL_ZONE_SIZE)) / SCROLL_ZONE_SIZE;
    chrome.runtime.sendMessage({ type: "SCROLL_BY", deltaY: depth * SCROLL_ZONE_SPEED }).catch(() => {});
  }
}

async function bootstrap() {
  log("module loaded — initializing Tracky Mouse");

  if (typeof TrackyMouse === "undefined") {
    errlog("TrackyMouse global not found — vendor script failed to load");
    return;
  }

  // Hidden offscreen documents have innerWidth/innerHeight = 0, but Tracky
  // uses those as the virtual screen dimensions. With 0×0 the cursor is
  // immediately clamped to (0,0) and onPointerMove always reports top-left.
  // Force a sane virtual viewport via getter overrides.
  try {
    Object.defineProperty(window, "innerWidth", { get: () => VIRTUAL_W, configurable: true });
    Object.defineProperty(window, "innerHeight", { get: () => VIRTUAL_H, configurable: true });
    log(`Patched window.innerWidth/innerHeight → ${VIRTUAL_W}×${VIRTUAL_H}`);
  } catch (e) {
    errlog("Failed to patch innerWidth/innerHeight:", e?.message || e);
  }

  TrackyMouse.dependenciesRoot = chrome.runtime.getURL("vendor/tracky-mouse").replace(/\/+$/, "");

  TrackyMouse.onPointerMove = (x, y) => {
    const nx = Math.max(0, Math.min(1, x / VIRTUAL_W));
    const ny = Math.max(0, Math.min(1, y / VIRTUAL_H));
    postCursor(nx, ny);
  };

  // Patch document.createElement so that the <video> element Tracky creates
  // for the camera stream gets `muted`, `autoplay`, and is appended to the
  // DOM. Detached / non-muted video elements often refuse to play() in MV3
  // offscreen contexts (no user gesture available), so the camera stream
  // never reaches `loadedmetadata` → clmtrackr never gets fed → cursor stuck.
  const _origCreate = document.createElement.bind(document);
  let _patchedVideo = null;
  document.createElement = function (tagName, ...rest) {
    const el = _origCreate(tagName, ...rest);
    if (typeof tagName === "string" && tagName.toLowerCase() === "video" && !_patchedVideo) {
      _patchedVideo = el;
      el.muted = true;
      el.autoplay = true;
      el.setAttribute("playsinline", "");
      el.style.position = "absolute";
      el.style.opacity = "0";
      el.style.pointerEvents = "none";
      el.style.width = "1px";
      el.style.height = "1px";
      // Append on next tick so srcObject etc. has a chance to be set first.
      Promise.resolve().then(() => {
        if (!el.isConnected) document.body.appendChild(el);
        log(`Patched video element appended to DOM; muted=${el.muted} autoplay=${el.autoplay}`);
      });
      // Diagnostic listeners — log every key lifecycle event on the camera
      // video so we can see the play/loadedmetadata sequence Tracky depends
      // on for clmtrackr initialization.
      ["loadstart", "loadedmetadata", "loadeddata", "canplay", "playing", "play", "pause", "stalled", "error"].forEach((evt) => {
        el.addEventListener(evt, () => log(`video[${evt}] readyState=${el.readyState} paused=${el.paused}`));
      });
      // Re-instate after first creation so we don't intercept other elements.
      document.createElement = _origCreate;
    }
    return el;
  };

  // Stub face-landmarks-detection (TFJS). MV3 CSP forbids 'unsafe-eval'.
  //
  // CRITICAL: estimateFaces must NEVER resolve. If it resolves with `[]`,
  // Tracky's then-handler (tracky-mouse.js:~3389) reads `predictions[0]`
  // → undefined → sets `useClmTracking = false` → disables clmtrackr forever.
  // A forever-pending promise keeps `facemeshEstimating` stuck on true and
  // leaves `useClmTracking` at its default `true` so clmtrackr stays on.
  window.faceLandmarksDetection = {
    SupportedModels: { MediaPipeFaceMesh: "mediapipe-face-mesh" },
    createDetector: async () => ({
      estimateFaces: () => new Promise(() => {}),
      dispose: () => {},
    }),
  };

  // Override loadDependencies to skip face-landmarks-detection.min.js — even
  // touching that file at parse time triggers the eval. We still load
  // clmtrackr, face_mesh (WASM-only), and OneEuroFilter.
  TrackyMouse.loadDependencies = function ({ statsJs = false } = {}) {
    TrackyMouse.dependenciesRoot = TrackyMouse.dependenciesRoot.replace(/\/+$/, "");
    const loadScript = (src) => new Promise((resolve, reject) => {
      const s = document.createElement("script");
      s.type = "text/javascript";
      s.onload = resolve;
      s.onerror = reject;
      s.src = src;
      document.head.append(s);
    });
    const files = [
      `${TrackyMouse.dependenciesRoot}/lib/no-eval.js`,
      `${TrackyMouse.dependenciesRoot}/lib/clmtrackr.js`,
      `${TrackyMouse.dependenciesRoot}/lib/OneEuroFilter.js`,
    ];
    if (statsJs) files.push(`${TrackyMouse.dependenciesRoot}/lib/stats.js`);
    return Promise.all(files.map(loadScript));
  };

  try {
    await TrackyMouse.loadDependencies();
    log("Tracky Mouse dependencies loaded (clmtrackr-only, eval-free)");

    // Instrument clmtrackr — methods are defined as INSTANCE properties
    // inside the constructor (this.track = function() {...}), not on the
    // prototype. So we wrap the constructor and patch instance methods
    // after construction.
    if (typeof clm !== "undefined" && typeof clm.tracker === "function") {
      const OrigTracker = clm.tracker;
      let trackCalls = 0, posReturns = { array: 0, false: 0, other: 0 };
      let lastFacePoint = "n/a", instrumented = 0;
      clm.tracker = function (...args) {
        const inst = new OrigTracker(...args);
        instrumented++;
        log(`Wrapping clm.tracker instance #${instrumented}; methods=${Object.keys(inst).filter(k => typeof inst[k] === "function").join(",")}`);
        const origTrack = inst.track;
        const origGetPos = inst.getCurrentPosition;
        const origInit = inst.init;
        const origInitFD = inst.initFaceDetector;
        const origReset = inst.reset;
        if (origTrack) inst.track = function (...a) {
          trackCalls++;
          try { return origTrack.apply(this, a); }
          catch (e) { errlog(`clmTracker.track threw: ${e?.message || e}`); throw e; }
        };
        if (origGetPos) inst.getCurrentPosition = function () {
          const r = origGetPos.apply(this, arguments);
          if (Array.isArray(r)) {
            posReturns.array++;
            const p = r[62];
            if (p) lastFacePoint = `(${(p[0]|0)},${(p[1]|0)})`;
          } else if (r === false) posReturns.false++;
          else posReturns.other++;
          return r;
        };
        if (origInit) inst.init = function (...a) {
          log("clmTracker.init() called");
          try { return origInit.apply(this, a); }
          catch (e) { errlog(`clmTracker.init threw: ${e?.message || e}`); throw e; }
        };
        if (origInitFD) inst.initFaceDetector = function (...a) {
          log(`clmTracker.initFaceDetector(${a[0]?.tagName}) videoSize=${a[0]?.videoWidth}x${a[0]?.videoHeight}`);
          try {
            const r = origInitFD.apply(this, a);
            log(`clmTracker.initFaceDetector returned ${typeof r}`);
            return r;
          } catch (e) { errlog(`clmTracker.initFaceDetector threw: ${e?.message || e}`); throw e; }
        };
        if (origReset) inst.reset = function (...a) {
          log("clmTracker.reset() called");
          try { return origReset.apply(this, a); }
          catch (e) { errlog(`clmTracker.reset threw: ${e?.message || e}`); throw e; }
        };
        return inst;
      };
      Object.assign(clm.tracker, OrigTracker);
      setInterval(() => {
        log(`clmtrackr: track/s=${trackCalls} pos: array=${posReturns.array} false=${posReturns.false} other=${posReturns.other} | nose=${lastFacePoint}`);
        trackCalls = 0; posReturns = { array: 0, false: 0, other: 0 };
      }, 1000);
    } else {
      errlog("clm.tracker constructor not available");
    }
    // Catch any unhandled errors anywhere in Tracky's pipeline.
    window.addEventListener("error", (e) => errlog(`window.onerror: ${e?.message || e?.error || e} at ${e?.filename}:${e?.lineno}`));
    window.addEventListener("unhandledrejection", (e) => errlog(`unhandledrejection: ${e?.reason?.message || e?.reason || e}`));
  } catch (err) {
    errlog("loadDependencies failed:", err?.message || err);
    chrome.runtime.sendMessage({
      type: "HEADTRACK_CAMERA_FAILED",
      error: `loadDependencies: ${String(err?.message || err)}`,
    }).catch(() => {});
    return;
  }

  try {
    const host = document.getElementById("tracky-host");
    const trackyHandle = TrackyMouse.init(host);
    log("TrackyMouse.init returned handle; waiting for settings to load");

    // Tracky's `paused` flag is reset by an `afterInitialLoad` hook fired AFTER
    // the async settings load. Clicking the Start button before that point
    // races and gets clobbered. Wait for settings, THEN force tracking on.
    if (trackyHandle && typeof trackyHandle._waitForSettingsLoaded === "function") {
      try { await trackyHandle._waitForSettingsLoaded(); } catch (_) {}
      log("Tracky settings loaded");
    }

    if (typeof TrackyMouse.useCamera === "function") {
      try {
        await TrackyMouse.useCamera();
        log("TrackyMouse.useCamera resolved (camera stream acquired)");
      } catch (e) {
        errlog("useCamera failed:", e?.message || e);
      }
    }

    if (trackyHandle && typeof trackyHandle._setPaused === "function") {
      trackyHandle._setPaused(false);
      log("Tracking unpaused via _setPaused(false)");
    } else {
      // Fallback: click the start button.
      const startBtn = host.querySelector(".tracky-mouse-start-stop-button");
      if (startBtn) { startBtn.click(); log("Fell back to clicking start button"); }
      else errlog("No way to start tracking — _setPaused unavailable and no start button");
    }

    // Diagnostic: count onPointerMove invocations + sample latest x,y so we
    // can tell whether the head tracker is producing real movement.
    let omCount = 0, lastX = 0, lastY = 0, minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
    const origOM = TrackyMouse.onPointerMove;
    TrackyMouse.onPointerMove = (x, y) => {
      omCount++;
      lastX = x; lastY = y;
      if (x < minX) minX = x; if (x > maxX) maxX = x;
      if (y < minY) minY = y; if (y > maxY) maxY = y;
      if (origOM) origOM(x, y);
    };
    // Pixel-sampling probe: draw the camera video to a tiny canvas and
    // hash the pixel contents. If the hash never changes despite
    // visible head movement, Chromium isn't actually decoding frames into
    // the hidden offscreen doc's video element (frame-decode throttling).
    const probeCanvas = document.createElement("canvas");
    probeCanvas.width = 32;
    probeCanvas.height = 24;
    const probeCtx = probeCanvas.getContext("2d", { willReadFrequently: true });
    let lastPixelHash = -1;
    let pixelHashChanges = 0;
    let pixelMeanBrightness = 0;
    function samplePixels() {
      const v = _patchedVideo;
      if (!v || v.readyState < 2) return;
      try {
        probeCtx.drawImage(v, 0, 0, probeCanvas.width, probeCanvas.height);
        const data = probeCtx.getImageData(0, 0, probeCanvas.width, probeCanvas.height).data;
        let sum = 0, hash = 0;
        for (let i = 0; i < data.length; i += 4) {
          const lum = (data[i] + data[i + 1] + data[i + 2]) / 3;
          sum += lum;
          hash = ((hash * 31) + (lum | 0)) | 0;
        }
        pixelMeanBrightness = sum / (data.length / 4);
        if (hash !== lastPixelHash) {
          if (lastPixelHash !== -1) pixelHashChanges++;
          lastPixelHash = hash;
        }
      } catch (e) {
        log(`pixel probe error: ${e?.message || e}`);
      }
    }
    setInterval(samplePixels, 100); // probe at 10 Hz

    setInterval(() => {
      const v = _patchedVideo;
      const vState = v ? `rs=${v.readyState} paused=${v.paused} w=${v.videoWidth}x${v.videoHeight}` : "no-video";
      log(`rate=${omCount}/s xy=(${lastX|0},${lastY|0}) range=(${minX|0}..${maxX|0},${minY|0}..${maxY|0}) | ${vState} | pixHashChanges/s=${pixelHashChanges} meanLum=${pixelMeanBrightness.toFixed(1)}`);
      omCount = 0; minX = Infinity; maxX = -Infinity; minY = Infinity; maxY = -Infinity;
      pixelHashChanges = 0;
    }, 1000);
  } catch (err) {
    errlog("TrackyMouse.init failed:", err?.message || err);
    chrome.runtime.sendMessage({
      type: "HEADTRACK_CAMERA_FAILED",
      error: `init: ${String(err?.message || err)}`,
    }).catch(() => {});
  }
}

bootstrap();
