-- ------------------------------------------------------------------------------ --
--                                TradeSkillMaster                                --
--                          https://tradeskillmaster.com                          --
--    All Rights Reserved - Detailed license information included with addon.     --
-- ------------------------------------------------------------------------------ --

--- Delay Functions
-- @module Delay

local _, TSM = ...
local Delay = TSM.Init("Util.Delay")
local Debug = TSM.Include("Util.Debug")
local Log = TSM.Include("Util.Log")
local TempTable = TSM.Include("Util.TempTable")
local private = {
	delays = {},
	frameNumber = 0,
	frame = nil,
}
local CALLBACK_TIME_WARNING_THRESHOLD_MS = 20
local MIN_TIME_DURATION = 0.0001



-- ============================================================================
-- Module Loading
-- ============================================================================

Delay:OnModuleLoad(function()
	private.frame = CreateFrame("Frame")
	private.frame:SetScript("OnUpdate", private.ProcessDelays)
	private.frame:Show()
end)



-- ============================================================================
-- Module Functions
-- ============================================================================

--- Call a callback after a set amount of time.
-- Note that the delay may be up to 1 frame time longer than requested.
-- @tparam[opt] string label A label for the delay (to allow it to be cancelled)
-- @tparam number duration The amount of time to delay for
-- @tparam function callback The function called when the delay is finished
-- @tparam[opt] number repeatDelay The amount of time to set this delay for once it completes
-- @param[opt=nil] context A context value to pass along to the callback (ignored if the delay was previously started)
function Delay.AfterTime(label, duration, callback, repeatDelay, context)
	if type(label) == "number" then
		-- no label specified
		assert(not repeatDelay)
		duration, callback, repeatDelay, context = label, duration, callback, repeatDelay
		label = nil
	end
	assert(type(duration) == "number" and type(callback) == "function" and (not repeatDelay or type(repeatDelay) == "number"))
	repeatDelay = repeatDelay and max(repeatDelay, MIN_TIME_DURATION) or nil
	duration = max(duration, MIN_TIME_DURATION)

	if label then
		for _, delay in ipairs(private.delays) do
			if delay.label == label then
				-- delay is already running, so just return
				return
			end
		end
	else
		label = Debug.GetStackLevelLocation(2)
	end

	local delayTbl = TempTable.Acquire()
	delayTbl.endTime = GetTime() + duration
	delayTbl.callback = callback
	delayTbl.label = label
	delayTbl.repeatDelay = repeatDelay
	delayTbl.context = context
	tinsert(private.delays, delayTbl)
end

--- Call a callback after a set number of frames.
-- Note that the delay may be up to 1 frame time longer than requested.
-- @tparam[opt] string label A label for the delay (to allow it to be cancelled)
-- @tparam number duration The number of frames to delay for
-- @tparam function callback The function called when the delay is finished
-- @tparam[opt] number repeatDelay The number of frames to set this delay for once it completes
-- @param[opt=nil] context A context value to pass along to the callback (ignored if the delay was previously started)
function Delay.AfterFrame(label, duration, callback, repeatDelay, context)
	if type(label) == "number" then
		-- no label specified
		assert(not repeatDelay)
		duration, callback, repeatDelay, context = label, duration, callback, repeatDelay
		label = nil
	end
	assert(type(duration) == "number" and type(callback) == "function" and (not repeatDelay or type(repeatDelay) == "number"))
	repeatDelay = repeatDelay and max(repeatDelay, 1) or nil
	duration = max(duration, 1)

	if label then
		for _, delay in ipairs(private.delays) do
			if delay.label == label then
				-- delay is already running, so just return
				return
			end
		end
	else
		label = Debug.GetStackLevelLocation(2)
		assert(label)
	end

	local delayTbl = TempTable.Acquire()
	delayTbl.endFrame = private.frameNumber + duration
	delayTbl.callback = callback
	delayTbl.label = label
	delayTbl.repeatDelay = repeatDelay
	delayTbl.context = context
	tinsert(private.delays, delayTbl)
end

--- Cancel a delay.
-- This works for both time and frame delays.
-- @tparam string label The label the delay was created with
function Delay.Cancel(label)
	for i, delay in ipairs(private.delays) do
		if delay.label == label then
			TempTable.Release(tremove(private.delays, i))
			return
		end
	end
end



-- ============================================================================
-- Main Delay Callback
-- ============================================================================

function private.ProcessDelays()
	private.frameNumber = private.frameNumber + 1
	-- the delays can change as we do our callbacks, so keep looping through them until there are no more pending
	while true do
		local pendingLabel, pendingCallback, pendingContext = nil, nil, nil
		for i, delay in ipairs(private.delays) do
			assert(delay.endFrame or delay.endTime)
			if delay.endFrame and delay.endFrame <= private.frameNumber then
				pendingLabel = delay.label
				pendingCallback = delay.callback
				pendingContext = delay.context
				if delay.repeatDelay then
					delay.endFrame = private.frameNumber + delay.repeatDelay
				else
					TempTable.Release(tremove(private.delays, i))
				end
				break
			elseif delay.endTime and delay.endTime <= GetTime() then
				pendingLabel = delay.label
				pendingCallback = delay.callback
				pendingContext = delay.context
				if delay.repeatDelay then
					delay.endTime = GetTime() + delay.repeatDelay
				else
					TempTable.Release(tremove(private.delays, i))
				end
				break
			end
		end
		if not pendingLabel then
			-- no more pending delays to process
			assert(not pendingCallback)
			break
		end
		local startTime = debugprofilestop()
		pendingCallback(pendingContext)
		local timeTaken = debugprofilestop() - startTime
		if timeTaken > CALLBACK_TIME_WARNING_THRESHOLD_MS then
			Log.Warn("Delay callback (%s) took %0.2fms", pendingLabel, timeTaken)
		end
	end
end
