/* WeVibe — live-coding terminal landing page
   Imperative typing engine (DOM refs, no per-char React re-render)
   + React only for the Tweaks panel + theme/handle plumbing.            */

const { useRef, useEffect } = React;

/* ------------------------------------------------------------------ *
 * 1. Tiny TS/JS syntax tokenizer
 *    Returns [{ t: substring, c: className }] preserving every char.
 * ------------------------------------------------------------------ */
const KEYWORDS = new Set([
  'import', 'from', 'export', 'default', 'class', 'extends', 'implements',
  'interface', 'async', 'await', 'return', 'new', 'const', 'let', 'var',
  'private', 'public', 'protected', 'readonly', 'this', 'void', 'true',
  'false', 'null', 'undefined', 'if', 'else', 'for', 'of', 'in', 'type',
  'static', 'get', 'set',
]);

function tokenizeLine(code) {
  const out = [];
  let i = 0;
  const n = code.length;
  const push = (t, c) => out.push({ t, c });

  while (i < n) {
    const ch = code[i];

    // line comment
    if (ch === '/' && code[i + 1] === '/') {
      push(code.slice(i), 'tok-comment');
      break;
    }
    // string (", ', `)
    if (ch === '"' || ch === "'" || ch === '`') {
      let j = i + 1;
      while (j < n && code[j] !== ch) {
        if (code[j] === '\\') j++;
        j++;
      }
      j = Math.min(j + 1, n);
      push(code.slice(i, j), 'tok-str');
      i = j;
      continue;
    }
    // whitespace
    if (/\s/.test(ch)) {
      let j = i;
      while (j < n && /\s/.test(code[j])) j++;
      push(code.slice(i, j), 'tok-ws');
      i = j;
      continue;
    }
    // number
    if (/[0-9]/.test(ch)) {
      let j = i;
      while (j < n && /[0-9_.]/.test(code[j])) j++;
      push(code.slice(i, j), 'tok-num');
      i = j;
      continue;
    }
    // identifier / keyword / type / function / property
    if (/[A-Za-z_$]/.test(ch)) {
      let j = i;
      while (j < n && /[A-Za-z0-9_$]/.test(code[j])) j++;
      const word = code.slice(i, j);
      // look ahead for ( and back for .
      let k = j;
      while (k < n && code[k] === ' ') k++;
      const isCall = code[k] === '(';
      let p = i - 1;
      while (p >= 0 && code[p] === ' ') p--;
      const afterDot = code[p] === '.';
      const beforeColon = code[k] === ':';

      let cls;
      if (KEYWORDS.has(word)) cls = 'tok-kw';
      else if (/^[A-Z]/.test(word)) cls = 'tok-type';
      else if (isCall) cls = 'tok-func';
      else if (afterDot) cls = 'tok-prop';
      else if (beforeColon) cls = 'tok-prop';
      else cls = 'tok-var';
      push(word, cls);
      i = j;
      continue;
    }
    // punctuation (single char)
    push(ch, 'tok-punct');
    i++;
  }
  return out;
}

const esc = (s) =>
  s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');

/* render the first `count` chars of a token list to HTML */
function renderTokens(tokens, count) {
  let html = '';
  let left = count;
  for (const tk of tokens) {
    if (left <= 0) break;
    const slice = tk.t.slice(0, left);
    html += `<span class="${tk.c}">${esc(slice)}</span>`;
    left -= slice.length;
  }
  return html;
}

/* ------------------------------------------------------------------ *
 * 2. The session script — the meat. Terms embedded as real code.
 *    kinds: prompt | build | ok | code | out | blank | cta
 * ------------------------------------------------------------------ */
const SCRIPT = [
  { k: 'prompt', cmd: 'wevibe init' },
  { k: 'ok', text: 'one introverted dev. no friends. many late nights.' },
  { k: 'blank' },
  { k: 'code', text: '// the most fair launch of the year' },
  { k: 'code', text: 'const vcs = 0, foundation = 0, insiders = 0' },
  { k: 'code', text: 'const privateSales = none   // of any kind' },
  { k: 'blank' },
  { k: 'prompt', cmd: 'wevibe deploy' },
  { k: 'ok', text: 'a real social layer for vibe coders who crave it' },
  { k: 'blank' },
  { k: 'cta' },
];

/* ------------------------------------------------------------------ *
 * 3. Engine
 * ------------------------------------------------------------------ */
const SPEEDS = { chill: 1.7, normal: 1, fast: 0.5 };
const rand = (a, b) => a + Math.random() * (b - a);

function makeEngine(root, body, refs) {
  let cancelled = false;
  const delay = (ms) => new Promise((r) => setTimeout(r, ms / 1)); // speed applied per-char
  const sp = () => SPEEDS[refs.speed.current] || 1;

  const scroll = () => { body.scrollTop = body.scrollHeight; };

  function newLine(cls) {
    const el = document.createElement('div');
    el.className = 'line ' + (cls || '');
    body.appendChild(el);
    scroll();
    return el;
  }

  const cursorHTML = '<span class="cursor"></span>';

  // per-character human-ish delay
  function charDelay(ch) {
    let d = rand(7, 18) * sp();
    if (ch === ' ') d *= 1.2;
    if ('{}()[];,'.includes(ch)) d *= 1.3;
    if (Math.random() < 0.02) d += rand(40, 110) * sp(); // micro think-pause
    return d;
  }

  async function typePlain(el, text, classed) {
    // classed: optional fn(charsSoFar)->html ; else plain text
    for (let i = 1; i <= text.length; i++) {
      if (cancelled) return;
      el.innerHTML = (classed ? classed(i) : esc(text.slice(0, i))) + cursorHTML;
      scroll();
      await delay(charDelay(text[i - 1]));
    }
    el.innerHTML = classed ? classed(text.length) : esc(text);
  }

  async function typeCode(step) {
    const el = newLine('code');
    const text = step.text;
    const tokens = tokenizeLine(text);
    const render = (count) => renderTokens(tokens, count);

    if (step.typo) {
      // type up to typo point, insert wrong chars, pause, backspace, continue
      const { at, wrong } = step.typo;
      // type correct chars [0, at)
      for (let i = 1; i <= at; i++) {
        if (cancelled) return;
        el.innerHTML = render(i) + cursorHTML;
        scroll(); await delay(charDelay(text[i - 1]));
      }
      // type wrong chars (rendered as plain prefix + wrong, retokenized loosely)
      let shown = text.slice(0, at);
      for (let w = 0; w < wrong.length; w++) {
        if (cancelled) return;
        shown += wrong[w];
        el.innerHTML = renderTokens(tokenizeLine(shown), shown.length) + cursorHTML;
        scroll(); await delay(charDelay(wrong[w]));
      }
      await delay(rand(260, 460) * sp()); // notice the mistake
      // backspace the wrong chars
      for (let w = wrong.length; w > 0; w--) {
        if (cancelled) return;
        const cur = text.slice(0, at) + wrong.slice(0, w - 1);
        el.innerHTML = renderTokens(tokenizeLine(cur), cur.length) + cursorHTML;
        scroll(); await delay(rand(40, 80) * sp());
      }
      // type the rest correctly
      for (let i = at + 1; i <= text.length; i++) {
        if (cancelled) return;
        el.innerHTML = render(i) + cursorHTML;
        scroll(); await delay(charDelay(text[i - 1]));
      }
      el.innerHTML = render(text.length);
      return;
    }

    await typePlain(el, text, render);
  }

  async function typePrompt(step) {
    const el = newLine('prompt');
    const prefix =
      '<span class="ps-user">vibe@wevibe</span>' +
      '<span class="ps-path"> ~ </span>' +
      '<span class="ps-sym">%</span> ';
    el.innerHTML = prefix + cursorHTML;
    await delay(rand(120, 260) * sp());
    const cmd = step.cmd;
    for (let i = 1; i <= cmd.length; i++) {
      if (cancelled) return;
      el.innerHTML = prefix + '<span class="cmd">' + esc(cmd.slice(0, i)) + '</span>' + cursorHTML;
      scroll(); await delay(charDelay(cmd[i - 1]));
    }
    el.innerHTML = prefix + '<span class="cmd">' + esc(cmd) + '</span>';
    await delay(rand(120, 240) * sp()); // press enter beat
  }

  async function runBuild(step) {
    const el = newLine('sys');
    const base = esc(step.text);
    el.innerHTML = base;
    scroll();
    // animate dots
    for (let round = 0; round < 6; round++) {
      if (cancelled) return;
      const dots = '.'.repeat((round % 3) + 1);
      el.innerHTML = base + '<span class="dim">' + dots + '</span>';
      await delay(220 * sp());
    }
    el.innerHTML = base + ' <span class="check">✓</span>';
    await delay(160 * sp());
  }

  async function runOut(step) {
    const el = newLine('out');
    await delay(rand(120, 300) * sp());
    const arrow = '<span class="arrow">→</span> ';
    el.innerHTML = arrow + esc(step.text) + ' <span class="dim">…</span>';
    scroll();
    await delay(rand(280, 620) * sp());
    let tail = '';
    if (step.tail) tail = ' <span class="tail">' + esc(step.tail) + '</span>';
    el.innerHTML = arrow + esc(step.text) + tail;
    await delay(120 * sp());
  }

  async function runOk(step) {
    const el = newLine('okline');
    await delay(rand(70, 160) * sp());
    el.innerHTML = '<span class="check">✓</span> <span class="ok-text">' + esc(step.text) + '</span>';
    scroll();
    await delay(130 * sp());
  }

  async function runCta() {
    await delay(180 * sp());
    // one last command, then the button materializes
    await typePrompt({ cmd: 'wevibe join --us' });
    await delay(160 * sp());
    const el = newLine('cta');
    const handle = refs.handle.current || 'WeVibe_Network';
    el.innerHTML =
      '<a class="join-btn" href="https://x.com/' + encodeURIComponent(handle) +
      '">' +
      '<span class="join-bracket">[</span>' +
      '<span class="join-label">Join Us</span>' +
      '<span class="join-arrow">→</span>' +
      '<span class="join-bracket">]</span>' +
      '</a>';
    refs.ctaEl.current = el.querySelector('.join-btn');
    requestAnimationFrame(() => el.querySelector('.join-btn').classList.add('lit'));
    scroll();
    // trailing live prompt with blinking cursor
    await delay(240 * sp());
    const tail = newLine('prompt tail-prompt');
    tail.innerHTML =
      '<span class="ps-user">vibe@wevibe</span><span class="ps-path"> ~ </span>' +
      '<span class="ps-sym">%</span> ' + cursorHTML;
    refs.done.current = true;
    scroll();
  }

  async function run() {
    for (const step of SCRIPT) {
      if (cancelled) return;
      switch (step.k) {
        case 'prompt': await typePrompt(step); break;
        case 'code':   await typeCode(step); break;
        case 'build':  await runBuild(step); break;
        case 'ok':     await runOk(step); break;
        case 'out':    await runOut(step); break;
        case 'cta':    await runCta(step); break;
        case 'blank':  newLine('blank').innerHTML = '&nbsp;'; await delay(60 * sp()); break;
        default: break;
      }
    }
  }

  run();
  return { cancel() { cancelled = true; } };
}

/* ------------------------------------------------------------------ *
 * 4. React app — terminal host + Tweaks
 * ------------------------------------------------------------------ */
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": "green",
  "speed": "normal",
  "handle": "WeVibe_Network",
  "scanlines": true
}/*EDITMODE-END*/;

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const rootRef = useRef(null);
  const bodyRef = useRef(null);
  const engineRef = useRef(null);
  const speedRef = useRef(t.speed);
  const handleRef = useRef(t.handle);
  const ctaEl = useRef(null);
  const doneRef = useRef(false);

  speedRef.current = t.speed;
  handleRef.current = t.handle;

  const start = () => {
    if (engineRef.current) engineRef.current.cancel();
    bodyRef.current.innerHTML = '';
    doneRef.current = false;
    engineRef.current = makeEngine(rootRef.current, bodyRef.current, {
      speed: speedRef, handle: handleRef, ctaEl, done: doneRef,
    });
  };

  // boot once
  useEffect(() => {
    start();
    const onKey = (e) => {
      if (e.key === 'Enter' && doneRef.current) start();
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
    // eslint-disable-next-line
  }, []);

  // live-update the CTA link if handle changes after render
  useEffect(() => {
    if (ctaEl.current) {
      ctaEl.current.href = 'https://x.com/' + encodeURIComponent(t.handle);
    }
  }, [t.handle]);

  return (
    <div
      className="screen"
      data-theme={t.theme}
      data-scanlines={t.scanlines ? 'on' : 'off'}
      ref={rootRef}
      onClick={(e) => {
        if (doneRef.current && !e.target.closest('a, button, .twk-panel, [class*="twk"]')) start();
      }}
    >
      <div className="vignette" />
      <div className="stage">
        <div className="term-body" ref={bodyRef} />
      </div>

      <TweaksPanel>
        <TweakSection label="Terminal" />
        <TweakRadio
          label="Theme" value={t.theme}
          options={['green', 'amber', 'midnight']}
          onChange={(v) => setTweak('theme', v)}
        />
        <TweakRadio
          label="Typing speed" value={t.speed}
          options={['chill', 'normal', 'fast']}
          onChange={(v) => setTweak('speed', v)}
        />
        <TweakToggle
          label="Scanlines" value={t.scanlines}
          onChange={(v) => setTweak('scanlines', v)}
        />
        <TweakSection label="Call to action" />
        <TweakText
          label="X handle" value={t.handle}
          onChange={(v) => setTweak('handle', v.replace(/^@/, ''))}
        />
        <TweakButton label="↻  Run it back" onClick={start} />
      </TweaksPanel>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
