
-- time in seconds between executor calls
local executor_dtime = 0.1

function epic.execute_player_state(playername, state, recursion_depth)
	local pos = state.ip
	local player = minetest.get_player_by_name(playername)

	if recursion_depth and recursion_depth > 100 then
		-- abort execution if the recursion limit is reached
		epic.state[playername] = nil
		minetest.log("warn", "[epic][executor] max recursion depth exceeded at: " .. minetest.pos_to_string(pos))
		return
	end

	if not player then
		minetest.log("warn", "[epic][executor] player not found, aborting: " .. playername)
		return
	end

	epic.debug("[executor] execute_player_state(" .. playername .. "))")

	if not pos then
		-- invalid state
		epic.state[playername] = nil
		minetest.log("warn", "[epic][executor] invalid opcode encountered, state-dump: " .. dump(state))
		return
	end

	local node = epic.get_node(pos)

	if not epic.is_epic(node) then
		-- no more instructions in this branch
		epic.debug("[executor] no more instructions in this branch @ " ..
		minetest.pos_to_string(pos) .. " node: " .. node.name)

		if #state.stack > 0 then
			-- pop stack
			state.ip = table.remove(state.stack, #state.stack)
			state.initialized = false
			state.step_data = {}
			epic.debug("[executor] pop stack result: " .. minetest.pos_to_string(state.ip))

			epic.execute_player_state(playername, state)
		else
			-- done
			epic.state[playername] = nil
			epic.run_hook("on_epic_exit", {playername, state})
		end

		return
	end

	local result_next = false
	local result_next_pos = nil
	local abort_flag = state.abort

	local ctx = {
		-- next step
		next = function(cpos)
			result_next = true
			result_next_pos = cpos
		end,
		-- abort epic with given reason
		abort = function(reason)
			abort_flag = reason or "ctx.abort"
		end,
		-- call another epic block
		call = function(cpos)
			-- push next ip
			local next_pos = epic.get_next_pos(pos)
			local next_node = epic.get_node(next_pos)
			if epic.is_epic(next_node) then
				-- this branch has more instructions, push the next onto the stack
				table.insert(state.stack, next_pos)
			end
			result_next = true
			result_next_pos = cpos
		end,
		-- set the new timeout
		settimeout = function(seconds)
			state.time = seconds
		end,
		data = state.data,
		step_data = state.step_data
	}

	local nodedef = minetest.registered_nodes[node.name]
	local epicdef = nodedef.epic
	local meta = minetest.get_meta(pos)

	if not state.initialized then
		state.initialized = true

		epic.run_hook("on_before_node_enter", { pos, player, ctx })
		if epicdef.on_enter then
			epicdef.on_enter(pos, meta, player, ctx)
		end
	end

	epic.run_hook("on_before_node_check", { pos, player, ctx })
	if epicdef.on_check then
		epicdef.on_check(pos, meta, player, ctx)
	end

	if state.time then
		state.time = state.time - executor_dtime
		if state.time < 0 then
			abort_flag = "epic_timeout"
		end
	end

	if abort_flag or result_next then
		epic.run_hook("on_before_node_exit", { pos, player, ctx })
		if epicdef.on_exit then
			epicdef.on_exit(pos, meta, player, ctx)
		end
	end

	if abort_flag then
		epic.state[playername] = nil
		epic.run_hook("on_epic_abort", { playername, state, abort_flag })
		return
	end

	if result_next then
		local next_pos = result_next_pos or epic.get_next_pos(pos)
		state.ip = next_pos
		state.initialized = false
		state.step_data = {}

		-- set the recursion depth if not set
		recursion_depth = recursion_depth or 1
		epic.execute_player_state(playername, state, recursion_depth + 1)
	end

end


local function executor()
	local t0 = minetest.get_us_time()
	for playername, state in pairs(epic.state) do
		if not minetest.get_player_by_name(playername) then
			-- player is gone, clear state
			epic.state[playername] = nil
		else
			-- player is still online
			epic.execute_player_state(playername, state)
		end
	end
	local t1 = minetest.get_us_time()
	local stats = {
		time = t1-t0
	}
	epic.run_hook("globalstep_stats", { stats })

	-- restart execution
	minetest.after(executor_dtime, executor)
end

-- initial delay
minetest.after(1.0, executor)

-- cleans up the remaining state, if any
local function cleanup_state(player, state)
	local pos = state.ip
	if pos then
		local node = epic.get_node(state.ip)
		local nodedef = minetest.registered_nodes[node.name]
		local epicdef = nodedef.epic
		if type(epicdef.on_exit) == "function" then
			local meta = minetest.get_meta(pos)
			epicdef.on_exit(pos, meta, player, state)
		end
	end
end

-- abort player helper function
-- called on leave and shutdown
local function abort_player(player, timed_out)
	local playername = player:get_player_name()
	local state = epic.state[playername]
	if state then
		local reason
		if timed_out then
			reason = "leave_timed_out"
		else
			reason = "leave"
		end
		if epic.log_executor then
			minetest.log("action", "[epic] player left the game: " .. playername)
		end

		cleanup_state(player, state)
		epic.state[playername] = nil
		epic.run_hook("on_epic_abort", { playername, state, reason })
	end
end

-- abort epic on leave
-- savepoints are not touched here
minetest.register_on_leaveplayer(abort_player)

minetest.register_on_shutdown(function()
	minetest.log("action", "[epic] shutdown detected, aborting all epics")
	for _, player in ipairs(minetest.get_connected_players()) do
		-- abort every players epics
		abort_player(player, false)
	end
end)

minetest.register_on_dieplayer(function(player)
	local playername = player:get_player_name()
	local state = epic.state[playername]
	if state then
		if epic.log_executor then
			minetest.log("action", "[epic] player died: " .. playername)
		end

		cleanup_state(player, state)
		epic.state[playername] = nil
		epic.run_hook("on_epic_abort", { playername, state, "died" })
	end
end)
