// ============================================================
// Beyond the Falls - Hero world flip grid (canvas, ~60fps)
// Organic noise brush + Y-flip living→dead. Offscreen layers,
// capped DPR, active-cell list — no per-frame full-grid scan.
// ============================================================
const { useEffect: useEffectHWG, useRef: useRefHWG, useState: useStateHWG, useMemo: useMemoHWG } = React;

const HWG_SRC_A = '/beyond-the-falls/uploads/in_01.webp';
const HWG_SRC_B = '/beyond-the-falls/uploads/out_002.webp';
const HWG_ASPECT = '1920 / 1085';
const HWG_WIDTH = 'min(166vw, 1944px)';
const HWG_COLS = 28;
const HWG_ROWS = 14;
const HWG_CELL_N = HWG_COLS * HWG_ROWS;
const HWG_BRUSH_R = 6.4;       // was 3.6 — larger organic blotch
const HWG_DECAY = 0.88;
const HWG_FLIP_AT = 0.38;
const HWG_FLIP_SPEED = 6.2;
const HWG_MAX_DPR = 1.25;      // 2× DPR was crushing fill-rate

function HeroWorldGrid() {
  const shellRef = useRefHWG(null);
  const canvasRef = useRefHWG(null);
  const [ready, setReady] = useStateHWG(false);
  const isTouch = useMemoHWG(() => {
    if (typeof window === 'undefined') return true;
    return window.matchMedia('(hover: none), (pointer: coarse)').matches;
  }, []);

  useEffectHWG(() => {
    let cancelled = false;
    Promise.all([loadImage(HWG_SRC_A), loadImage(HWG_SRC_B)]).then((imgs) => {
      if (!cancelled) {
        if (shellRef.current) shellRef.current._hwgImgs = imgs;
        setReady(true);
      }
    });
    return () => { cancelled = true; };
  }, []);

  useEffectHWG(() => {
    if (isTouch || !ready) return;
    const shell = shellRef.current;
    const canvas = canvasRef.current;
    if (!shell || !canvas) return;
    const imgs = shell._hwgImgs;
    if (!imgs) return;
    const [imgA, imgB] = imgs;

    const ctx = canvas.getContext('2d', { alpha: false, desynchronized: true });
    // Pre-scaled layers: cell draws sample already-sized bitmaps (cheap)
    const layerA = document.createElement('canvas');
    const layerB = document.createElement('canvas');
    const ctxA = layerA.getContext('2d', { alpha: false });
    const ctxB = layerB.getContext('2d', { alpha: false });

    const strengths = new Float32Array(HWG_CELL_N);
    const progress = new Float32Array(HWG_CELL_N);
    const speeds = new Float32Array(HWG_CELL_N);
    const active = []; // sparse list of cell indices that need work
    const inActive = new Uint8Array(HWG_CELL_N);
    for (let i = 0; i < HWG_CELL_N; i++) speeds[i] = 0.75 + hash11(i * 97 + 3) * 1.0;

    let cursor = null;
    let raf = 0;
    let lastTs = 0;
    let cssW = 0;
    let cssH = 0;
    let tw = 1;
    let th = 1;
    let dirty = true;

    const mark = (i) => {
      if (!inActive[i]) {
        inActive[i] = 1;
        active.push(i);
      }
    };

    const resize = () => {
      const r = shell.getBoundingClientRect();
      cssW = Math.max(1, r.width);
      cssH = Math.max(1, r.height);
      const dpr = Math.min(window.devicePixelRatio || 1, HWG_MAX_DPR);
      const pw = (cssW * dpr) | 0;
      const ph = (cssH * dpr) | 0;
      if (canvas.width !== pw || canvas.height !== ph) {
        canvas.width = pw;
        canvas.height = ph;
        canvas.style.width = cssW + 'px';
        canvas.style.height = cssH + 'px';
        layerA.width = pw;
        layerA.height = ph;
        layerB.width = pw;
        layerB.height = ph;
        ctxA.drawImage(imgA, 0, 0, pw, ph);
        ctxB.drawImage(imgB, 0, 0, pw, ph);
      }
      tw = pw / HWG_COLS;
      th = ph / HWG_ROWS;
      dirty = true;
      ensureLoop();
    };

    const stampBrush = (cx, cy, t) => {
      const R = HWG_BRUSH_R * (0.88 + 0.18 * Math.sin(t * 1.4 + cx * 0.25));
      const stretch = 0.72 + 0.22 * hash11(((t * 2.5) | 0) + cx * 17 + cy);
      const R2 = R * R;
      const maxD = (R + 1) | 0;
      for (let dy = -maxD; dy <= maxD; dy++) {
        const dyS = dy * stretch;
        const dy2 = dyS * dyS;
        for (let dx = -maxD; dx <= maxD; dx++) {
          const dist2 = dx * dx + dy2;
          if (dist2 > R2) continue;
          const cc = cx + dx;
          const rr = cy + dy;
          if (cc < 0 || rr < 0 || cc >= HWG_COLS || rr >= HWG_ROWS) continue;
          const dist = Math.sqrt(dist2);
          const fall = 1 - dist / R;
          // Cheaper single noise sample (still organic)
          const n = valueNoise2(cc * 0.48 + t * 0.32, rr * 0.48 - t * 0.26);
          const gate = fall * fall * (0.5 + 0.6 * n);
          const keep = (dx === 0 && dy === 0) || gate > 0.28 + 0.18 * (1 - fall);
          if (!keep) continue;
          const i = rr * HWG_COLS + cc;
          const v = 0.5 + fall * 0.55;
          if (v > strengths[i]) strengths[i] = v > 1 ? 1 : v;
          mark(i);
        }
      }
    };

    const draw = () => {
      const w = canvas.width;
      const h = canvas.height;
      if (w < 2 || h < 2) return;
      // No clearRect — full living layer covers
      ctx.drawImage(layerA, 0, 0);

      const n = active.length;
      for (let a = 0; a < n; a++) {
        const i = active[a];
        const p = progress[i];
        if (p <= 0.001) continue;

        const c = i % HWG_COLS;
        const r = (i / HWG_COLS) | 0;
        const x = c * tw;
        const y = r * th;

        if (p >= 0.999) {
          // Settled dead tile — straight blit, no trig
          ctx.drawImage(layerB, x, y, tw, th, x, y, tw, th);
          continue;
        }

        const cos = Math.cos(p * Math.PI);
        const scaleX = cos < 0 ? -cos : cos;
        if (scaleX < 0.04) {
          ctx.fillStyle = '#06140F';
          ctx.fillRect(x + tw * 0.48, y, tw * 0.04, th);
          continue;
        }
        const src = cos >= 0 ? layerA : layerB;
        const cw = tw * scaleX;
        ctx.drawImage(src, x, y, tw, th, x + (tw - cw) * 0.5, y, cw, th);
      }
    };

    const tick = (ts) => {
      raf = 0;
      if (!lastTs) lastTs = ts;
      let dt = (ts - lastTs) * 0.001;
      if (dt > 0.05) dt = 0.05;
      lastTs = ts;
      const t = ts * 0.001;

      if (cursor) stampBrush(cursor.c, cursor.r, t);

      let still = !!cursor;
      // Compact active list in-place
      let w = 0;
      for (let a = 0; a < active.length; a++) {
        const i = active[a];
        if (strengths[i] > 0) {
          strengths[i] *= HWG_DECAY;
          if (strengths[i] < 0.05) strengths[i] = 0;
        }
        const target = strengths[i] >= HWG_FLIP_AT ? 1 : 0;
        let cur = progress[i];
        if (cur !== target) {
          const step = HWG_FLIP_SPEED * speeds[i] * dt;
          cur = target > cur
            ? (cur + step >= 1 ? 1 : cur + step)
            : (cur - step <= 0 ? 0 : cur - step);
          progress[i] = cur;
          dirty = true;
        }
        const keep = strengths[i] > 0 || progress[i] > 0.001;
        if (keep) {
          active[w++] = i;
          still = true;
        } else {
          inActive[i] = 0;
          progress[i] = 0;
        }
      }
      active.length = w;

      if (dirty || still) {
        draw();
        dirty = false;
      }

      if (still) raf = requestAnimationFrame(tick);
      else lastTs = 0;
    };

    const ensureLoop = () => {
      if (!raf) {
        lastTs = 0;
        raf = requestAnimationFrame(tick);
      }
    };

    const onMove = (e) => {
      const r = shell.getBoundingClientRect();
      if (r.width < 1 || r.height < 1) return;
      const c = ((e.clientX - r.left) / r.width * HWG_COLS) | 0;
      const row = ((e.clientY - r.top) / r.height * HWG_ROWS) | 0;
      if (c < 0 || row < 0 || c >= HWG_COLS || row >= HWG_ROWS) {
        cursor = null;
        return;
      }
      cursor = { c, r: row };
      ensureLoop();
    };

    const onLeave = () => {
      cursor = null;
      ensureLoop();
    };

    resize();
    draw();
    const ro = new ResizeObserver(resize);
    ro.observe(shell);
    shell.addEventListener('mousemove', onMove, { passive: true });
    shell.addEventListener('mouseleave', onLeave);

    return () => {
      cancelAnimationFrame(raf);
      ro.disconnect();
      shell.removeEventListener('mousemove', onMove);
      shell.removeEventListener('mouseleave', onLeave);
    };
  }, [isTouch, ready]);

  const shellStyle = {
    top: '50%',
    left: '50%',
    transform: 'translate(-50%, -50%)',
    width: HWG_WIDTH,
    aspectRatio: HWG_ASPECT,
    '--img-a': `url(${HWG_SRC_A})`,
  };

  if (isTouch) {
    return (
      <div
        ref={shellRef}
        className={`hwg-shell hwg-shell--static absolute z-[5] pointer-events-none ${ready ? 'is-ready' : ''}`}
        style={shellStyle}
        aria-hidden="true"
      >
        <div className="hwg-base" />
        <div className="hwg-veil" />
      </div>
    );
  }

  return (
    <div
      ref={shellRef}
      className={`hwg-shell hwg-shell--live absolute z-[5] ${ready ? 'is-ready' : ''}`}
      style={shellStyle}
      aria-hidden="true"
    >
      <canvas ref={canvasRef} className="hwg-canvas" />
      <div className="hwg-veil" />
    </div>
  );
}

function hash11(n) {
  const x = Math.sin(n * 127.1) * 43758.5453;
  return x - Math.floor(x);
}

function valueNoise2(x, y) {
  const x0 = Math.floor(x);
  const y0 = Math.floor(y);
  const fx = x - x0;
  const fy = y - y0;
  const ux = fx * fx * (3 - 2 * fx);
  const uy = fy * fy * (3 - 2 * fy);
  const a = hash11(x0 * 374761 + y0 * 668265);
  const b = hash11((x0 + 1) * 374761 + y0 * 668265);
  const c = hash11(x0 * 374761 + (y0 + 1) * 668265);
  const d = hash11((x0 + 1) * 374761 + (y0 + 1) * 668265);
  return a + (b - a) * ux + (c - a) * uy + (a - b - c + d) * ux * uy;
}

function loadImage(src) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.decoding = 'async';
    img.onload = () => {
      if (img.decode) img.decode().then(() => resolve(img)).catch(() => resolve(img));
      else resolve(img);
    };
    img.onerror = reject;
    img.src = src;
  });
}

Object.assign(window, { HeroWorldGrid });
