// effects.jsx — scroll reveals, parallax, scroll progress, custom cursor, counters
// Exports: Cursor, Counter, useSiteFX  → window

const { useState, useEffect, useRef } = React;

const PRM = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;

// ── Master site effects: reveal observer + parallax (fully imperative) ───────
// IMPORTANT: reveals are flagged with a `data-in` ATTRIBUTE, not a class.
// React owns `className`, so adding a class here would be wiped on the next
// re-render; a data-* attribute React doesn't manage survives. This hook also
// must NEVER call setState — otherwise it would re-render the app on scroll.
function useSiteFX() {
  // Reveal: observe any .reveal / .line-mask in the DOM. Re-scan a few times
  // after mount so async-rendered (Babel) sections get picked up.
  // ROBUSTNESS: never depend on IO alone. We also (a) force-reveal anything in
  // the viewport on load + on scroll/resize, and (b) re-scan on DOM mutations.
  // This guarantees above-the-fold content (the hero H1) paints even if the
  // IntersectionObserver callback never fires in a given environment.
  useEffect(() => {
    const SEL = '.reveal:not([data-in]), .line-mask:not([data-in])';
    // Flag the element revealed. For .line-mask we ALSO flag the inner span that
    // actually carries the transform — keying the child's transform off the
    // PARENT's attribute (`.line-mask[data-in] > span`) doesn't reliably trigger
    // a style recalc in Chromium, so we use a self-selector (`> span[data-in]`).
    const flag = (el) => {
      el.setAttribute('data-in', '');
      if (el.classList.contains('line-mask')) {
        const sp = el.firstElementChild;
        if (sp) { sp.setAttribute('data-in', ''); void sp.offsetWidth; }
      }
    };
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (e.isIntersecting) { flag(e.target); io.unobserve(e.target); }
      });
    }, { threshold: 0.16, rootMargin: '0px 0px -8% 0px' });

    const scan = () => document.querySelectorAll(SEL).forEach((el) => io.observe(el));
    const revealInView = () => {
      const vh = window.innerHeight;
      document.querySelectorAll(SEL).forEach((el) => {
        const r = el.getBoundingClientRect();
        if (r.bottom > 0 && r.top < vh * 0.92) flag(el);
      });
    };

    scan();
    revealInView();
    const timers = [80, 300, 700, 1400, 2200].map((ms) => setTimeout(() => { scan(); revealInView(); }, ms));

    let ticking = false;
    const onScroll = () => {
      if (!ticking) { ticking = true; requestAnimationFrame(() => { ticking = false; revealInView(); }); }
    };
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', onScroll);

    // Re-scan + reveal when the tree changes (e.g. hero variant switched).
    let raf = 0;
    const mo = new MutationObserver(() => {
      cancelAnimationFrame(raf);
      raf = requestAnimationFrame(() => { scan(); revealInView(); });
    });
    mo.observe(document.body, { childList: true, subtree: true });

    return () => {
      io.disconnect(); mo.disconnect();
      cancelAnimationFrame(raf); timers.forEach(clearTimeout);
      window.removeEventListener('scroll', onScroll);
      window.removeEventListener('resize', onScroll);
    };
  }, []);

  // Parallax — rAF-throttled, imperative transforms only. No React state.
  useEffect(() => {
    if (PRM) return;
    let ticking = false;
    const run = () => {
      ticking = false;
      const vh = window.innerHeight;
      document.querySelectorAll('[data-speed]').forEach((el) => {
        const r = el.getBoundingClientRect();
        const center = r.top + r.height / 2 - vh / 2;
        const speed = parseFloat(el.dataset.speed) || 0;
        el.style.transform = `translate3d(0, ${(-center * speed).toFixed(1)}px, 0)`;
      });
    };
    const onScroll = () => { if (!ticking) { ticking = true; requestAnimationFrame(run); } };
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', onScroll);
    run();
    return () => { window.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); };
  }, []);
}

// ── Nav-local scroll state (isolated so the app doesn't re-render on scroll) ──
function useNavScroll() {
  const [scrolled, setScrolled] = useState(false);
  const [progress, setProgress] = useState(0);
  useEffect(() => {
    let ticking = false;
    const run = () => {
      ticking = false;
      const y = window.scrollY || window.pageYOffset;
      const h = document.documentElement.scrollHeight - window.innerHeight;
      setProgress(h > 0 ? (y / h) * 100 : 0);
      setScrolled(y > 40);
    };
    const onScroll = () => { if (!ticking) { ticking = true; requestAnimationFrame(run); } };
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', onScroll);
    run();
    return () => { window.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); };
  }, []);
  return { scrolled, progress };
}

// ── Custom cursor: survey-crosshair ring + yellow dot, lagged follow ─────────
function Cursor() {
  const ring = useRef(null);
  const dot = useRef(null);
  useEffect(() => {
    if (PRM) return;
    const fine = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
    if (!fine) return;
    document.body.classList.add('gr-cursor-on');
    let mx = window.innerWidth / 2, my = window.innerHeight / 2;
    let rx = mx, ry = my;
    let raf;
    const loop = () => {
      rx = mx; ry = my;
      if (ring.current) ring.current.style.transform = `translate3d(${rx}px, ${ry}px, 0)`;
      if (dot.current) dot.current.style.transform = `translate3d(${mx}px, ${my}px, 0)`;
      raf = requestAnimationFrame(loop);
    };
    const move = (e) => { mx = e.clientX; my = e.clientY; };
    const HOT = 'a, button, input, textarea, select, [data-hot]';
    const over = (e) => { if (e.target.closest && e.target.closest(HOT)) ring.current && ring.current.classList.add('is-hot'); };
    const out  = (e) => { if (e.target.closest && e.target.closest(HOT)) ring.current && ring.current.classList.remove('is-hot'); };
    window.addEventListener('mousemove', move);
    document.addEventListener('mouseover', over);
    document.addEventListener('mouseout', out);
    raf = requestAnimationFrame(loop);
    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener('mousemove', move);
      document.removeEventListener('mouseover', over);
      document.removeEventListener('mouseout', out);
      document.body.classList.remove('gr-cursor-on');
    };
  }, []);
  return (
    <>
      <div ref={ring} className="gr-cursor"><i /></div>
      <div ref={dot} className="gr-cursor-dot" />
    </>
  );
}

// ── Counter: animates 0 → value when scrolled into view ──────────────────────
function Counter({ value, duration = 1600, prefix = '', suffix = '', decimals = 0 }) {
  const ref = useRef(null);
  const [n, setN] = useState(0);
  useEffect(() => {
    const el = ref.current; if (!el) return;
    if (PRM) { setN(value); return; }
    let raf, started = false;
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (e.isIntersecting && !started) {
          started = true;
          const t0 = performance.now();
          const tick = (t) => {
            const p = Math.min(1, (t - t0) / duration);
            const eased = 1 - Math.pow(1 - p, 3);
            setN(value * eased);
            if (p < 1) raf = requestAnimationFrame(tick);
          };
          raf = requestAnimationFrame(tick);
          io.disconnect();
        }
      });
    }, { threshold: 0.5 });
    io.observe(el);
    return () => { io.disconnect(); cancelAnimationFrame(raf); };
  }, [value, duration]);
  const display = decimals > 0 ? n.toFixed(decimals) : Math.round(n).toLocaleString('es-AR');
  return <span ref={ref}>{prefix}{display}{suffix}</span>;
}

Object.assign(window, { useSiteFX, useNavScroll, Cursor, Counter });
