var util = use('util')

var Ease = {
  linear(t) {
    return t
  },
  in(t) {
    return t * t
  },
  out(t) {
    var d = 1 - t
    return 1 - d * d
  },
  inout(t) {
    var d = -2 * t + 2
    return t < 0.5 ? 2 * t * t : 1 - (d * d) / 2
  },
}

function make_easing_fns(num) {
  var obj = {}

  obj.in = function (t) {
    return Math.pow(t, num)
  }

  obj.out = function (t) {
    return 1 - Math.pow(1 - t, num)
  }

  var mult = Math.pow(2, num - 1)
  obj.inout = function (t) {
    return t < 0.5 ? mult * Math.pow(t, num) : 1 - Math.pow(-2 * t + 2, num) / 2
  }

  return obj
}

Ease.quad = make_easing_fns(2)
Ease.cubic = make_easing_fns(3)
Ease.quart = make_easing_fns(4)
Ease.quint = make_easing_fns(5)

Ease.expo = {
  in(t) {
    return t == 0 ? 0 : Math.pow(2, 10 * t - 10)
  },
  out(t) {
    return t == 1 ? 1 : 1 - Math.pow(2, -10 * t)
  },
  inout(t) {
    return t == 0
      ? 0
      : t == 1
      ? 1
      : t < 0.5
      ? Math.pow(2, 20 * t - 10) / 2
      : (2 - Math.pow(2, -20 * t + 10)) / 2
  },
}

Ease.bounce = {
  in(t) {
    return 1 - this.out(t - 1)
  },
  out(t) {
    var n1 = 7.5625
    var d1 = 2.75
    if (t < 1 / d1) {
      return n1 * t * t
    } else if (t < 2 / d1) {
      return n1 * (t -= 1.5 / d1) * t + 0.75
    } else if (t < 2.5 / d1) {
      return n1 * (t -= 2.25 / d1) * t + 0.9375
    } else return n1 * (t -= 2.625 / d1) * t + 0.984375
  },
  inout(t) {
    return t < 0.5 ? (1 - this.out(1 - 2 * t)) / 2 : (1 + this.out(2 * t - 1)) / 2
  },
}

Ease.sine = {
  in(t) {
    return 1 - Math.cos((t * Math.PI) / 2)
  },
  out(t) {
    return Math.sin((t * Math.PI) / 2)
  },
  inout(t) {
    return -(Math.cos(Math.PI * t) - 1) / 2
  },
}

Ease.elastic = {
  in(t) {
    return t == 0
      ? 0
      : t == 1
      ? 1
      : -Math.pow(2, 10 * t - 10) *
          Math.sin((t * 10 - 10.75) * this.c4)
  },
  out(t) {
    return t == 0
      ? 0
      : t == 1
      ? 1
      : Math.pow(2, -10 * t) *
          Math.sin((t * 10 - 0.75) * this.c4) +
          1
  },
  inout(t) {
    t == 0
      ? 0
      : t == 1
      ? 1
      : t < 0.5
      ? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * this.c5)) / 2
      : (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * this.c5)) / 2 + 1
  },
}

Ease.elastic.c4 = (2 * Math.PI) / 3
Ease.elastic.c5 = (2 * Math.PI) / 4.5

var tween = function (from, to, time, fn, cb) {
  var start = os.now()

  function cleanup() {
    stop()
    fn = null
    stop = null
    cb = null
    update = null
  }

  var update = function tween_update(dt) {
    var elapsed = os.now() - start
    fn(util.obj_lerp(from, to, elapsed / time))
    if (elapsed >= time) {
      fn(to)
      cb?.()
      cleanup()
    }
  }
  var stop = Register.update.register(update)
  return cleanup
}

var Tween = {
  default: {
    loop: "hold",
    time: 1,
    ease: Ease.linear,
    whole: true,
    cb: function () {},
  },
  start(obj, target, tvals, options) {
    var defn = Object.create(this.default)
    Object.assign(defn, options)

    if (defn.loop == "circle") tvals.push(tvals[0])
    else if (defn.loop == "yoyo") {
      for (var i = tvals.length - 2; i >= 0; i--) tvals.push(tvals[i])
    }

    defn.accum = 0
    var slices = tvals.length - 1
    var slicelen = 1 / slices

    defn.fn = function (dt) {
      defn.accum += dt
      if (defn.accum >= defn.time && defn.loop == "hold") {
        if (typeof target == "string") obj[target] = tvals[tvals.length - 1]
        else target(tvals[tvals.length - 1])
        defn.pause()
        defn.cb.call(obj)
        return
      }
      defn.pct = (defn.accum % defn.time) / defn.time
      if (defn.loop == "none" && defn.accum >= defn.time) defn.stop()

      var t = defn.whole ? defn.ease(defn.pct) : defn.pct
      var nval = t / slicelen
      var i = Math.trunc(nval)
      nval -= i
      if (!defn.whole) nval = defn.ease(nval)

      if (typeof target == "string") obj[target] = tvals[i].lerp(tvals[i + 1], nval)
      else target(tvals[i].lerp(tvals[i + 1], nval))
    }

    var playing = false

    defn.play = function () {
      if (playing) return
      defn._end = Register.update.register(defn.fn.bind(defn))
      playing = true
    }
    defn.restart = function () {
      defn.accum = 0
      if (typeof target == "string") obj[target] = tvals[0]
      else target(tvals[0])
    }
    defn.stop = function () {
      if (!playing) return
      defn.pause()
      defn.restart()
    }
    defn.pause = function () {
      defn._end()
      if (!playing) return
      playing = false
    }

    return defn
  },
}

Tween.make = Tween.start

Ease[cell.DOC] = `
This object provides multiple easing functions that remap a 0..1 input to produce
a smoothed or non-linear output. They can be used standalone or inside tweens.

Available functions:
- linear(t)
- in(t), out(t), inout(t)
- quad.in, quad.out, quad.inout
- cubic.in, cubic.out, cubic.inout
- quart.in, quart.out, quart.inout
- quint.in, quint.out, quint.inout
- expo.in, expo.out, expo.inout
- bounce.in, bounce.out, bounce.inout
- sine.in, sine.out, sine.inout
- elastic.in, elastic.out, elastic.inout

All easing functions expect t in [0..1] and return a remapped value in [0..1].
`

tween[cell.DOC] = `
:param from: The starting object or value to interpolate from.
:param to: The ending object or value to interpolate to.
:param time: The total duration of the tween in milliseconds or some time unit.
:param fn: A callback function that receives the interpolated value at each update.
:param cb: (Optional) A callback invoked once the tween completes.
:return: A function that, when called, cleans up and stops the tween.

Creates a simple tween that linearly interpolates from "from" to "to" over "time"
and calls "fn" with each interpolated value. Once finished, "fn" is called with "to",
then "cb" is invoked if provided, and the tween is cleaned up.
`

Tween[cell.DOC] = `
An object providing methods to create and control tweens with additional features
like looping, custom easing, multiple stages, etc.

Properties:
- default: A template object with loop/time/ease/whole/cb properties.
Methods:
- start(obj, target, tvals, options): Create a tween over multiple target values.
- make: Alias of start.
`

Tween.start[cell.DOC] = `
:param obj: The object whose property is being tweened, or context for the callback.
:param target: A string property name in obj or a callback function receiving interpolated values.
:param tvals: An array of values to tween through (each must support .lerp()).
:param options: An optional object overriding defaults (loop type, time, ease, etc.).
:return: A tween definition object with .play(), .pause(), .stop(), .restart(), etc.

Set up a multi-stage tween. You can specify looping modes (none, hold, restart, yoyo, circle),
time is the total duration, and "ease" can be any function from Ease. Once started, it updates
every frame until completion or stop/pause is called.
`

Tween.make[cell.DOC] = `
Alias of Tween.start. See Tween.start for usage details.
`

return { Tween, Ease, tween }
