// @wuys/map · core/animation.js
//
// Animation loop + easing. Exposes:
//   - createAnimator(opts) → { kick, snap, isRunning, stop }
//   - easings: a small set of curves (easeOutCubic, easeInOutQuad, …)
//
// The animator interpolates a SET of named scalars from current → target.
// Each frame it calls `onStep` with the interpolated current values.
// When all scalars are within their tolerance, it calls `onSettle` and stops.
//
// This is the universal "smooth state chase" used by nav + future polish layers.

export const easings = {
  linear:        t => t,
  easeOutQuad:   t => 1 - (1 - t) ** 2,
  easeOutCubic:  t => 1 - (1 - t) ** 3,
  easeOutQuart:  t => 1 - (1 - t) ** 4,
  easeInOutQuad: t => t < 0.5 ? 2 * t * t : 1 - ((-2 * t + 2) ** 2) / 2,
  easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : 1 - ((-2 * t + 2) ** 3) / 2,
};

/**
 * Create a target-chasing animator.
 *
 * @param {Object} opts
 * @param {() => Record<string, number>} opts.getCurrent  Returns named scalars (current)
 * @param {() => Record<string, number>} opts.getTarget   Returns named scalars (target)
 * @param {(name: string, value: number) => void} opts.setCurrent  Writes one current scalar
 * @param {() => void} opts.onStep        Called each frame after current is updated
 * @param {() => void} [opts.onSettle]    Called once when motion ends
 * @param {Record<string, number>} [opts.tolerance]  Per-key min delta to keep chasing
 * @param {number} [opts.k=0.22]          Easing factor (0..1). Higher = snappier.
 * @param {(name: string, delta: number) => number} [opts.adjustDelta]
 *        Optional per-key delta transform (e.g. shortest-path wrap for longitude)
 */
export function createAnimator(opts) {
  const tol = opts.tolerance ?? {};
  const k = opts.k ?? 0.22;
  let rafId = 0;

  function step() {
    rafId = 0;
    const cur = opts.getCurrent();
    const tgt = opts.getTarget();
    let stillMoving = false;
    for (const key of Object.keys(tgt)) {
      let d = tgt[key] - cur[key];
      if (opts.adjustDelta) d = opts.adjustDelta(key, d);
      const t = tol[key] ?? 0.001;
      if (Math.abs(d) > t) {
        opts.setCurrent(key, cur[key] + d * k);
        stillMoving = true;
      } else {
        opts.setCurrent(key, tgt[key]);
      }
    }
    if (opts.onStep) opts.onStep();
    if (stillMoving) {
      rafId = requestAnimationFrame(step);
    } else if (opts.onSettle) {
      opts.onSettle();
    }
  }

  return {
    kick() {
      if (!rafId) rafId = requestAnimationFrame(step);
    },
    snap() {
      if (rafId) { cancelAnimationFrame(rafId); rafId = 0; }
      const tgt = opts.getTarget();
      for (const key of Object.keys(tgt)) opts.setCurrent(key, tgt[key]);
      if (opts.onStep) opts.onStep();
    },
    stop() {
      if (rafId) { cancelAnimationFrame(rafId); rafId = 0; }
    },
    isRunning() { return rafId !== 0; },
  };
}

/** Shortest-path delta for longitude wrap. Use as `adjustDelta` for the lon scalar. */
export function shortestLonDelta(key, delta) {
  if (key !== "lon" && key !== "tLon") return delta;
  if (delta >  180) return delta - 360;
  if (delta < -180) return delta + 360;
  return delta;
}

/**
 * Fixed-duration tween with easing — for properly-paced fly-to / search-result jumps.
 * @param {Object} opts
 * @param {Record<string, number>} opts.from
 * @param {Record<string, number>} opts.to
 * @param {number} opts.duration                 — milliseconds
 * @param {string|((t:number)=>number)} [opts.easing="easeOutCubic"]
 * @param {(values: Record<string, number>) => void} opts.onStep
 * @param {() => void} [opts.onDone]
 * @returns {{ stop(): void, isRunning(): boolean }}
 */
export function runTween(opts) {
  const ease = typeof opts.easing === "function"
    ? opts.easing
    : (easings[opts.easing] || easings.easeOutCubic);
  const start = performance.now();
  const keys = Object.keys(opts.to);
  let rafId = 0;
  let running = true;

  function step(now) {
    rafId = 0;
    if (!running) return;
    const elapsed = now - start;
    const t = Math.min(1, elapsed / opts.duration);
    const e = ease(t);
    const out = {};
    for (const k of keys) {
      const a = opts.from[k] ?? 0;
      let b = opts.to[k];
      // Shortest-path lon wrap
      if (k === "lon" || k === "tLon") {
        let d = b - a;
        if (d >  180) b -= 360;
        else if (d < -180) b += 360;
      }
      out[k] = a + (b - a) * e;
    }
    opts.onStep(out);
    if (t < 1) {
      rafId = requestAnimationFrame(step);
    } else {
      running = false;
      if (opts.onDone) opts.onDone();
    }
  }
  rafId = requestAnimationFrame(step);
  return {
    stop() { running = false; if (rafId) cancelAnimationFrame(rafId); },
    isRunning() { return running; },
  };
}
