// scroll.jsx — Scroll-driven animation utilities
// Includes: useScrollProgress, useElementProgress, Parallax, CountUp,
// WordReveal, LineReveal, StickyHeading, ScrollProgressBar, ScaleOnScroll

/* ───────── Hooks ───────── */

// 0..1 page scroll progress
function useScrollProgress() {
  const [p, setP] = React.useState(0);
  React.useEffect(() => {
    const calc = () => {
      const h = document.documentElement;
      const max = (h.scrollHeight - h.clientHeight) || 1;
      setP(Math.max(0, Math.min(1, window.scrollY / max)));
    };
    calc();
    window.addEventListener('scroll', calc, { passive: true });
    window.addEventListener('resize', calc);
    return () => {
      window.removeEventListener('scroll', calc);
      window.removeEventListener('resize', calc);
    };
  }, []);
  return p;
}

// Per-element scroll progress: 0 when bottom of element enters viewport bottom,
// 1 when top of element exits viewport top. Smooth across the whole transition.
function useElementProgress(ref) {
  const [p, setP] = React.useState(0);
  React.useEffect(() => {
    if (!ref.current) return;
    const el = ref.current;
    const calc = () => {
      const r = el.getBoundingClientRect();
      const vh = window.innerHeight || 1;
      const total = r.height + vh;
      const passed = vh - r.top;
      setP(Math.max(0, Math.min(1, passed / total)));
    };
    calc();
    window.addEventListener('scroll', calc, { passive: true });
    window.addEventListener('resize', calc);
    return () => {
      window.removeEventListener('scroll', calc);
      window.removeEventListener('resize', calc);
    };
  }, [ref]);
  return p;
}

/* ───────── Parallax ───────── */

function Parallax({ speed = 0.3, children, as: As = 'div', style, ...rest }) {
  const ref = React.useRef(null);
  const [y, setY] = React.useState(0);
  React.useEffect(() => {
    if (!ref.current) return;
    const el = ref.current;
    const onScroll = () => {
      const r = el.getBoundingClientRect();
      const vh = window.innerHeight || 1;
      // Distance from element center to viewport center
      const center = r.top + r.height / 2 - vh / 2;
      setY(-center * speed);
    };
    onScroll();
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', onScroll);
    return () => {
      window.removeEventListener('scroll', onScroll);
      window.removeEventListener('resize', onScroll);
    };
  }, [speed]);
  return (
    <As ref={ref} style={{ ...style, transform: `translate3d(0, ${y}px, 0)`, willChange: 'transform' }} {...rest}>
      {children}
    </As>
  );
}

/* ───────── CountUp ───────── */
// Counts up from 0 to `to` once the element enters viewport. `duration` in ms.

function CountUp({ to, duration = 1400, format = (n) => Math.round(n), prefix = '', suffix = '' }) {
  const ref = React.useRef(null);
  const [val, setVal] = React.useState(0);
  const [seen, setSeen] = React.useState(false);
  React.useEffect(() => {
    if (!ref.current || seen) return;
    const io = new IntersectionObserver((entries) => {
      entries.forEach(e => {
        if (e.isIntersecting) {
          setSeen(true);
          const start = performance.now();
          const tick = (t) => {
            const p = Math.min(1, (t - start) / duration);
            // ease-out cubic
            const eased = 1 - Math.pow(1 - p, 3);
            setVal(to * eased);
            if (p < 1) requestAnimationFrame(tick);
            else setVal(to);
          };
          requestAnimationFrame(tick);
          io.disconnect();
        }
      });
    }, { threshold: 0.4 });
    io.observe(ref.current);
    return () => io.disconnect();
  }, [to, duration, seen]);
  return <span ref={ref}>{prefix}{format(val)}{suffix}</span>;
}

/* ───────── Word-by-word text reveal ───────── */

function WordReveal({ text, as: As = 'span', delay = 0, stagger = 40, className = '' }) {
  const ref = React.useRef(null);
  const [seen, setSeen] = React.useState(false);
  React.useEffect(() => {
    if (!ref.current) return;
    const io = new IntersectionObserver((entries) => {
      entries.forEach(e => {
        if (e.isIntersecting) {
          setTimeout(() => setSeen(true), delay);
          io.disconnect();
        }
      });
    }, { threshold: 0.2, rootMargin: '0px 0px -8% 0px' });
    io.observe(ref.current);
    return () => io.disconnect();
  }, [delay]);
  const words = text.split(' ');
  return (
    <As ref={ref} className={"word-reveal " + className}>
      {words.map((w, i) => (
        <span key={i} className="wr-word">
          <span className="wr-inner" style={{
            transitionDelay: seen ? `${i * stagger}ms` : '0ms',
            transform: seen ? 'translateY(0)' : 'translateY(110%)',
            opacity: seen ? 1 : 0.001,
          }}>{w}</span>
          {i < words.length - 1 && '\u00A0'}
        </span>
      ))}
    </As>
  );
}

/* ───────── Line reveal — for long paragraphs ───────── */

function LineReveal({ children, delay = 0, className = '' }) {
  const ref = React.useRef(null);
  const [seen, setSeen] = React.useState(false);
  React.useEffect(() => {
    if (!ref.current) return;
    const io = new IntersectionObserver((entries) => {
      entries.forEach(e => {
        if (e.isIntersecting) {
          setTimeout(() => setSeen(true), delay);
          io.disconnect();
        }
      });
    }, { threshold: 0.2, rootMargin: '0px 0px -5% 0px' });
    io.observe(ref.current);
    return () => io.disconnect();
  }, [delay]);
  return (
    <div ref={ref} className={"line-reveal " + (seen ? 'in ' : '') + className}>
      <div className="lr-inner">{children}</div>
    </div>
  );
}

/* ───────── ScrollProgressBar (top of viewport) ───────── */

function ScrollProgressBar() {
  const p = useScrollProgress();
  return (
    <div style={{
      position: 'fixed', top: 0, left: 0, right: 0, height: 2,
      background: 'transparent', zIndex: 200, pointerEvents: 'none',
    }}>
      <div style={{
        width: `${p * 100}%`, height: '100%',
        background: 'var(--accent)',
        transition: 'width 50ms linear',
        boxShadow: '0 0 12px var(--accent)',
      }} />
    </div>
  );
}

/* ───────── ScaleOnScroll — element scales/fades based on its own progress ───────── */

function ScaleOnScroll({ children, from = 0.92, to = 1, style, as: As = 'div', ...rest }) {
  const ref = React.useRef(null);
  const p = useElementProgress(ref);
  // Scale up to peak at 50% progress then back down
  const t = 1 - Math.abs(p - 0.5) * 2;
  const scale = from + (to - from) * Math.max(0, t);
  return (
    <As ref={ref} style={{ ...style, transform: `scale(${scale})`, transition: 'none', willChange: 'transform' }} {...rest}>
      {children}
    </As>
  );
}

/* ───────── BgShift — color shifts based on element progress ───────── */

function MarqueeRibbon({ items, accent, speed = 18 }) {
  // Reusable marquee ribbon; for adding kinetic separators between sections
  const doubled = [...items, ...items];
  return (
    <div className="ribbon">
      <div className="ribbon-row" style={{ animationDuration: `${speed}s` }}>
        {doubled.map((it, i) => (
          <span key={i} className="ribbon-item">
            <span className="ribbon-text">{it}</span>
            <span className="ribbon-dot" style={{ background: accent || 'var(--accent)' }} />
          </span>
        ))}
      </div>
    </div>
  );
}

/* ───────── Typing-style scroll reveal ─────────
   Splits text children into per-character spans and reveals them in sequence
   when the host enters the viewport. Handles nested React elements
   (e.g. <span className="it">…</span>) so styling is preserved per character. */

function TypeReveal({ children, speed = 32, delay = 0, caret = true, className = '', as: As = 'span' }) {
  const ref = React.useRef(null);
  const [seen, setSeen] = React.useState(false);
  const [done, setDone] = React.useState(false);

  // Process children -> array of char spans with running index, total count
  const total = React.useMemo(() => {
    let n = 0;
    const walk = (node) => {
      if (node == null || typeof node === 'boolean') return;
      if (typeof node === 'string' || typeof node === 'number') {
        const s = String(node);
        for (const ch of s) { if (ch !== '\n') n++; }
        return;
      }
      if (Array.isArray(node)) { node.forEach(walk); return; }
      if (React.isValidElement(node)) { walk(node.props.children); return; }
    };
    walk(children);
    return n;
  }, [children]);

  const rendered = React.useMemo(() => {
    let idx = 0;
    const render = (node, key = 'r') => {
      if (node == null || typeof node === 'boolean') return null;
      if (typeof node === 'string' || typeof node === 'number') {
        const s = String(node);
        const chars = [];
        for (const ch of s) {
          const i = idx++;
          chars.push(
            <span key={`${key}-c${i}`} className="ty-ch" style={{
              transitionDelay: seen ? `${delay + i * speed}ms` : '0ms',
            }}>{ch === ' ' ? '\u00A0' : ch}</span>
          );
        }
        return chars;
      }
      if (Array.isArray(node)) return node.map((n, i) => render(n, `${key}-a${i}`));
      if (React.isValidElement(node)) {
        const inner = render(node.props.children, `${key}-e`);
        return React.cloneElement(node, { key: `${key}-el`, ...node.props }, inner);
      }
      return node;
    };
    return render(children);
  }, [children, seen, delay, speed]);

  React.useEffect(() => {
    if (!ref.current) return;
    const io = new IntersectionObserver((entries) => {
      entries.forEach(e => {
        if (e.isIntersecting) { setSeen(true); io.disconnect(); }
      });
    }, { threshold: 0.18, rootMargin: '0px 0px -6% 0px' });
    io.observe(ref.current);
    return () => io.disconnect();
  }, []);

  React.useEffect(() => {
    if (!seen) return;
    const t = setTimeout(() => setDone(true), delay + total * speed + 400);
    return () => clearTimeout(t);
  }, [seen, delay, total, speed]);

  return (
    <As ref={ref} className={"type-reveal" + (seen ? " in" : "") + (done ? " done" : "") + (className ? " " + className : "")}>
      {rendered}
      {caret && <span className="ty-caret" />}
    </As>
  );
}

Object.assign(window, { TypeReveal });

Object.assign(window, {
  useScrollProgress, useElementProgress,
  Parallax, CountUp, WordReveal, LineReveal,
  ScrollProgressBar, ScaleOnScroll, MarqueeRibbon,
});
