/* Shared primitives for the Flywheel pitch.
   Reveals + counters are driven by a vanilla DOM scanner (NOT React state) so
   they flush reliably even when the React scheduler / rAF is throttled. */

const { useState, useRef, useEffect, useCallback } = React;

/* ---------- Reveal scanner (vanilla) ---------- */
function _animateCount(el) {
  if (el.__counting) return; el.__counting = true;
  const to = +el.dataset.to, from = +el.dataset.from || 0, dur = +el.dataset.dur || 1600;
  const dec = +el.dataset.dec || 0, suf = el.dataset.suffix || '', pre = el.dataset.prefix || '';
  const fmt = (v) => dec > 0 ? v.toFixed(dec) : Math.round(v).toLocaleString('en-US');
  const t0 = Date.now();
  const iv = setInterval(() => {
    const p = Math.min((Date.now() - t0) / dur, 1);
    const e = 1 - Math.pow(1 - p, 3);
    el.textContent = pre + fmt(from + (to - from) * e) + suf;
    if (p >= 1) { clearInterval(iv); el.textContent = pre + fmt(to) + suf; }
  }, 32);
}
function _scan() {
  const vh = window.innerHeight || document.documentElement.clientHeight;
  const inView = (el) => { const r = el.getBoundingClientRect(); return r.top < vh * 0.92 && r.bottom > vh * 0.03; };
  document.querySelectorAll('.reveal:not(.in),.reveal-sc:not(.in),.reveal-cl:not(.in),.split:not(.in)').forEach((el) => {
    if (!inView(el)) return;
    el.classList.add('in');
    const d = parseInt(el.style.getPropertyValue('--d')) || 0;
    setTimeout(() => el.classList.add('settled'), d + 1700);
  });
  document.querySelectorAll('.countup').forEach((el) => { if (inView(el)) _animateCount(el); });
}
let _scanBound = false;
function initReveals() {
  if (_scanBound) return; _scanBound = true;
  let t = 0;
  const h = () => { const n = Date.now(); if (n - t > 40) { t = n; _scan(); } };
  window.addEventListener('scroll', h, { passive: true });
  window.addEventListener('resize', h);
  [0, 80, 200, 400, 700, 1000].forEach((d) => setTimeout(_scan, d));
  setInterval(_scan, 250); // continuous failsafe sweep
}

/* ---------- Components ---------- */
function Reveal({ children, delay = 0, variant = 'rise', as = 'div', className = '', style = {} }) {
  const cls = variant === 'clip' ? 'reveal-cl' : variant === 'scale' ? 'reveal-sc' : 'reveal';
  return React.createElement(as, { className: `${cls} ${className}`, style: { '--d': `${delay}ms`, ...style } }, children);
}

function Split({ text, className = '', tag = 'h2', style = {}, delay = 0 }) {
  // Break on sentence boundaries so each full sentence sits on its own line
  const sentences = String(text).match(/\S[^.!?]*[.!?]*[\u201D\u2019"')\]]*/g) || [String(text)];
  let wi = 0;
  return React.createElement(tag, { className: `split ${className}`, style: { '--d': `${delay}ms`, ...style } },
    sentences.map((s, si) => {
      const words = s.trim().split(/\s+/);
      return React.createElement('span', { key: si, className: 'sline', style: { display: 'block' } },
        words.map((w, i) => React.createElement('span', { key: i, className: 'w', style: { '--i': wi++ } },
          w + (i < words.length - 1 ? '\u00A0' : ''))));
    }));
}

function CountUp({ to, from = 0, dur = 1600, suffix = '', prefix = '', decimals = 0, className = '', style = {} }) {
  return (
    <span className={`countup ${className}`} style={style}
      data-to={to} data-from={from} data-dur={dur} data-suffix={suffix} data-prefix={prefix} data-dec={decimals}>
      {prefix}{Number(from).toFixed(decimals)}{suffix}
    </span>
  );
}

function OrbitMark({ size = 120, spin = true, src = 'assets/flywheel-mark-trans.png', style = {} }) {
  return <img src={src} alt="Flywheel" className={spin ? 'orbit-spin' : ''} style={{ width: size, height: size, ...style }} />;
}

const ICONS = {
  play: 'M8 5v14l11-7z',
  arrowRight: 'M5 12h14M13 6l6 6-6 6',
  arrowDown: 'M12 5v14M6 13l6 6 6-6',
  check: 'M20 6 9 17l-5-5',
  spark: 'M12 3v6M12 15v6M3 12h6M15 12h6',
  mail: 'M4 6h16v12H4zM4 7l8 6 8-6',
  clock: 'M12 7v5l3 2M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z',
};
function Ico({ name, size = 20, color = 'currentColor', sw = 2, style = {}, fill = 'none' }) {
  const d = ICONS[name];
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill={fill} stroke={color}
      strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block', ...style }}>
      {name === 'play' ? <path d={d} fill={color} stroke="none" /> : <path d={d} />}
    </svg>
  );
}

function VideoFrame({ src, poster, label = 'Video', duration, light = false, badge, className, fitVideo = false }) {
  const [playing, setPlaying] = useState(false);
  const [started, setStarted] = useState(false);
  const [ar, setAr] = useState(null);
  const [blobSrc, setBlobSrc] = useState(null);
  const vref = useRef(null);
  // Fetch the file into a blob URL: blob sources are fully seekable even when
  // the server doesn't support HTTP range requests (which breaks scrubbing).
  useEffect(() => {
    if (!src || /^(blob|data):/.test(src)) { setBlobSrc(null); return; }
    let url = null, dead = false;
    fetch(src).then((r) => r.ok ? r.blob() : null).then((b) => {
      if (dead || !b) return;
      url = URL.createObjectURL(b);
      setBlobSrc(url);
    }).catch(() => {});
    return () => { dead = true; if (url) URL.revokeObjectURL(url); };
  }, [src]);
  const play = () => { if (src && vref.current) { vref.current.play(); setPlaying(true); setStarted(true); } };
  const onMeta = (e) => {
    const v = e.target;
    if (fitVideo && v.videoWidth) setAr(v.videoWidth + ' / ' + v.videoHeight);
    // MediaRecorder-style files report Infinity duration → scrubber is dead.
    // Seeking far past the end forces the browser to compute the real duration.
    if (!isFinite(v.duration)) {
      const reset = () => { if (isFinite(v.duration)) { v.currentTime = 0; v.removeEventListener('durationchange', reset); } };
      v.addEventListener('durationchange', reset);
      v.currentTime = 1e7;
    }
  };
  return (
    <div className={`videoframe ${light ? 'light' : ''} ${className || ''}`} style={fitVideo && ar ? { aspectRatio: ar } : undefined}>
      {src ? (
        <>
          <video ref={vref} src={blobSrc || src} poster={poster} controls={started} playsInline preload="metadata" onPlay={() => { setPlaying(true); setStarted(true); }} onPause={() => setPlaying(false)}
            onLoadedMetadata={onMeta} />
          {!started && (
            <div className="vf-overlay" onClick={play}>
              <div className="vf-play"><Ico name="play" size={30} color="#101010" /></div>
            </div>
          )}
        </>
      ) : (
        <div className="poster">
          {poster && <img src={poster} alt="" style={{ position: 'absolute', inset: 0, opacity: 0.35 }} />}
          <div className="vf-play" onClick={play}><Ico name="play" size={30} color="#101010" /></div>
          <div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.6)' }}>Drop video here</div>
        </div>
      )}
      {badge && <div className="vf-badge">{badge}</div>}
    </div>
  );
}

Object.assign(window, { Reveal, Split, CountUp, OrbitMark, OrbitSVG, Ico, VideoFrame, initReveals });

/* The real Flywheel orbit mark (raster), gently rotating. Faithful to the logo,
   far crisper than a hand-built vector. White+lime mark for dark surfaces. */
function OrbitSVG({ style = {}, src = 'assets/flywheel-mark.png', spin = true }) {
  return (
    <img src={src} alt="" aria-hidden="true" className={spin ? 'orbit-spin' : ''}
      style={{ width: '100%', height: '100%', objectFit: 'contain', display: 'block', ...style }} />
  );
}
