// ============================================================
// BVS — Constellation particle field (canvas, section-scoped)
// Drifting/twinkling stars that link to nearby neighbours and
// reach toward the cursor — the same node-network concept used
// on the legal pages, applied to every deep-blue ("depth") band.
// Pauses when its section scrolls offscreen.
// ============================================================
function Particles({ density = 1, color = '255,255,255', link = true, accent = '120,235,150' }) {
  const ref = useRef(null);
  useEffect(() => {
    const canvas = ref.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    const host = canvas.parentElement;
    const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    let dpr = Math.min(2, window.devicePixelRatio || 1);
    let W = 0, H = 0, parts = [], raf = 0, t = 0, visible = true, rect = null;
    const mouse = { x: -9999, y: -9999, on: false };

    const seed = () => {
      const area = W * H;
      const count = Math.min(120, Math.round((area / 15000) * density));
      parts = new Array(count).fill(0).map(() => ({
        x: Math.random() * W,
        y: Math.random() * H,
        z: Math.random() * 0.8 + 0.2,        // depth → size & speed & brightness
        vx: (Math.random() - 0.5) * 0.12,
        vy: (Math.random() - 0.5) * 0.12 - 0.05, // gentle upward drift
        ph: Math.random() * Math.PI * 2,      // twinkle phase
        tw: Math.random() * 0.6 + 0.3,        // twinkle speed
        mint: Math.random() < 0.14,           // green accent node
      }));
    };
    const resize = () => {
      W = host.clientWidth; H = host.clientHeight;
      canvas.width = W * dpr; canvas.height = H * dpr;
      canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      rect = host.getBoundingClientRect();
      seed();
    };

    const LINK = 130, MLINK = 168;
    const draw = () => {
      t += 1;
      ctx.clearRect(0, 0, W, H);
      // advance
      for (const p of parts) {
        p.x += p.vx * p.z * 2.2;
        p.y += p.vy * p.z * 2.2;
        if (p.x < -4) p.x = W + 4; else if (p.x > W + 4) p.x = -4;
        if (p.y < -4) p.y = H + 4; else if (p.y > H + 4) p.y = -4;
      }
      // neighbour links (constellation)
      if (link) {
        const lim = LINK * LINK;
        for (let i = 0; i < parts.length; i++) {
          const a = parts[i];
          for (let j = i + 1; j < parts.length; j++) {
            const b = parts[j];
            const dx = a.x - b.x, dy = a.y - b.y;
            const d2 = dx * dx + dy * dy;
            if (d2 < lim) {
              const al = (1 - Math.sqrt(d2) / LINK) * 0.16;
              ctx.strokeStyle = 'rgba(150,178,255,' + al.toFixed(3) + ')';
              ctx.lineWidth = 1;
              ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke();
            }
          }
        }
      }
      // cursor links
      if (mouse.on) {
        const lim = MLINK * MLINK;
        for (const p of parts) {
          const dx = p.x - mouse.x, dy = p.y - mouse.y;
          const d2 = dx * dx + dy * dy;
          if (d2 < lim) {
            const al = (1 - Math.sqrt(d2) / MLINK) * 0.45;
            ctx.strokeStyle = 'rgba(185,202,255,' + al.toFixed(3) + ')';
            ctx.lineWidth = 1;
            ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(mouse.x, mouse.y); ctx.stroke();
          }
        }
      }
      // nodes
      for (const p of parts) {
        const twinkle = 0.55 + 0.45 * Math.sin(t * 0.02 * p.tw + p.ph);
        const a = (0.15 + p.z * 0.6) * twinkle;
        const r = p.z * 1.5 + 0.3;
        ctx.beginPath();
        ctx.arc(p.x, p.y, r, 0, 6.283);
        ctx.fillStyle = p.mint ? `rgba(${accent},${(a * 0.95).toFixed(3)})` : `rgba(${color},${a.toFixed(3)})`;
        ctx.fill();
        // soft glow on the brightest stars / accent nodes
        if (p.z > 0.85 || p.mint) {
          ctx.beginPath();
          ctx.arc(p.x, p.y, r * 2.6, 0, 6.283);
          ctx.fillStyle = p.mint ? `rgba(${accent},${(a * 0.14).toFixed(3)})` : `rgba(${color},${(a * 0.10).toFixed(3)})`;
          ctx.fill();
        }
      }
      raf = (!reduce && visible) ? requestAnimationFrame(draw) : 0;
    };

    resize();
    const onResize = () => { dpr = Math.min(2, window.devicePixelRatio || 1); resize(); };
    const onScroll = () => { rect = host.getBoundingClientRect(); };
    const onMove = (e) => {
      if (!rect) rect = host.getBoundingClientRect();
      const x = e.clientX - rect.left, y = e.clientY - rect.top;
      if (x >= 0 && x <= W && y >= 0 && y <= H) { mouse.x = x; mouse.y = y; mouse.on = true; }
      else mouse.on = false;
    };
    window.addEventListener('resize', onResize);
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('pointermove', onMove, { passive: true });
    const onLeave = () => { mouse.on = false; };
    window.addEventListener('pointerleave', onLeave);

    // re-measure when the host grows (content lays out after mount)
    let ro = null;
    if ('ResizeObserver' in window) {
      ro = new ResizeObserver(() => {
        if (host.clientWidth !== W || host.clientHeight !== H) resize();
        else rect = host.getBoundingClientRect();
      });
      ro.observe(host);
    }
    // pause the loop while the section is scrolled offscreen
    let io = null;
    if ('IntersectionObserver' in window) {
      io = new IntersectionObserver((es) => {
        visible = es[0].isIntersecting;
        if (visible && !reduce && !raf) raf = requestAnimationFrame(draw);
      }, { rootMargin: '140px' });
      io.observe(host);
    }

    if (!reduce) raf = requestAnimationFrame(draw); else draw();
    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener('resize', onResize);
      window.removeEventListener('scroll', onScroll);
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerleave', onLeave);
      if (ro) ro.disconnect();
      if (io) io.disconnect();
    };
  }, [density, color, link, accent]);
  return <canvas ref={ref} aria-hidden="true" style={{ position: 'absolute', inset: 0, zIndex: -1, pointerEvents: 'none' }} />;
}

Object.assign(window, { Particles });
