import 'CoreLibs/easing'

playdate.timer = {}

-- private varables
local timers = {}
local timersToRemove = {}
local max = math.max

-- metatable - to support legacy playdate.timer variables, and other variables that need to do other work when being set
playdate.timer.__index = function(table, key)
	if key == "running" then
		return not table.paused
	elseif key == "currentTime" then
		return table._currentTime
	elseif key == "timeLeft" then
		return max(0, table.duration - table._currentTime)
	elseif key == "startValue" then
		return table._startValue
	elseif key == "endValue" then
		return table._endValue
	elseif key == "easingFunction" then
		return table._easingFunction
	else
		return rawget(playdate.timer, key)
	end
end

playdate.timer.__newindex = function(table, key, value)
	if key == "running" then
		table.paused = not value
	elseif key == "timeLeft" or key == "currentTime" then -- read-only variables
		print("ERROR: playdate.timer."..key.." is read-only.")
	elseif key == "startValue" then
		table._startValue = value
		table.originalValues.startValue = value
	elseif key == "endValue" then
		table._endValue = value
		table.originalValues.endValue = value
	elseif key == "easingFunction" then
		table._easingFunction = value
		table.originalValues.easingFunction = value
	else
		rawset(table, key, value)
	end
end


function playdate.timer:__tostring()
	local repeats
	if self.repeats then repeats = "true" else repeats = "false" end
	local reverses
	if self.reverses then reverses = "true" else reverses = "false" end
    return "playdate.timer(duration:" .. self.duration .. ", repeats: " .. repeats .. ", reverses: " .. reverses  .. ")"
end

-- duration is in milliseconds
-- playdate.timer.new(duration, startValue, endValue, easingFunction)
-- also allow playdate.timer.new(duration, f, args), where f is the timer ended callback, args are passed to the callback
function playdate.timer.new(duration, startValue, ...)

	assert(duration~=playdate.timer, 'Please use playdate.timer.new() instead of playdate.timer:new()')

	local f = nil
	local args = nil
	local endValue = nil
	local easingFunction = nil

	if type(startValue) == "function" then	-- playdate.timer.new(duration, f, args)
		f = startValue
		if select("#", ...) > 0 then
			args = table.pack(...)
		end
		startValue = nil
	else
		endValue, easingFunction = select(1, ...)
	end

	local o = {}
	setmetatable(o, playdate.timer)

	if not duration then print("playdate.timer.new() requires a duration"); return nil end

	o.duration = duration
	o._startValue = startValue or 0
	o._endValue = endValue or 0

	o._currentTime = 0
	o.value = o._startValue
	o.active = true
	o.delay = 0
	o.remainingDelay = nil
	o.paused = false
	o.reverses = false
	o.hasReversed = false
	o.reverseEasingFunction = nil
	o.repeats = false
	o.discardOnCompletion = true

	o._easingFunction = easingFunction or playdate.easingFunctions.linear
	o.easingAmplitude = nil
	o.easingPeriod = nil

	o.updateCallback = nil
	o.timerEndedCallback = f or nil
	o.timerEndedArgs = args or nil

	o.attributes = {}
	o.originalValues = {}
	o.originalValues.startValue = o._startValue
	o.originalValues.endValue = o._endValue
	o.originalValues.easingFunction = o._easingFunction

	timers[#timers+1] = o

	return o
end

-- timer:performAfterDelay(delay, func, [...])
-- params:
	-- delay: milliseconds until the function is fired
	-- func: the function to be called when the timer fires
	-- ...: optional list of parameters to func. If none are provided the timer will be passed as the single argument

function playdate.timer.performAfterDelay(delay, func, ...)
	assert(type(delay) == "number", "playdate.timer.performAfterDelay: 'delay' must be a number.")
	assert(type(func) == "function", "playdate.timer.performAfterDelay: 'callback' must be a function.")
	local timer = playdate.timer.new(delay)
	timer.timerEndedCallback = func
	if select("#", ...) > 0 then
		timer.timerEndedArgs = table.pack(...)
	end
	return timer
end


-- timer:keyRepeatTimer(delay, func, [...])
-- returns:
	-- a timer that calls a callback function at key-repeat intervals. Once immediately, then once after `initialDelay` milliseconds, then every `repeatDelay milliseconds
-- params:
	-- initialDelay: the delay before the first repeated callback is fired
	-- repeatDelay: the delay before each subsequent repeated callback is fired
	-- func: the function to be called when the timer fires
	-- ...: optional list of parameters to func. If none are provided the timer will be passed as the single argument

function playdate.timer.keyRepeatTimerWithDelay(initialDelay, repeatDelay, func, ...)

	-- print("func", initialDelay, repeatDelay, func)

	local function callCallback(timer)
		if func ~= nil then
			if timer.timerEndedArgs == nil then
				func(timer)
			else
				func(table.unpack(timer.timerEndedArgs))
			end
		end
	end

	local timer = playdate.timer.new(initialDelay)
	if select("#", ...) > 0 then
		timer.timerEndedArgs = table.pack(...)
	end
	timer.repeats = true

	-- call the callback function once immediately, then once after initialDelay milliseconds, then every repeatDelay milliseconds
	callCallback(timer)

	local function timerRemoved(timer)
		for i = 1, #timers do
			if timers[i] == timer then
				return false
			end
		end
		return true
	end

	local function initialTimerFired(...)

		callCallback(timer)

		if timerRemoved(timer) == false then
			timer.duration = repeatDelay
			timer._currentTime = repeatDelay
			timer.timerEndedCallback = func
		end
	end

	timer.timerEndedCallback = initialTimerFired

	return timer
end


-- timer:keyRepeatTimer(func, [...])
-- returns:
	-- a timer that calls a callback function at key-repeat intervals. Once immediately, then once after 0.3 seconds, then every 0.1 seconds
-- params:
	-- func: the function to be called when the timer fires
	-- ...: optional list of parameters to func. If none are provided the timer will be passed as the single argument

function playdate.timer.keyRepeatTimer(func, ...)
	return playdate.timer.keyRepeatTimerWithDelay(300, 100, func, ...)
end


local pauseOffset = 0

function playdate.timer:pause()
	self.paused = true
	if self._lastTime ~= nil and self.active == true then
		pauseOffset = playdate.getCurrentTimeMilliseconds() - self._lastTime
	else
		pauseOffset = 0
	end
end


function playdate.timer:start()
	self.paused = false
	self._lastTime = playdate.getCurrentTimeMilliseconds() - pauseOffset
end

function playdate.timer:reset()
	self._startValue = self.originalValues.startValue
	self._endValue = self.originalValues.endValue
	self._easingFunction = self.originalValues.easingFunction
	self._currentTime = 0
	self._lastTime = nil
	self.active = true
	self.hasReversed = false
	self.remainingDelay = self.delay
	self.value = self._startValue
	self._calledOnRepeat = nil
end


function playdate.timer:remove()
	self.active = false
	self.discardOnCompletion = false -- in case this is being called from within the timer ended callback, don't remove the timer twice
	for i = 1, #timers do
		if timers[i] == self then
			timersToRemove[#timersToRemove+1] = self
			break
		end
	end
end


function playdate.timer.updateTimers()

	local currentTime = playdate.getCurrentTimeMilliseconds()


	local function updateActiveTimer(timer)

		if timer._startValue ~= timer._endValue and timer.duration ~= 0 then
			timer.value = timer._easingFunction(timer._currentTime, timer._startValue, timer._endValue - timer._startValue, timer.duration, timer.easingAmplitude, timer.easingPeriod)
		else
			timer.value = timer._endValue
		end

		if timer.updateCallback ~= nil then
			if timer.timerEndedArgs == nil then
				timer.updateCallback(timer)
			else
				timer.updateCallback(table.unpack(timer.timerEndedArgs))
			end
		end
	end


	for i = 1, #timers do

		local timer = timers[i]

		if timer.active == true and timer.paused == false then

			local dt = 0
			if timer._lastTime then
				dt = currentTime - timer._lastTime
			end
			timer._lastTime = currentTime
			
			if timer.remainingDelay == nil then		-- doing it this way so delay can be set after the timer is created without having to call reset() afterwards
				timer.remainingDelay = timer.delay
			end

			if timer.remainingDelay > 0 then
				timer.remainingDelay = timer.remainingDelay - dt
			else

				timer._currentTime = timer._currentTime + dt

				if timer._currentTime > timer.duration then
					
					local function callEndedCallback()
						if timer.timerEndedCallback ~= nil then
							if timer.timerEndedArgs == nil then
								timer.timerEndedCallback(timer)
							else
								timer.timerEndedCallback(table.unpack(timer.timerEndedArgs))
							end
						end
					end

					if timer.reverses and timer.hasReversed == false then

						local temp = timer._startValue
						timer._startValue = timer._endValue
						timer._endValue = temp
						timer._currentTime = 0
						timer.remainingDelay = timer.delay

						if timer.reverseEasingFunction ~= nil then
							timer._easingFunction = timer.reverseEasingFunction
						end

						timer.hasReversed = true -- so we don't reverse a second time (set repeats = true to do that)

					elseif timer.repeats then

						callEndedCallback()

						local ct = timer._currentTime
						timer:reset()
						timer._calledOnRepeat = true -- record that the callback was invoked while repeating
						timer._currentTime = ct - timer.duration -- so we don't get continues values rather than a hard reset and pause

						updateActiveTimer(timer)

					else

						timer.active = false
						timer._currentTime = timer.duration	-- end up on the max time
						timer.value = timer._endValue

						-- handle the case when .repeats is true, then false
						-- in that case, if the callback has been called and .repeats is set back to false
						-- we shouldn't ever call the callback again.
						if timer._calledOnRepeat ~= true then
							callEndedCallback()
						end

						if timer.discardOnCompletion == true then	-- remove the timer
							timersToRemove[#timersToRemove+1] = timer
						end
					end

				else	-- timer is active
					updateActiveTimer(timer)
				end
			end
		end	-- active timer
	end

	for _, timer in pairs( timersToRemove ) do
		local timerIndex = table.indexOfElement( timers, timer )
		if timerIndex then 
			table.remove( timers, timerIndex )
		end
	end

	timersToRemove = {}
end

function playdate.timer.allTimers()
	return timers
end
