'use strict';
(function(global) {

  global.LabFX = global.LabFX || {};

  /* ─────────────────────────────────────────────
     EASINGS — standard Penner equations
  ───────────────────────────────────────────── */
  var easings = {
    linear: function(t) { return t; },

    easeInQuad:    function(t) { return t * t; },
    easeOutQuad:   function(t) { return t * (2 - t); },
    easeInOutQuad: function(t) { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; },

    easeInCubic:    function(t) { return t * t * t; },
    easeOutCubic:   function(t) { var u = t - 1; return u * u * u + 1; },
    easeInOutCubic: function(t) {
      return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
    },

    easeInOutQuint: function(t) {
      return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t;
    },

    easeOutBack: function(t) {
      var c1 = 1.70158;
      var c3 = c1 + 1;
      return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
    },

    easeOutElastic: function(t) {
      if (t === 0 || t === 1) return t;
      var c4 = (2 * Math.PI) / 3;
      return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
    },

    easeInExpo:  function(t) { return t === 0 ? 0 : Math.pow(2, 10 * t - 10); },
    easeOutExpo: function(t) { return t === 1 ? 1 : 1 - Math.pow(2, -10 * t); }
  };

  /* ─────────────────────────────────────────────
     TWEEN
  ───────────────────────────────────────────── */
  /**
   * Tween a numeric value from `from` to `to` over `durMs` milliseconds.
   *
   * @param {number}   from
   * @param {number}   to
   * @param {number}   durMs
   * @param {function|string} easing  - easing fn or key in LabFX.motion.easings
   * @param {function} onUpdate - called with current value each frame
   * @param {function} [onDone]
   * @returns {{ cancel: function, running: boolean }}
   */
  function tween(from, to, durMs, easing, onUpdate, onDone) {
    var easeFn = (typeof easing === 'function')
      ? easing
      : (easings[easing] || easings.linear);

    var start   = null;
    var handle  = { running: true, cancel: null };
    var rafId;

    function step(now) {
      if (!handle.running) return;
      if (start === null) start = now;
      var elapsed  = now - start;
      var progress = Math.min(elapsed / durMs, 1);
      var value    = from + (to - from) * easeFn(progress);

      onUpdate(value);

      if (progress < 1) {
        rafId = requestAnimationFrame(step);
      } else {
        handle.running = false;
        if (typeof onDone === 'function') onDone();
      }
    }

    handle.cancel = function() {
      handle.running = false;
      cancelAnimationFrame(rafId);
    };

    rafId = requestAnimationFrame(step);
    return handle;
  }

  /* ─────────────────────────────────────────────
     SPRING  (critically-damped simulation)
  ───────────────────────────────────────────── */
  /**
   * Returns a spring factory.
   * Usage: LabFX.motion.spring(170, 26)(from, to, onUpdate, onDone) → handle
   *
   * @param {number} stiffness  (default 170)
   * @param {number} damping    (default 26)
   */
  function springFactory(stiffness, damping) {
    stiffness = stiffness != null ? stiffness : 170;
    damping   = damping   != null ? damping   : 26;

    return function(from, to, onUpdate, onDone) {
      var pos  = from;
      var vel  = 0;
      var prev = null;
      var handle = { running: true, cancel: null };
      var rafId;

      function step(now) {
        if (!handle.running) return;
        if (prev === null) { prev = now; }
        var dt = Math.min((now - prev) / 1000, 0.05); /* cap at 50ms */
        prev = now;

        var force = -stiffness * (pos - to) - damping * vel;
        vel += force * dt;
        pos += vel * dt;

        onUpdate(pos);

        var settled = Math.abs(pos - to) < 0.01 && Math.abs(vel) < 0.01;
        if (!settled) {
          rafId = requestAnimationFrame(step);
        } else {
          onUpdate(to);
          handle.running = false;
          if (typeof onDone === 'function') onDone();
        }
      }

      handle.cancel = function() {
        handle.running = false;
        cancelAnimationFrame(rafId);
      };

      rafId = requestAnimationFrame(step);
      return handle;
    };
  }

  /* ─────────────────────────────────────────────
     EXPORT
  ───────────────────────────────────────────── */
  global.LabFX.motion = {
    tween:   tween,
    spring:  springFactory,
    easings: easings
  };

})(window);
