// particles.jsx — interactive constellation background
const { useEffect: useEffectP, useRef: useRefP, useState: useStateP } = React;

// Site-wide fixed particle field. Lives behind ALL content (z-index: 0)
// with pointer-events: none so it never blocks interaction.
// Accepts an imperative ref with { pushX(strength) } so sibling components
// (like Nav) can trigger a horizontal sweep on page changes.
function ParticleField({ density = 90, color = 'rgba(139, 233, 253, 0.5)', linkColor = 'rgba(139, 233, 253, 0.15)', fixed = true, controlRef }) {
  const isMobile = useIsMobile(640);
  const canvasRef = useRefP(null);
  const mouseRef = useRefP({ x: -9999, y: -9999 });
  const scrollRef = useRefP({ lastY: 0, force: 0 });
  const sweepRef = useRefP({ force: 0 });
  if (isMobile) return null;

  useEffectP(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    let w, h, dots = [], raf;
    const dpr = Math.min(window.devicePixelRatio || 1, 2);

    const resize = () => {
      const rect = canvas.getBoundingClientRect();
      w = rect.width; h = rect.height;
      canvas.width = w * dpr; canvas.height = h * dpr;
      ctx.setTransform(1, 0, 0, 1, 0, 0);
      ctx.scale(dpr, dpr);
      // Denser field: was 1 particle per 16000px² — now 1 per 7000px²
      // so large viewports actually get a rich, filled constellation
      // up to the density cap.
      const n = fixed
        ? Math.min(density, Math.floor((w * h) / 7000))
        : Math.min(density, Math.floor((w * h) / 6000));
      dots = Array.from({ length: n }, () => ({
        x: Math.random() * w,
        y: Math.random() * h,
        vx: (Math.random() - 0.5) * 0.25,
        vy: (Math.random() - 0.5) * 0.25,
        r: Math.random() * 1.4 + 0.4,
      }));
    };

    const onMove = (e) => {
      // For fixed canvases, use viewport coords directly
      if (fixed) {
        mouseRef.current = { x: e.clientX, y: e.clientY };
      } else {
        const r = canvas.getBoundingClientRect();
        mouseRef.current = { x: e.clientX - r.left, y: e.clientY - r.top };
      }
    };
    const onLeave = () => { mouseRef.current = { x: -9999, y: -9999 }; };

    // Scroll-driven vertical force: down-scroll pushes particles up,
    // up-scroll pushes them down. Force decays over time so motion
    // feels like a gentle current rather than a jolt.
    scrollRef.current.lastY = window.scrollY || 0;
    const onScroll = () => {
      const y = window.scrollY || 0;
      const delta = y - scrollRef.current.lastY;
      scrollRef.current.lastY = y;
      // Clamp per-event delta so flings don't launch particles offscreen
      const clamped = Math.max(-60, Math.min(60, delta));
      // Accumulate, then clamp total — negative force = upward
      // Halved from 0.08 / ±8 → particles shift roughly half as far on scroll
      // while keeping the same direction, decay, and feel.
      scrollRef.current.force += clamped * 0.04;
      scrollRef.current.force = Math.max(-4, Math.min(4, scrollRef.current.force));
    };
    window.addEventListener('scroll', onScroll, { passive: true });

    // Expose an imperative API via controlRef — lets Nav push a
    // horizontal sweep force when the user changes tabs.
    if (controlRef) {
      controlRef.current = {
        pushX: (strength) => {
          // Negative = drift left (user moved right), positive = drift right
          sweepRef.current.force += strength;
          // Higher ceiling so multi-tab jumps truly feel stronger
          sweepRef.current.force = Math.max(-40, Math.min(40, sweepRef.current.force));
        },
      };
    }

    // Globe swirl: every frame, look up the globe element's current
    // screen position and radius. Particles within a soft falloff
    // radius get a tangential push in the direction of the globe's
    // spin, so the field appears to rotate with the Earth — like a
    // weak gravitational drag. Element lookup is cached and refreshed
    // lazily; if the globe isn't on screen, this whole block is a
    // no-op.
    let globeEl = null;
    let lastGlobeLookup = 0;
    const getGlobe = (now) => {
      // Re-query every 250ms so we pick it up after route changes
      if (!globeEl || now - lastGlobeLookup > 250) {
        globeEl = document.querySelector('.bento-hub svg');
        lastGlobeLookup = now;
      }
      return globeEl;
    };

    const tick = () => {
      ctx.clearRect(0, 0, w, h);
      const mx = mouseRef.current.x, my = mouseRef.current.y;

      // Read and decay the scroll force each frame
      const sf = scrollRef.current.force;
      scrollRef.current.force *= 0.92;

      // Read and decay the horizontal sweep force each frame.
      // Slower decay so the sweep carries further.
      const xf = sweepRef.current.force;
      sweepRef.current.force *= 0.965;

      // Sample the globe's screen position and radius for this frame.
      // If it's visible, set up a swirl field around it.
      const now = performance.now();
      const el = getGlobe(now);
      let gx = 0, gy = 0, gR = 0, gInfluence = 0;
      if (el) {
        const r = el.getBoundingClientRect();
        // Use fixed/viewport coords — this matches how mouseRef is
        // tracked (clientX/clientY). Skip if offscreen.
        if (r.width > 0 && r.bottom > 0 && r.top < h) {
          gx = r.left + r.width / 2;
          gy = r.top + r.height / 2;
          // Radius of the circle the globe occupies inside the SVG
          // is ~150 in a 400 viewBox — so 150/400 of the shorter side.
          gR = Math.min(r.width, r.height) * (150 / 400);
          gInfluence = 1;
        }
      }

      for (const d of dots) {
        const dx = d.x - mx, dy = d.y - my;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist < 120) {
          const force = (120 - dist) / 120 * 0.6;
          d.vx += (dx / dist) * force * 0.1;
          d.vy += (dy / dist) * force * 0.1;
        }
        // Apply scroll current: scrolling down (positive sf) pushes
        // particles up (negative vy). Tiny random lateral wobble keeps
        // it from looking like a rigid sheet moving.
        if (Math.abs(sf) > 0.05) {
          d.vy -= sf * 0.05;
          d.vx += (Math.random() - 0.5) * Math.abs(sf) * 0.02;
        }
        // Apply tab-change horizontal sweep with a vertical wobble so
        // it doesn't look mechanical. Stronger push coefficient than
        // scroll so tab changes really read as a sweep.
        if (Math.abs(xf) > 0.05) {
          d.vx += xf * 0.09;
          d.vy += (Math.random() - 0.5) * Math.abs(xf) * 0.025;
        }

        // Globe swirl: tangential force around the globe's position,
        // matching the globe's spin direction (rotY increases →
        // longitude rotates one way → on the front face, points move
        // left-to-right at the equator). In screen space, the spin
        // reads as counter-clockwise. Force falls off smoothly with
        // distance — zero at ~2.5×globe radius, strongest at the
        // globe's rim.
        if (gInfluence > 0) {
          const ddx = d.x - gx, ddy = d.y - gy;
          const dd = Math.hypot(ddx, ddy);
          const falloffR = gR * 2.5;
          if (dd < falloffR && dd > 1) {
            // Normalized outward vector
            const ox = ddx / dd, oy = ddy / dd;
            // Tangent = rotate outward vector 90° counter-clockwise
            //   (counter-clockwise because globe spins that way
            //   on screen: left-edge points move down, right-edge up)
            const tx = -oy;
            const ty = ox;
            // Intensity: peaks near the globe rim, fades to zero at
            // the outer radius. Use a simple 1-d/R falloff.
            const k = 1 - dd / falloffR;
            const swirlStrength = 0.035 * k * k;
            d.vx += tx * swirlStrength;
            d.vy += ty * swirlStrength;
          }
        }

        d.x += d.vx; d.y += d.vy;
        d.vx *= 0.98; d.vy *= 0.98;

        if (d.x < 0) d.x = w; if (d.x > w) d.x = 0;
        if (d.y < 0) d.y = h; if (d.y > h) d.y = 0;
      }

      for (let i = 0; i < dots.length; i++) {
        for (let j = i + 1; j < dots.length; j++) {
          const a = dots[i], b = dots[j];
          const dx = a.x - b.x, dy = a.y - b.y;
          const dist = Math.sqrt(dx * dx + dy * dy);
          if (dist < 130) {
            ctx.strokeStyle = linkColor.replace(/[\d\.]+\)$/, `${(1 - dist/130) * 0.2})`);
            ctx.lineWidth = 0.5;
            ctx.beginPath();
            ctx.moveTo(a.x, a.y);
            ctx.lineTo(b.x, b.y);
            ctx.stroke();
          }
        }
        const dmx = dots[i].x - mx, dmy = dots[i].y - my;
        const dmd = Math.sqrt(dmx * dmx + dmy * dmy);
        if (dmd < 160) {
          ctx.strokeStyle = color.replace(/[\d\.]+\)$/, `${(1 - dmd/160) * 0.5})`);
          ctx.lineWidth = 0.7;
          ctx.beginPath();
          ctx.moveTo(dots[i].x, dots[i].y);
          ctx.lineTo(mx, my);
          ctx.stroke();
        }
      }

      for (const d of dots) {
        ctx.fillStyle = color;
        ctx.beginPath();
        ctx.arc(d.x, d.y, d.r, 0, Math.PI * 2);
        ctx.fill();
      }

      raf = requestAnimationFrame(tick);
    };

    resize();
    window.addEventListener('resize', resize);
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseleave', onLeave);
    tick();

    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener('resize', resize);
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseleave', onLeave);
      window.removeEventListener('scroll', onScroll);
    };
  }, [density, color, linkColor, fixed]);

  const style = fixed
    ? {
        position: 'fixed',
        inset: 0,
        width: '100vw',
        height: '100vh',
        pointerEvents: 'none',
        zIndex: 0,
      }
    : {
        position: 'absolute',
        inset: 0,
        width: '100%',
        height: '100%',
        pointerEvents: 'none',
      };

  return <canvas ref={canvasRef} style={style} />;
}

window.ParticleField = ParticleField;

