--[[
Mesecons uses something it calls an ActionQueue.

The ActionQueue holds functions and actions.
Functions are added on load time with a specified name.
Actions are preserved over server restarts.

Each action consists of a position, the name of an added function to be called,
the params that should be used in this function call (additionally to the pos),
the time after which it should be executed, an optional overwritecheck and a
priority.

If time = 0, the action will be executed in the next globalstep, otherwise the
earliest globalstep when it will be executed is the after next globalstep.

It is guaranteed, that for two actions ac1, ac2 where ac1 ~= ac2,
ac1.time == ac2.time, ac1.priority == ac2.priority and ac1 was added earlier
than ac2, ac1 will be executed before ac2 (but in the same globalstep).

Note: Do not pass references in params, as they can not be preserved.

Also note: Some of the guarantees here might be dropped at some time.
]]


-- localize for speed
local queue = mesecon.queue

queue.actions = {} -- contains all ActionQueue actions

function queue:add_function(name, func)
	self.funcs[name] = func
end

-- If add_action with twice the same overwritecheck and same position are called, the first one is overwritten
-- use overwritecheck nil to never overwrite, but just add the event to the queue
-- priority specifies the order actions are executed within one globalstep, highest first
-- should be between 0 and 1
function queue:add_action(pos, func, params, time, overwritecheck, priority)
	-- Create Action Table:
	time = time or 0 -- time <= 0 --> execute, time > 0 --> wait time until execution
	priority = priority or 1
	local action = {
		pos = mesecon.tablecopy(pos),
		func = func,
		params = mesecon.tablecopy(params or {}),
		time = time,
		owcheck = (overwritecheck and mesecon.tablecopy(overwritecheck)) or nil,
		priority = priority
	}

	 -- check if old action has to be overwritten / removed:
	if overwritecheck then
		for i, ac in ipairs(self.actions) do
			if vector.equals(pos, ac.pos)
					and mesecon.cmpAny(overwritecheck, ac.owcheck) then
				-- remove the old action
				table.remove(self.actions, i)
				break
			end
		end
	end

	table.insert(self.actions, action)
end

-- execute the stored functions on a globalstep
-- if however, the pos of a function is not loaded (get_node_or_nil == nil), do NOT execute the function
-- this makes sure that resuming mesecons circuits when restarting minetest works fine (hm, where do we do this?)
-- However, even that does not work in some cases, that's why we delay the time the globalsteps
-- start to be execute by 4 seconds

local function globalstep_func(dtime)
	local actions = queue.actions
	-- split into two categories:
	-- actions_now: actions to execute now
	-- queue.actions: actions to execute later
	local actions_now = {}
	queue.actions = {}

	for _, ac in ipairs(actions) do
		if ac.time > 0 then
			-- action ac is to be executed later
			-- ~> insert into queue.actions
			ac.time = ac.time - dtime
			table.insert(queue.actions, ac)
		else
			-- action ac is to be executed now
			-- ~> insert into actions_now
			table.insert(actions_now, ac)
		end
	end

	-- stable-sort the executed actions after their priority
	-- some constructions might depend on the execution order, hence we first
	-- execute the actions that had a lower index in actions_now
	local old_action_order = {}
	for i, ac in ipairs(actions_now) do
		old_action_order[ac] = i
	end
	table.sort(actions_now, function(ac1, ac2)
		if ac1.priority ~= ac2.priority then
			return ac1.priority > ac2.priority
		else
			return old_action_order[ac1] < old_action_order[ac2]
		end
	end)

	-- execute highest priorities first, until all are executed
	for _, ac in ipairs(actions_now) do
		queue:execute(ac)
	end
end

-- delay the time the globalsteps start to be execute by 4 seconds
do
	local m_time = 0
	local resumetime = mesecon.setting("resumetime", 4)
	local globalstep_func_index = #minetest.registered_globalsteps + 1

	minetest.register_globalstep(function(dtime)
		m_time = m_time + dtime
		-- don't even try if server has not been running for XY seconds; resumetime = time to wait
		-- after starting the server before processing the ActionQueue, don't set this too low
		if m_time < resumetime then
			return
		end
		-- replace this globalstep function
		minetest.registered_globalsteps[globalstep_func_index] = globalstep_func
	end)
end

function queue:execute(action)
	-- ignore if action queue function name doesn't exist,
	-- (e.g. in case the action queue savegame was written by an old mesecons version)
	if self.funcs[action.func] then
		self.funcs[action.func](action.pos, unpack(action.params))
	end
end


-- Store and read the ActionQueue to / from a file
-- so that upcoming actions are remembered when the game
-- is restarted
queue.actions = mesecon.file2table("mesecon_actionqueue")

minetest.register_on_shutdown(function()
	mesecon.table2file("mesecon_actionqueue", queue.actions)
end)
