/* global React */
/* ============================================================
   MYTHOS SCROLL — site-wide scroll-motion engine.
   Three motion levels, driven by the Tweaks panel and reflected
   as html[data-myth-motion]:
     off    — everything static & visible (also when the user
              prefers reduced motion)
     subtle — richer staggered reveals + stat count-ups
     full   — subtle + parallax drift (chapter numerals, plates,
              figures, hero media) + engraved plate wipes (CSS)
   The engine never animates layout — transforms + text only.
   ============================================================ */

(function () {
  const PRM = () => window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const clampN = (n, a, b) => Math.max(a, Math.min(b, n));

  /* ---------- auto-reveal ----------
     The Mythos home carries hand-placed [data-reveal]; the inner
     pages (products, case study, FAQ…) share markup with the main
     site and carry none. Rather than tag hundreds of elements in
     shared files, scan each rendered page and tag section-level
     blocks at runtime. Tagged elements get the same stately rise
     (+ stagger, figure wipes, plate prints) from styles-mythos.css.
     Self-contained: own IntersectionObserver, full cleanup.
     Safety rules — never tag:
       • sticky/fixed/absolute elements, or anything containing them
         (transforms break position:sticky)
       • the 3D hero / scroll-driven sections
       • anything already in or containing a [data-reveal]
       • elements in the viewport right now (no pop-in on arrival)
       • blocks taller than the viewport (descend a level instead) */
  function applyAutoReveal(cleanups) {
    const STICKY = '.hub-sticky,.pcalc-summary,.rtb-vra-card-head,.nav';
    const vh = window.innerHeight;
    const tagged = [];
    function skip(el) {
      if (!el || el.nodeType !== 1) return true;
      const tn = el.tagName;
      if (tn === 'STYLE' || tn === 'SCRIPT' || tn === 'TEMPLATE') return true;
      if (el.hasAttribute('data-reveal') || el.closest('[data-reveal]') || el.querySelector('[data-reveal]')) return true;
      if (el.closest('.b110-hero, .myth-hero, [data-no-autoreveal]')) return true;
      if (el.matches(STICKY) || el.querySelector(STICKY) || el.querySelector('[style*="sticky"], [style*="fixed"]')) return true;
      const cs = getComputedStyle(el);
      if (cs.position === 'sticky' || cs.position === 'fixed' || cs.position === 'absolute') return true;
      if (cs.display === 'none' || cs.visibility === 'hidden') return true;
      if (cs.transform && cs.transform !== 'none') return true;
      return false;
    }
    function tag(el, depth) {
      if (skip(el)) return;
      const r = el.getBoundingClientRect();
      if (r.height < 12 || r.width < 12) return;          /* empty / rules */
      if (r.top < vh * 0.92) return;                      /* already on screen */
      if (r.height > vh * 1.15) {                         /* too tall to move whole */
        if (depth < 2) Array.prototype.forEach.call(el.children, (c) => tag(c, depth + 1));
        return;
      }
      el.setAttribute('data-reveal', '');
      el.setAttribute('data-auto-reveal', '');
      tagged.push(el);
    }
    document.querySelectorAll('.page-head, .section, .cta-banner').forEach((sec) => {
      if (sec.closest('[data-no-autoreveal]')) return;
      /* tag the children of the section's content container, so each
         block (head, figure, card grid, table) rises on its own */
      let roots = sec.querySelectorAll(':scope > .container-wide, :scope > .container');
      if (!roots.length) roots = [sec];
      roots.forEach((root) => {
        if (root.children.length > 40) return;
        Array.prototype.forEach.call(root.children, (c) => tag(c, 0));
      });
    });
    if (!tagged.length) return;
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (!e.isIntersecting) return;
        e.target.classList.add('is-revealed');
        io.unobserve(e.target);
      });
    }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
    tagged.forEach((el) => io.observe(el));
    cleanups.push(() => {
      io.disconnect();
      tagged.forEach((el) => {
        el.removeAttribute('data-reveal');
        el.removeAttribute('data-auto-reveal');
        el.classList.remove('is-revealed');
      });
    });
  }

  /* ---------- sibling stagger ----------
     Direct children of a grid/flex parent that each carry
     [data-reveal] reveal together — give them a small cascade
     via --mr-delay (consumed by styles-mythos.css). */
  function applyStagger(cleanups) {
    const parents = new Set();
    document.querySelectorAll('[data-reveal]').forEach((el) => {
      if (el.parentElement) parents.add(el.parentElement);
    });
    parents.forEach((p) => {
      const kids = Array.prototype.filter.call(p.children, (c) => c.hasAttribute('data-reveal'));
      if (kids.length < 2) return;
      const disp = getComputedStyle(p).display;
      if (disp.indexOf('grid') < 0 && disp.indexOf('flex') < 0) return;
      kids.forEach((k, i) => k.style.setProperty('--mr-delay', (Math.min(i, 4) * 0.09).toFixed(2) + 's'));
      cleanups.push(() => kids.forEach((k) => k.style.removeProperty('--mr-delay')));
    });
  }

  /* ---------- stat count-ups ----------
     .myth-stat-num like "−18%" / "+24%" / "3,000" tick up from 0
     when they enter view. Non-numeric stats are left alone. */
  function applyCounters(cleanups) {
    const els = document.querySelectorAll('.myth-stat-num, .cs-bigstat-num, .emi-roi-num');
    if (!els.length) return;
    const items = [];
    els.forEach((el) => {
      const txt = (el.textContent || '').trim();
      const m = txt.match(/^([+\-−±]?)([\d\s,]*\d(?:[.,]\d+)?)(.*)$/);
      if (!m) return;
      const numStr = m[2].replace(/[\s,]/g, '').replace(',', '.');
      const target = parseFloat(numStr.replace(',', '.'));
      if (!isFinite(target)) return;
      const dec = (m[2].split(/[.,](?=\d+$)/)[1] || '').length;
      const grouped = /,/.test(m[2]);
      items.push({ el, sign: m[1], target, dec, grouped, suffix: m[3], original: txt, done: false });
    });
    if (!items.length) return;
    const rafs = new Set();
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (!e.isIntersecting) return;
        const it = items.find((x) => x.el === e.target);
        io.unobserve(e.target);
        if (!it || it.done) return;
        it.done = true;
        it.el.style.fontVariantNumeric = 'tabular-nums';
        const t0 = performance.now(), DUR = 1300, DELAY = 250;
        const fmt = (n) => {
          let s = n.toFixed(it.dec);
          if (it.grouped) s = Number(s).toLocaleString('en-ZA', { minimumFractionDigits: it.dec, maximumFractionDigits: it.dec });
          return it.sign + s + it.suffix;
        };
        const tick = (t) => {
          const k = clampN((t - t0 - DELAY) / DUR, 0, 1);
          const eased = 1 - Math.pow(1 - k, 3); /* instruments don't bounce */
          it.el.textContent = fmt(it.target * eased);
          if (k < 1) { const r = requestAnimationFrame(tick); rafs.add(r); }
          else it.el.textContent = it.original;
        };
        const r = requestAnimationFrame(tick); rafs.add(r);
      });
    }, { threshold: 0.5 });
    items.forEach((it) => io.observe(it.el));
    cleanups.push(() => {
      io.disconnect();
      rafs.forEach((r) => cancelAnimationFrame(r));
      items.forEach((it) => { it.el.textContent = it.original; it.el.style.removeProperty('font-variant-numeric'); });
    });
  }

  /* ---------- parallax drift (full only) ----------
     Gentle counter-drift on ornaments and imagery. Lerped, rAF,
     transform-only. Skips the 3D hero (it drives itself) and any
     element that already carries a transform. */
  function applyParallax(cleanups) {
    const targets = [];
    const add = (el, speed, scale) => {
      if (!el || el.closest('.b110-hero')) return;
      if (targets.some((t) => t.el === el)) return;
      const tf = getComputedStyle(el).transform;
      if (tf && tf !== 'none') return;
      targets.push({ el, speed, scale: scale || 1, cur: 0, base: 0, h: 0 });
    };
    document.querySelectorAll('.myth-chapter-numeral').forEach((el) => add(el, -0.085));
    document.querySelectorAll('.myth-plate-frame').forEach((el) => add(el, 0.05));
    document.querySelectorAll('.myth-hero-media').forEach((el) => add(el, 0.14, 1.1));
    document.querySelectorAll('div[data-img-file]').forEach((el) => {
      if (el.closest('.myth-plate') || el.closest('.myth-hero')) return;
      if (el.getBoundingClientRect().height < 160) return;
      add(el, 0.04);
    });
    if (!targets.length) return;

    const measure = () => {
      const sy = window.scrollY;
      targets.forEach((t) => {
        const r = t.el.getBoundingClientRect();
        t.base = r.top + sy - t.cur; /* untransformed document offset */
        t.h = r.height;
      });
    };
    measure();
    targets.forEach((t) => { t.el.style.willChange = 'transform'; });

    let alive = true, raf, frame = 0;
    const loop = () => {
      if (!alive) return;
      frame++;
      if (frame % 180 === 0) measure(); /* layout drifts as images load */
      const vh = window.innerHeight, sy = window.scrollY;
      targets.forEach((t) => {
        const rel = t.base - sy + t.h / 2 - vh / 2; /* px from viewport centre */
        if (Math.abs(rel) > vh * 1.4) return;        /* off screen — leave it */
        const want = clampN(-rel * t.speed, -44, 44);
        t.cur += (want - t.cur) * 0.12;              /* stately glide */
        t.el.style.transform = 'translate3d(0,' + t.cur.toFixed(2) + 'px,0)' + (t.scale !== 1 ? ' scale(' + t.scale + ')' : '');
      });
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    window.addEventListener('resize', measure);
    cleanups.push(() => {
      alive = false;
      cancelAnimationFrame(raf);
      window.removeEventListener('resize', measure);
      targets.forEach((t) => { t.el.style.removeProperty('transform'); t.el.style.removeProperty('will-change'); });
    });
  }

  function startEngine(level) {
    if (PRM() || level === 'off') return () => {};
    const cleanups = [];
    applyAutoReveal(cleanups);
    applyStagger(cleanups);
    applyCounters(cleanups);
    if (level === 'full') applyParallax(cleanups);
    return () => cleanups.forEach((fn) => { try { fn(); } catch (_) {} });
  }

  /* Hook for the app — rescans on route or level change. The 380ms
     delay lets the page render + first reveals queue up first. */
  window.useMythosMotion = function useMythosMotion(route, level) {
    React.useEffect(() => {
      let stop = null;
      const t = setTimeout(() => { stop = startEngine(level); }, 380);
      return () => { clearTimeout(t); if (stop) stop(); };
    }, [route, level]);
  };
})();
