// particles.jsx — ambient 3D particle field.
//
// A fixed full-viewport canvas rendered behind the page content. ~1300 particles
// live in a unit-ish 3D space, slowly rotating, and morph between target point
// clouds as the visitor scrolls. Each <section data-shape="..."> declares which
// shape the field should form while that section is in view.
//
// Shapes: asterisk (brand mark), lattice (AI), reel (media), spiral (ventures),
// sphere (contact). Exposed as a global <ParticleField /> component, same
// pattern as tweaks-panel.jsx.

const PF_COUNT = 1300;
const PF_FOV = 3.0;          // perspective strength (unit space)
const PF_EASE = 0.055;       // morph easing per frame
const PF_ROT_SPEED = 0.0028; // radians per frame around Y

// ─── Shape builders ─────────────────────────────────────────
// Each returns a Float32Array of n*3 positions in roughly [-1, 1]³.

function pfAsterisk(n) {
  const p = new Float32Array(n * 3);
  for (let i = 0; i < n; i++) {
    const arm = i % 6;
    const a = arm * (Math.PI / 3) + Math.PI / 2;
    const t = 0.1 + Math.pow(Math.random(), 0.75) * 0.9;
    p[i * 3]     = Math.cos(a) * t + (Math.random() - 0.5) * 0.07;
    p[i * 3 + 1] = Math.sin(a) * t + (Math.random() - 0.5) * 0.07;
    p[i * 3 + 2] = (Math.random() - 0.5) * 0.16;
  }
  return p;
}

function pfLattice(n) {
  // Even round-robin over a 4×4×4 grid → equal-density node clusters that
  // read as a crisp dot-matrix cube instead of noise.
  const p = new Float32Array(n * 3);
  const side = 4, span = 1.4;
  const nodes = side * side * side;
  for (let i = 0; i < n; i++) {
    const node = i % nodes;
    const cx = node % side;
    const cyy = Math.floor(node / side) % side;
    const cz = Math.floor(node / (side * side));
    const cells = [cx, cyy, cz];
    for (let k = 0; k < 3; k++) {
      p[i * 3 + k] = (cells[k] / (side - 1) - 0.5) * span + (Math.random() - 0.5) * 0.05;
    }
  }
  return p;
}

function pfReel(n) {
  const p = new Float32Array(n * 3);
  for (let i = 0; i < n; i++) {
    const r = Math.random();
    let x, y;
    if (r < 0.52) {          // outer ring
      const a = Math.random() * Math.PI * 2;
      const rad = 1 + (Math.random() - 0.5) * 0.08;
      x = Math.cos(a) * rad; y = Math.sin(a) * rad;
    } else if (r < 0.7) {    // hub
      const a = Math.random() * Math.PI * 2;
      const rad = 0.28 + (Math.random() - 0.5) * 0.07;
      x = Math.cos(a) * rad; y = Math.sin(a) * rad;
    } else {                 // six spokes
      const a = Math.floor(Math.random() * 6) * (Math.PI / 3) + Math.PI / 6;
      const t = 0.32 + Math.random() * 0.62;
      x = Math.cos(a) * t + (Math.random() - 0.5) * 0.05;
      y = Math.sin(a) * t + (Math.random() - 0.5) * 0.05;
    }
    p[i * 3] = x;
    p[i * 3 + 1] = y;
    p[i * 3 + 2] = (Math.random() - 0.5) * 0.1;
  }
  return p;
}

function pfSpiral(n) {
  const p = new Float32Array(n * 3);
  for (let i = 0; i < n; i++) {
    if (Math.random() < 0.12) { // rising dust around the funnel
      const a = Math.random() * Math.PI * 2;
      const rad = Math.random() * 1.1;
      p[i * 3]     = Math.cos(a) * rad;
      p[i * 3 + 1] = (Math.random() - 0.5) * 1.9;
      p[i * 3 + 2] = Math.sin(a) * rad;
      continue;
    }
    const t = Math.random();
    const a = t * Math.PI * 5.5;
    const rad = 0.12 + t * 0.85 + (Math.random() - 0.5) * 0.06;
    p[i * 3]     = Math.cos(a) * rad;
    p[i * 3 + 1] = (t - 0.5) * 1.9 + (Math.random() - 0.5) * 0.05;
    p[i * 3 + 2] = Math.sin(a) * rad;
  }
  return p;
}

function pfSphere(n) {
  const p = new Float32Array(n * 3);
  const golden = Math.PI * (3 - Math.sqrt(5));
  for (let i = 0; i < n; i++) {
    const t = (i + 0.5) / n;
    const y = 1 - 2 * t;
    const rad = Math.sqrt(Math.max(0, 1 - y * y));
    const a = golden * i;
    const R = 0.82;
    p[i * 3]     = Math.cos(a) * rad * R + (Math.random() - 0.5) * 0.04;
    p[i * 3 + 1] = y * R + (Math.random() - 0.5) * 0.04;
    p[i * 3 + 2] = Math.sin(a) * rad * R + (Math.random() - 0.5) * 0.04;
  }
  return p;
}

const PF_SHAPES = {
  asterisk: pfAsterisk,
  lattice: pfLattice,
  reel: pfReel,
  spiral: pfSpiral,
  sphere: pfSphere,
};

// ─── Component ──────────────────────────────────────────────
function ParticleField() {
  const ref = React.useRef(null);

  React.useEffect(() => {
    const canvas = ref.current;
    const ctx = canvas.getContext("2d");
    // ?static=1 forces the no-animation path (also used for visual testing)
    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches ||
      new URLSearchParams(window.location.search).has("static");

    let W = 0, H = 0, DPR = 1, raf = 0, frame = 0;

    const pos = new Float32Array(PF_COUNT * 3);
    const tgt = new Float32Array(PF_COUNT * 3);
    const phase = new Float32Array(PF_COUNT);
    const accent = new Uint8Array(PF_COUNT);
    for (let i = 0; i < PF_COUNT; i++) {
      phase[i] = Math.random() * Math.PI * 2;
      accent[i] = i % 11 === 0 ? 1 : 0;
      // start as a loose cloud so the first shape assembles on load
      pos[i * 3]     = (Math.random() - 0.5) * 3.2;
      pos[i * 3 + 1] = (Math.random() - 0.5) * 3.2;
      pos[i * 3 + 2] = (Math.random() - 0.5) * 3.2;
    }

    const cache = new Map();
    let currentShape = null;
    function setShape(name) {
      if (!name || name === currentShape || !PF_SHAPES[name]) return;
      currentShape = name;
      if (!cache.has(name)) cache.set(name, PF_SHAPES[name](PF_COUNT));
      tgt.set(cache.get(name));
      if (reduced) { pos.set(tgt); draw(); }
    }

    let colFg = "#0c0c0a", colAccent = "#2546ff";
    function readColors() {
      const cs = getComputedStyle(document.documentElement);
      colFg = cs.getPropertyValue("--fg").trim() || colFg;
      colAccent = cs.getPropertyValue("--accent").trim() || colAccent;
      if (reduced) draw();
    }
    const mo = new MutationObserver(readColors);
    mo.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "style"] });

    function resize() {
      DPR = Math.min(window.devicePixelRatio || 1, 2);
      W = window.innerWidth; H = window.innerHeight;
      canvas.width = W * DPR; canvas.height = H * DPR;
      ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
      if (reduced) draw();
    }

    let sections = [];
    function pickShape() {
      const mid = window.scrollY + window.innerHeight * 0.45;
      let name = sections.length ? sections[0].dataset.shape : null;
      for (const s of sections) if (s.offsetTop <= mid) name = s.dataset.shape;
      setShape(name);
    }

    let rotY = -0.4, rotX = 0.18, mx = 0, my = 0;
    function onMouse(e) {
      mx = e.clientX / W - 0.5;
      my = e.clientY / H - 0.5;
    }

    let time = 0, lastTs = 0;
    function draw() {
      ctx.clearRect(0, 0, W, H);
      // Mobile dimming is handled by CSS (.particle-field opacity); the mask
      // fades the field over the text column on wide screens.
      const small = W < 880;
      const S = Math.min(W, H) * (small ? 0.32 : 0.3);
      const ax = small ? W * 0.5 : W * 0.72;
      const ay = small ? H * 0.42 : H * 0.5;

      const cy = Math.cos(rotY), sy = Math.sin(rotY);
      const cx = Math.cos(rotX), sx = Math.sin(rotX);

      for (let i = 0; i < PF_COUNT; i++) {
        let x = pos[i * 3], y = pos[i * 3 + 1], z = pos[i * 3 + 2];
        if (!reduced) {
          x += Math.sin(time * 0.7 + phase[i]) * 0.012;
          y += Math.cos(time * 0.6 + phase[i] * 1.7) * 0.012;
        }
        const xr = x * cy - z * sy;
        let zr = x * sy + z * cy;
        const yr = y * cx - zr * sx;
        zr = y * sx + zr * cx;

        const persp = PF_FOV / (PF_FOV + zr);
        if (persp <= 0) continue;
        const px = ax + xr * S * persp;
        const py = ay - yr * S * persp;
        if (px < -8 || px > W + 8 || py < -8 || py > H + 8) continue;

        const isA = accent[i] === 1;
        const size = (isA ? 1.7 : 1.05) * persp;
        const a = Math.max(0.04, Math.min(1, (persp - 0.55) * 1.6)) * (isA ? 0.55 : 0.28);
        ctx.globalAlpha = a;
        ctx.fillStyle = isA ? colAccent : colFg;
        ctx.beginPath();
        ctx.arc(px, py, size, 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.globalAlpha = 1;
    }

    function tick(ts) {
      // No document.hidden guard: browsers already suspend rAF in hidden tabs,
      // and some embedded contexts report "hidden" while still rendering.
      raf = requestAnimationFrame(tick);
      frame++;
      // dt in "60fps frames", capped so a background pause doesn't jump-cut
      const dt = lastTs ? Math.min((ts - lastTs) / 16.7, 3) : 1;
      lastTs = ts;
      time += dt / 60;
      rotY += (PF_ROT_SPEED + mx * 0.0007) * dt;
      rotX += ((0.18 + my * 0.5) - rotX) * 0.04 * dt;
      const k = 1 - Math.pow(1 - PF_EASE, dt);
      for (let i = 0; i < PF_COUNT * 3; i++) pos[i] += (tgt[i] - pos[i]) * k;
      if (frame % 10 === 0) pickShape();
      draw();
    }

    resize();
    readColors();
    sections = Array.from(document.querySelectorAll("[data-shape]"));
    pickShape();

    window.addEventListener("resize", resize);
    window.addEventListener("mousemove", onMouse, { passive: true });
    if (reduced) {
      window.addEventListener("scroll", pickShape, { passive: true });
      draw();
    } else {
      raf = requestAnimationFrame(tick);
    }

    return () => {
      cancelAnimationFrame(raf);
      mo.disconnect();
      window.removeEventListener("resize", resize);
      window.removeEventListener("mousemove", onMouse);
      window.removeEventListener("scroll", pickShape);
    };
  }, []);

  return <canvas ref={ref} className="particle-field" aria-hidden="true" />;
}
