// EyeGrid — grid of eyes that track the cursor with proper spherical rotation.
// Each eye is modeled as a sphere; iris position is computed by rotating the
// sphere so the front-pole points at the cursor, then projecting to 2D.

const { useState, useEffect, useRef, useMemo } = React;

function EyeGrid({ tweaks, exclusionZones = [] }) {
  const {
    eyeSize,
    density,
    irisColor,
    scleraColor,
    pupilColor,
    parallax,
    revealed,
    simultaneousOpen,
  } = tweaks;

  const containerRef = useRef(null);
  const [layout, setLayout] = useState({ cols: 0, rows: 0, cellW: 0, cellH: 0, w: 0, h: 0 });

  // Recompute layout on resize
  useEffect(() => {
    function measure() {
      const el = containerRef.current;
      if (!el) return;
      const w = el.clientWidth;
      const h = el.clientHeight;
      const cellW = eyeSize * density;
      const cellH = cellW * 0.72;
      const cols = Math.ceil(w / cellW) + 1;
      const rows = Math.ceil(h / cellH) + 1;
      setLayout({ cols, rows, cellW, cellH, w, h });
    }
    measure();
    window.addEventListener('resize', measure);
    return () => window.removeEventListener('resize', measure);
  }, [eyeSize, density]);

  // Build eye positions via Poisson-disk-style scatter — haphazard but non-overlapping.
  const eyes = useMemo(() => {
    const arr = [];
    const { cellW, cellH, w, h } = layout;
    if (!w) return arr;

    // Min spacing between eye centers — derived from cell size; vary radius slightly
    // for organic feel.
    const minDist = cellW * 0.78;

    // Seeded PRNG so the layout is stable across renders for the same dims
    let s = (Math.round(w) * 374761393) ^ (Math.round(h) * 668265263) ^ Math.round(eyeSize * density * 1000);
    function rand() {
      s = (s * 1664525 + 1013904223) | 0;
      return ((s >>> 0) / 4294967295);
    }

    const eyeRadiusX = eyeSize / 2;
    const eyeRadiusY = (eyeSize * 0.6) / 2;

    function inExclusion(px, py) {
      for (const z of exclusionZones) {
        if (z.shape === 'ellipse') {
          const dx = (px - z.cx) / (z.rx + eyeRadiusX);
          const dy = (py - z.cy) / (z.ry + eyeRadiusY);
          if (dx * dx + dy * dy <= 1) return true;
        } else if (z.shape === 'rect') {
          if (
            px >= z.x - eyeRadiusX && px <= z.x + z.w + eyeRadiusX &&
            py >= z.y - eyeRadiusY && py <= z.y + z.h + eyeRadiusY
          ) return true;
        }
      }
      return false;
    }

    // Spatial hash for fast neighbor lookup
    const cell = minDist;
    const grid = new Map();
    const key = (cx, cy) => cx + ',' + cy;
    function tryPlace(px, py) {
      if (px < -eyeRadiusX || px > w + eyeRadiusX || py < -eyeRadiusY || py > h + eyeRadiusY) return false;
      if (inExclusion(px, py)) return false;
      const gx = Math.floor(px / cell);
      const gy = Math.floor(py / cell);
      for (let ox = -2; ox <= 2; ox++) {
        for (let oy = -2; oy <= 2; oy++) {
          const list = grid.get(key(gx + ox, gy + oy));
          if (!list) continue;
          for (const p of list) {
            const ddx = p.x - px;
            const ddy = p.y - py;
            if (ddx * ddx + ddy * ddy < minDist * minDist) return false;
          }
        }
      }
      const list = grid.get(key(gx, gy)) || [];
      list.push({ x: px, y: py });
      grid.set(key(gx, gy), list);
      return true;
    }

    // Bridson-ish: seed with a few random points, then dart-throw aggressively
    const targetCount = Math.floor((w * h) / (minDist * minDist) * 0.85);
    let attempts = 0;
    let placed = 0;
    const maxAttempts = targetCount * 30;
    let id = 0;

    while (placed < targetCount && attempts < maxAttempts) {
      attempts++;
      const px = rand() * (w + eyeSize) - eyeSize / 2;
      const py = rand() * (h + eyeSize) - eyeSize / 2;
      if (tryPlace(px, py)) {
        placed++;
        const dxc = (px - w / 2) / w;
        const dyc = (py - h / 2) / h;
        const dist = Math.sqrt(dxc * dxc + dyc * dyc);
        const revealDelay = dist * 900 + rand() * 250;
        // Per-eye visual variation: rotation, scale jitter
        const rotation = (rand() - 0.5) * 50; // ±25°
        const scale = 0.78 + rand() * 0.42;   // 0.78–1.20
        arr.push({ id: id++, x: px, y: py, revealDelay, rotation, scale, seed: id });
      }
    }
    return arr;
  }, [layout, exclusionZones, eyeSize, density]);

  const cursor = useRef({ x: 0, y: 0, has: false });
  const eyeRefs = useRef([]);
  const rafRef = useRef(null);

  useEffect(() => {
    function onMove(e) {
      cursor.current.x = e.clientX;
      cursor.current.y = e.clientY;
      cursor.current.has = true;
    }
    function onLeave() {
      cursor.current.has = false;
    }
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseleave', onLeave);
    return () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseleave', onLeave);
    };
  }, []);

  const blinks = useRef(new Map());
  useEffect(() => {
    if (!eyes.length) return;
    const interval = setInterval(() => {
      const n = 1 + Math.floor(Math.random() * 3);
      for (let k = 0; k < n; k++) {
        const id = Math.floor(Math.random() * eyes.length);
        if (!blinks.current.has(id)) {
          blinks.current.set(id, { start: performance.now(), dur: 160 });
        }
      }
    }, 800);
    return () => clearInterval(interval);
  }, [eyes.length]);

  const revealStartRef = useRef(null);
  useEffect(() => {
    if (revealed) revealStartRef.current = performance.now();
  }, [revealed]);

  // rAF loop — sphere model for iris position
  useEffect(() => {
    function tick() {
      const now = performance.now();
      const cur = cursor.current;
      const revealStart = revealStartRef.current;

      for (let i = 0; i < eyes.length; i++) {
        const eye = eyes[i];
        const node = eyeRefs.current[i];
        if (!node) continue;

        // Reveal lid: simultaneousOpen ignores per-eye delay
        let revealLid = 1;
        if (revealStart != null) {
          const delay = simultaneousOpen ? 0 : eye.revealDelay;
          const dur = simultaneousOpen ? 520 : 350;
          const t = now - revealStart - delay;
          if (t >= 0) revealLid = Math.max(0, 1 - t / dur);
        }

        // Idle blink lid
        let blinkLid = 0;
        const b = blinks.current.get(eye.id);
        if (b) {
          const t = now - b.start;
          if (t > b.dur) {
            blinks.current.delete(eye.id);
          } else {
            const half = b.dur / 2;
            blinkLid = t < half ? t / half : 1 - (t - half) / half;
          }
        }
        const lid = Math.max(revealLid, blinkLid);

        // === Spherical eye rotation ===
        // Sphere center at eye position, radius R (in viewBox units = 30).
        // Rotate so that the front pole faces the cursor direction.
        // Iris sits at the front pole; project to 2D.
        //
        // viewBox is 100x60, eye center at (50, 30). Sphere radius ~ 24.
        // Angle theta = how far we rotate; max ~ 38° so iris stays mostly in almond.

        let irisCx = 50;
        let irisCy = 30;
        let irisScaleX = 1; // foreshortening on horizontal
        let irisScaleY = 1; // foreshortening on vertical

        if (cur.has) {
          const dx = cur.x - eye.x;
          const dy = cur.y - eye.y;
          const mag = Math.sqrt(dx * dx + dy * dy);

          if (mag > 0.001) {
            // How far to rotate: saturate quickly so distant cursor still gives full rotation
            // (real eyes also saturate — we don't keep rolling further at infinite distance)
            const saturation = Math.min(1, mag / 250);
            // Max angle: 42° feels strong but not bug-eyed
            const baseMax = 42 * Math.PI / 180;
            // Optional parallax: closer eyes rotate slightly more aggressively
            const parallaxBoost = parallax
              ? 1 + Math.min(0.15, 80 / (mag + 80) * 0.4)
              : 1;
            const theta = baseMax * saturation * parallaxBoost;

            // Direction unit vector in screen space
            const ux = dx / mag;
            const uy = dy / mag;

            // Rotate the iris point on the sphere.
            // In sphere coords, iris starts at (0, 0, R) (front pole).
            // After rotation by theta around an axis perpendicular to (ux, uy):
            //   x' = R * sin(theta) * ux
            //   y' = R * sin(theta) * uy
            //   z' = R * cos(theta)
            // Orthographic projection drops z. The iris disc, originally facing +z,
            // now tilts — its 2D projection becomes an ellipse with:
            //   minor axis along (ux, uy), scaled by cos(theta)
            //   major axis perpendicular to (ux, uy), scale 1
            const sphereR = 24; // in viewBox units
            const sin = Math.sin(theta);
            const cos = Math.cos(theta);

            irisCx = 50 + sphereR * sin * ux;
            irisCy = 30 + sphereR * sin * uy * 0.9; // slight vertical squash for almond shape

            // Foreshortening: iris ellipse axes in screen space
            // The iris disc's normal is now (sin*ux, sin*uy, cos).
            // Projected to xy plane, the disc becomes an ellipse with
            // semi-axis cos along the (ux,uy) direction and 1 perpendicular.
            // We approximate by setting scaleX/scaleY based on direction:
            // for purely horizontal cursor (ux=±1, uy=0), scaleX = cos, scaleY = 1.
            // For vertical, scaleX = 1, scaleY = cos.
            // For diagonal, blend.
            const ax = Math.abs(ux);
            const ay = Math.abs(uy);
            irisScaleX = 1 - (1 - cos) * ax * ax;
            irisScaleY = 1 - (1 - cos) * ay * ay;
          }
        }

        // Apply to DOM
        const irisG = node._irisG;
        const topLid = node._topLid;
        const botLid = node._botLid;

        if (irisG) {
          // Translate to iris center, then scale for foreshortening
          irisG.setAttribute(
            'transform',
            `translate(${irisCx} ${irisCy}) scale(${irisScaleX} ${irisScaleY})`
          );
        }

        const lidCover = lid * 30;
        if (topLid) topLid.setAttribute('height', lidCover);
        if (botLid) {
          botLid.setAttribute('y', 60 - lidCover);
          botLid.setAttribute('height', lidCover);
        }
      }
      rafRef.current = requestAnimationFrame(tick);
    }
    rafRef.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(rafRef.current);
  }, [eyes, parallax]);

  return (
    <div
      ref={containerRef}
      style={{
        position: 'absolute',
        inset: 0,
        overflow: 'hidden',
        pointerEvents: 'none',
      }}
    >
      {eyes.map((eye, i) => (
        <RawEye
          key={eye.id}
          x={eye.x}
          y={eye.y}
          size={eyeSize * (eye.scale || 1)}
          rotation={eye.rotation || 0}
          irisColor={irisColor}
          scleraColor={scleraColor}
          pupilColor={pupilColor}
          registerRef={(refs) => {
            eyeRefs.current[i] = refs;
          }}
        />
      ))}
    </div>
  );
}

// RawEye — iris/pupil/highlight live inside a <g> we transform every frame.
function RawEye({ x, y, size, rotation = 0, irisColor, scleraColor, pupilColor, registerRef }) {
  const irisGRef = useRef(null);
  const topLidRef = useRef(null);
  const botLidRef = useRef(null);

  useEffect(() => {
    registerRef({
      _irisG: irisGRef.current,
      _topLid: topLidRef.current,
      _botLid: botLidRef.current,
    });
  }, []);

  const w = size;
  const h = size * 0.6;
  const clipId = `clip-${Math.round(x)}-${Math.round(y)}`;

  return (
    <svg
      width={w}
      height={h}
      viewBox="0 0 100 60"
      style={{
        position: 'absolute',
        left: x - w / 2,
        top: y - h / 2,
        overflow: 'visible',
        transform: `rotate(${rotation}deg)`,
      }}
    >
      <defs>
        <clipPath id={clipId}>
          <path d="M 4 30 Q 50 -2 96 30 Q 50 62 4 30 Z" />
        </clipPath>
      </defs>
      {/* Sclera */}
      <path d="M 4 30 Q 50 -2 96 30 Q 50 62 4 30 Z" fill={scleraColor} />
      {/* Iris group — transform applied every frame */}
      <g clipPath={`url(#${clipId})`}>
        <g ref={irisGRef} transform="translate(50 30)">
          <circle cx={0} cy={0} r={14} fill={irisColor} />
          <circle cx={0} cy={0} r={6.5} fill={pupilColor} />
          <circle cx={-3.5} cy={-3.5} r={1.6} fill="#ffffff" opacity={0.9} />
        </g>
      </g>
      {/* Lids */}
      <g clipPath={`url(#${clipId})`}>
        <rect ref={topLidRef} x={0} y={0} width={100} height={30} fill={pupilColor} />
        <rect ref={botLidRef} x={0} y={30} width={100} height={30} fill={pupilColor} />
      </g>
      {/* Lash line */}
      <path
        d="M 4 30 Q 50 -2 96 30 Q 50 62 4 30 Z"
        fill="none"
        stroke={pupilColor}
        strokeWidth={1.2}
        strokeLinejoin="round"
      />
    </svg>
  );
}

window.EyeGrid = EyeGrid;
