// Animation primitives built on framer-motion (exposed via window.Motion by the UMD build)
const { motion, useInView, useScroll, useTransform } = window.Motion;
const { useRef, useMemo, useState, useEffect } = React;

// Reduced-motion preference, read once at module load. The CSS layer
// already honors prefers-reduced-motion (see index.html @media block);
// this exposes the same signal at the JS layer so the scroll engine and
// canvas effects in app.jsx can fall back to a still state. Read defensively
// (SSR-safe / matchMedia-absent guards) and published on window so the
// later-loaded files (app.jsx) can read it via source order.
const PREFERS_REDUCED = (typeof window !== 'undefined' && window.matchMedia)
  ? window.matchMedia('(prefers-reduced-motion: reduce)').matches
  : false;
window.PREFERS_REDUCED = PREFERS_REDUCED;

/* Hook: returns `true` once the ref is intersecting (via IntersectionObserver),
   OR unconditionally after `fallbackMs` — whichever happens first. This keeps
   above-the-fold elements from getting permanently stuck at opacity:0 when the
   observer fails to fire (which can happen with certain scroll-container /
   transform-scale setups). */
function useInViewOnce(ref, { amount = 0.15, fallbackMs = 700 } = {}) {
  const [visible, setVisible] = useState(false);
  useEffect(() => {
    if (!ref.current || visible) return;
    const el = ref.current;
    const io = new IntersectionObserver(
      (entries) => {
        if (entries.some((e) => e.isIntersecting && e.intersectionRatio >= amount)) {
          setVisible(true);
          io.disconnect();
        }
      },
      { threshold: [0, amount, 0.5, 1] }
    );
    io.observe(el);
    const t = setTimeout(() => {
      setVisible(true);
      io.disconnect();
    }, fallbackMs);
    return () => { io.disconnect(); clearTimeout(t); };
  }, [ref, amount, fallbackMs, visible]);
  return visible;
}

/**
 * WordsPullUp — splits text into words; each slides up from y:20 with stagger.
 * If showAsterisk is true, appends a superscript "*" positioned on the last "a".
 */
function WordsPullUp({ text, className = '', wordClassName = '', delay = 0, stagger = 0.08, showAsterisk = false, style }) {
  const ref = useRef(null);
  const inView = useInViewOnce(ref);
  const words = text.split(' ').filter(Boolean);

  return (
    <span ref={ref} className={className} style={style}>
      {words.map((w, i) => {
        const isLast = i === words.length - 1;
        return (
          <motion.span
            key={`${i}-${w}`}
            className={`inline-block whitespace-nowrap ${wordClassName}`}
            style={{ position: 'relative', paddingRight: i < words.length - 1 ? '0.22em' : 0 }}
            initial={{ y: 20, opacity: 0 }}
            animate={inView ? { y: 0, opacity: 1 } : { y: 20, opacity: 0 }}
            transition={{ type: 'spring', mass: 1, stiffness: 120, damping: 24, delay: delay + i * stagger }}
          >
            {w}
            {showAsterisk && isLast && (
              <span
                aria-hidden="true"
                style={{
                  position: 'absolute',
                  top: '0.65em',
                  right: '-0.3em',
                  fontSize: '0.31em',
                  lineHeight: 1,
                  fontWeight: 400,
                }}
              >*</span>
            )}
          </motion.span>
        );
      })}
    </span>
  );
}

/**
 * WordsPullUpMultiStyle — takes segments [{ text, className }], each word animates in
 * with its own className preserved.
 */
function WordsPullUpMultiStyle({ segments, className = '', delay = 0, stagger = 0.08 }) {
  const ref = useRef(null);
  const inView = useInViewOnce(ref);

  const wordList = useMemo(() => {
    const out = [];
    segments.forEach((seg, si) => {
      const words = seg.text.split(' ').filter(Boolean);
      words.forEach((w, wi) => {
        out.push({
          text: w,
          className: seg.className || '',
          key: `${si}-${wi}`,
          trailingSpace: !(si === segments.length - 1 && wi === words.length - 1),
        });
      });
    });
    return out;
  }, [segments]);

  return (
    <span ref={ref} className={`block ${className}`}>
      {wordList.map((w, i) => (
        <motion.span
          key={w.key}
          className={`inline-block whitespace-nowrap ${w.className}`}
          style={{ marginRight: w.trailingSpace ? '0.25em' : 0, verticalAlign: 'baseline' }}
          initial={{ y: 20, opacity: 0 }}
          animate={inView ? { y: 0, opacity: 1 } : { y: 20, opacity: 0 }}
          transition={{ type: 'spring', mass: 1, stiffness: 120, damping: 24, delay: delay + i * stagger }}
        >
          {w.text}
        </motion.span>
      ))}
    </span>
  );
}

/**
 * AnimatedLetter — opacity driven by scroll progress.
 */
function AnimatedLetter({ char, index, totalChars, scrollYProgress }) {
  const charProgress = index / totalChars;
  const opacity = useTransform(
    scrollYProgress,
    [Math.max(0, charProgress - 0.1), charProgress + 0.05],
    [0.2, 1]
  );
  return (
    <motion.span style={{ opacity, display: 'inline' }}>
      {char === ' ' ? '\u00A0' : char}
    </motion.span>
  );
}

/**
 * ScrollRevealText — uses AnimatedLetter across a body paragraph.
 * Characters are grouped into word-level inline-block spans so normal
 * word-wrapping still works (otherwise per-character spans break at any char).
 */
function ScrollRevealText({ text, className = '', style }) {
  const ref = useRef(null);
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ['start 0.8', 'end 0.2'],
  });

  // Build per-word groups while keeping a single running character index so
  // each letter's scroll threshold reflects its position in the whole text.
  const words = text.split(' ');
  const totalChars = text.length;
  let runningIdx = 0;
  const wordSpans = words.map((word, wi) => {
    const chars = Array.from(word);
    const span = (
      <span key={wi} className="inline-block whitespace-nowrap">
        {chars.map((c, ci) => {
          const node = (
            <AnimatedLetter
              key={ci}
              char={c}
              index={runningIdx}
              totalChars={totalChars}
              scrollYProgress={scrollYProgress}
            />
          );
          runningIdx += 1;
          return node;
        })}
      </span>
    );
    // account for the space between words in the running index
    if (wi < words.length - 1) runningIdx += 1;
    return span;
  });

  return (
    <p ref={ref} className={className} style={style}>
      {wordSpans.map((s, i) => (
        <React.Fragment key={i}>
          {s}
          {i < wordSpans.length - 1 ? ' ' : null}
        </React.Fragment>
      ))}
    </p>
  );
}

/**
 * stableMobileResize — wrap a resize handler so iOS Safari URL-bar
 * collapses don't retrigger expensive scroll-engine recomputes mid-scroll.
 *
 * Why: the entire scroll choreography (falling figures, About lock,
 * hand-catcher, slide snaps) reads `window.innerHeight` and bakes it
 * into transforms. On iOS Safari the URL bar is visible at first paint
 * (~750px on iPhone 14) and collapses on first scroll (~844px) — a
 * ~100px height delta with NO actual layout change. Browsers fire
 * `resize` for that, the existing handlers recompute geometry, and
 * everything jumps mid-scroll. DevTools device mode never reproduces
 * it because there's no URL bar to collapse.
 *
 * Filter rule: on mobile (<640px), if width is unchanged AND height
 * delta is <150px, it's a URL-bar event — drop it. Real events
 * (rotation, browser resize, iPad split view) always change width OR
 * push the height delta past 150px, so they pass through.
 *
 * Desktop is unaffected — the filter only engages when innerWidth < 640.
 *
 * Usage:
 *   const onResize = stableMobileResize(() => sizeCanvas());
 *   window.addEventListener('resize', onResize);
 *   // ... in cleanup
 *   window.removeEventListener('resize', onResize);
 */
function stableMobileResize(fn) {
  let lastIH = (typeof window !== 'undefined') ? window.innerHeight : 800;
  let lastIW = (typeof window !== 'undefined') ? window.innerWidth  : 1280;
  return function wrappedResize() {
    const ih = window.innerHeight;
    const iw = window.innerWidth;
    if (iw < 640 && iw === lastIW && Math.abs(ih - lastIH) < 150) {
      return; // URL-bar collapse — ignore
    }
    lastIH = ih;
    lastIW = iw;
    fn();
  };
}

Object.assign(window, { WordsPullUp, WordsPullUpMultiStyle, AnimatedLetter, ScrollRevealText, stableMobileResize });
