/* ────────────────────────────────────────────────────────────
   <CountUp value="~60%" duration={1400} /> - animates a number
   from 0 to its target the first time it scrolls into view.

   Parsing
     "~60%"     → prefix "~", num 60, suffix "%"
     "1,300×"   → comma preserved through every intermediate frame
     "87GW"     → unit suffix stays attached to the rolling number
     "10×"      → single-character suffix stays put

   Motion (per spec)
     • ease-out cubic   t => 1 - (1 - t)^3   (applied to the value
                                              interpolation, not to
                                              elapsed time)
     • 1400 ms          same duration for every counter so siblings
                        finish together regardless of magnitude
     • 24 fps DOM cap   rAF runs at 60+; React state only flushes
                        when ≥42 ms have passed since the last flush.
                        Eased progress is still sampled per-frame —
                        only the rendered value is throttled, so the
                        animation visibly steps from one readable
                        integer to the next instead of churning.

   Lifecycle
     • Starts when the element crosses 40 % visibility (first time).
     • If the user scrolls AWAY while the count is still in flight,
       the rAF loop is cancelled and the display is snapped to the
       canonical final string — so a scroll-back never shows a
       partial mid-count.
     • Completed counters do NOT replay on re-entry. One play per
       page load.

   Honors prefers-reduced-motion (renders the final value).
   ──────────────────────────────────────────────────────────── */

function CountUp({ value, duration = 1400, delay = 0 }) {
  const ref = React.useRef(null);
  const [text, setText] = React.useState(value);
  const startedRef = React.useRef(false);
  const doneRef = React.useRef(false);
  const rafRef = React.useRef(0);
  const delayRef = React.useRef(0);

  // Parse "~60%" -> { prefix:"~", num:60, suffix:"%", decimals:0,
  //                   hasComma:false }. Handles "10×", "1,300×",
  //                   "96%", "3 km", "90 GW", "5,500 GW".
  const parsed = React.useMemo(() => {
    const m = String(value).match(/^([^\d-]*)([\d,]+(?:\.\d+)?)(.*)$/);
    if (!m) return null;
    const prefix = m[1];
    const numRaw = m[2];
    const suffix = m[3];
    const hasComma = numRaw.includes(",");
    const num = parseFloat(numRaw.replace(/,/g, ""));
    const decimals = (numRaw.split(".")[1] || "").length;
    return { prefix, num, suffix, decimals, hasComma };
  }, [value]);

  React.useEffect(() => {
    if (!parsed) return;
    const reduce = window.matchMedia &&
      window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (reduce) { setText(value); doneRef.current = true; return; }

    setText(formatNum(0, parsed));
    const node = ref.current;
    if (!node) return;

    // Single IO instance handles BOTH the first-entry trigger AND
    // the leave-while-animating cancel. We keep observing after the
    // animation starts so we can detect departure mid-flight; once
    // the count completes we unobserve (no more work to do, and we
    // never want to replay on re-entry).
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (e.isIntersecting) {
          // Only kick off once per page-load. If we're already done,
          // do nothing on re-entry (counter stays at its final
          // value — no replay).
          if (!startedRef.current && !doneRef.current) {
            startedRef.current = true;
            delayRef.current = window.setTimeout(() => {
              delayRef.current = 0;
              animate(parsed, duration, setText, value, rafRef, doneRef);
            }, delay);
          }
        } else {
          // Left the viewport. If we're still mid-flight (rAF
          // scheduled OR start-delay pending), cancel and snap to
          // the canonical final string — never freeze mid-count,
          // because a return-scroll would otherwise show a wrong
          // value.
          if (startedRef.current && !doneRef.current) {
            if (delayRef.current) {
              clearTimeout(delayRef.current);
              delayRef.current = 0;
            }
            if (rafRef.current) {
              cancelAnimationFrame(rafRef.current);
              rafRef.current = 0;
            }
            setText(value);
            doneRef.current = true;
            io.unobserve(node);
          }
        }
      });
    }, { threshold: 0.4 });
    io.observe(node);
    return () => {
      io.disconnect();
      if (delayRef.current) clearTimeout(delayRef.current);
      if (rafRef.current) cancelAnimationFrame(rafRef.current);
    };
  }, [parsed, value, duration, delay]);

  return <span ref={ref}>{text}</span>;
}

function formatNum(n, p) {
  const fixed = n.toFixed(p.decimals);
  const [whole, dec] = fixed.split(".");
  const wholeFmt = p.hasComma
    ? whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",")
    : whole;
  const numStr = dec ? `${wholeFmt}.${dec}` : wholeFmt;
  return `${p.prefix}${numStr}${p.suffix}`;
}

/* The animation loop.

   Sampling vs rendering are decoupled by design:
     - sampling : every rAF tick computes the eased value, so the
                  curve is faithfully integrated even if the
                  rendered output is throttled or a frame is missed.
     - rendering: state only flushes when ≥ FRAME_MS have passed
                  since the last flush (or it's the final frame).
                  This gives each integer a readable beat instead of
                  blurring through at 60fps. 42ms ≈ 24fps.

   The rAF id is hoisted into the caller's ref so the
   IntersectionObserver's "leave" branch can cancel it. */
const FRAME_MS = 42;
function animate(parsed, duration, setText, finalValue, rafRef, doneRef) {
  const start = performance.now();
  let lastFlush = -Infinity;
  // ease-out cubic: t => 1 - (1 - t)^3 — fast start, decelerates
  // into the final value. Applied to the value interpolation, not
  // to the time variable, per spec.
  const ease = (t) => 1 - Math.pow(1 - t, 3);
  function frame(now) {
    const t = Math.min(1, (now - start) / duration);
    const v = parsed.num * ease(t);
    if (now - lastFlush >= FRAME_MS || t >= 1) {
      lastFlush = now;
      setText(formatNum(v, parsed));
    }
    if (t < 1) {
      rafRef.current = requestAnimationFrame(frame);
    } else {
      rafRef.current = 0;
      doneRef.current = true;
      setText(finalValue); // snap to canonical string at the end
    }
  }
  rafRef.current = requestAnimationFrame(frame);
}

window.CountUp = CountUp;
