// teaser-shared.jsx — Octant Systems teaser: palette, copy, hooks, atoms.
// Reuses OctantCube, Wordmark, shades from cube-color.jsx.
const { useState, useEffect, useRef, useCallback, useLayoutEffect } = React;

/* ---------------- palette ---------------- */
const T = {
  ink:'oklch(0.155 0.012 258)', inkDeep:'oklch(0.118 0.012 262)',
  onyx:'oklch(0.185 0.032 256)', charcoal:'oklch(0.235 0.013 258)',
  paper:'oklch(0.985 0.004 250)',
  slate:'oklch(0.66 0.085 252)', slateLt:'oklch(0.78 0.075 250)',
  slateAcc:'oklch(0.675 0.085 252)', glow:'oklch(0.72 0.11 250)',
  txt:'oklch(0.93 0.006 250)', dim:'oklch(0.76 0.012 252)', faint:'oklch(0.6 0.013 255)',
  line:'oklch(1 0 0 / 0.10)', lineSoft:'oklch(1 0 0 / 0.055)',
  mono:'"Space Mono", monospace', hel:'"Helvetica Neue", Helvetica, Arial, sans-serif',
};
const ACC_DK = shades(0.675, 0.085, 252);   // octant trio on dark
const CUBE_VB = '22 16 76 88';

/* ---------------- copy ----------------
   This object is the FALLBACK copy. The live site overwrites it from
   content.md at boot (see the loader below + teaser-app.jsx). To change
   the words on the site, edit content.md — not this file. */
const DEFAULT_COPY = {
  tagline:'The operating system for spatial intelligence',
  hero:{
    kicker:'STEALTH · 2026',
    line:'We build the world the\u00A0machines have to operate in.',
    sub:'Verifiable worlds — reconstructed from the real one, and run by physics.',
  },
  sections:[
    { n:'01', label:'THE SHIFT',
      head:'The platform stopped predicting the next word.',
      body:'Intelligence turned from the next token to the next state of the physical world. The capital followed — and almost all of it went to imagining worlds. We went the other way.' },
    { n:'02', label:'TWO ROADS',
      head:'One camp dreams worlds. One camp copies them. Neither does both.',
      body:'Generative models hallucinate plausible 3D from a prompt — vivid, controllable, and never your world. Reconstruction rebuilds the real site from sensors — faithful, rigid, impossible to run. Between them sat an empty square.' },
    { n:'03', label:'VERIFIABLE WORLDS',
      head:'A faithful copy of a real place — that physics can run on.',
      body:'Cameras, LiDAR, thermal, radar — every modality fused into one representation. Simulation-ready by construction: real physics runs directly on the voxels. Grounded. Controllable. Editable. Feed-forward in seconds, on a single GPU.' },
    { n:'04', label:'THE LINE',
      head:'Everyone is racing to build the brain.',
      body:'Many are racing to dream up worlds for it. We build the real one it has to operate in.' },
  ],
  props:[
    { k:'FAITHFUL',   v:'a specific real site · from your own data' },
    { k:'MULTIMODAL', v:'images · LiDAR · thermal · radar · fields' },
    { k:'SIM-READY',  v:'real physics, run on the voxels' },
    { k:'ACCESSIBLE', v:'seconds · one GPU · via API' },
  ],
  board:[
    { k:'GENERATIVE', sub:'imagine', note:'never your world', state:'dim' },
    { k:'RECONSTRUCTION', sub:'capture', note:'cannot be run', state:'dim' },
    { k:'VERIFIABLE', sub:'reconstruct + run', note:'the empty square', state:'lit' },
  ],
  closing:'We build the real one\u00A0it has to operate in.',
  status:[ 'STATUS — STEALTH', 'ACCESS — RESTRICTED', '0xOCT · 2026' ],
};

/* ----------------------------------------------------------------
   content.md loader — the editable copy lives in content.md.
   parseContent() turns that markdown into the COPY shape above;
   applyContent() swaps it in at boot. If content.md is missing or
   malformed, DEFAULT_COPY is used, so the page never renders empty.
   ---------------------------------------------------------------- */
let COPY = DEFAULT_COPY;

// Headings that are NOT sections — every other "## Foo" is a section titled "Foo".
const RESERVED_BLOCKS = new Set(['hero', 'properties', 'board', 'closing', 'status']);

function parseContent(md){
  const copy = { tagline:'', hero:{ kicker:'', line:'', sub:'' },
    sections:[], props:[], board:[], closing:'', status:[] };
  if (typeof md !== 'string') return copy;
  md = md.replace(/<!--[\s\S]*?-->/g, '');                 // drop HTML comments
  md = md.replace(/&nbsp;/gi, String.fromCharCode(160)); // &nbsp; -> non-breaking space (keeps words on one line)
  const field = (s) => { const i = s.indexOf(':'); return i < 0 ? null
    : { key:s.slice(0, i).trim().toLowerCase(), val:s.slice(i + 1).trim() }; };
  const cols = (s) => s.replace(/^[-*]\s*/, '').split('|').map((x) => x.trim());
  let block = '', sec = null;
  for (const raw of md.split(/\r?\n/)){
    const t = raw.trim();
    if (!t) continue;
    if (t.startsWith('## ')){                               // new block heading
      if (sec){ copy.sections.push(sec); sec = null; }
      const h = t.slice(3).trim(), key = h.toLowerCase();
      if (RESERVED_BLOCKS.has(key)){ block = key; }         // Hero / Properties / Board / Closing / Status
      else {                                                // any other heading is a section; its text is the title
        block = 'section';
        const title = h.replace(/^section\s+\d+\s*/i, '').trim() || h;  // tolerate legacy "Section NN …"
        sec = { title, label:'', head:'', body:'' };
      }
      continue;
    }
    if (t.startsWith('# ')) continue;                       // page H1 — ignore
    if (block === 'section' && sec){
      const f = field(t); if (!f) continue;
      if (f.key === 'label') sec.label = f.val;
      else if (f.key === 'head') sec.head = f.val;
      else if (f.key === 'body') sec.body = f.val;
    } else if (block === 'hero'){
      const f = field(t); if (!f) continue;
      if (f.key === 'kicker') copy.hero.kicker = f.val;
      else if (f.key === 'headline') copy.hero.line = f.val;
      else if (f.key === 'subhead') copy.hero.sub = f.val;
    } else if (block === 'properties'){
      if (/^[-*]/.test(t)){ const c = cols(t); if (c.length >= 2) copy.props.push({ k:c[0], v:c.slice(1).join(' | ') }); }
    } else if (block === 'board'){
      if (/^[-*]/.test(t)){ const c = cols(t); if (c.length >= 4) copy.board.push({ k:c[0], sub:c[1], note:c[2], state:c[3].toLowerCase() }); }
    } else if (block === 'status'){
      if (/^[-*]/.test(t)) copy.status.push(t.replace(/^[-*]\s*/, ''));
    } else if (block === 'closing'){
      if (!copy.closing) copy.closing = t;
    } else {                                                // preamble (before first ##)
      const f = field(t); if (f && f.key === 'tagline') copy.tagline = f.val;
    }
  }
  if (sec) copy.sections.push(sec);
  return copy;
}

function applyContent(md){
  try {
    const parsed = parseContent(md);
    if (!parsed.hero.line || !parsed.sections.length) throw new Error('content.md missing required fields');
    COPY = parsed;
  } catch (err){
    console.warn('[octant] content.md unusable — falling back to built-in copy.', err);
    COPY = DEFAULT_COPY;
  }
  window.COPY = COPY;
  return COPY;
}

/* ---------------- hooks ---------------- */
// Reveal via a single global polling timer. This environment can pause the
// compositor: IntersectionObserver never fires, rAF stalls, CSS transitions
// freeze, and even scroll events get dropped — but setInterval keeps ticking
// (telemetry proves it). So we poll registered element rects on a timer and
// flip opacity instantly (see .reveal CSS). Bulletproof across embed/capture.
const _revealItems = new Set();
let _revealTimer = 0;
function _pollReveals(){
  const vh = window.innerHeight || 800;
  _revealItems.forEach((it) => {
    if (!it.el || !it.el.isConnected){ _revealItems.delete(it); return; }
    const r = it.el.getBoundingClientRect();
    if (r.top < vh * (1 - it.margin) && r.bottom > 0){ it.cb(); _revealItems.delete(it); }
  });
  if (_revealItems.size === 0 && _revealTimer){ clearInterval(_revealTimer); _revealTimer = 0; }
}
function _registerReveal(el, margin, cb){
  const vh = window.innerHeight || 800;
  const r = el.getBoundingClientRect();
  if (r.top < vh * (1 - margin) && r.bottom > 0){ cb(); return () => {}; }
  const it = { el, margin, cb };
  _revealItems.add(it);
  if (!_revealTimer) _revealTimer = setInterval(_pollReveals, 130);
  return () => { _revealItems.delete(it); };
}
function useReveal(opts){
  const ref = useRef(null);
  const [inView, setInView] = useState(false);
  useEffect(() => {
    const el = ref.current; if (!el) return;
    if (document.body.classList.contains('no-motion')) { setInView(true); return; }
    const margin = (opts && opts.margin != null) ? opts.margin : 0.12;
    let alive = true;
    const unregister = _registerReveal(el, margin, () => { if (alive) setInView(true); });
    return () => { alive = false; unregister(); };
  }, []);
  return [ref, inView];
}

function Reveal({ children, delay = 0, blur = false, as = 'div', className = '', style }){
  const [ref, inView] = useReveal();
  const Tag = as;
  const base = blur ? 'reveal-blur' : 'reveal';
  return <Tag ref={ref} className={base + (inView ? ' in' : '') + (className ? ' ' + className : '')}
    style={{ transitionDelay: delay + 'ms', ...style }}>{children}</Tag>;
}

// writes --sy (scrollY px) and --sp (0..1 progress) on <html>
function useScrollVars(){
  useEffect(() => {
    let raf = 0;
    const tick = () => {
      raf = 0;
      const y = window.scrollY || 0;
      const max = Math.max(1, document.documentElement.scrollHeight - window.innerHeight);
      const root = document.documentElement.style;
      root.setProperty('--sy', y.toFixed(1));
      root.setProperty('--sp', (y / max).toFixed(4));
    };
    const on = () => { if (!raf) raf = requestAnimationFrame(tick); };
    window.addEventListener('scroll', on, { passive: true });
    window.addEventListener('resize', on);
    tick();
    return () => { window.removeEventListener('scroll', on); window.removeEventListener('resize', on); };
  }, []);
}

/* ---------------- live telemetry ---------------- */
let _vox = 2.413e9, _frame = 48217;
function genTel(){
  _vox += Math.round((Math.random() * 9 - 2) * 1e5);
  _frame += Math.round(1 + Math.random() * 3);
  const j = (b, r) => (b + (Math.random() * 2 - 1) * r);
  return {
    lat: j(37.77629, 0.00007).toFixed(5),
    lon: (-122.41703 + (Math.random() * 2 - 1) * 0.00007).toFixed(5),
    alt: j(14.2, 0.06).toFixed(2),
    vox: (_vox / 1e9).toFixed(3),
    recon: j(1.74, 0.16).toFixed(2),
    mod: 7,
    frame: _frame,
  };
}
// One-shot intro gate: true for `ms`, then false. Removing the driving class
// lets the logo rest in its static visible base state — so a paused compositor
// can never freeze it mid-build (hidden). Off entirely when motion is off.
function useIntro(active, ms){
  const [on, setOn] = useState(!!active);
  useEffect(() => {
    if (!active) { setOn(false); return; }
    setOn(true);
    const t = setTimeout(() => setOn(false), ms || 3200);
    return () => clearTimeout(t);
  }, [active]);
  return on;
}

function useTelemetry(active){
  const [tel, setTel] = useState(genTel);
  useEffect(() => {
    if (!active) return;
    const id = setInterval(() => setTel(genTel()), 780);
    return () => clearInterval(id);
  }, [active]);
  return tel;
}

/* ---------------- atoms ---------------- */
function Kicker({ children, color = T.slateAcc, style }){
  return <span style={{ fontFamily: T.mono, textTransform: 'uppercase', letterSpacing: '0.24em',
    fontSize: 11, color, ...style }}>{children}</span>;
}

function Read({ k, v, kcolor = T.faint, vcolor = T.dim, size = 11, gap = 7, style }){
  return <span style={{ fontFamily: T.mono, fontSize: size, letterSpacing: '0.1em',
    display: 'inline-flex', gap, whiteSpace: 'nowrap', ...style }}>
    <span style={{ color: kcolor }}>{k}</span>
    <span style={{ color: vcolor, fontVariantNumeric: 'tabular-nums' }}>{v}</span>
  </span>;
}

// cube + Sora wordmark, static (no animation). Wordmark stays the only Sora use.
function StaticLockup({ h = 30, scale, tagline = false, gap }){
  const cw = Math.round(h * 76 / 88);
  const s = scale != null ? scale : h / 30;
  return <div style={{ display: 'inline-flex', alignItems: 'center', gap: gap != null ? gap : Math.round(h * 0.36) }}>
    <OctantCube vb={CUBE_VB} w={cw} h={h} line="oklch(1 0 0/0.95)" t={ACC_DK.t} r={ACC_DK.r} l={ACC_DK.l} />
    <Wordmark scale={s} color={T.txt} sub={tagline} />
  </div>;
}

// drifting starfield on a canvas; renders one static frame when inactive.
function ParticleField({ active = true, density = 78, color = 'oklch(0.82 0.05 250)', style }){
  const ref = useRef(null);
  useEffect(() => {
    const cv = ref.current; if (!cv) return;
    const ctx = cv.getContext('2d');
    let w = 0, h = 0, dpr = 1, raf = 0, pts = [], running = true;
    const init = () => {
      dpr = Math.min(2, window.devicePixelRatio || 1);
      w = cv.clientWidth; h = cv.clientHeight;
      cv.width = Math.max(1, Math.round(w * dpr)); cv.height = Math.max(1, Math.round(h * dpr));
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      pts = Array.from({ length: density }, () => ({
        x: Math.random() * w, y: Math.random() * h, z: Math.random(),
        vx: (Math.random() * 2 - 1) * 0.06, vy: (Math.random() * 2 - 1) * 0.04,
        ph: Math.random() * Math.PI * 2,
      }));
    };
    const frame = (t) => {
      ctx.clearRect(0, 0, w, h);
      for (const p of pts){
        if (active){
          p.x += p.vx * (0.45 + p.z); p.y += p.vy * (0.45 + p.z);
          if (p.x < 0) p.x += w; if (p.x > w) p.x -= w;
          if (p.y < 0) p.y += h; if (p.y > h) p.y -= h;
        }
        const tw = active ? (0.6 + 0.4 * Math.sin(t * 0.0011 + p.ph)) : 1;
        const r = 0.4 + p.z * 1.5;
        ctx.globalAlpha = (0.1 + p.z * 0.55) * tw;
        ctx.fillStyle = color;
        ctx.beginPath(); ctx.arc(p.x, p.y, r, 0, 6.2832); ctx.fill();
      }
      ctx.globalAlpha = 1;
      if (running && active) raf = requestAnimationFrame(frame);
    };
    init(); frame(0);
    const onR = () => { init(); frame(0); };
    window.addEventListener('resize', onR);
    return () => { running = false; cancelAnimationFrame(raf); window.removeEventListener('resize', onR); };
  }, [active, density, color]);
  return <canvas ref={ref} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%',
    display: 'block', ...style }} />;
}

// scroll hint — a thin line that drifts down, with a mono label.
function ScrollCue({ label = 'SCROLL', color = T.faint }){
  return <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
    <span style={{ fontFamily: T.mono, fontSize: 10, letterSpacing: '0.3em', color,
      textTransform: 'uppercase' }}>{label}</span>
    <span className="cue-line" style={{ width: 1, height: 46, background:
      'linear-gradient(' + color + ', transparent)', position: 'relative', overflow: 'hidden' }}>
      <span className="cue-dot" style={{ position: 'absolute', left: -1.5, top: 0, width: 4, height: 4,
        borderRadius: '50%', background: T.slateAcc }} />
    </span>
  </div>;
}

Object.assign(window, {
  T, ACC_DK, CUBE_VB, COPY, DEFAULT_COPY, parseContent, applyContent,
  useReveal, Reveal, useScrollVars, useIntro, useTelemetry, genTel,
  Kicker, Read, StaticLockup, ParticleField, ScrollCue,
});
