/* ============================================================
   Home animations — vanilla React + CSS re-implementations of the
   Aceternity effects (no Tailwind, no framer-motion), styled to the
   Spade palette. Each honours prefers-reduced-motion. Globals on window.*
   CSS lives in home.css.
   ============================================================ */

const _reduceMotion = () =>
  typeof window !== 'undefined' && window.matchMedia
    ? window.matchMedia('(prefers-reduced-motion: reduce)').matches
    : false;

/* ---- 1. FlipWords — cycles a word in place with a soft blur-rise ---- */
const FlipWords = ({ words = [], interval = 2600, color, style, className = '' }) => {
  const [i, setI] = React.useState(0);
  const reduce = React.useMemo(_reduceMotion, []);
  React.useEffect(() => {
    if (reduce || words.length < 2) return;
    const id = setInterval(() => setI((n) => (n + 1) % words.length), interval);
    return () => clearInterval(id);
  }, [reduce, words.length, interval]);
  if (!words.length) return null;
  // Hidden sizer (longest word) reserves width so following text never reflows.
  const longest = words.reduce((a, b) => (String(b).length > String(a).length ? b : a), '');
  return (
    <span className={`flip-words ${className}`} style={{ position: 'relative', ...style }}>
      <span style={{ visibility: 'hidden' }}>{longest}</span>
      <span key={i} className="flip-word" style={{ position: 'absolute', left: 0, color }}>
        {words[i]}
      </span>
    </span>
  );
};
window.FlipWords = FlipWords;

/* ---- 2. TypewriterEffect — types segments out, blinking caret ----
   segments: [{ text, color, className, style }]. Reveals char-by-char. */
const TypewriterEffect = ({ segments = [], speed = 52, startDelay = 250, cursor = true, onDone, style, className = '' }) => {
  const full = segments.map((s) => s.text).join('');
  const [n, setN] = React.useState(0);
  const reduce = React.useMemo(_reduceMotion, []);
  const doneRef = React.useRef(false);
  React.useEffect(() => {
    if (reduce) { setN(full.length); if (onDone && !doneRef.current) { doneRef.current = true; onDone(); } return; }
    let i = 0, timer;
    const tick = () => {
      i += 1; setN(i);
      if (i >= full.length) { if (onDone && !doneRef.current) { doneRef.current = true; onDone(); } return; }
      timer = setTimeout(tick, speed);
    };
    const start = setTimeout(tick, startDelay);
    return () => { clearTimeout(start); clearTimeout(timer); };
  }, [full, speed, startDelay, reduce]);
  // Walk segments, emitting only the first `n` characters across them.
  let remaining = n;
  const done = n >= full.length;
  return (
    <span className={className} style={style}>
      {segments.map((s, idx) => {
        const take = Math.max(0, Math.min(s.text.length, remaining));
        remaining -= s.text.length;
        if (take <= 0) return null;
        if (s.text === '\n') return <br key={idx} />;
        return (
          <span key={idx} className={s.className} style={{ color: s.color, ...s.style }}>
            {s.text.slice(0, take)}
          </span>
        );
      })}
      {cursor && <span className="tw-cursor" style={{ opacity: done && reduce ? 0 : 1 }} />}
    </span>
  );
};
window.TypewriterEffect = TypewriterEffect;

/* ---- 3. TextGenerate — each word blurs in, staggered, when scrolled into view ---- */
const TextGenerate = ({ text = '', wordDelay = 65, duration = 620, style, className = '', as = 'p', highlight, highlightColor = 'var(--lime)' }) => {
  const ref = React.useRef(null);
  const [shown, setShown] = React.useState(_reduceMotion());
  React.useEffect(() => {
    if (_reduceMotion()) { setShown(true); return; }
    const el = ref.current; if (!el) return;
    const io = new IntersectionObserver((es) => {
      es.forEach((e) => { if (e.isIntersecting) { setShown(true); io.unobserve(el); } });
    }, { threshold: 0.25 });
    io.observe(el);
    // Safety net: reveal the words if the observer never fires but the line is
    // already on-screen, so the heading is never left invisible on mobile.
    const fb = setTimeout(() => {
      const r = el.getBoundingClientRect();
      if (r.top < window.innerHeight && r.bottom > 0) setShown(true);
    }, 1400);
    return () => { io.disconnect(); clearTimeout(fb); };
  }, []);
  const words = String(text).split(/\s+/).filter(Boolean);
  const Tag = as;
  return (
    <Tag ref={ref} className={className} style={style}>
      {words.map((w, i) => (
        <React.Fragment key={i}>
          <span
            className={`tg-word ${shown ? 'in' : ''}`}
            style={{ transitionDelay: `${i * wordDelay}ms`, transitionDuration: `${duration}ms`, ...(highlight && highlight.test(w) ? { color: highlightColor } : null) }}>
            {w}
          </span>
          {i < words.length - 1 ? ' ' : ''}
        </React.Fragment>
      ))}
    </Tag>
  );
};
window.TextGenerate = TextGenerate;

/* ---- 4. TextReveal — drag/hover a spotlight to swap claim → evidence ----
   `text` shows by default; sweeping right reveals `revealText` underneath. */
const TextReveal = ({ text, revealText, textColor = '#E8EFDF', revealColor = 'var(--lime)', height = 200, fontSize = 40, hint, style, className = '' }) => {
  const ref = React.useRef(null);
  const [p, setP] = React.useState(0);       // 0..1 reveal fraction
  const [live, setLive] = React.useState(false);
  const reduce = React.useMemo(_reduceMotion, []);

  const at = (clientX) => {
    const el = ref.current; if (!el) return;
    const r = el.getBoundingClientRect();
    setP(Math.max(0, Math.min(1, (clientX - r.left) / r.width)));
  };

  // Reduced motion: static stacked comparison, no spotlight.
  if (reduce) {
    return (
      <div className={className} style={{ textAlign: 'center', ...style }}>
        <div style={{ fontSize, fontWeight: 600, letterSpacing: '-0.03em', color: textColor }}>{text}</div>
        <div style={{ fontSize, fontWeight: 600, letterSpacing: '-0.03em', color: revealColor, marginTop: 8 }}>{revealText}</div>
      </div>
    );
  }

  const layerType = { fontSize, fontWeight: 600, letterSpacing: '-0.03em', lineHeight: 1.05 };
  const clipTrans = live ? 'clip-path 90ms linear' : 'clip-path 520ms var(--ease-quiet)';
  return (
    <div
      ref={ref}
      className={`tr-card ${live ? 'is-live' : ''} ${className}`}
      style={{ height, ...style }}
      onMouseEnter={() => setLive(true)}
      onMouseMove={(e) => at(e.clientX)}
      onMouseLeave={() => { setLive(false); setP(0); }}
      onTouchStart={() => setLive(true)}
      onTouchMove={(e) => { if (e.touches[0]) at(e.touches[0].clientX); }}
      onTouchEnd={() => { setLive(false); setP(0); }}>
      {/* sizer keeps the card height stable */}
      <div className="tr-base" style={{ ...layerType, visibility: 'hidden' }}>{revealText}</div>
      {/* revealed (evidence) — shows the left p fraction */}
      <div className="tr-layer" style={{ ...layerType, color: revealColor,
        clipPath: `inset(0 ${(1 - p) * 100}% 0 0)`, transition: clipTrans }}>{revealText}</div>
      {/* claim text — shows the right (1-p) fraction */}
      <div className="tr-layer" style={{ ...layerType, color: textColor,
        clipPath: `inset(0 0 0 ${p * 100}%)`, transition: clipTrans }}>{text}</div>
      <div className="tr-bar" style={{ left: `${p * 100}%` }} />
      {hint && <div className="tr-hint mono-eyebrow" style={{ opacity: live ? 0 : 0.7, transition: 'opacity 220ms' }}>{hint}</div>}
    </div>
  );
};
window.TextReveal = TextReveal;

/* ---- 5. HeroParallax — rows drift sideways as the section scrolls ----
   rows: array of arrays of React nodes (cards). Alternating drift direction. */
const HeroParallax = ({ rows = [], amplitude = 150, children, style, className = '' }) => {
  const ref = React.useRef(null);
  const rowRefs = React.useRef([]);
  const reduce = React.useMemo(_reduceMotion, []);

  React.useEffect(() => {
    if (reduce) return;
    let raf = 0;
    const onScroll = () => {
      if (raf) return;
      raf = requestAnimationFrame(() => {
        raf = 0;
        const el = ref.current; if (!el) return;
        const r = el.getBoundingClientRect();
        const vh = window.innerHeight || 1;
        // progress 0..1 as the section travels through the viewport
        const t = Math.max(0, Math.min(1, (vh - r.top) / (vh + r.height)));
        const k = (t - 0.5) * 2; // -1..1
        rowRefs.current.forEach((rowEl, i) => {
          if (!rowEl) return;
          const dir = i % 2 === 0 ? 1 : -1;
          rowEl.style.transform = `translate3d(${(-k * dir * amplitude).toFixed(1)}px,0,0)`;
        });
      });
    };
    onScroll();
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', onScroll);
    return () => { window.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); cancelAnimationFrame(raf); };
  }, [reduce, amplitude, rows.length]);

  return (
    <div ref={ref} className={className} style={style}>
      {children}
      <div className="hp-rows">
        {rows.map((cards, i) => (
          <div key={i} ref={(el) => (rowRefs.current[i] = el)} className={`hp-row ${i % 2 ? 'rev' : ''}`}>
            {cards}
          </div>
        ))}
      </div>
    </div>
  );
};
window.HeroParallax = HeroParallax;

/* ---- 6. EncryptedText — scrambled glyphs resolve into the real line ----
   Decrypts left-to-right when scrolled into view; re-scrambles on hover.
   The settled prefix reads in the inherited colour, the still-scrambling
   tail glows lime. Screen readers get the plain text via aria-label. */
const _ENC_GLYPHS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#%&/\\<>*+=$?';
const _encGlyph = () => _ENC_GLYPHS[(Math.random() * _ENC_GLYPHS.length) | 0];
const _encScramble = (chars, rev) => {
  let s = '';
  for (let i = 0; i < chars.length; i++) {
    const c = chars[i];
    s += (c === ' ' || c === '\n' || i < rev) ? c : _encGlyph();
  }
  return s;
};

const EncryptedText = ({ text = '', speed = 45, duration = 1400, reScrambleOnHover = true, className = '', style }) => {
  const ref = React.useRef(null);
  const timer = React.useRef(null);
  const reduce = React.useMemo(_reduceMotion, []);
  const chars = React.useMemo(() => Array.from(String(text)), [text]);
  // Start as a full-length scramble (no layout shift); decrypt fires on view.
  const [st, setSt] = React.useState(
    reduce ? { out: String(text), rev: chars.length } : { out: _encScramble(chars, 0), rev: 0 }
  );
  const done = st.rev >= chars.length;

  const run = React.useCallback(() => {
    clearInterval(timer.current);
    if (reduce) { setSt({ out: String(text), rev: chars.length }); return; }
    const frames = Math.max(1, Math.round(duration / speed));
    let f = 0;
    timer.current = setInterval(() => {
      f += 1;
      const rev = Math.min(chars.length, Math.round((f / frames) * chars.length));
      setSt({ out: _encScramble(chars, rev), rev });
      if (f >= frames) { clearInterval(timer.current); setSt({ out: String(text), rev: chars.length }); }
    }, speed);
  }, [chars, text, speed, duration, reduce]);

  React.useEffect(() => {
    if (reduce) { setSt({ out: String(text), rev: chars.length }); return; }
    const el = ref.current; if (!el) return;
    const io = new IntersectionObserver((es) => {
      es.forEach((e) => { if (e.isIntersecting) { run(); io.unobserve(el); } });
    }, { threshold: 0.4 });
    io.observe(el);
    // Safety net: resolve the scramble if the observer never fires but the line
    // is already on-screen, so it never stays as gibberish on mobile.
    const fb = setTimeout(() => {
      const r = el.getBoundingClientRect();
      if (r.top < window.innerHeight && r.bottom > 0) run();
    }, 1400);
    return () => { io.disconnect(); clearInterval(timer.current); clearTimeout(fb); };
  }, [run, reduce, text]);

  return (
    <span
      ref={ref}
      className={`enc-text ${done ? '' : 'is-scrambling'} ${className}`}
      style={style}
      aria-label={text}
      onMouseEnter={reScrambleOnHover && done ? run : undefined}>
      <span aria-hidden="true" className="enc-done-part">{st.out.slice(0, st.rev)}</span>
      <span aria-hidden="true" className="enc-live">{st.out.slice(st.rev)}</span>
    </span>
  );
};
window.EncryptedText = EncryptedText;

/* ---- 7. EvervaultCard — a cursor spotlight sweeps a field of random
   glyphs to reveal the "raw signal"; a clear figure sits at its core.
   Pointer-driven radial mask (no framer-motion). Calm under reduced motion. */
const _encNoise = (len) => {
  let s = '';
  for (let i = 0; i < len; i++) s += _encGlyph();
  return s;
};

const EvervaultCard = ({ text, caption, radius = 210, density = 1800, className = '', style }) => {
  const reduce = React.useMemo(_reduceMotion, []);
  const [pos, setPos] = React.useState({ x: 0, y: 0, set: false });
  const [active, setActive] = React.useState(false);
  const [noise, setNoise] = React.useState(() => _encNoise(density));

  const onMove = (e) => {
    if (reduce) return;
    const r = e.currentTarget.getBoundingClientRect();
    setPos({ x: e.clientX - r.left, y: e.clientY - r.top, set: true });
  };
  const onEnter = () => { if (reduce) return; setNoise(_encNoise(density)); setActive(true); };
  const onLeave = () => setActive(false);

  const mask = `radial-gradient(${radius}px circle at ${pos.x}px ${pos.y}px, #000 5%, rgba(0,0,0,0.35) 38%, transparent 70%)`;
  const reveal = active && pos.set
    ? { opacity: 1, WebkitMaskImage: mask, maskImage: mask }
    : { opacity: 0 };

  return (
    <div
      className={`evervault tick-frame ${active ? 'is-live' : ''} ${className}`}
      style={style}
      onMouseMove={onMove}
      onMouseEnter={onEnter}
      onMouseLeave={onLeave}>
      <span className="tk tk-tl"></span><span className="tk tk-tr"></span>
      <span className="tk tk-bl"></span><span className="tk tk-br"></span>
      <div className="evervault-glow" style={reveal} aria-hidden="true"></div>
      <div className="evervault-noise" style={reveal} aria-hidden="true">{noise}</div>
      <div className="evervault-core">
        <span className="evervault-blob" aria-hidden="true"></span>
        <span className="evervault-text">{text}</span>
        {caption && <span className="evervault-caption">{caption}</span>}
      </div>
    </div>
  );
};
window.EvervaultCard = EvervaultCard;

/* ---- 8. Tabs — animated tab bar (sliding pill) + stacked-card deck ----
   Vanilla re-implementation of the Aceternity Tabs (no framer-motion / Tailwind):
   the active panel sits in front; the rest peek behind in a deck that fans out
   on hover. The active pill is measured and slid (not layout-animated). The tab
   row scrolls horizontally when the tabs outrun the width. */
const Tabs = ({ tabs = [], initial = 0, onChange, className = '', barClassName = '' }) => {
  const [active, setActive] = React.useState(initial);
  const [hovering, setHovering] = React.useState(false);
  const reduce = React.useMemo(_reduceMotion, []);
  const btnRefs = React.useRef([]);
  const [pill, setPill] = React.useState({ left: 0, width: 0, ready: false });

  const measure = React.useCallback(() => {
    const b = btnRefs.current[active];
    if (b) setPill({ left: b.offsetLeft, width: b.offsetWidth, ready: true });
  }, [active]);

  // Defer to a frame so the bar/buttons are laid out before we read offsets
  // (measuring synchronously on mount can read a 0-width button).
  React.useEffect(() => {
    const raf = requestAnimationFrame(measure);
    return () => cancelAnimationFrame(raf);
  }, [measure, tabs.length]);
  React.useEffect(() => {
    const on = () => measure();
    window.addEventListener('resize', on);
    return () => window.removeEventListener('resize', on);
  }, [measure]);

  const select = (i) => {
    setActive(i);
    const b = btnRefs.current[i];
    if (b && b.scrollIntoView) b.scrollIntoView({ block: 'nearest', inline: 'center' });
    if (onChange) onChange(i, tabs[i]);
  };

  // Paint order: active card in front, the rest behind it in sequence.
  const order = [active];
  for (let i = 0; i < tabs.length; i++) if (i !== active) order.push(i);

  return (
    <div className={`tabs-comp ${className}`}>
      <div className={`tabs-bar ${barClassName}`} role="tablist">
        <span className="tabs-pill" aria-hidden="true"
          style={{ transform: `translateX(${pill.left}px)`, width: pill.width, opacity: pill.ready ? 1 : 0 }} />
        {tabs.map((t, i) => (
          <button
            key={t.value != null ? t.value : i}
            ref={(el) => (btnRefs.current[i] = el)}
            type="button" role="tab" aria-selected={i === active}
            className={`tabs-btn ${i === active ? 'is-active' : ''}`}
            onClick={() => select(i)}>
            {t.title}
          </button>
        ))}
      </div>

      <div className="tabs-deck" onMouseEnter={() => setHovering(true)} onMouseLeave={() => setHovering(false)}>
        {order.map((tabIdx, depth) => {
          const isTop = depth === 0;
          const d = Math.min(depth, 3);                 // only the top few peek
          const lift = reduce ? 0 : d * (hovering ? 44 : 20);
          const scale = reduce ? 1 : 1 - d * 0.05;
          return (
            <div
              key={tabs[tabIdx].value != null ? tabs[tabIdx].value : tabIdx}
              className={`tabs-card ${isTop ? 'is-top' : ''}`}
              aria-hidden={!isTop}
              style={{
                transform: `translateY(${lift}px) scale(${scale})`,
                opacity: depth > 3 ? 0 : 1,
                filter: isTop ? 'none' : `brightness(${(1 - d * 0.12).toFixed(2)})`,
                zIndex: tabs.length - depth,
                pointerEvents: isTop ? 'auto' : 'none',
              }}>
              {tabs[tabIdx].content}
            </div>
          );
        })}
      </div>
    </div>
  );
};
window.Tabs = Tabs;
