/* ===================================================================
   ROVIA — Shared hooks
   NOTE: IntersectionObserver is unreliable inside the preview iframe,
   so in-view detection uses a single shared scroll/raf listener.
   =================================================================== */
const { useState, useEffect, useRef, useCallback } = React;

/* ---- shared in-view registry (scroll/resize/raf based) ---- */
const _watchers = new Set();
let _ticking = false;

function _check() {
  _ticking = false;
  const vh = window.innerHeight || document.documentElement.clientHeight;
  _watchers.forEach((w) => {
    const el = w.el;
    if (!el || !el.isConnected) { _watchers.delete(w); return; }
    const r = el.getBoundingClientRect();
    if (r.height === 0 && r.width === 0) return;
    const visible = r.top < vh * (1 - w.threshold) && r.bottom > vh * 0.02;
    if (visible) {
      w.cb();
      _watchers.delete(w);
    }
  });
}
function _schedule() {
  if (_ticking) return;
  _ticking = true;
  setTimeout(_check, 0);
}
function _ensureListeners() {
  if (_ensureListeners._on) return;
  _ensureListeners._on = true;
  window.addEventListener("scroll", _schedule, { passive: true });
  window.addEventListener("resize", _schedule, { passive: true });
}

/* register an element; cb fires once when it scrolls into view */
function watchInView(el, cb, threshold = 0.12) {
  if (!el) return () => {};
  _ensureListeners();
  const w = { el, cb, threshold };
  _watchers.add(w);
  _schedule();
  return () => _watchers.delete(w);
}

/* Reveal-on-scroll: adds .in when element enters viewport */
function useReveal() {
  useEffect(() => {
    const els = document.querySelectorAll(".reveal:not(.in)");
    const unsubs = [];
    els.forEach((el) => {
      unsubs.push(watchInView(el, () => el.classList.add("in"), 0.08));
    });
    // immediate synchronous pass so above-the-fold content paints at once
    _check();
    // deferred checks catch anything settling after layout/fonts
    const t1 = setTimeout(_check, 60);
    const t2 = setTimeout(_check, 300);
    return () => { unsubs.forEach((u) => u()); clearTimeout(t1); clearTimeout(t2); };
  }, []);
}

/* fire a callback once when ref enters view */
function useInView(ref, cb, threshold = 0.25) {
  useEffect(() => {
    if (!ref.current) return;
    return watchInView(ref.current, cb, threshold);
  }, []);
}

/* Count-up number that triggers when scrolled into view */
function CountUp({ end, duration = 1600, suffix = "", prefix = "", decimals = 0 }) {
  const ref = useRef(null);
  const [val, setVal] = useState(0);

  useEffect(() => {
    if (!ref.current) return;
    let raf;
    const unsub = watchInView(ref.current, () => {
      const start = performance.now();
      const tick = (now) => {
        const p = Math.min((now - start) / duration, 1);
        const eased = 1 - Math.pow(1 - p, 3);
        setVal(end * eased);
        if (p < 1) raf = requestAnimationFrame(tick);
      };
      raf = requestAnimationFrame(tick);
    }, 0.3);
    return () => { unsub(); cancelAnimationFrame(raf); };
  }, [end, duration]);

  const display =
    decimals > 0 ? val.toFixed(decimals) : Math.round(val).toLocaleString("en-US");

  return (
    <span ref={ref}>
      {prefix}
      {display}
      {suffix}
    </span>
  );
}

Object.assign(window, { useReveal, useInView, watchInView, CountUp });
