// ─────────────────────────────────────────────────────────────────
// Motion system — spring physics, stagger, cadence, counters
// Everything here is DETERMINISTIC as a function of time (t seconds
// since the Sprite's local start). That means scrubbing backward
// works correctly — no state mutation, no RAF loops.
// ─────────────────────────────────────────────────────────────────

// Critically-damped spring as a function of time.
// Returns position given: from, to, stiffness (k), damping (c), mass (m), t.
// Using analytic solution for underdamped/critically-damped linear spring.
// Equilibrium at `to`, starting at `from` with zero velocity.
function springValue(from, to, t, { stiffness = 170, damping = 26, mass = 1 } = {}) {
  if (t <= 0) return from;
  const w0 = Math.sqrt(stiffness / mass);
  const zeta = damping / (2 * Math.sqrt(stiffness * mass));
  const dx = from - to;
  let x;
  if (zeta < 1) {
    // underdamped
    const wd = w0 * Math.sqrt(1 - zeta * zeta);
    x = Math.exp(-zeta * w0 * t) * (dx * Math.cos(wd * t) + (zeta * w0 * dx / wd) * Math.sin(wd * t));
  } else if (zeta === 1) {
    // critically damped
    x = Math.exp(-w0 * t) * (dx + (w0 * dx) * t);
  } else {
    // overdamped
    const r1 = -w0 * (zeta - Math.sqrt(zeta * zeta - 1));
    const r2 = -w0 * (zeta + Math.sqrt(zeta * zeta - 1));
    const c1 = (dx * r2) / (r2 - r1);
    const c2 = dx - c1;
    x = c1 * Math.exp(r1 * t) + c2 * Math.exp(r2 * t);
  }
  // Settle threshold — snap to target once imperceptible
  if (Math.abs(x) < 0.0001 && t > 2) return to;
  return to + x;
}

// Preset spring configs
const Springs = {
  gentle:  { stiffness: 110, damping: 24, mass: 1 },     // soft, natural
  snappy:  { stiffness: 220, damping: 22, mass: 1 },     // UI click response
  bouncy:  { stiffness: 280, damping: 14, mass: 1 },     // playful
  stiff:   { stiffness: 400, damping: 40, mass: 1 },     // instant-feeling
  wobbly:  { stiffness: 180, damping: 12, mass: 1 },     // overshoot a lot
};

// Animate a single number from->to starting at `startT`, with spring.
function spring(t, from, to, startT = 0, preset = Springs.gentle) {
  return springValue(from, to, Math.max(0, t - startT), preset);
}

// Animate opacity/scale in (0→1). Uses a snappy spring internally.
function springIn(t, startT = 0, preset = Springs.snappy) {
  return clampN(spring(t, 0, 1, startT, preset), 0, 1.2);
}

// Staggered spring — i is the index, delay is seconds between items.
function stagger(t, i, delay = 0.06, startT = 0, preset = Springs.snappy) {
  return springIn(t, startT + i * delay, preset);
}

function clampN(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function lerp(a, b, t) { return a + (b - a) * t; }

// Smooth count-up with spring — returns { display, digits } where `display`
// is the animated value and can be formatted for you.
function countUp(t, from, to, startT = 0, preset = { stiffness: 80, damping: 26, mass: 1 }) {
  const v = spring(t, from, to, startT, preset);
  return v;
}

// Variable-cadence typewriter.
// Returns the substring of `text` typed so far, given t, startT, and a base
// chars-per-second rate. Adds pauses at punctuation and slight speedups on
// common bigrams, slow down on rare letters.
function typewrite(text, t, startT = 0, cps = 40) {
  const elapsed = t - startT;
  if (elapsed <= 0) return '';
  // Build per-char delay array deterministically
  let cursor = 0;
  const baseDelay = 1 / cps;
  const punctPause = { ',': 0.18, '.': 0.32, '?': 0.32, '!': 0.32, ':': 0.18, ';': 0.18, '\n': 0.28 };
  for (let i = 0; i < text.length; i++) {
    const ch = text[i];
    let d = baseDelay;
    // Speed variance: spaces a hair slower (finger lift), repeats faster
    if (ch === ' ') d *= 1.1;
    else if (i > 0 && text[i - 1] === ch) d *= 0.7;
    // Apply jitter deterministically via char code
    const jitter = 0.85 + 0.3 * ((text.charCodeAt(i) * 9301 + 49297) % 233) / 233;
    d *= jitter;
    cursor += d;
    if (cursor > elapsed) return text.slice(0, i);
    // Add punctuation pause AFTER emitting the char
    if (punctPause[ch]) cursor += punctPause[ch];
  }
  return text;
}

// ─────────────────────────────────────────────────────────────────
// FLIP — animate between two rects (old → new layout).
// Usage: pass the element's old & new bounding rects + progress (0..1) and
// apply the returned transform to the element.
// ─────────────────────────────────────────────────────────────────
function flipTransform(oldRect, newRect, p) {
  if (!oldRect || !newRect) return '';
  const dx = (oldRect.x - newRect.x) * (1 - p);
  const dy = (oldRect.y - newRect.y) * (1 - p);
  const sx = lerp(oldRect.w / newRect.w, 1, p);
  const sy = lerp(oldRect.h / newRect.h, 1, p);
  return `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})`;
}

// ─────────────────────────────────────────────────────────────────
// Cursor physics — the cursor follows waypoints with a trailing spring.
// Given waypoints [{t, x, y, click?}], returns current {x, y, scale, clicking}.
// The cursor has inertia: it doesn't snap — it springs toward the current
// target. When clicking, it squashes briefly (scale < 1).
// ─────────────────────────────────────────────────────────────────
function cursorPhysics(waypoints, t) {
  if (!waypoints.length) return { x: 0, y: 0, scale: 1, clicking: false };

  // Find the current target waypoint (the latest one whose t <= current t)
  let current = waypoints[0];
  let prev = waypoints[0];
  for (let i = 0; i < waypoints.length; i++) {
    if (waypoints[i].t <= t) {
      prev = current;
      current = waypoints[i];
    }
  }

  // Spring toward current target. Use time since current waypoint started,
  // but the spring's "from" is the prev target. This gives continuous motion.
  const dt = Math.max(0, t - current.t);
  const x = springValue(prev.x, current.x, dt, { stiffness: 120, damping: 22, mass: 1 });
  const y = springValue(prev.y, current.y, dt, { stiffness: 120, damping: 22, mass: 1 });

  // Click state: true during a short window around a click waypoint
  let clicking = false;
  let clickScale = 1;
  for (const wp of waypoints) {
    if (wp.click) {
      const clickDt = t - wp.t;
      if (clickDt > -0.05 && clickDt < 0.35) {
        clicking = true;
        // Squash curve: dip to 0.82 at t=0, recover with spring
        if (clickDt < 0) {
          clickScale = 1 - 0.18 * (1 + clickDt / 0.05);
        } else {
          clickScale = 0.82 + (1 - 0.82) * (1 - Math.exp(-clickDt * 10));
        }
      }
    }
  }

  return { x, y, scale: clickScale, clicking };
}

// ─────────────────────────────────────────────────────────────────
// Motion blur helper — given a velocity (px/s) returns a CSS filter
// ─────────────────────────────────────────────────────────────────
function motionBlur(vx, vy = 0) {
  const v = Math.sqrt(vx * vx + vy * vy);
  const blur = Math.min(12, v / 200);
  return blur > 0.2 ? `blur(${blur.toFixed(2)}px)` : 'none';
}

Object.assign(window, {
  springValue, spring, springIn, stagger, Springs,
  countUp, typewrite, flipTransform, cursorPhysics, motionBlur,
  clampN, lerp,
});
