/* fx.jsx — ParticleField, CountUp, MiniSpark, useReveal */
const { useRef, useEffect, useState } = React;

/* Mouse-reactive particle network on canvas */
function ParticleField({ density = 1, colors = ['#2563eb', '#7c3aed', '#06b6d4'] }) {
  const ref = useRef(null);
  useEffect(() => {
    const cv = ref.current; if (!cv) return;
    const ctx = cv.getContext('2d');
    const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    let raf, W = 0, H = 0, dpr = Math.min(window.devicePixelRatio || 1, 2);
    const mouse = { x: -9999, y: -9999 };

    function resize() {
      const r = cv.parentElement.getBoundingClientRect();
      W = r.width; H = r.height;
      cv.width = W * dpr; cv.height = H * dpr;
      cv.style.width = W + 'px'; cv.style.height = H + 'px';
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    }
    resize();
    const ro = new ResizeObserver(resize); ro.observe(cv.parentElement);

    const N = Math.round(70 * density);
    const pts = Array.from({ length: N }, () => ({
      x: Math.random(), y: Math.random(),
      vx: (Math.random() - .5) * .00045, vy: (Math.random() - .5) * .00045,
      r: 1.2 + Math.random() * 1.8, c: colors[Math.floor(Math.random() * colors.length)],
    }));

    const onMove = (e) => {
      const r = cv.getBoundingClientRect();
      mouse.x = e.clientX - r.left; mouse.y = e.clientY - r.top;
    };
    const onLeave = () => { mouse.x = -9999; mouse.y = -9999; };
    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerleave', onLeave);

    const LINK = 130;
    function step() {
      ctx.clearRect(0, 0, W, H);

      // mouse halo
      if (mouse.x > -999) {
        const g = ctx.createRadialGradient(mouse.x, mouse.y, 0, mouse.x, mouse.y, 220);
        g.addColorStop(0, 'rgba(124,58,237,0.07)');
        g.addColorStop(1, 'rgba(124,58,237,0)');
        ctx.fillStyle = g;
        ctx.fillRect(0, 0, W, H);
      }

      for (const p of pts) {
        p.x += p.vx; p.y += p.vy;
        if (p.x < 0 || p.x > 1) p.vx *= -1;
        if (p.y < 0 || p.y > 1) p.vy *= -1;
        const px = p.x * W, py = p.y * H;
        // gentle mouse attraction
        const dx = mouse.x - px, dy = mouse.y - py;
        const d2 = dx * dx + dy * dy;
        if (d2 < 48400 && d2 > 1) {
          const f = 0.0000035;
          p.vx += dx * f / W; p.vy += dy * f / H;
        }
        // velocity cap
        p.vx = Math.max(-.0009, Math.min(.0009, p.vx));
        p.vy = Math.max(-.0009, Math.min(.0009, p.vy));
      }

      // links
      for (let i = 0; i < pts.length; i++) {
        const a = pts[i], ax = a.x * W, ay = a.y * H;
        for (let j = i + 1; j < pts.length; j++) {
          const b = pts[j], bx = b.x * W, by = b.y * H;
          const dx = ax - bx, dy = ay - by;
          const d = Math.sqrt(dx * dx + dy * dy);
          if (d < LINK) {
            const o = (1 - d / LINK) * 0.13;
            ctx.strokeStyle = `rgba(80,90,180,${o})`;
            ctx.lineWidth = 1;
            ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke();
          }
        }
      }
      // nodes
      for (const p of pts) {
        const px = p.x * W, py = p.y * H;
        const dx = mouse.x - px, dy = mouse.y - py;
        const near = Math.max(0, 1 - Math.sqrt(dx * dx + dy * dy) / 240);
        ctx.beginPath();
        ctx.arc(px, py, p.r + near * 1.6, 0, Math.PI * 2);
        ctx.fillStyle = p.c;
        ctx.globalAlpha = 0.35 + near * 0.5;
        ctx.fill();
        ctx.globalAlpha = 1;
      }
      raf = requestAnimationFrame(step);
    }
    if (reduce) { step(); cancelAnimationFrame(raf); }
    else raf = requestAnimationFrame(step);

    return () => {
      cancelAnimationFrame(raf); ro.disconnect();
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerleave', onLeave);
    };
  }, [density, colors.join()]);
  return <canvas ref={ref} style={{ display: 'block', width: '100%', height: '100%' }} />;
}

/* viewport check — IntersectionObserver is unreliable in some embedded
   iframes, so visibility is driven by scroll/resize + rect checks. */
function inView(el, margin = 0.92) {
  const r = el.getBoundingClientRect();
  const vh = window.innerHeight || document.documentElement.clientHeight;
  return r.top < vh * margin && r.bottom > 0;
}

/* Animated count-up when scrolled into view */
function CountUp({ to, dur = 1600 }) {
  const ref = useRef(null);
  const [val, setVal] = useState(0);
  useEffect(() => {
    const el = ref.current; if (!el) return;
    let started = false;
    const start = () => {
      if (started) return; started = true;
      cleanup();
      const t0 = performance.now();
      const tick = (t) => {
        const p = Math.min(1, (t - t0) / dur);
        const ease = 1 - Math.pow(1 - p, 3);
        setVal(Math.round(to * ease));
        if (p < 1) requestAnimationFrame(tick);
      };
      requestAnimationFrame(tick);
    };
    const check = () => { if (inView(el, 0.96)) start(); };
    const events = ['scroll', 'resize', 'wheel', 'touchmove'];
    const cleanup = () => {
      events.forEach((e) => window.removeEventListener(e, check));
      clearInterval(iv);
      clearTimeout(failsafe);
    };
    events.forEach((e) => window.addEventListener(e, check, { passive: true }));
    const iv = setInterval(check, 350); // catches programmatic scrolls / anchor jumps
    const failsafe = setTimeout(start, 7000); // never leave counters stuck at 0
    check();
    return cleanup;
  }, [to, dur]);
  return <span ref={ref}>{val.toLocaleString('en-US')}</span>;
}

/* tiny smooth sparkline */
function MiniSpark({ data, w = 150, h = 36, color = '#2563eb' }) {
  const max = Math.max(...data), min = Math.min(...data), rng = max - min || 1;
  const pts = data.map((v, i) => [(i / (data.length - 1)) * (w - 2) + 1, h - 3 - ((v - min) / rng) * (h - 8)]);
  let d = `M ${pts[0][0]},${pts[0][1]}`;
  for (let i = 0; i < pts.length - 1; i++) {
    const p0 = pts[i === 0 ? 0 : i - 1], p1 = pts[i], p2 = pts[i + 1], p3 = pts[i + 2] || p2;
    d += ` C ${p1[0] + (p2[0] - p0[0]) / 6},${p1[1] + (p2[1] - p0[1]) / 6} ${p2[0] - (p3[0] - p1[0]) / 6},${p2[1] - (p3[1] - p1[1]) / 6} ${p2[0]},${p2[1]}`;
  }
  const id = 'mg' + color.replace(/[^a-z0-9]/gi, '') + w;
  return (
    <svg width={w} height={h} style={{ display: 'block', maxWidth: '100%', overflow: 'visible' }}>
      <defs>
        <linearGradient id={id} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor={color} stopOpacity=".18" />
          <stop offset="100%" stopColor={color} stopOpacity="0" />
        </linearGradient>
      </defs>
      <path d={d + ` L ${w - 1},${h} L 1,${h} Z`} fill={`url(#${id})`} />
      <path d={d} fill="none" stroke={color} strokeWidth="1.8" strokeLinecap="round" />
      <circle cx={pts[pts.length - 1][0]} cy={pts[pts.length - 1][1]} r="2.6" fill={color} />
    </svg>
  );
}

/* rAF-driven reveal tween — CSS transitions don't tick in some embedded
   iframes, so the fade/slide is animated manually (rAF provably works). */
function revealEl(el) {
  el.classList.add('in');
  el.style.transition = 'none';
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (reduce) { el.style.opacity = '1'; el.style.transform = 'none'; return; }
  const dur = 650, t0 = performance.now();
  const tick = (t) => {
    const p = Math.min(1, (t - t0) / dur);
    const e = 1 - Math.pow(1 - p, 3);
    el.style.opacity = String(e);
    el.style.transform = `translateY(${(1 - e) * 26}px)`;
    if (p < 1) requestAnimationFrame(tick);
    else { el.style.opacity = '1'; el.style.transform = 'none'; }
  };
  requestAnimationFrame(tick);
}

/* scroll-reveal: rAF tween when visible (no IO, no CSS-transition dependency).
   Re-queries the DOM on every sweep so elements (re)mounted by React filter
   changes are picked up too; per-node 6s failsafe so nothing stays hidden. */
function useRevealAll(dep) {
  useEffect(() => {
    let raf = null;
    const sweep = () => {
      raf = null;
      const now = performance.now();
      document.querySelectorAll('.reveal:not(.in)').forEach((el) => {
        if (!el.dataset.rvt) el.dataset.rvt = String(now);
        if (inView(el) || now - parseFloat(el.dataset.rvt) > 6000) revealEl(el);
      });
    };
    const onScroll = () => { if (!raf) raf = requestAnimationFrame(sweep); };
    const events = ['scroll', 'resize', 'wheel', 'touchmove', 'keydown'];
    events.forEach((e) => window.addEventListener(e, onScroll, { passive: true }));
    const iv = setInterval(onScroll, 350); // also catches React re-renders + anchor jumps
    sweep();
    return () => {
      events.forEach((e) => window.removeEventListener(e, onScroll));
      clearInterval(iv);
      if (raf) cancelAnimationFrame(raf);
    };
  }, [dep]);
}

Object.assign(window, { ParticleField, CountUp, MiniSpark, useRevealAll });
