var Ease = use('ease')
var time = use('time')

var rate = 1/240

function make_engine(default_clock) {
  return {
    tweens: [],
    default_clock: default_clock || null,
    add(tween) {
      push(this.tweens, tween)
    },
    remove(tween) {
      this.tweens = filter(this.tweens, t => t != tween)
    },
    update(current_time) {
      var ct = current_time
      if (ct == null) {
        ct = this.default_clock ? this.default_clock() : time.number()
      }
      arrfor(this.tweens, function(tween) {
        tween._update(ct)
      })
    },
    clear() {
      this.tweens = []
    }
  }
}

// Global fire-and-forget engine
var TweenEngine = make_engine(null)

var TweenProto = {
  obj: null,
  startVals: null,
  endVals: null,
  duration: 0,
  easing: null,
  startTime: 0,
  onCompleteCallback: null,
  onUpdateCallback: null,
  engine: null,

  to: function(props, duration, start_time) {
    arrfor(array(props), key => {
      var value = props[key]
      if (is_object(value)) {
        arrfor(array(value), subkey => {
          var flatKey = key + '.' + subkey
          this.startVals[flatKey] = this.obj[key] ? this.obj[key][subkey] : null
          this.endVals[flatKey] = value[subkey]
        })
      } else {
        this.startVals[key] = this.obj[key]
        this.endVals[key] = value
      }
    })

    this.duration = duration

    this.engine = this.engine || TweenEngine

    if (start_time == null) {
      this.startTime = this.engine.default_clock ? this.engine.default_clock() : time.number()
    } else {
      this.startTime = start_time
    }

    this.engine.add(this)
    return this
  },

  ease: function(easingFn) {
    this.easing = easingFn
    return this
  },

  onComplete: function(callback) {
    this.onCompleteCallback = callback
    return this
  },

  onUpdate: function(cb) {
    this.onUpdateCallback = cb
    return this
  },

  _update: function(now) {
    this.seek(now)
    if (this.onUpdateCallback) this.onUpdateCallback()
  },

  seek: function(global_time) {
    var elapsed = global_time - this.startTime
    var t = min(max(elapsed / this.duration, 0), 1)
    var eased = this.easing(t)

    arrfor(array(this.endVals), key => {
      var start = this.startVals[key]
      var end = this.endVals[key]
      var value = start + (end - start) * eased
      var parts = null
      var objKey = null
      var subKey = null

      if (search(key, '.') != null) {
        parts = array(key, '.')
        objKey = parts[0]
        subKey = parts[1]

        if (!this.obj[objKey]) {
          this.obj[objKey] = {}
        }

        this.obj[objKey][subKey] = value
      } else {
        this.obj[key] = value
      }
    })

    if (t == 1 && this.engine) {
      this.onCompleteCallback()
      this.engine.remove(this)
    }
  },

  cancel: function() {
    if (this.engine) {
      this.engine.remove(this)
    }
  },

  toJSON: function() {
    return {
      startVals: this.startVals,
      endVals: this.endVals,
      duration: this.duration,
      startTime: this.startTime,
      easing: this.easing.name || 'linear'
    }
  }
}

function create_tween(obj) {
  var tw = meme(TweenProto)
  tw.obj = obj
  tw.startVals = {}
  tw.endVals = {}
  tw.duration = 0
  tw.easing = Ease.linear
  tw.startTime = 0
  tw.onCompleteCallback = function() {}
  tw.onUpdateCallback = null
  tw.engine = null
  return tw
}

var TimelineProto = {
  current_time: 0,
  events: null,
  playing: false,
  last_tick: 0,
  engine: null,

  add_event: function(time_value, fn) {
    push(this.events, { time: time_value, fn, fired: false })
  },

  add_tween: function(obj, props, duration, start_time) {
    var tw = create_tween(obj)
    tw.engine = this.engine
    return tw.to(props, duration, start_time)
  },

  play: function() {
    this.playing = true
    this.last_tick = time.number()

    var self = this
    var loop = () => {
      if (!self.playing) return
      var now = time.number()
      var dt = now - self.last_tick
      self.last_tick = now
      self.current_time += dt
      self.seek(self.current_time)
      $delay(loop, rate)
    }

    loop()
  },

  pause: function() {
    this.playing = false
  },

  seek: function(t) {
    this.current_time = t

    // Update all tweens in this timeline
    this.engine.update(t)

    // Fire any events
    arrfor(this.events, function(ev) {
      if (!ev.fired && t >= ev.time) {
        ev.fn()
        ev.fired = true
      } else if (ev.fired && t < ev.time) {
        ev.fired = false
      }
    })
  },

  toJSON: function() {
    return {
      current_time: this.current_time,
      events: array(this.events, e => ({ time: e.time, fired: e.fired })),
      tweens: array(this.engine.tweens, t => t.toJSON())
    }
  }
}

function Timeline() {
  var tl = meme(TimelineProto)
  tl.current_time = 0
  tl.events = []
  tl.playing = false
  tl.last_tick = 0
  tl.engine = make_engine(() => tl.current_time)
  return tl
}

// Live update loop for fire-and-forget tweens
function live_update_loop() {
  if (is_function(TweenEngine.update)) TweenEngine.update()
  $delay(live_update_loop, rate)
}

function tween(obj, engine) {
  var tw = create_tween(obj)
  if (engine) tw.engine = engine
  return tw
}

function init(default_clock) {
  TweenEngine.default_clock = default_clock || (() => time.number())
  live_update_loop()
}

$delay(() => {
  if (!TweenEngine.default_clock) {
    init()
  }
}, 0)

var tween_ret = {
  init,
  Timeline,
  TweenEngine,
  tween
}

return tween_ret
