/* Polarisation field: a grid of double-headed polarisation ticks lying over the beam,
   invisible at rest, revealed by a cursor-following probe with trailing decay.
   Double-headed because polarisation is an orientation axis, not a direction. */
function PolarisationField({ mode = 'azimuthal', spacing = 34, radius = 210, style }) {
  const canvasRef = React.useRef(null);
  const stateRef = React.useRef({ mx: -9999, my: -9999, tx: -9999, ty: -9999, active: false, cells: [], w: 0, h: 0, dpr: 1, rgb: [255, 32, 32] });

  React.useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    const S = stateRef.current;
    let raf = 0;

    const build = () => {
      const rect = canvas.getBoundingClientRect();
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      S.w = rect.width; S.h = rect.height; S.dpr = dpr;
      canvas.width = Math.round(rect.width * dpr);
      canvas.height = Math.round(rect.height * dpr);
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      const cx = rect.width / 2, cy = rect.height / 2;
      const cells = [];
      const cols = Math.ceil(rect.width / spacing) + 1;
      const rows = Math.ceil(rect.height / spacing) + 1;
      const ox = (rect.width - (cols - 1) * spacing) / 2;
      const oy = (rect.height - (rows - 1) * spacing) / 2;
      for (let j = 0; j < rows; j++) {
        for (let i = 0; i < cols; i++) {
          const x = ox + i * spacing;
          const y = oy + j * spacing;
          const dx = x - cx, dy = y - cy;
          const phi = Math.atan2(dy, dx);
          // The mode's own polarisation orientation at this point.
          const base = mode === 'radial' ? phi : mode === 'linear' ? 0 : phi + Math.PI / 2;
          cells.push({ x, y, base, a: 0, tilt: 0 });
        }
      }
      S.cells = cells;
    };

    const draw = () => {
      raf = requestAnimationFrame(draw);
      const { cells } = S;
      if (!cells.length) return;
      // ease the probe toward the pointer — the lag is what makes it feel like a field
      S.mx += (S.tx - S.mx) * 0.16;
      S.my += (S.ty - S.my) * 0.16;
      ctx.clearRect(0, 0, S.w, S.h);
      const r2 = radius * radius;
      const len = 7.5;
      for (let k = 0; k < cells.length; k++) {
        const c = cells[k];
        let target = 0, prox = 0;
        if (S.active) {
          const dx = c.x - S.mx, dy = c.y - S.my;
          const d2 = dx * dx + dy * dy;
          if (d2 < r2) {
            prox = 1 - Math.sqrt(d2) / radius;
            target = prox * prox * (3 - 2 * prox); // smoothstep
          }
        }
        // rise fast, fall slow — leaves a trail behind the probe
        c.a += (target - c.a) * (target > c.a ? 0.34 : 0.045);
        if (c.a < 0.004) { c.a = 0; c.tilt += (0 - c.tilt) * 0.08; continue; }
        // the probe locally perturbs the orientation it passes through
        const perturb = S.active ? Math.atan2(c.y - S.my, c.x - S.mx) + Math.PI / 2 : c.base;
        let delta = perturb - c.base;
        while (delta > Math.PI / 2) delta -= Math.PI;
        while (delta < -Math.PI / 2) delta += Math.PI;
        c.tilt += (delta * prox * 0.42 - c.tilt) * 0.12;
        const ang = c.base + c.tilt;
        const ca = Math.cos(ang), sa = Math.sin(ang);
        const l = len * (0.62 + 0.38 * c.a);
        const x1 = c.x - ca * l, y1 = c.y - sa * l;
        const x2 = c.x + ca * l, y2 = c.y + sa * l;
        const hot = Math.min(1, c.a * 1.25);
        const br = S.rgb;
        // cool toward white as a tick brightens
        const mix = (v) => Math.round(v + (255 - v) * (1 - hot) * 0.55);
        ctx.strokeStyle = 'rgba(' + mix(br[0]) + ',' + mix(br[1]) + ',' + mix(br[2]) + ',' + (c.a * 0.92).toFixed(3) + ')';
        ctx.lineWidth = 1.15;
        ctx.beginPath();
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.stroke();
        // arrowheads on both ends once the tick is bright enough
        if (c.a > 0.28) {
          const hl = 3.4, spread = 0.62;
          ctx.beginPath();
          ctx.moveTo(x2, y2);
          ctx.lineTo(x2 - Math.cos(ang - spread) * hl, y2 - Math.sin(ang - spread) * hl);
          ctx.moveTo(x2, y2);
          ctx.lineTo(x2 - Math.cos(ang + spread) * hl, y2 - Math.sin(ang + spread) * hl);
          ctx.moveTo(x1, y1);
          ctx.lineTo(x1 + Math.cos(ang - spread) * hl, y1 + Math.sin(ang - spread) * hl);
          ctx.moveTo(x1, y1);
          ctx.lineTo(x1 + Math.cos(ang + spread) * hl, y1 + Math.sin(ang + spread) * hl);
          ctx.stroke();
        }
      }
    };

    const onMove = (e) => {
      const rect = canvas.getBoundingClientRect();
      S.tx = e.clientX - rect.left;
      S.ty = e.clientY - rect.top;
      if (!S.active) { S.mx = S.tx; S.my = S.ty; S.active = true; }
    };
    const onLeave = () => { S.active = false; };
    const onFreq = (e) => { if (e.detail && e.detail.rgb) S.rgb = e.detail.rgb; };
    window.addEventListener('bo:frequency', onFreq);

    build();
    raf = requestAnimationFrame(draw);
    const parent = canvas.parentElement;
    parent.addEventListener('mousemove', onMove, { passive: true });
    parent.addEventListener('mouseleave', onLeave);
    const ro = new ResizeObserver(build);
    ro.observe(canvas);
    return () => {
      cancelAnimationFrame(raf);
      parent.removeEventListener('mousemove', onMove);
      parent.removeEventListener('mouseleave', onLeave);
      window.removeEventListener('bo:frequency', onFreq);
      ro.disconnect();
    };
  }, [mode, spacing, radius]);

  return <canvas ref={canvasRef} aria-hidden="true" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', pointerEvents: 'none', mixBlendMode: 'screen', ...style }} />;
}

window.PolarisationField = PolarisationField;
