/* global React */
const { useEffect, useRef, useState } = React;

/* ============== REVEAL — fade + 12px up on scroll into view ============== */
function Reveal({ children, delay = 0, as = "div", style }) {
  const ref = useRef(null);
  const [shown, setShown] = useState(false);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    if (typeof IntersectionObserver === "undefined") { setShown(true); return; }
    const reduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (reduced) { setShown(true); return; }
    const io = new IntersectionObserver((entries) => {
      entries.forEach(e => {
        if (e.isIntersecting) {
          setTimeout(() => setShown(true), delay);
          io.disconnect();
        }
      });
    }, { threshold: 0.12, rootMargin: "0px 0px -60px 0px" });
    io.observe(el);
    return () => io.disconnect();
  }, [delay]);

  const Tag = as;
  return (
    <Tag ref={ref} style={{
      ...style,
      opacity: shown ? 1 : 0,
      transform: shown ? "translateY(0)" : "translateY(14px)",
      transition: "opacity 700ms cubic-bezier(.16,1,.3,1), transform 700ms cubic-bezier(.16,1,.3,1)",
      willChange: "opacity, transform",
    }}>
      {children}
    </Tag>
  );
}

/* ============== COUNT-UP — animates from 0 to value on view ============== */
function CountUp({ value, duration = 1600, prefix = "", suffix = "", thousands = ".", decimals = 0 }) {
  const ref = useRef(null);
  const [n, setN] = useState(0);
  const started = useRef(false);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const reduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (reduced) { setN(value); return; }
    if (typeof IntersectionObserver === "undefined") { setN(value); return; }
    const io = new IntersectionObserver((entries) => {
      entries.forEach(e => {
        if (e.isIntersecting && !started.current) {
          started.current = true;
          const t0 = performance.now();
          const tick = (now) => {
            const t = Math.min(1, (now - t0) / duration);
            // easeOutQuart
            const eased = 1 - Math.pow(1 - t, 4);
            setN(value * eased);
            if (t < 1) requestAnimationFrame(tick);
            else setN(value);
          };
          requestAnimationFrame(tick);
          io.disconnect();
        }
      });
    }, { threshold: 0.4 });
    io.observe(el);
    return () => io.disconnect();
  }, [value, duration]);

  const formatted = (() => {
    const v = decimals > 0 ? n.toFixed(decimals) : Math.round(n).toString();
    // thousands separator
    const parts = v.split(".");
    parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousands);
    return parts.join(",");
  })();

  return <span ref={ref} style={{ fontVariantNumeric: "tabular-nums" }}>{prefix}{formatted}{suffix}</span>;
}

/* ============== WORD-REVEAL — animates words one-by-one on mount or in-view ============== */
function WordReveal({ text, delay = 0, stagger = 60, duration = 700, style, as = "span", lineBreaks = true, onView = false }) {
  const ref = useRef(null);
  const [shown, setShown] = useState(!onView);

  useEffect(() => {
    if (!onView) return;
    const el = ref.current;
    if (!el) return;
    const reduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (reduced) { setShown(true); return; }
    if (typeof IntersectionObserver === "undefined") { setShown(true); return; }
    const io = new IntersectionObserver((entries) => {
      entries.forEach(e => {
        if (e.isIntersecting) { setShown(true); io.disconnect(); }
      });
    }, { threshold: 0.2 });
    io.observe(el);
    return () => io.disconnect();
  }, [onView]);

  // honour explicit <br/> in input by splitting on \n
  const lines = lineBreaks ? text.split("\n") : [text];
  let wordIdx = 0;
  const Tag = as;

  return (
    <Tag ref={ref} style={{ display: "inline-block", ...style }}>
      {lines.map((line, li) => (
        <span key={li} style={{ display: "block" }}>
          {line.split(/(\s+)/).map((tok, i) => {
            if (/^\s+$/.test(tok)) return <span key={i}>{tok}</span>;
            const myIdx = wordIdx++;
            const reduced = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
            return (
              <span key={i} style={{ display: "inline-block", overflow: "hidden", verticalAlign: "top", paddingBottom: "0.06em" }}>
                <span style={{
                  display: "inline-block",
                  opacity: shown ? 1 : 0,
                  transform: shown ? "translateY(0)" : "translateY(0.7em)",
                  transition: reduced ? "none" : `opacity ${duration}ms cubic-bezier(.2,.8,.2,1) ${delay + myIdx * stagger}ms, transform ${duration}ms cubic-bezier(.2,.8,.2,1) ${delay + myIdx * stagger}ms`,
                  willChange: "opacity, transform",
                }}>{tok}</span>
              </span>
            );
          })}
        </span>
      ))}
    </Tag>
  );
}

Object.assign(window, { Reveal, CountUp, WordReveal });

