// Inversion Engine — viewport-fixed, scroll-aware
// ------------------------------------------------
// The canvas is sized to the VIEWPORT only and is fixed-positioned. Scenes work
// in document-space coordinates and subtract `scrollY` when drawing — so we
// never paint pixels we can't see. This is essential for FPS on long pages.
//
// Compositing: everything draws white with `difference` blend, producing the
// signature inversion wherever a shape overlaps content (page bg, header DOM,
// other shapes). The base fill is white so non-shape areas read as page bg.

const InversionEngine = (() => {
  function createStage(canvas, { dpr: dprOpt, fps: fpsOpt } = {}) {
    // DPR=1.5 — sweet spot. 2.0 doubles fill cost on the difference-blended
    // header, 1.0 looks mushy on text overlaps. 1.5 is the smooth original.
    const dpr = dprOpt ?? Math.min(1.5, window.devicePixelRatio || 1);
    // Default: uncapped (browser native, 60 or 120Hz). Pass fps to throttle.
    const targetFps = fpsOpt ?? 0;
    const ctx = canvas.getContext('2d', { alpha: false });
    let width = 0, height = 0; // viewport
    const scenes = [];

    const resize = () => {
      const w = window.innerWidth;
      const h = window.innerHeight;
      if (w === width && h === height) return;
      width = w;
      height = h;
      canvas.style.width = w + 'px';
      canvas.style.height = h + 'px';
      canvas.width = Math.floor(w * dpr);
      canvas.height = Math.floor(h * dpr);
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };

    const addScene = (scene) => { scenes.push(scene); };

    // Base color for the canvas — flips between paper and ink. With
    // `globalCompositeOperation: 'difference'`, drawing white over a white
    // base = black output (current default), drawing white over a black
    // base = white output. So flipping baseColor inverts the entire scene
    // (e.g. above-the-wave = ink page, white circles; below = paper page,
    // black circles).
    let baseColor = '#ffffff';
    let pageBg = '#fafaf7'; // body bg below the canvas — kept in sync via CSS var
    const setBaseColor = (color) => {
      baseColor = color;
    };

    let running = false, last = 0, start = 0;

    const frame = (now) => {
      if (!running) return;
      // Optional throttle. When targetFps is 0, run at native refresh rate
      // (60Hz on standard monitors, 120Hz on high-refresh displays).
      if (targetFps > 0) {
        const minFrameMs = 1000 / targetFps;
        const elapsed = now - last;
        if (elapsed < minFrameMs - 1) {
          requestAnimationFrame(frame);
          return;
        }
      }
      const elapsed = now - last;
      const dt = Math.min(0.05, elapsed / 1000);
      const t = (now - start) / 1000;
      last = now;

      // Clear viewport with current base color (flippable via setBaseColor)
      ctx.globalCompositeOperation = 'source-over';
      ctx.fillStyle = baseColor;
      ctx.fillRect(0, 0, width, height);

      // Difference compositing — drawing white inverts whatever's beneath.
      // Over a WHITE base → output is black (default scene).
      // Over a BLACK base → output is white (inverted scene).
      ctx.globalCompositeOperation = 'difference';
      ctx.fillStyle = '#ffffff';

      const docH = Math.max(window.innerHeight, document.documentElement.scrollHeight);
      const info = {
        width,
        height: docH,        // document height (for legacy scenes that key off this)
        viewW: width,
        viewH: height,
        t, dt,
        scrollY: window.scrollY,
      };
      for (const s of scenes) {
        if (s.enabled === false) continue;
        s.render(ctx, info);
      }

      ctx.globalCompositeOperation = 'source-over';
      requestAnimationFrame(frame);
    };

    const startLoop = () => {
      if (running) return;
      running = true;
      last = performance.now();
      start = last;
      requestAnimationFrame(frame);
    };
    const stop = () => { running = false; };

    // Recover from background-tab pauses and BFCache restores.
    //
    // When Firefox/Chrome put the tab in the background, requestAnimationFrame
    // is throttled or fully paused, so neither `now` nor scene physics advance.
    // On return, `now` jumps forward by however long we were away. Two issues
    // cascade from that:
    //   1. The frame-time clock `t` (now - start) leaps, so any scene logic
    //      keyed off `t - someTimestamp` gets a huge delta.
    //   2. Scene-internal state (e.g. bubble positions) is frozen at the
    //      pre-pause configuration; the new frame draws those stale positions
    //      and they appear stacked / clumped.
    //
    // Fix: on visibilitychange→visible, rebase the time clock so `t` resumes
    // from where it left off (no leap), and call each scene's optional reset()
    // hook so it can wipe its own simulation state. pageshow with persisted
    // covers the BFCache case where the page is restored wholesale (Firefox
    // back-button is the most common trigger).
    const handleResume = () => {
      const now = performance.now();
      // Rebase: shift `start` forward by however long we were paused so
      // that t = (now - start)/1000 stays continuous across the gap.
      if (last > 0) {
        start += (now - last);
      }
      last = now;
      for (const s of scenes) {
        if (typeof s.reset === 'function') s.reset();
      }
    };
    const onVisibility = () => {
      if (document.visibilityState === 'visible') handleResume();
    };
    const onPageShow = (e) => {
      // e.persisted === true for BFCache restores (Firefox back/forward).
      if (e.persisted) handleResume();
    };
    document.addEventListener('visibilitychange', onVisibility);
    window.addEventListener('pageshow', onPageShow);

    window.addEventListener('resize', resize);
    resize();

    return {
      ctx,
      addScene,
      setBaseColor,
      start: startLoop,
      stop,
      destroy: () => {
        window.removeEventListener('resize', resize);
        document.removeEventListener('visibilitychange', onVisibility);
        window.removeEventListener('pageshow', onPageShow);
        stop();
      },
      resize,
    };
  }

  return { createStage };
})();

window.InversionEngine = InversionEngine;
