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

var rate = 1/240

var TweenEngine = {
  tweens: [],
  default_clock: null, // Will be set during init
  add(tween) {
    this.tweens.push(tween)
  },
  remove(tween) {
    this.tweens = this.tweens.filter(t => t != tween)
  },
  update(current_time) {
    // If no time provided, use real time
    if (current_time == null) {
      current_time = time.number()
    }
    for (var tween of this.tweens.slice()) {
      tween._update(current_time)
    }
  },
  clear() {
    this.tweens = []
  }
}

function Tween(obj) {
  this.obj = obj
  this.startVals = {}
  this.endVals = {}
  this.duration = 0
  this.easing = Ease.linear
  this.startTime = 0
  this.onCompleteCallback = function() {}
  this.onUpdateCallback = null
  this.engine = null // Track which engine owns this tween
}

Tween.prototype.to = function(props, duration, start_time) {
  for (var key in props) {
    var value = props[key]
    if (is_object(value)) {
      // Handle nested objects by flattening them
      for (var subkey in value) {
        var flatKey = key + '.' + subkey
        this.startVals[flatKey] = this.obj[key] ? this.obj[key][subkey] : undefined
        this.endVals[flatKey] = value[subkey]
      }
    } else {
      this.startVals[key] = this.obj[key]
      this.endVals[key] = value
    }
  }
  this.duration = duration
  
  // If no start_time provided, use the default clock
  if (start_time == null) {
    this.startTime = TweenEngine.default_clock ? TweenEngine.default_clock() : time.number()
  } else {
    this.startTime = start_time
  }
  
  this.engine = this.engine || TweenEngine
  this.engine.add(this)
  return this
}

Tween.prototype.ease = function(easingFn) {
  this.easing = easingFn
  return this
}

Tween.prototype.onComplete = function(callback) {
  this.onCompleteCallback = callback
  return this
}

Tween.prototype.onUpdate = function(cb) {
  this.onUpdateCallback = cb
  return this
}

Tween.prototype._update = function(now) {
  this.seek(now)
  this.onUpdateCallback?.()
}

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

  for (var key in this.endVals) {
    var start = this.startVals[key]
    var end = this.endVals[key]
    var value = start + (end - start) * eased
    
    if (key.includes('.')) {
      // Handle nested object properties
      var parts = key.split('.')
      var objKey = parts[0]
      var subKey = parts[1]
      
      // Ensure the nested object exists
      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)
  }
}

Tween.prototype.cancel = function() {
  if (this.engine) {
    this.engine.remove(this)
  }
}

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

function Timeline() {
  this.current_time = 0
  this.events = [] // { time, fn, fired }
  this.playing = false
  this.last_tick = 0
  this.engine = {
    tweens: [],
    add: TweenEngine.add.bind(this.engine),
    remove: TweenEngine.remove.bind(this.engine),
    update: TweenEngine.update.bind(this.engine),
    clear: TweenEngine.clear.bind(this.engine)
  }
  this.engine.tweens = []
}

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

Timeline.prototype.add_tween = function(obj, props, duration, start_time) {
  var tw = new Tween(obj)
  tw.engine = this.engine
  return tw.to(props, duration, start_time)
}

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

Timeline.prototype.pause = function() {
  this.playing = false
}

Timeline.prototype.seek = function(t) {
  this.current_time = t
  // Update all tweens in this timeline
  this.engine.update(t)
  // Fire any events
  for (var ev of this.events) {
    if (!ev.fired && t >= ev.time) {
      ev.fn()
      ev.fired = true
    } else if (ev.fired && t < ev.time) {
      // Reset fired flag when seeking backwards
      ev.fired = false
    }
  }
}

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

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

// Factory function
function tween(obj, engine) {
  var tw = new Tween(obj)
  if (engine) {
    tw.engine = engine
  }
  return tw
}

// Initialize with a default clock that returns real time
function init(default_clock) {
  TweenEngine.default_clock = default_clock || (() => time.number())
  // Start the live update loop
  live_update_loop()
}

// Auto-init with real time if not explicitly initialized
$delay(() => {
  if (!TweenEngine.default_clock) {
    init()
  }
}, 0)

tween.init = init
tween.Timeline = Timeline
tween.TweenEngine = TweenEngine

return tween
