/* MY GENIE — thinking-orbs integration (v1)
 *
 * Bridges the MIT-licensed `thinking-orbs` canvas engine (bundled as
 * window.OrbEngine from the npm package) into the no-build mock.
 *
 *   • <Orb state size />            — the library's React-equivalent wrapper:
 *                                     20/64 use the package's own hand-tuned
 *                                     presets (OrbEngine.resolvePreset); any
 *                                     other size resolves through the LARGE
 *                                     preset below.
 *   • <GeniVoiceOrb stateRef size />— voice-mode orb: maps the voice session
 *                                     states onto library states and lets the
 *                                     live mic level pace the animation.
 *
 * The large preset is NOT a CSS upscale: it re-derives {count, size, speed}
 * with the same paired-scaler math the package uses between its tuned sizes
 * (densities scale with (s/64)^1.234, radii with (64/s)^0.75, per-mode speed
 * exponents measured from the package's own 64→32 tuning), then runs the
 * package's own scaleCounts/scaleRadii + painters.
 */

// ---- MIT tables extracted verbatim from thinking-orbs@0.3.2 ------------

const ORB_STATE_TO_MODE = {
  working: 'orbits', searching: 'globe', solving: 'rubik', listening: 'wave',
  connecting: 'web', weaving: 'braid', composing: 'ribbon', breathing: 'ring', shaping: 'morph',
};

const ORB_BASE_PROFILES = {
  globe: { latRings: 17, lonDensity: 44, rBase: 0.6, rDepth: 1.7, rBoost: 1, inkFar: 0.62, inkSpan: 0.54, rsPow: 0.6, rMin: 0.3 },
  orbits: { orbitN: 12, ghostN: 40, ghostR: 0.9, ghostA: 0.5, particles: 3, partR: 1.2, partRDepth: 1.6, rsPow: 0.6, rMin: 0.3 },
  rubik: { latRings: 15, lonDensity: 40, moveCount: 14, rBase: 0.6, rDepth: 1.7, rActive: 0.3, inkFar: 0.62, inkSpan: 0.54, rsPow: 0.6, rMin: 0.3 },
  wave: { rings: 15, lonDensity: 40, rBase: 0.6, rDepth: 1.7, rsPow: 0.6, rMin: 0.3 },
  web: { nodeN: 30, thr: 0.72, signals: 5, nodeR: 1.4, nodeRDepth: 1.8, lineW: 0.8, rsPow: 0.6, rMin: 0.3 },
  braid: { strandN: 52, turns: 3, ghostN: 150, rBase: 1.2, rDepth: 1.8, rsPow: 0.6, rMin: 0.3 },
  ribbon: { lanes: 5, segs: 88, ghostN: 150, rBase: 1.1, rDepth: 1.7, rsPow: 0.6, rMin: 0.3 },
  ring: { lanes: 5, segs: 88, ghostN: 0, faceOn: 1, rBase: 1.1, rDepth: 1.7, rsPow: 0.6, rMin: 0.3 },
  morph: { rDot: 0.021, iconD: 1, rMin: 0.25 },
};

const ORB_PRESETS_64 = {
  orbits: { speed: 1.885, count: 1, size: 1 },
  globe: { speed: 2.015, count: 0.42, size: 1.15, extra: { scanMul: 4.08, dimBase: 0.45 } },
  rubik: { speed: 1.82, count: 0.35, size: 1.05 },
  wave: { speed: 4.388, count: 0.341, size: 1 },
  web: { speed: 3.315, count: 1.35, size: 0.95 },
  braid: { speed: 1.625, count: 0.5, size: 1 },
  ribbon: { speed: 2.34, count: 0.25, size: 0.85, extra: { spin: 0, bandMul: 3.9, wobMul: 1 } },
  ring: { speed: 3.24, count: 0.25, size: 0.956, extra: { spin: 0, bandMul: 3.627, wobMul: 0.368 } },
  morph: { speed: 2.405, count: 0.702, size: 0.395, extra: { spread: 1.45 } },
};

// Per-mode speed exponents, measured from the package's 64→32 tuning
// (exp = ln(speed32/speed64)/ln2) and inverted for sizes above 64.
const ORB_SPEED_EXP = {
  orbits: 0.625, globe: 0.2405, rubik: 0.0594, wave: -0.0801, web: 0.5961,
  braid: 0.4526, ribbon: 0.2473, ring: 0.1326, morph: -0.1249,
};

const orbLargeCache = {};
function resolveOrbLarge(state, size) {
  const key = state + '-' + size;
  if (orbLargeCache[key]) return orbLargeCache[key];
  const mode = ORB_STATE_TO_MODE[state];
  const p64 = ORB_PRESETS_64[mode];
  let opts = Object.assign({}, ORB_BASE_PROFILES[mode]);
  opts = OrbEngine.scaleCounts(opts, Math.pow(size / 64, 1.234));
  opts = OrbEngine.scaleRadii(opts, Math.pow(64 / size, 0.75));
  if (p64.extra) opts = Object.assign({}, opts, p64.extra);
  const resolved = {
    mode: mode,
    speed: p64.speed * Math.pow(64 / size, ORB_SPEED_EXP[mode] != null ? ORB_SPEED_EXP[mode] : 0.625),
    opts: opts,
  };
  orbLargeCache[key] = resolved;
  return resolved;
}

// ---- React wrappers -----------------------------------------------------

function Orb({ state = 'working', size = 64, dark = false, speed = 1, dots = 1, dotSize = 1, levelRef, className, ariaLabel }) {
  const canvasRef = React.useRef(null);
  const stateRef = React.useRef(state);
  stateRef.current = state;

  React.useEffect(function () {
    const canvas = canvasRef.current;
    if (!canvas || !window.OrbEngine) return;
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = Math.round(size * dpr);
    canvas.height = Math.round(size * dpr);
    const ctx = canvas.getContext('2d');
    const tuned = (size === 20 || size === 32 || size === 64);
    let resolved = tuned
      ? window.OrbEngine.resolvePreset(stateRef.current, size)
      : resolveOrbLarge(stateRef.current, size);

    let raf = 0;
    let t = Math.random() * 90;
    let last = performance.now();

    function draw(now) {
      const dt = Math.min(0.05, (now - last) / 1000);
      last = now;
      // Re-resolve when the requested state differs from the resolved one.
      if (resolved.__state !== stateRef.current) {
        resolved = tuned
          ? window.OrbEngine.resolvePreset(stateRef.current, size)
          : resolveOrbLarge(stateRef.current, size);
        resolved.__state = stateRef.current;
      }
      const lvl = levelRef && levelRef.current ? Math.max(0, Math.min(1, levelRef.current.level || 0)) : 0;
      t += dt * resolved.speed * speed * (1 + lvl * 1.15);
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      ctx.clearRect(0, 0, size, size);
      let opts = resolved.opts;
      if (dots !== 1 || dotSize !== 1) {
        opts = Object.assign({}, opts);
        if (dots !== 1) opts = window.OrbEngine.scaleCounts(opts, dots);
        if (dotSize !== 1) opts = window.OrbEngine.scaleRadii(opts, dotSize);
      }
      window.OrbEngine.MODE_DRAWS[resolved.mode](ctx, size, t, dark, opts);
      raf = requestAnimationFrame(draw);
    }
    raf = requestAnimationFrame(draw);
    return function () { cancelAnimationFrame(raf); };
  }, [size, dark, speed, dots, dotSize]);

  return (
    <canvas
      ref={canvasRef}
      className={className}
      style={{ width: size, height: size, display: 'block' }}
      role="img"
      aria-label={ariaLabel || ('Geni orb — ' + state)}
    />
  );
}

// Voice-session state → library state. The mic/speech level (stateRef.level)
// paces the animation so the orb visibly reacts to the user's voice.
const ORB_VOICE_MAP = {
  idle: 'breathing',
  connecting: 'connecting',
  listening: 'weaving',
  thinking: 'searching',
  speaking: 'composing',
  error: 'breathing',
};

function GeniVoiceOrb({ stateRef, size = 224 }) {
  const session = (stateRef && stateRef.current) || {};
  const libState = ORB_VOICE_MAP[session.state] || 'breathing';
  return <Orb state={libState} size={size} dark={true} levelRef={stateRef} className="geni-voice-orb" ariaLabel={'Geni voice — ' + libState} />;
}

Object.assign(window, { Orb, GeniVoiceOrb, resolveOrbLarge });
