/* Spade-style shared components: CornerFrame, DataChip, StatFrame, StatBand, CountInView */

/* Corner-tick frame wrapper */
const CornerFrame = ({ children, className = '', style, as = 'div' }) => {
  const Tag = as;
  return (
    <Tag className={`tick-frame ${className}`} style={style}>
      <span className="tk tk-tl"></span>
      <span className="tk tk-tr"></span>
      <span className="tk tk-bl"></span>
      <span className="tk tk-br"></span>
      {children}
    </Tag>
  );
};
window.CornerFrame = CornerFrame;

/* Monospace data chip — ► LABEL + value */
const DataChip = ({ label, children, ticks = false, className = '', style }) => {
  const inner = (
    <>
      <div className="dc-label">{label}</div>
      <div className="dc-value">{children}</div>
    </>
  );
  if (ticks) {
    return (
      <div className={`data-chip ${className}`} style={{ border: 'none', ...style }}>
        <CornerFrame style={{ padding: 0 }}>
          <div style={{ padding: '6px 16px' }}>{inner}</div>
        </CornerFrame>
      </div>
    );
  }
  return <div className={`data-chip ${className}`} style={style}>{inner}</div>;
};
window.DataChip = DataChip;

/* Count-up number that fires when scrolled into view */
const CountInView = ({ value, decimals = 0, prefix = '', suffix = '', duration = 2200, group = false, repeat = true, style, className }) => {
  const ref = React.useRef(null);
  const [shown, setShown] = React.useState(false);
  const [cycle, setCycle] = React.useState(0); // bump to replay the count each time it re-enters view
  React.useEffect(() => {
    const el = ref.current; if (!el) return;
    const io = new IntersectionObserver((es) => {
      es.forEach(e => {
        if (e.isIntersecting) { setShown(true); setCycle(c => c + 1); if (!repeat) io.unobserve(el); }
        else if (repeat) { setShown(false); }
      });
    }, { threshold: 0.2, rootMargin: '0px 0px -10% 0px' });
    io.observe(el);
    // Safety net (see SpReveal): start the count if the observer never fires but
    // the figure is already on-screen, so it never sits frozen at 0 on mobile.
    const fb = setTimeout(() => {
      const r = el.getBoundingClientRect();
      if (r.top < window.innerHeight && r.bottom > 0) { setShown(true); setCycle(c => c + 1); }
    }, 1400);
    return () => { io.disconnect(); clearTimeout(fb); };
  }, [repeat]);
  return (
    <span ref={ref} className={className} style={style}>
      {shown
        ? <AnimatedNumber key={cycle} value={value} decimals={decimals} prefix={prefix} suffix={suffix} duration={duration} group={group} />
        : <span className="tabular">{prefix}{(0).toFixed(decimals)}{suffix}</span>}
    </span>
  );
};
window.CountInView = CountInView;

/* Stat frame — corner ticks + mono eyebrow + giant animated number */
const StatFrame = ({ eyebrow, value, decimals = 0, prefix = '', suffix = '', raw, style }) => (
  <CornerFrame className="stat-frame" style={style}>
    <div className="sf-eyebrow">{eyebrow}</div>
    <div className="sf-value">
      {raw != null
        ? raw
        : <CountInView value={value} decimals={decimals} prefix={prefix} suffix={suffix} />}
    </div>
  </CornerFrame>
);
window.StatFrame = StatFrame;

/* Reveal-on-scroll wrapper (vanilla class toggle) */
const SpReveal = ({ children, delay = 0, style, className = '' }) => {
  const ref = React.useRef(null);
  React.useEffect(() => {
    const el = ref.current; if (!el) return;
    const io = new IntersectionObserver((es) => {
      es.forEach(e => { if (e.isIntersecting) { setTimeout(() => el.classList.add('in'), delay); io.unobserve(el); } });
    }, { threshold: 0.12 });
    io.observe(el);
    // Safety net: if the observer never fires (some embedded webviews / older
    // mobile browsers throttle it) but the element is already on-screen, reveal
    // it so content is never stuck hidden. Below-fold content still waits for the
    // observer to fire on scroll, so the scroll-in animation is preserved.
    const fb = setTimeout(() => {
      if (el.classList.contains('in')) return;
      const r = el.getBoundingClientRect();
      if (r.top < window.innerHeight && r.bottom > 0) el.classList.add('in');
    }, 1400);
    return () => { io.disconnect(); clearTimeout(fb); };
  }, [delay]);
  return <div ref={ref} className={`sp-reveal ${className}`} style={style}>{children}</div>;
};
window.SpReveal = SpReveal;

/* Topographic "coin" — concentric wavy contour lines on a disc, slowly rotating.
   Spade's signature motif (replaces illustrations). */
const TopoCoin = ({ size = 340, color = 'var(--moss)', opacity = 0.5, lines = 26 }) => {
  const id = React.useMemo(() => 'tc' + Math.random().toString(36).slice(2, 7), []);
  const cx = size / 2, cy = size / 2, r = size / 2;
  const paths = [];
  for (let i = 1; i < lines; i++) {
    const y = (i / lines) * size;
    // distance from vertical center drives amplitude (bulge in the middle)
    const d = 1 - Math.abs(y - cy) / cy;            // 0 at edges, 1 at center
    const amp = 6 + d * 26;
    const phase = i * 0.9;
    const seg = 6;
    let dpath = `M 0 ${y.toFixed(1)}`;
    for (let s = 1; s <= seg; s++) {
      const x = (s / seg) * size;
      const yy = y + Math.sin(phase + (s / seg) * Math.PI * 2) * amp * (0.5 + 0.5 * Math.sin(s));
      const xp = ((s - 0.5) / seg) * size;
      const yp = y + Math.sin(phase + ((s - 0.5) / seg) * Math.PI * 2) * amp;
      dpath += ` Q ${xp.toFixed(1)} ${yp.toFixed(1)} ${x.toFixed(1)} ${yy.toFixed(1)}`;
    }
    paths.push(dpath);
  }
  return (
    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{ display: 'block', overflow: 'visible' }}>
      <defs>
        <clipPath id={id}><circle cx={cx} cy={cy} r={r} /></clipPath>
      </defs>
      <g clipPath={`url(#${id})`} style={{ opacity }}>
        {paths.map((d, i) => (
          <path key={i} d={d} fill="none" stroke={color} strokeWidth="1" strokeLinecap="round" />
        ))}
      </g>
    </svg>
  );
};
window.TopoCoin = TopoCoin;

/* Stat band — dark forest panel of corner-tick stat frames with neon figures */
const StatBand = () => {
  const stats = [
    { eyebrow: 'DISCLOSURES CLASSIFIED ACROSS THE CORPUS', value: 2047, raw: null, suffix: '' },
    { eyebrow: 'PRIMARY-LABEL RELIABILITY · FLEISS\u2019 \u03BA', raw: '0.822' },
    { eyebrow: 'FULL FIVE-OF-FIVE RUN AGREEMENT', value: 77.2, decimals: 1, suffix: '%' },
    { eyebrow: 'BINARY AGREEMENT WITH EXPERT CODER', value: 85, suffix: '%' },
  ];
  return (
    <section className="section tight">
      <div className="site-wrap">
        <div className="dark-section panel-round" style={{ padding: '56px 56px 60px' }}>
          <div className="site-wrap" style={{ padding: 0, maxWidth: 'none' }}>
            <div className="mono-eyebrow" style={{ justifyContent: 'center', display: 'flex', marginBottom: 56 }}>
              Independent analysis across global mining sustainability reports
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 0 }}>
              {stats.map((s, i) => (
                <SpReveal key={i} delay={i * 110}>
                  <StatFrame eyebrow={s.eyebrow} value={s.value} decimals={s.decimals} suffix={s.suffix} raw={s.raw} />
                </SpReveal>
              ))}
            </div>
          </div>
        </div>
      </div>
    </section>
  );
};
window.StatBand = StatBand;

/* Glowing cursor-follow border — ported from Aceternity UI's "GlowingEffect",
   reimplemented in plain React + rAF (no framer-motion / Tailwind) and recoloured
   to the Kestrel moss palette. Drop it in as the last child of a `position: relative`
   wrapper; its ::after paints a conic-gradient arc masked to the host's rounded
   border, and the arc rotates to point at the cursor while it's within `proximity`.
   Styling lives in spade.css (.glow-effect). */
const GlowingEffect = ({
  radius = 12,
  spread = 45,
  proximity = 72,
  inactiveZone = 0.01,
  borderWidth = 2,
  movementDuration = 2,
  glow = true,
  disabled = false,
}) => {
  const ref = React.useRef(null);
  const st = React.useRef({ raf: 0, cur: 0, target: 0, lx: 0, ly: 0 });

  React.useEffect(() => {
    const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    const el = ref.current;
    if (!el || disabled || reduce) return;
    const s = st.current;
    // per-frame easing tuned so the arc reaches its target in ~movementDuration seconds at 60fps
    const ease = 1 - Math.pow(0.002, 1 / Math.max(1, movementDuration * 60));

    const tick = () => {
      const diff = ((s.target - s.cur + 540) % 360) - 180; // shortest signed path
      if (Math.abs(diff) < 0.15) {
        s.cur = s.target;
        el.style.setProperty('--ge-start', String(s.cur));
        s.raf = 0;
        return;
      }
      s.cur += diff * ease;
      el.style.setProperty('--ge-start', String(s.cur));
      s.raf = requestAnimationFrame(tick);
    };
    const kick = () => { if (!s.raf) s.raf = requestAnimationFrame(tick); };

    const handle = (e) => {
      const r = el.getBoundingClientRect();
      if (!r.width) return;
      const mx = e ? e.clientX : s.lx;
      const my = e ? e.clientY : s.ly;
      if (e) { s.lx = mx; s.ly = my; }
      const cx = r.left + r.width / 2;
      const cy = r.top + r.height / 2;
      const inactiveR = 0.5 * Math.min(r.width, r.height) * inactiveZone;
      if (Math.hypot(mx - cx, my - cy) < inactiveR) {
        el.style.setProperty('--ge-active', '0');
        return;
      }
      const active = mx > r.left - proximity && mx < r.right + proximity &&
                     my > r.top - proximity && my < r.bottom + proximity;
      el.style.setProperty('--ge-active', active ? '1' : '0');
      if (!active) return;
      s.target = (180 * Math.atan2(my - cy, mx - cx)) / Math.PI + 90;
      kick();
    };

    const onMove = (e) => handle(e);
    const onScroll = () => handle(null);
    document.body.addEventListener('pointermove', onMove, { passive: true });
    window.addEventListener('scroll', onScroll, { passive: true });
    return () => {
      if (s.raf) cancelAnimationFrame(s.raf);
      s.raf = 0;
      document.body.removeEventListener('pointermove', onMove);
      window.removeEventListener('scroll', onScroll);
    };
  }, [disabled, proximity, inactiveZone, borderWidth, movementDuration]);

  return (
    <div
      ref={ref}
      className="glow-effect"
      aria-hidden="true"
      style={{
        '--ge-spread': spread,
        '--ge-border': borderWidth + 'px',
        '--ge-radius': radius + 'px',
        '--ge-start': 0,
        '--ge-active': 0,
        opacity: glow ? 1 : 0,
      }}
    />
  );
};
window.GlowingEffect = GlowingEffect;

/* ---------- Global scroll-reveal: animate each section's content in ---------- */
(function () {
  const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (reduce) return;
  const io = new IntersectionObserver((entries) => {
    entries.forEach((e) => {
      if (e.isIntersecting) { e.target.classList.add('sec-in'); io.unobserve(e.target); }
    });
  }, { threshold: 0.08, rootMargin: '0px 0px -6% 0px' });

  let raf = 0;
  const tag = () => {
    document.querySelectorAll('.section > .site-wrap').forEach((el) => {
      if (!el.dataset.revBound) {
        el.dataset.revBound = '1';
        el.classList.add('sec-reveal');
        io.observe(el);
      }
    });
  };
  const schedule = () => { cancelAnimationFrame(raf); raf = requestAnimationFrame(tag); };

  const start = () => {
    const root = document.getElementById('root');
    if (!root) { setTimeout(start, 80); return; }
    schedule();
    new MutationObserver(schedule).observe(root, { childList: true, subtree: true });
    // safety: never leave content hidden if observer misfires
    setTimeout(() => document.querySelectorAll('.sec-reveal:not(.sec-in)').forEach((el) => {
      const r = el.getBoundingClientRect();
      if (r.top < window.innerHeight) el.classList.add('sec-in');
    }), 1400);
  };
  start();
})();
