-- Internal.lua - The core of mesecons
--
-- For more practical developer resources see http://mesecons.net/developers.php
--
-- Function overview
-- mesecon.get_effector(nodename)	--> Returns the mesecons.effector -specifictation in the nodedef by the nodename
-- mesecon.get_receptor(nodename)	--> Returns the mesecons.receptor -specifictation in the nodedef by the nodename
-- mesecon.get_conductor(nodename)	--> Returns the mesecons.conductor-specifictation in the nodedef by the nodename
-- mesecon.get_any_inputrules (node)	--> Returns the rules of a node if it is a conductor or an effector
-- mesecon.get_any_outputrules (node)	--> Returns the rules of a node if it is a conductor or a receptor

-- RECEPTORS
-- mesecon.is_receptor(nodename)	--> Returns true if nodename is a receptor
-- mesecon.is_receptor_on(nodename)	--> Returns true if nodename is an receptor with state = mesecon.state.on
-- mesecon.is_receptor_off(nodename)	--> Returns true if nodename is an receptor with state = mesecon.state.off
-- mesecon.receptor_get_rules(node)	--> Returns the rules of the receptor (mesecon.rules.default if none specified)

-- EFFECTORS
-- mesecon.is_effector(nodename)	--> Returns true if nodename is an effector
-- mesecon.is_effector_on(nodename)	--> Returns true if nodename is an effector with nodedef.mesecons.effector.action_off
-- mesecon.is_effector_off(nodename)	--> Returns true if nodename is an effector with nodedef.mesecons.effector.action_on
-- mesecon.effector_get_rules(node)	--> Returns the input rules of the effector (mesecon.rules.default if none specified)

-- SIGNALS
-- mesecon.activate(pos, node, depth)				--> Activates   the effector node at the specific pos (calls nodedef.mesecons.effector.action_on), higher depths are executed later
-- mesecon.deactivate(pos, node, depth)				--> Deactivates the effector node at the specific pos (calls nodedef.mesecons.effector.action_off), higher depths are executed later
-- mesecon.changesignal(pos, node, rulename, newstate, depth)	--> Changes     the effector node at the specific pos (calls nodedef.mesecons.effector.action_change), higher depths are executed later

-- CONDUCTORS
-- mesecon.is_conductor(nodename)	--> Returns true if nodename is a conductor
-- mesecon.is_conductor_on(node		--> Returns true if node is a conductor with state = mesecon.state.on
-- mesecon.is_conductor_off(node)	--> Returns true if node is a conductor with state = mesecon.state.off
-- mesecon.get_conductor_on(node_off)	--> Returns the onstate  nodename of the conductor
-- mesecon.get_conductor_off(node_on)	--> Returns the offstate nodename of the conductor
-- mesecon.conductor_get_rules(node)	--> Returns the input+output rules of a conductor (mesecon.rules.default if none specified)

-- HIGH-LEVEL Internals
-- mesecon.is_power_on(pos)				--> Returns true if pos emits power in any way
-- mesecon.is_power_off(pos)				--> Returns true if pos does not emit power in any way
-- mesecon.is_powered(pos)				--> Returns true if pos is powered by a receptor or a conductor

-- RULES ROTATION helpers
-- mesecon.rotate_rules_right(rules)
-- mesecon.rotate_rules_left(rules)
-- mesecon.rotate_rules_up(rules)
-- mesecon.rotate_rules_down(rules)
-- These functions return rules that have been rotated in the specific direction

-- See fifo_queue.lua for documentation.
mesecon.fifo_queue = dofile(minetest.get_modpath("mesecons").."/fifo_queue.lua")

-- General
function mesecon.get_effector(nodename)
	local def = minetest.registered_nodes[nodename]
	return def and def.mesecons and def.mesecons.effector
end

function mesecon.get_receptor(nodename)
	local def = minetest.registered_nodes[nodename]
	return def and def.mesecons and def.mesecons.receptor
end

function mesecon.get_conductor(nodename)
	local def = minetest.registered_nodes[nodename]
	return def and def.mesecons and def.mesecons.conductor
end

function mesecon.get_any_outputrules(node)
	if not node then return nil end

	if mesecon.is_conductor(node.name) then
		return mesecon.conductor_get_rules(node)
	elseif mesecon.is_receptor(node.name) then
		return mesecon.receptor_get_rules(node)
	end
end

function mesecon.get_any_inputrules(node)
	if not node then return nil end

	if mesecon.is_conductor(node.name) then
		return mesecon.conductor_get_rules(node)
	elseif mesecon.is_effector(node.name) then
		return mesecon.effector_get_rules(node)
	end
end

function mesecon.get_any_rules(node)
	return mesecon.merge_rule_sets(mesecon.get_any_inputrules(node),
		mesecon.get_any_outputrules(node))
end

-- Receptors
-- Nodes that can power mesecons
function mesecon.is_receptor_on(nodename)
	local receptor = mesecon.get_receptor(nodename)
	if receptor and receptor.state == mesecon.state.on then
		return true
	end
	return false
end

function mesecon.is_receptor_off(nodename)
	local receptor = mesecon.get_receptor(nodename)
	if receptor and receptor.state == mesecon.state.off then
		return true
	end
	return false
end

function mesecon.is_receptor(nodename)
	local receptor = mesecon.get_receptor(nodename)
	if receptor then
		return true
	end
	return false
end

function mesecon.receptor_get_rules(node)
	local receptor = mesecon.get_receptor(node.name)
	if receptor then
		local rules = receptor.rules
		if type(rules) == 'function' then
			return rules(node)
		elseif rules then
			return rules
		end
	end

	return mesecon.rules.default
end

-- Effectors
-- Nodes that can be powered by mesecons
function mesecon.is_effector_on(nodename)
	local effector = mesecon.get_effector(nodename)
	if effector and effector.action_off then
		return true
	end
	return false
end

function mesecon.is_effector_off(nodename)
	local effector = mesecon.get_effector(nodename)
	if effector and effector.action_on then
		return true
	end
	return false
end

function mesecon.is_effector(nodename)
	local effector = mesecon.get_effector(nodename)
	if effector then
		return true
	end
	return false
end

function mesecon.effector_get_rules(node)
	local effector = mesecon.get_effector(node.name)
	if effector then
		local rules = effector.rules
		if type(rules) == 'function' then
			return rules(node)
		elseif rules then
			return rules
		end
	end
	return mesecon.rules.default
end

-- #######################
-- # Signals (effectors) #
-- #######################

-- Activation:
mesecon.queue:add_function("activate", function (pos, rulename)
	local node = mesecon.get_node_force(pos)
	if not node then return end

	local effector = mesecon.get_effector(node.name)

	if effector and effector.action_on then
		effector.action_on(pos, node, rulename)
	end
end)

function mesecon.activate(pos, node, rulename, depth)
	if rulename == nil then
		for _,rule in ipairs(mesecon.effector_get_rules(node)) do
			mesecon.activate(pos, node, rule, depth + 1)
		end
		return
	end
	mesecon.queue:add_action(pos, "activate", {rulename}, nil, rulename, 1 / depth)
end


-- Deactivation
mesecon.queue:add_function("deactivate", function (pos, rulename)
	local node = mesecon.get_node_force(pos)
	if not node then return end

	local effector = mesecon.get_effector(node.name)

	if effector and effector.action_off then
		effector.action_off(pos, node, rulename)
	end
end)

function mesecon.deactivate(pos, node, rulename, depth)
	if rulename == nil then
		for _,rule in ipairs(mesecon.effector_get_rules(node)) do
			mesecon.deactivate(pos, node, rule, depth + 1)
		end
		return
	end
	mesecon.queue:add_action(pos, "deactivate", {rulename}, nil, rulename, 1 / depth)
end


-- Change
mesecon.queue:add_function("change", function (pos, rulename, changetype)
	local node = mesecon.get_node_force(pos)
	if not node then return end

	local effector = mesecon.get_effector(node.name)

	if effector and effector.action_change then
		effector.action_change(pos, node, rulename, changetype)
	end
end)

function mesecon.changesignal(pos, node, rulename, newstate, depth)
	if rulename == nil then
		for _,rule in ipairs(mesecon.effector_get_rules(node)) do
			mesecon.changesignal(pos, node, rule, newstate, depth + 1)
		end
		return
	end

	-- Include "change" in overwritecheck so that it cannot be overwritten
	-- by "active" / "deactivate" that will be called upon the node at the same time.
	local overwritecheck = {"change", rulename}
	mesecon.queue:add_action(pos, "change", {rulename, newstate}, nil, overwritecheck, 1 / depth)
end

-- Conductors

function mesecon.is_conductor_on(node, rulename)
	if not node then return false end

	local conductor = mesecon.get_conductor(node.name)
	if conductor then
		if conductor.state then
			return conductor.state == mesecon.state.on
		end
		if conductor.states then
			if not rulename then
				return mesecon.getstate(node.name, conductor.states) ~= 1
			end
			local bit = mesecon.rule2bit(rulename, mesecon.conductor_get_rules(node))
			local binstate = mesecon.getbinstate(node.name, conductor.states)
			return mesecon.get_bit(binstate, bit)
		end
	end

	return false
end

function mesecon.is_conductor_off(node, rulename)
	if not node then return false end

	local conductor = mesecon.get_conductor(node.name)
	if conductor then
		if conductor.state then
			return conductor.state == mesecon.state.off
		end
		if conductor.states then
			if not rulename then
				return mesecon.getstate(node.name, conductor.states) == 1
			end
			local bit = mesecon.rule2bit(rulename, mesecon.conductor_get_rules(node))
			local binstate = mesecon.getbinstate(node.name, conductor.states)
			return not mesecon.get_bit(binstate, bit)
		end
	end

	return false
end

function mesecon.is_conductor(nodename)
	local conductor = mesecon.get_conductor(nodename)
	if conductor then
		return true
	end
	return false
end

function mesecon.get_conductor_on(node_off, rulename)
	local conductor = mesecon.get_conductor(node_off.name)
	if conductor then
		if conductor.onstate then
			return conductor.onstate
		end
		if conductor.states then
			local bit = mesecon.rule2bit(rulename, mesecon.conductor_get_rules(node_off))
			local binstate = mesecon.getbinstate(node_off.name, conductor.states)
			binstate = mesecon.set_bit(binstate, bit, "1")
			return conductor.states[tonumber(binstate,2)+1]
		end
	end
	return nil
end

function mesecon.get_conductor_off(node_on, rulename)
	local conductor = mesecon.get_conductor(node_on.name)
	if conductor then
		if conductor.offstate then
			return conductor.offstate
		end
		if conductor.states then
			local bit = mesecon.rule2bit(rulename, mesecon.conductor_get_rules(node_on))
			local binstate = mesecon.getbinstate(node_on.name, conductor.states)
			binstate = mesecon.set_bit(binstate, bit, "0")
			return conductor.states[tonumber(binstate,2)+1]
		end
	end
	return nil
end

function mesecon.conductor_get_rules(node)
	local conductor = mesecon.get_conductor(node.name)
	if conductor then
		local rules = conductor.rules
		if type(rules) == 'function' then
			return rules(node)
		elseif rules then
			return rules
		end
	end
	return mesecon.rules.default
end

-- some more general high-level stuff

function mesecon.is_power_on(pos, rulename)
	local node = mesecon.get_node_force(pos)
	if node and (mesecon.is_conductor_on(node, rulename) or mesecon.is_receptor_on(node.name)) then
		return true
	end
	return false
end

function mesecon.is_power_off(pos, rulename)
	local node = mesecon.get_node_force(pos)
	if node and (mesecon.is_conductor_off(node, rulename) or mesecon.is_receptor_off(node.name)) then
		return true
	end
	return false
end

-- The set of conductor states which require light updates when they change.
local light_update_conductors

-- Calculate the contents of the above set if they have not been calculated.
local function find_light_update_conductors()
	-- The expensive calculation is only done the first time.
	if light_update_conductors then return end

	light_update_conductors = {}

	-- Find conductors whose lighting characteristics change depending on their state.
	local checked = {}
	for name, def in pairs(minetest.registered_nodes) do
		local conductor = mesecon.get_conductor(name)
		if conductor and not checked[name] then
			-- Find the other states of the conductor besides the current one.
			local other_states
			if conductor.onstate then
				other_states = {conductor.onstate}
			elseif conductor.offstate then
				other_states = {conductor.offstate}
			else
				other_states = conductor.states
			end

			-- Check the conductor. Other states are marked as checked.
			for _, other_state in ipairs(other_states) do
				local other_def = minetest.registered_nodes[other_state]
				if (def.paramtype == "light") ~= (other_def.paramtype == "light")
				or def.sunlight_propagates ~= other_def.sunlight_propagates
				or def.light_source ~= other_def.light_source then
					-- The light characteristics change depending on the state.
					-- The states are added to the set.
					light_update_conductors[name] = true
					for _, other_state in ipairs(other_states) do
						light_update_conductors[other_state] = true
						checked[other_state] = true
					end
					break
				end
				checked[other_state] = true
			end
		end
	end
end

-- Turn off an equipotential section starting at `pos`, which outputs in the direction of `link`.
-- Breadth-first search. Map is abstracted away in a voxelmanip.
-- Follow all all conductor paths replacing conductors that were already
-- looked at, activating / changing all effectors along the way.
function mesecon.turnon(pos, link)
	find_light_update_conductors()

	local frontiers = mesecon.fifo_queue.new()
	frontiers:add({pos = pos, link = link})
	local pos_can_be_skipped = {}

	local depth = 1
	for f in frontiers:iter() do
		local node = mesecon.get_node_force(f.pos)

		if not node then
			-- Area does not exist; do nothing
			pos_can_be_skipped[minetest.hash_node_position(f.pos)] = true
		elseif mesecon.is_conductor(node.name) then
			local rules = mesecon.conductor_get_rules(node)

			if mesecon.is_conductor_off(node, f.link) then
				-- Call turnon on neighbors
				for _, r in ipairs(mesecon.rule2meta(f.link, rules)) do
					local np = vector.add(f.pos, r)
					if not pos_can_be_skipped[minetest.hash_node_position(np)] then
						for _, l in ipairs(mesecon.rules_link_rule_all(f.pos, r)) do
							frontiers:add({pos = np, link = l})
						end
					end
				end

				mesecon.swap_node_force(f.pos, mesecon.get_conductor_on(node, f.link), light_update_conductors[node.name] ~= nil)
			end

			-- Only conductors with flat rules can be reliably skipped later
			if not rules[1] or rules[1].x then
				pos_can_be_skipped[minetest.hash_node_position(f.pos)] = true
			end
		elseif mesecon.is_effector(node.name) then
			mesecon.changesignal(f.pos, node, f.link, mesecon.state.on, depth)
			if mesecon.is_effector_off(node.name) then
				mesecon.activate(f.pos, node, f.link, depth)
			end
		else
			pos_can_be_skipped[minetest.hash_node_position(f.pos)] = true
		end
		depth = depth + 1
	end
end

-- Turn on an equipotential section starting at `pos`, which outputs in the direction of `link`.
-- Breadth-first search. Map is abstracted away in a voxelmanip.
-- Follow all all conductor paths replacing conductors that were already
-- looked at, deactivating / changing all effectors along the way.
-- In case an onstate receptor is discovered, abort the process by returning false, which will
-- cause `receptor_off` to discard all changes made in the voxelmanip.
-- Contrary to turnon, turnoff has to cache all change and deactivate signals so that they will only
-- be called in the very end when we can be sure that no conductor was found along the path.
--
-- Signal table entry structure:
-- {
--	pos = position of effector,
--	node = node descriptor (name, param1 and param2),
--	link = link the effector is connected to,
--	depth = indicates order in which signals wire fired, higher is later
-- }
function mesecon.turnoff(pos, link)
	find_light_update_conductors()

	local frontiers = mesecon.fifo_queue.new()
	frontiers:add({pos = pos, link = link})
	local signals = {}
	local pos_can_be_skipped = {}

	local depth = 1
	for f in frontiers:iter() do
		local node = mesecon.get_node_force(f.pos)

		if not node then
			-- Area does not exist; do nothing
			pos_can_be_skipped[minetest.hash_node_position(f.pos)] = true
		elseif mesecon.is_conductor(node.name) then
			local rules = mesecon.conductor_get_rules(node)

			if mesecon.is_conductor_on(node, f.link) then
				for _, r in ipairs(mesecon.rule2meta(f.link, rules)) do
					local np = vector.add(f.pos, r)

					if not pos_can_be_skipped[minetest.hash_node_position(np)] then
						-- Check if an onstate receptor is connected. If that is the case,
						-- abort this turnoff process by returning false. `receptor_off` will
						-- discard all the changes that we made in the voxelmanip:
						if mesecon.rules_link_rule_all_inverted(f.pos, r)[1] then
							if mesecon.is_receptor_on(mesecon.get_node_force(np).name) then
								return false
							end
						end

						-- Call turnoff on neighbors
						for _, l in ipairs(mesecon.rules_link_rule_all(f.pos, r)) do
							frontiers:add({pos = np, link = l})
						end
					end
				end

				mesecon.swap_node_force(f.pos, mesecon.get_conductor_off(node, f.link), light_update_conductors[node.name] ~= nil)
			end

			-- Only conductors with flat rules can be reliably skipped later
			if not rules[1] or rules[1].x then
				pos_can_be_skipped[minetest.hash_node_position(f.pos)] = true
			end
		elseif mesecon.is_effector(node.name) then
			table.insert(signals, {
				pos = f.pos,
				node = node,
				link = f.link,
				depth = depth
			})
		else
			pos_can_be_skipped[minetest.hash_node_position(f.pos)] = true
		end
		depth = depth + 1
	end

	for _, sig in ipairs(signals) do
		-- If sig.depth is 1, it has not yet been checked that the power source is actually off.
		if sig.depth > 1 or not mesecon.is_powered(sig.pos, sig.link) then
			mesecon.changesignal(sig.pos, sig.node, sig.link, mesecon.state.off, sig.depth)
			if mesecon.is_effector_on(sig.node.name) and not mesecon.is_powered(sig.pos) then
				mesecon.deactivate(sig.pos, sig.node, sig.link, sig.depth)
			end
		end
	end

	return true
end

-- Get all linking inputrules of inputnode (effector or conductor) that is connected to
-- outputnode (receptor or conductor) at position `output` and has an output in direction `rule`
function mesecon.rules_link_rule_all(output, rule)
	local input = vector.add(output, rule)
	local inputnode = mesecon.get_node_force(input)
	local inputrules = mesecon.get_any_inputrules(inputnode)
	if not inputrules then
		return {}
	end
	local rules = {}

	for _, inputrule in ipairs(mesecon.flattenrules(inputrules)) do
		-- Check if input accepts from output
		if  vector.equals(vector.add(input, inputrule), output) then
			table.insert(rules, inputrule)
		end
	end

	return rules
end

-- Get all linking outputnodes of outputnode (receptor or conductor) that is connected to
-- inputnode (effector or conductor) at position `input` and has an input in direction `rule`
function mesecon.rules_link_rule_all_inverted(input, rule)
	local output = vector.add(input, rule)
	local outputnode = mesecon.get_node_force(output)
	local outputrules = mesecon.get_any_outputrules(outputnode)
	if not outputrules then
		return {}
	end
	local rules = {}

	for _, outputrule in ipairs(mesecon.flattenrules(outputrules)) do
		if  vector.equals(vector.add(output, outputrule), input) then
			table.insert(rules, mesecon.invertRule(outputrule))
		end
	end
	return rules
end

function mesecon.is_powered(pos, rule)
	local node = mesecon.get_node_force(pos)
	local rules = mesecon.get_any_inputrules(node)
	if not rules then return false end

	-- List of nodes that send out power to pos
	local sourcepos = {}

	if not rule then
		for _, rule in ipairs(mesecon.flattenrules(rules)) do
			local rulenames = mesecon.rules_link_rule_all_inverted(pos, rule)
			for _, rname in ipairs(rulenames) do
				local np = vector.add(pos, rname)
				local nn = mesecon.get_node_force(np)

				if (mesecon.is_conductor_on(nn, mesecon.invertRule(rname))
				or mesecon.is_receptor_on(nn.name)) then
					table.insert(sourcepos, np)
				end
			end
		end
	else
		local rulenames = mesecon.rules_link_rule_all_inverted(pos, rule)
		for _, rname in ipairs(rulenames) do
			local np = vector.add(pos, rname)
			local nn = mesecon.get_node_force(np)
			if (mesecon.is_conductor_on (nn, mesecon.invertRule(rname))
			or mesecon.is_receptor_on (nn.name)) then
				table.insert(sourcepos, np)
			end
		end
	end

	-- Return FALSE if not powered, return list of sources if is powered
	if (#sourcepos == 0) then return false
	else return sourcepos end
end
