local MAJOR, MINOR = "LibAsync", 1.7
local async, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
if not async then return end -- the same or newer version of this lib is already loaded into memory

if async.Unload then
	async:Unload()
end

local em = GetEventManager()
local remove, min = table.remove, math.min

local function RemoveCall(job, callstackIndex)
	remove(job.callstack, callstackIndex)
	job.lastCallIndex = min(job.lastCallIndex, #job.callstack)
end

local current, call
local function safeCall() return call(current) end

local function DoCallback(job, callstackIndex)
	local success, shouldContinue = pcall(safeCall)
	if success then
		-- If the call returns true, the call wants to be called again
		if not shouldContinue then RemoveCall(job, callstackIndex) end
	else
		-- shouldContinue is the value returned by error or assert
		job.Error = shouldContinue
		RemoveCall(job, callstackIndex)

		call = job.onError
		if call then
			pcall(safeCall)
		else
			job:Suspend()
			error(job.Error)
		end
	end
end

local jobs = async.jobs or { }
async.jobs = jobs
-- async.registered = { }

local function DoJob(job)
	current = job
	local index = #job.callstack
	call = job.callstack[index]
	if call then
		DoCallback(job, index)
	else
		-- assert(index == 0, "No call on non-empty stack?!")
		jobs[job.name] = nil
		call = job.finally
		if call then pcall(safeCall) end
	end
	current, call = nil, nil
end

-- time we can spend until the next frame must be shown
local frameTimeTarget = GetCVar("VSYNC") == "1" and 14 or(tonumber(GetCVar("MinFrameTime.2")) * 1000)

-- we allow a function to use 25% of the frame time before it gets critical
local spendTimeDef = frameTimeTarget * 0.75
local spendTimeDefNoHUD = 15
local spendTime = spendTimeDef

local debug = false

local running
local GetFrameTimeMilliseconds, GetGameTimeMilliseconds = GetFrameTimeMilliseconds, GetGameTimeMilliseconds

local function GetThreshold()
	return(HUD_SCENE:IsShowing() or HUD_UI_SCENE:IsShowing()) and spendTimeDef or spendTimeDefNoHUD
end

local job = nil
local cpuLoad = 0
local name
local function Scheduler()
	if not running then return end

	job = nil
	local start = GetFrameTimeMilliseconds()
	local runTime, cpuLoad = start, GetGameTimeMilliseconds() - start
	if cpuLoad > spendTime then
		spendTime = math.min(30, spendTime + spendTime * 0.02)
		if debug then
			df("initial gap: %ims. skip. new threshold: %ims", GetGameTimeMilliseconds() - start, spendTime)
		end
		return
	end
	if debug then
		df("initial gap: %ims", GetGameTimeMilliseconds() - start)
	end
	while (GetGameTimeMilliseconds() - start) <= spendTime do
		name, job = next(jobs, name)
		if not job then name, job = next(jobs) end
		if job then
			runTime = GetGameTimeMilliseconds()
			DoJob(job)
			spendTime = spendTime - 0.001
		else
			-- Finished
			running = false
			spendTime = GetThreshold()
			return
		end
	end
	-- spendTime = GetThreshold()
	if debug and job then
		local now = GetGameTimeMilliseconds()
		local freezeTime = now - start
		if freezeTime >= 16 then
			runTime = now - runTime
			df("%s freeze. allowed: %ims, used %ims, resulting fps %i.", job.name, spendTime, runTime, 1000 / freezeTime)
		end
	end
end

function async:GetDebug()
	return debug
end

function async:SetDebug(enabled)
	debug = enabled
end

function async:GetCpuLoad()
	return cpuLoad / frameTimeTarget
end

-- Class task

local task = async.task or ZO_Object:Subclass()
async.task = task

-- Called from async:Create()
function task:New(name)
	local instance = ZO_Object.New(self)
	instance.name = name or tostring(instance)
	instance:Initialize()
	return instance
end

function task:Initialize()
	self.callstack = { }
	self.lastCallIndex = 0
	-- async.registered[#async.registered + 1] = self
end

-- Resume the execution context.
function task:Resume()
	running = true
	jobs[self.name] = self
	return self
end

-- Suspend the execution context and allow to resume anytime later.
function task:Suspend()
	jobs[self.name] = nil
	return self
end

-- Interupt and fully stop the execution context. Can be called from outside to stop everything.
function task:Cancel()
	ZO_ClearNumericallyIndexedTable(self.callstack)
	self.lastCallIndex = 0
	if jobs[self.name] then
		if not self.finally then
			jobs[self.name] = nil
			-- else run job with empty callstack to run finalizer
		end
	end
	return self
end

do
	-- Run the given FuncOfTask in your task context execution.
	function task:Call(funcOfTask)
		self.lastCallIndex = #self.callstack + 1
		self.callstack[self.lastCallIndex] = funcOfTask
		return self:Resume()
	end

	local insert = table.insert
	-- Continue your task context execution with the given FuncOfTask after the previous as finished.
	function task:Then(funcOfTask)
		-- assert(self.lastCallIndex > 0 and self.lastCallIndex <= #self.callstack, "cap!")
		insert(self.callstack, self.lastCallIndex, funcOfTask)
		return self
	end
end

-- Start an interruptible for-loop.
function task:For(p1, p2, p3)
	-- If called as a normal job, false will prevent it is kept in callstack doing an endless loop
	self.callstack[#self.callstack + 1] = function() return false, p1, p2, p3 end
	return self
end

do
	local function ForConditionAlreadyFalse() end
	local function ContinueForward(index, endIndex) return index <= endIndex end
	local function ContinueBackward(index, endIndex) return index >= endIndex end

	local function asyncForWithStep(self, func, index, endIndex, step)
		step = step or 1
		if step == 0 then error("step is zero") end

		local ShouldContinue
		if step > 0 then
			if index > endIndex then return ForConditionAlreadyFalse end
			ShouldContinue = ContinueForward
		else
			if index < endIndex then return ForConditionAlreadyFalse end
			ShouldContinue = ContinueBackward
		end
		return function()
			if func(index) ~= async.BREAK then
				index = index + step
				return ShouldContinue(index, endIndex)
			end
		end
	end

	local function asyncForPairs(self, func, iter, list, key)
		return function()
			local value
			key, value = iter(list, key)
			return key and func(key, value) ~= async.BREAK
		end
	end

	-- Execute the async-for with the given step-function. The parameters of the step-function are those you would use in your for body.
	function task:Do(func)
		local callstackIndex = #self.callstack
		local shouldBeFalse, p1, p2, p3 = self.callstack[callstackIndex]()
		assert(shouldBeFalse == false and p1, "Do without For")
		remove(self.callstack, callstackIndex)

		local DoLoop = type(p1) == "number" and
		asyncForWithStep(self, func, p1, p2, p3) or
		asyncForPairs(self, func, p1, p2, p3)

		if current or #self.callstack == 0 then return self:Call(DoLoop) else return self:Then(DoLoop) end
	end
end

-- Suspend the execution of your task context for the given delay in milliseconds and then call the given FuncOfTask to continue.
function task:Delay(delay, funcOfTask)
	self:StopTimer()
	if delay < 10 then return self:Call(funcOfTask) end
	self:Suspend()
	em:RegisterForUpdate(self.name, delay, function()
		em:UnregisterForUpdate(self.name)
		self:Call(funcOfTask)
	end )
	return self
end

-- Stop the delay created by task:Delay or task:Interval.
function task:StopTimer()
	em:UnregisterForUpdate(self.name)
	return self
end

-- Set a FuncOfTask as a final handler. If you call Called if something went wrong in your context.
function task:Finally(funcOfTask)
	self.finally = funcOfTask
	return self
end

-- Set a FuncOfTask as an error handler. Called if something went wrong in your context.
function task:OnError(funcOfTask)
	self.onError = funcOfTask
	return self
end

do
	-- Thanks to: https://de.wikipedia.org/wiki/Quicksort

	local function simpleCompare(a, b) return a < b end
	local function sort(task, array, compare)
		local function quicksort(left, right)
			if left >= right then return end

			-- partition
			local i, j, pivot = left, right - 1, array[right]

			task:Call( function()
				while i < right and compare(array[i], pivot) do i = i + 1 end
				while j > left and not compare(array[j], pivot) do j = j - 1 end
				if i < j then
					array[i], array[j] = array[j], array[i]
					-- repeatly call this function until i >= j
					return true
				end
			end )
			task:Then( function()
				if compare(pivot, array[i]) then array[i], array[right] = array[right], array[i] end
				quicksort(left, i - 1)
				quicksort(i + 1, right)
			end )
		end
		quicksort(1, #array)
	end

	-- This sort function works like table.sort(). The compare function is optional.
	function task:Sort(array, compare)
		local sortJob = function(task) sort(task, array, compare or simpleCompare) end
		if current or #self.callstack == 0 then return self:Call(sortJob) else return self:Then(sortJob) end
	end
end

-- Class async

-- Get the current context, if you are within a FuncOfTask or nil.
function async:GetCurrent()
	return current
end

-- Create an interruptible task context.
function async:Create(name)
	return task:New(name)
end

do
	local Default = task:New("*Default Task*")
	function Default:Cancel() error("Not allowed on default task. Use your_lib_var:Create(optional_name) for an interruptible task context.") end
	Default.Finally = Default.Cancel
	Default.OnError = Default.Cancel

	-- Start a non-interruptible task or start a nested call in the current context.
	function async:Call(funcOfTask)
		-- if async:Call is called from within a task callback (the moment where GetCurrent() is not nil) use it for nested calls
		return(async:GetCurrent() or Default):Call(funcOfTask)
	end
	-- Start a non-interruptible for-loop or start a nested for-loop in the current context.
	function async:For(p1, p2, p3)
		return(self:GetCurrent() or Default):For(p1, p2, p3)
	end

	-- Start a non-interruptible sort or start a nested sort in the current context.
	function async:Sort(array, compare)
		return(self:GetCurrent() or Default):Sort(array, compare)
	end
end

-- async.BREAK is the new 'break' for breaking loops. As Lua would not allowed the keyword 'break' in that context.
-- To break a for-loop, return async.BREAK
async.BREAK = true

local function stateChange(oldState, newState)
	if newState == SCENE_SHOWN or newState == SCENE_HIDING then
		spendTime = GetThreshold()
	end
end

local identifier = "ASYNCTASKS_JOBS"

HUD_SCENE:RegisterCallback("StateChange", stateChange)
HUD_UI_SCENE:RegisterCallback("StateChange", stateChange)

function async:Unload()
	HUD_SCENE:UnregisterCallback("StateChange", stateChange)
	HUD_UI_SCENE:UnregisterCallback("StateChange", stateChange)
end

em:UnregisterForUpdate(identifier)
em:RegisterForUpdate(identifier, 0, Scheduler)