// motion-engine.jsx — Timeline-driven motion primitives for Charter to Code
// Exposes hooks + components on window. Pair with motion-canvas.html.
//
// Concepts:
//   <Timeline duration={ms}> wraps a scene and exposes a current `t` (ms) via context.
//   useT() returns current ms.
//   <Track> children render based on the current t.
//   <Show from to>...</Show> shows children only between t=from and t=to (with fade).
//   <Typewriter text from durationMs caretAfter> types char-by-char.
//   <DrawOn from durationMs> animates SVG path strokeDashoffset → 0.
//   <FadeIn from to>...</FadeIn>, <FadeOut from to>...</FadeOut>.
//   <SceneBg from to bg>...</SceneBg> for whole-scene background swaps (cream ↔ terminal).

const TimelineCtx = React.createContext({ t: 0, duration: 0 });

function useT() {
  return React.useContext(TimelineCtx).t;
}
function useTimeline() {
  return React.useContext(TimelineCtx);
}

// ── Timeline host ─────────────────────────────────────────────
// Owns playback state, exposes scrubber + play/pause via render-prop.
// width/height are the design size of the scene (1920×1080).
function Timeline({ duration, children, controls = true, autoplay = true, loop = true, label, sceneId }) {
  const [t, setT] = React.useState(0);
  const [playing, setPlaying] = React.useState(autoplay);
  const [doLoop, setLoop] = React.useState(loop);
  const lastFrame = React.useRef(0);
  const tRef = React.useRef(0);
  const playingRef = React.useRef(playing);
  const loopRef = React.useRef(doLoop);

  React.useEffect(() => { playingRef.current = playing; }, [playing]);
  React.useEffect(() => { loopRef.current = doLoop; }, [doLoop]);
  React.useEffect(() => { tRef.current = t; }, [t]);

  React.useEffect(() => {
    let raf;
    function tick(now) {
      if (!lastFrame.current) lastFrame.current = now;
      const dt = now - lastFrame.current;
      lastFrame.current = now;
      if (playingRef.current) {
        let next = tRef.current + dt;
        if (next >= duration) {
          if (loopRef.current) next = next % duration;
          else { next = duration; setPlaying(false); }
        }
        tRef.current = next;
        setT(next);
      }
      raf = requestAnimationFrame(tick);
    }
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [duration]);

  const scrub = (ms) => {
    tRef.current = ms;
    setT(ms);
  };

  const ctx = React.useMemo(() => ({ t, duration }), [t, duration]);

  return (
    <TimelineCtx.Provider value={ctx}>
      <div className="motion-scene" data-scene={sceneId}>
        <div className="motion-stage">
          {children}
        </div>
      </div>
      {controls && (
        <div className="motion-controls">
          <button
            className="mc-btn"
            onClick={() => {
              if (!playing && t >= duration) scrub(0);
              setPlaying(!playing);
            }}
          >
            {playing ? '❚❚' : '▶'}
          </button>
          <input
            type="range"
            min="0" max={duration} step="10"
            value={t}
            onChange={(e) => { setPlaying(false); scrub(parseInt(e.target.value, 10)); }}
            className="mc-scrub"
          />
          <span className="mc-time">{(t/1000).toFixed(1)}s</span>
          <span className="mc-dur">/ {(duration/1000).toFixed(1)}s</span>
          <button
            className="mc-btn mc-loop"
            onClick={() => setLoop(!doLoop)}
            data-on={doLoop}
            title="Loop"
          >↻</button>
        </div>
      )}
    </TimelineCtx.Provider>
  );
}

// ── TimeShift: re-provide TimelineCtx with t offset by `offset` ms.
// Children rendered inside see (t - offset) — useful for prepending an
// intro overlay without rewriting every `from=` in a scene.
function TimeShift({ offset = 0, children }) {
  const ctx = React.useContext(TimelineCtx);
  const value = React.useMemo(
    () => ({ t: Math.max(0, (ctx?.t || 0) - offset), duration: (ctx?.duration || 0) - offset }),
    [ctx, offset]
  );
  return <TimelineCtx.Provider value={value}>{children}</TimelineCtx.Provider>;
}

// ── Show: render only between [from, to] with optional fade in/out ─
function Show({ from = 0, to = Infinity, fadeIn = 200, fadeOut = 200, children, style }) {
  const t = useT();
  if (t < from - fadeIn || t > to + fadeOut) return null;
  let opacity = 1;
  if (t < from) opacity = 1 - (from - t) / fadeIn;
  else if (t > to) opacity = 1 - (t - to) / fadeOut;
  opacity = Math.max(0, Math.min(1, opacity));
  return (
    <div style={{ opacity, transition: 'none', ...style }}>
      {children}
    </div>
  );
}

// ── FadeIn / FadeOut helpers ─────────────────────────────────
function FadeIn({ from, duration = 400, children, style, translateY = 0 }) {
  const t = useT();
  const p = Math.max(0, Math.min(1, (t - from) / duration));
  const eased = 1 - Math.pow(1 - p, 2);
  return (
    <div style={{
      opacity: eased,
      transform: translateY ? `translateY(${(1 - eased) * translateY}px)` : undefined,
      ...style,
    }}>
      {children}
    </div>
  );
}

// ── Typewriter ───────────────────────────────────────────────
// Types `text` over `duration` starting at `from`. Optional caret.
function Typewriter({ text, from, duration, caret = true, blinkAfter = true, style, className, ease = 1.2 }) {
  const t = useT();
  const into = t - from;
  let chars = 0;
  let progress = 0;
  if (into >= 0) {
    progress = Math.max(0, Math.min(1, into / duration));
    const eased = 1 - Math.pow(1 - progress, ease);
    chars = Math.floor(eased * text.length);
  }
  const done = into >= duration;
  return (
    <span className={className} style={style}>
      {text.slice(0, chars)}
      {caret && into >= 0 && (
        <span
          className={`tw-caret ${done && blinkAfter ? 'tw-caret--blink' : ''}`}
          style={{
            display: 'inline-block',
            width: '0.55em',
            height: '1em',
            verticalAlign: '-0.12em',
            background: 'currentColor',
            marginLeft: '0.08em',
          }}
        />
      )}
    </span>
  );
}

// ── DrawOn (SVG path stroke draw-on) ─────────────────────────
// Wrap any <path> / <line> / <circle> by passing a ref or by using
// the styled <DrawPath> below. Simpler: use <DrawPath d=... from=... duration=...>
function DrawPath({ d, from, duration, stroke = 'currentColor', strokeWidth = 2, fill = 'none', style, ...rest }) {
  const t = useT();
  const ref = React.useRef(null);
  const [len, setLen] = React.useState(1000);

  React.useEffect(() => {
    if (ref.current && ref.current.getTotalLength) {
      setLen(ref.current.getTotalLength());
    }
  }, [d]);

  const into = t - from;
  const p = Math.max(0, Math.min(1, into / duration));
  const eased = 1 - Math.pow(1 - p, 2);
  const offset = len * (1 - eased);

  return (
    <path
      ref={ref}
      d={d}
      stroke={stroke}
      strokeWidth={strokeWidth}
      fill={fill}
      strokeDasharray={len}
      strokeDashoffset={offset}
      strokeLinecap="round"
      strokeLinejoin="round"
      style={style}
      {...rest}
    />
  );
}

// Generic ref-based draw-on for any SVG element with getTotalLength
function useDraw(ref, from, duration) {
  const t = useT();
  const [len, setLen] = React.useState(1);
  React.useEffect(() => {
    if (ref.current && ref.current.getTotalLength) {
      setLen(ref.current.getTotalLength() || 1);
    }
  });
  const into = t - from;
  const p = Math.max(0, Math.min(1, into / duration));
  const eased = 1 - Math.pow(1 - p, 2);
  return { dasharray: len, dashoffset: len * (1 - eased), progress: eased };
}

// ── Pulse helper (orange pulse on a node) ────────────────────
function Pulse({ from, duration = 800, children, style }) {
  const t = useT();
  const into = t - from;
  let s = 1;
  let glow = 0;
  if (into >= 0 && into <= duration) {
    const p = into / duration;
    s = 1 + Math.sin(p * Math.PI) * 0.06;
    glow = Math.sin(p * Math.PI);
  }
  return (
    <div style={{
      transform: `scale(${s})`,
      filter: glow ? `drop-shadow(0 0 ${20 * glow}px rgba(212,107,62,${0.45 * glow}))` : undefined,
      ...style,
    }}>
      {children}
    </div>
  );
}

// ── Lerp / interp helper exposed to scenes ───────────────────
function lerp(t, from, fromV, to, toV, ease = (x) => x) {
  if (t <= from) return fromV;
  if (t >= to) return toV;
  const p = (t - from) / (to - from);
  return fromV + (toV - fromV) * ease(p);
}
const easeOut = (x) => 1 - Math.pow(1 - x, 2);
const easeInOut = (x) => x < 0.5 ? 2*x*x : 1 - Math.pow(-2*x + 2, 2) / 2;

// ── Caption (typed pill at bottom of scene) ──────────────────
// `text`, `from`, `typeDur`, `holdDur` (after type completes), `fadeOut` ms.
// Auto-handles backgrounding the underlying scene via context callback.
function Caption({ text, from, typeDur = 1500, hold = 2200, fadeOut = 400, terminal = false, onActiveChange }) {
  const t = useT();
  const start = from;
  const typeEnd = from + typeDur;
  const holdEnd = typeEnd + hold;
  const fadeEnd = holdEnd + fadeOut;

  const visible = t >= start - 200 && t <= fadeEnd;
  const active = t >= start && t <= holdEnd;

  // Notify parent for scene dim
  React.useEffect(() => {
    if (onActiveChange) onActiveChange(active);
  }, [active, onActiveChange]);

  if (!visible) return null;

  let opacity = 1;
  let ty = 0;
  if (t < start) {
    const p = (t - (start - 200)) / 200;
    opacity = p; ty = (1 - p) * 8;
  } else if (t > holdEnd) {
    const p = Math.max(0, Math.min(1, (t - holdEnd) / fadeOut));
    opacity = 1 - p; ty = -p * 6;
  }

  return (
    <div
      className={`mt-caption ${terminal ? 'mt-caption--terminal' : ''}`}
      style={{ opacity, transform: `translateX(-50%) translateY(${ty}px)` }}
    >
      <Typewriter
        text={text}
        from={start}
        duration={typeDur}
        caret
        blinkAfter
      />
    </div>
  );
}

// Caption controller: auto-backgrounds the scene while caption active.
// Use <CaptionScene> as wrapper: tracks one caption-at-a-time and applies
// .is-backgrounded to the .scene-bg child.
function CaptionScene({ children }) {
  const [active, setActive] = React.useState(false);
  // Provide setter via context so child Captions can register
  return (
    <CaptionActiveCtx.Provider value={setActive}>
      <div className={`caption-scene ${active ? 'is-caption-active' : ''}`}>
        {children}
      </div>
    </CaptionActiveCtx.Provider>
  );
}
const CaptionActiveCtx = React.createContext(() => {});

// Smart caption: hooks into CaptionScene to dim background automatically
function SceneCaption(props) {
  const setActive = React.useContext(CaptionActiveCtx);
  return <Caption {...props} onActiveChange={setActive} />;
}

// Expose to global
Object.assign(window, {
  TimelineCtx, Timeline, useT, useTimeline,
  Show, FadeIn, TimeShift,
  Typewriter, DrawPath, useDraw,
  Pulse, lerp, easeOut, easeInOut,
  Caption, CaptionScene, SceneCaption,
});
