
-- Runner Lib
local _, app = ...;

-- Concepts:
-- Capability to add to and run an entire set of Functions each frame, removing those which do not return a status
-- Capability to add and run coroutine Functions, with one loop of iteration per frame
-- Capability to add to and run a sequence of Functions with a specific allotment being processed individually each frame

-- Global locals
local wipe, math_max, tonumber, unpack, coroutine, type, select, tremove, tinsert, C_Timer_After =
	  wipe, math.max, tonumber, unpack, coroutine, type, select, tremove, tinsert, C_Timer.After;

local Stack = {};
local StackParams = {};
-- Tracks whether the Stack has already been requested to begin running
local RunningStack;
-- Function that begins a once-per-frame pass of the StackCo to run all Functions in the Stack
local RunStack;
-- A static coroutine which can be invoked to reverse-sequentially process all Functions within the Stack,
-- passing the corresponding Stack param to each called Function.
-- Any Functions which do not return a status will be removed
local StackCo = coroutine.create(function()
	while true do
		RunningStack = nil;
		-- app.PrintDebug("StackCo:Call",#Stack)
		for i=#Stack,1,-1 do
			-- app.PrintDebug("StackCo:Run",i,Stack[i],StackParams[i])
			if not Stack[i](StackParams[i]) then
				-- app.PrintDebug("StackCo:Remove",i)
				tremove(Stack, i);
				tremove(StackParams, i);
			end
		end
		-- app.PrintDebug("StackCo:Done")
		-- Re-call StackCo if anything remains in the Stack
		if #Stack > 0 then
			-- app.PrintDebug("StackCo:RunStack",#Stack)
			C_Timer_After(0, RunStack);
		end
		-- after processing the Stack, yield this coroutine
		-- app.PrintDebug("StackCo:Yield")
		coroutine.yield();
	end
end);
RunStack = function()
	-- app.PrintDebug("RunStack",RunningStack and "REPEAT" or "FIRST")
	if RunningStack then return; end
	RunningStack = true;
	coroutine.resume(StackCo);
end
-- Accepts a param and Function which will execute on the following frame using the provided param
local function Push(param, name, func)
	-- app.PrintDebug("Push",name,func,param)
	tinsert(Stack, func);
	tinsert(StackParams, param);
	C_Timer_After(0, RunStack);
end
app.Push = Push;

-- Represents a key-weak table containing a cache of functions which are desired to be run within a coroutine.
-- If the function is temporary and all references are removed externally, then the respective cache entry can be removed as well when garbagecollection
-- happens to run. This makes sure that we don't permanently hold references to every coroutined-function during the lifetime of the client
local CoroutineCache = setmetatable({}, { __mode = "k" });
-- Represents a small set of Push functions which are used to allow the Stack to handle coroutine processing. As these functions have no bearing
-- on the coroutine they run, they can be reused and only created when the current amount is not enough to handle all concurrent coroutines.
-- We will make this a weak-value cache, such that the Push methods can be cleaned up/recreated if needed
local PushCache = setmetatable({}, { __mode = "v" });
local function GetCoroutine(func, name)
	local co = CoroutineCache[func];
	if not co then
		-- Coroutines are typically not designed to be re-run, so wrap the func in a permanent loop so it can simply be restarted
		-- when retrieved from the cache instead of needing to be re-created each time it is used
		co = coroutine.create(function() while true do func(); coroutine.yield(false); end end);
		-- app.PrintDebug("CO:New",name,co,func)
		CoroutineCache[func] = co;
	-- else app.PrintDebug("CO:Cache",name,co,func)
	end
	-- Mark this name/coroutine until the coroutine is returned
	CoroutineCache[name] = true;
	CoroutineCache[co] = name;
	return co;
end
-- Allows freeing a coroutine and the respective name used to create it initially
local function ReturnCoroutine(co)
	local name = CoroutineCache[co];
	-- app.PrintDebug("CO:Return",name,co)
	CoroutineCache[name] = nil;
	CoroutineCache[co] = nil;
end
local function GetPusher()
	local pusher = PushCache[1];
	if pusher then
		tremove(PushCache, 1);
		-- app.PrintDebug("PUSH:Cache",#PushCache)
		return pusher;
	end

	-- app.PrintDebug("PUSH:New",#PushCache + 1)
	local function pushfunc(co)
			-- Check the status of the coroutine
			-- app.PrintDebug("PUSH:Run",pushfunc,"=>",co)
			if co and coroutine.status(co) ~= "dead" then
				local ok, err = coroutine.resume(co);
				if ok then
					if err == false then
						-- This means the coroutine signals completion by returning false
						-- app.PrintDebug("PUSH.Run.Complete",co)
					else
						-- This means more work is required.
						-- app.PrintDebug("PUSH.Run.Yielded",co)
						return true;
					end
				else
					app.PrintDebug("PUSH.Run.Error",co)
					-- Throw the error. Returning nothing is the same as canceling the work.
					-- local instanceTrace = debugstack(instance);
					error(err,2);
					-- print(debugstack(instance));
					-- print(err);
					-- app.report();
				end
			end
			-- After the pusher is done running the coroutine, it can return itself to the cache
			-- app.PrintDebug("PUSH:Return",pushfunc,"=>",#PushCache + 1)
			tinsert(PushCache, pushfunc);
			-- Then grab the corresponding Name of this coroutine based on the coroutine cache
			-- and swap in the coroutine for the Name, and un-flag the Name from the NameCache
			ReturnCoroutine(co);
		end;
	return pushfunc;
end
-- Allows running a function on a coroutine until it completes
local function StartCoroutine(name, func, delay)
	if not func or CoroutineCache[name] then return; end
	-- app.PrintDebug("CO:Prep",name);

	local co = GetCoroutine(func, name);
	local pusher = GetPusher();

	if delay and delay > 0 then
		-- app.PrintDebug("CO:Delay",delay,name,pusher,co);
		C_Timer_After(delay, function() Push(co, name, pusher) end);
	else
		-- app.PrintDebug("CO:Start",name,pusher,co);
		Push(co, name, pusher);
	end
end
app.StartCoroutine = StartCoroutine;

-- Iterative Function Runner
-- Creates a Function Runner which can execute a sequence of Functions on a set iteration per frame update
local function CreateRunner(name)
	local FunctionQueue, ParameterBucketQueue, ParameterSingleQueue, Config = {}, {}, {}, { PerFrame = 1 };
	local Name = "Runner:"..name;
	local QueueIndex = 1;
	local Pushed;
	local function SetPerFrame(count)
		Config.PerFrame = math_max(1, tonumber(count) or 1);
		-- app.PrintDebug("FR.PerFrame."..name,Config.PerFrame)
	end
	local function Reset()
		Config.PerFrame = 1;
		-- when done with all functions in the queue, reset the queue index and clear the queues of data
		QueueIndex = 1;
		wipe(FunctionQueue);
		wipe(ParameterBucketQueue);
		wipe(ParameterSingleQueue);
		-- app.PrintDebug("FR:Reset."..name)
	end

	-- Static coroutine for the Runner which runs one loop each time the Runner is called, and yields on the Stack
	local RunnerCoroutine = coroutine.create(function()
		while true do
			local i, perFrame = 1, Config.PerFrame;
			local params;
			local func = FunctionQueue[i];
			-- app.PrintDebug("FRC.Running."..name)
			while func do
				perFrame = perFrame - 1;
				params = ParameterBucketQueue[i];
				if params then
					-- app.PrintDebug("FRC.Run.N."..name,i,unpack(params))
					func(unpack(params));
				else
					-- app.PrintDebug("FRC.Run.1."..name,i,ParameterSingleQueue[i])
					func(ParameterSingleQueue[i]);
				end
				-- app.PrintDebug("FRC.Done."..name,i)
				if perFrame <= 0 then
					-- app.PrintDebug("FRC.Yield."..name)
					coroutine.yield();
					perFrame = Config.PerFrame;
				end
				i = i + 1;
				func = FunctionQueue[i];
			end
			-- Run the OnEnd function if it exists
			local OnEnd = FunctionQueue[0];
			if OnEnd then
				-- app.PrintDebug("FRC.End."..name,#FunctionQueue)
				OnEnd();
			end
			Reset();
			Pushed = nil;
			-- Yield false to kick the StackRun off the Stack to stop calling this coroutine since it is complete until Run is called again
			coroutine.yield(false);
		end
	end);

	-- Static Function that handles the Stack-Run for the Runner-Coroutine
	local function StackRun()
		-- app.PrintDebug("Stack.Run",Name)
		local ok, err = coroutine.resume(RunnerCoroutine);
		if ok then
			if err == false then
				-- app.PrintDebug("Stack.Run.Complete",Name)
				return;			-- This means the coroutine signals completion by returning false
			else
				-- app.PrintDebug("Stack.Run.Yielded",Name)
				return true;	-- This means more work is required.
			end
		else
			app.PrintDebug("Stack.Run.Error",Name)
			-- Throw the error. Returning nothing is the same as canceling the work.
			-- local instanceTrace = debugstack(instance);
			error(err,2);
			-- print(debugstack(instance));
			-- print(err);
			-- app.report();
		end
	end

	-- Provides a utility which will process a given number of functions each frame in a Queue
	local Runner = {
		-- Adds a function to be run with any necessary parameters
		Run = function(func, ...)
			if type(func) ~= "function" then
				error("Must be a 'function' type!")
			end
			FunctionQueue[QueueIndex] = func;
			-- app.PrintDebug("FR.Add."..name,QueueIndex,...)
			local arrs = select("#", ...);
			if arrs == 1 then
				ParameterSingleQueue[QueueIndex] = ...;
			elseif arrs > 1 then
				ParameterBucketQueue[QueueIndex] = { ... };
			end
			QueueIndex = QueueIndex + 1;
			-- Only push the coroutine onto the Stack once until it is completed
			if Pushed then return; end
			Pushed = true;
			Push(nil, Name, StackRun);
		end,
		-- Set a function to be run once the queue is empty. This function takes no parameters.
		OnEnd = function(func)
			FunctionQueue[0] = func;
		end,
	};
	-- Defines how many functions will be executed per frame. Executes via the Runner when encountered in the Queue, unless specified as 'instant'
	Runner.SetPerFrame = function(count, instant)
		if instant then
			SetPerFrame(count);
		else
			Runner.Run(SetPerFrame, count);
		end
	end
	Runner.Reset = Reset; -- for testing

	return Runner;
end
app.CreateRunner = CreateRunner;