--[[

	SmartLine
	=========

	Copyright (C) 2017-2020 Joachim Stolberg

	AGPL v3
	See LICENSE.txt for more information

	controller.lua:

	REPLACED BY SMARTLINE CONTROLLER2 !!!
]]--


local NUM_RULES = 10

local mail_exists = minetest.get_modpath("mail") and mail ~= nil

local sHELP = [[SmartLine Controller Help

Control other nodes by means of rules, according to:
    IF <cond1> and/or <cond2> THEN <action>

These rules allow to execute actions based on conditions.
Examples for conditions are:
 - the Player Detector detects a player
 - a button is pressed
 - a node state is fault, blocked, standby,...
 - a timer is expired

Actions are:
 - switch on/off tubelib nodes, like lamps, door blocks, machines
 - send mail/chat messages to the owner
 - output a text message to the display
 - set timer variables
 - set/reset flag variables

Variables and timers:
 - 8 flags (set/reset) can be used to store conditions
    for later use.
 - Action flags (one flag for each rule, set when action is executed)
    The flag can be used as condition for subsequent rules.
 - 8 timers (resolution in seconds) can be use
   for delayed actions.

The controller executes all rules once per second.
Independent how long the input condition stays 'true',
the corresponding action will be triggered only once.
The condition has to become false and then true again, to
re-trigger/execute the action again.

The 'label' has no function. It is only used
to give rules a name.

Edit command examples:
 - 'x 1 8'  exchange rows 1 with row 8
 - 'c 1 2'  copy row 1 to 2
 - 'd 3'    delete row 3

The state view shows the current state of all rules.
The colors show, if conditions are true or false and
if actions were already executed or not.
It has a 'update' button to update the view.

]]

local sOUTPUT = "Press 'help' for edit commands"

--
-- Helper functions
--
local function create_kv_list(elem)
	local a = {}
	for i,v in ipairs(elem) do
		a[v] = i
	end
	return a
end

local function output(label, prefix, postfix, kvTbl)
	local tbl = {label..": "}
	for k,v in pairs(kvTbl) do
		tbl[#tbl+1] = prefix..k..postfix.."="..v..", "
	end
	return table.concat(tbl)
end


--
-- Conditions
--

-- tables with all data from condition/action registrations
local kvRegisteredCond = {}
local kvRegisteredActn = {}

-- list of keys for conditions/actions
local aCondTypes = {}
local aActnTypes = {}

-- list of titles for conditions/actions
local aCondTitles = {}
local aActnTitles = {}

-- table with runtime functions
local CondRunTimeHandlers = {}
local ActnRunTimeHandlers = {}


local function eval_cond(data, environ)
	return CondRunTimeHandlers[data.__idx__](data, environ) and 1 or 0
end

local function exec_action(data, environ, number)
	ActnRunTimeHandlers[data.__idx__](data, environ, number)
end

--
-- API functions for condition/action registrations
--
function smartline.register_condition(key, tData)
	table.insert(CondRunTimeHandlers, tData.on_execute)
	table.insert(aCondTypes, key)
	table.insert(aCondTitles, tData.title)
	tData.__idx__ = #aCondTypes
	if kvRegisteredCond[key] ~= nil then
		print("[SmartLine] Condition registration error "..key)
		return
	end
	kvRegisteredCond[key] = tData
	for _,item in ipairs(tData.formspec) do
		if item.type == "textlist" then
			item.tChoices = string.split(item.choices, ",")
			item.num_choices = #item.tChoices
		end
	end
end

function smartline.register_action(key, tData)
	table.insert(ActnRunTimeHandlers, tData.on_execute)
	table.insert(aActnTypes, key)
	table.insert(aActnTitles, tData.title)
	tData.__idx__ = #aActnTypes
	if kvRegisteredActn[key] ~= nil then
		print("[SmartLine] Action registration error "..key)
		return
	end
	kvRegisteredActn[key] = tData
	for _,item in ipairs(tData.formspec) do
		if item.type == "textlist" then
			item.tChoices = string.split(item.choices, ",")
			item.num_choices = #item.tChoices
		end
	end
end


--
-- Formspec
--

-- Determine the selected submenu and return the corresponding
-- formspec definition.
-- postfix: row/culumn info like "11" or "a2"
-- type: "cond" or "actn"
-- fs_data: formspec data
local function get_active_subm_definition(postfix, type, fs_data)
	local idx = 1
	local fs_definition = {}
	if type == "cond" then
		idx = fs_data["subm"..postfix.."_cond"] or 1
		local key = aCondTypes[idx]
		fs_definition = kvRegisteredCond[key]
	elseif type == "actn" then
		idx = fs_data["subm"..postfix.."_actn"] or 1
		local key = aActnTypes[idx]
		fs_definition = kvRegisteredActn[key]
	end
	return idx, fs_definition
end

-- Extract runtime relevant data from the given submenu
-- postfix: row/culum info like "11" or "a2"
-- fs_definition: submenu formspec definition
-- fs_data: formspec data
local function get_subm_data(postfix, fs_definition, fs_data)
	local data = {}
	for idx,elem in ipairs(fs_definition.formspec) do
		if elem.type == "field" then
			data[elem.name] = fs_data["subm"..postfix.."_"..elem.name] or "?"
		elseif elem.type == "textlist" then
			local num = tonumber(fs_data["subm"..postfix.."_"..elem.name]) or 1
			num = math.min(num, elem.num_choices)
			data[elem.name] = num
			data[elem.name.."_text"] = elem.tChoices[num]
		end
	end
	-- type of the condition/action
	data.__idx__ = fs_definition.__idx__
	return data
end

-- Copy field/formspec data to the table fs_data
-- fs_definition: submenu formspec definituion
-- fields: formspec input
-- fs_data: formspec data
local function field2fs_data(fs_definition, fields, fs_data)
	for idx,elem in ipairs(fs_definition.formspec) do
		local key = "subm"..fields._postfix_.."_"..elem.name
		if elem.type == "field" then
			if fields[elem.name] then
				fs_data[key] = fields[elem.name]
			end
		elseif elem.type == "textlist" then
			local evt = minetest.explode_textlist_event(fields[elem.name])
			if evt.type == "CHG" then
				fs_data[key] = evt.index
			end
		end
		if fs_data[key] == nil then
			fs_data[key] = elem.default
		end
	end
	return fs_data
end

local function add_controls_to_table(tbl, postfix, fs_data, fs_definition)
	local val = ""
	local offs = 2.4
	for idx,elem in ipairs(fs_definition.formspec) do
		if elem.label then
			tbl[#tbl+1] = "label[0,"..offs..";"..elem.label.."]"
			offs = offs + 0.5
		end
		if elem.type == "field" then
			val = fs_data["subm"..postfix.."_"..elem.name] or elem.default
			tbl[#tbl+1] = "field[0.3,"..(offs+0.2)..";8.2,1;"..elem.name..";;"..val.."]"
			offs = offs + 0.9
		elseif elem.type == "textlist" then
			val = fs_data["subm"..postfix.."_"..elem.name] or elem.default
			tbl[#tbl+1] = "textlist[0.0,"..(offs)..";8,1.4;"..elem.name..";"..elem.choices..";"..val.."]"
			offs = offs + 1.8
		end
	end
	return tbl
end

local function runtime_data(postfix, type, fs_data)
	local _,fs_definition = get_active_subm_definition(postfix, type, fs_data)
	return get_subm_data(postfix, fs_definition, fs_data)
end

local function decrement_timers(timers)
	if timers ~= nil then
		for idx,_ in pairs(timers) do
			timers[idx] = tonumber(timers[idx])
			if timers[idx] >= 0 then
				timers[idx] = timers[idx] - 1
			end
		end
	end
end

local function toggle_flag(meta, environ)
	environ.toggle = (meta:get_int("runtime") or 0) % 4 >= 2
end


--
-- Condition formspec
--
local function formspec_cond(_postfix_, fs_data)
	local tbl = {"size[8.2,9]"..
		default.gui_bg..
		default.gui_bg_img..
		default.gui_slots..
		"field[0,0;0,0;_type_;;cond]"..
		"field[0,0;0,0;_postfix_;;".._postfix_.."]"}

	local sConditions = table.concat(aCondTitles, ",")
	local cond_idx, fs_definition = get_active_subm_definition(_postfix_, "cond", fs_data)
	tbl[#tbl+1] = "label[0,0.1;Condition type:]"
	tbl[#tbl+1] = "textlist[0,0.6;8,1.4;cond;"..sConditions..";"..cond_idx.."]"
	tbl = add_controls_to_table(tbl, _postfix_, fs_data, fs_definition)
	tbl[#tbl+1] = "button[4,8.4;2,1;_cancel_;cancel]"
	tbl[#tbl+1] = "button[6,8.4;2,1;_exit_;ok]"
	return table.concat(tbl)
end

-- evaluate the row condition
local function eval_formspec_cond(meta, fs_data, fields, readonly)
	if readonly then return fs_data end
	-- determine condition type
	local cond = minetest.explode_textlist_event(fields.cond)
	if cond.type == "CHG" then
		fs_data["subm"..fields._postfix_.."_cond"] = cond.index
	end
	-- prepare data
	local _, fs_definition = get_active_subm_definition(fields._postfix_, "cond", fs_data)
	fs_data = field2fs_data(fs_definition, fields, fs_data)
	local data = get_subm_data(fields._postfix_, fs_definition, fs_data)
	-- update button for main menu
	fs_data["cond"..fields._postfix_] = fs_definition.button_label(data)

	if fields._exit_ == nil then
		-- update formspec if exit is not pressed
		meta:set_string("formspec", formspec_cond(fields._postfix_, fs_data))
	end
	return fs_data
end


--
-- Action formspec
--
local function formspec_actn(_postfix_, fs_data)
	local tbl = {"size[8.2,9]"..
		default.gui_bg..
		default.gui_bg_img..
		default.gui_slots..
		"field[0,0;0,0;_type_;;actn]"..
		"field[0,0;0,0;_postfix_;;".._postfix_.."]"}

	local sActions = table.concat(aActnTitles, ",")
	local actn_idx, fs_definition = get_active_subm_definition(_postfix_, "actn", fs_data)
	tbl[#tbl+1] = "label[0,0.1;Action type:]"
	tbl[#tbl+1] = "textlist[0,0.6;8,1.4;actn;"..sActions..";"..actn_idx.."]"
	tbl = add_controls_to_table(tbl, _postfix_, fs_data, fs_definition)
	tbl[#tbl+1] = "button[4,8.4;2,1;_cancel_;cancel]"
	tbl[#tbl+1] = "button[6,8.4;2,1;_exit_;ok]"
	return table.concat(tbl)
end

-- evaluate the row action
local function eval_formspec_actn(meta, fs_data, fields, readonly)
	if readonly then return fs_data end
	-- determine action type
	local actn = minetest.explode_textlist_event(fields.actn)
	if actn.type == "CHG" then
		fs_data["subm"..fields._postfix_.."_actn"] = actn.index
	end
	-- prepare data
	local _, fs_definition = get_active_subm_definition(fields._postfix_, "actn", fs_data)
	fs_data = field2fs_data(fs_definition, fields, fs_data)
	local data = get_subm_data(fields._postfix_, fs_definition, fs_data)
	-- update button for main menu
	fs_data["actn"..fields._postfix_] = fs_definition.button_label(data)

	if fields._exit_ == nil then
		-- update formspec if exit is not pressed
		meta:set_string("formspec", formspec_actn(fields._postfix_, fs_data))
	end
	if fields._cancel_ then
		fields._exit_ = true
	end
	return fs_data
end


--
-- Label text formspec
--
local function formspec_label(_postfix_, fs_data)
	local label = fs_data["label".._postfix_] or "<any text>"
	return "size[6,4]"..
		default.gui_bg..
		default.gui_bg_img..
		default.gui_slots..
		"field[0,0;0,0;_type_;;label]"..
		"field[0,0;0,0;_postfix_;;".._postfix_.."]"..
		"label[0.2,0.3;Label:]"..
		"field[0.3,1.5;5,1;label;;"..label.."]"..
		"button[4.5,3;1.5,1;_exit_;ok]"
end

-- evaluate the row label
local function eval_formspec_label(meta, fs_data, fields, readonly)
	if readonly then return fs_data end
	fs_data["subml"..fields._postfix_.."_label"] = fields.label
	if fields._exit_ == nil then
		meta:set_string("formspec", formspec_label(fields._postfix_, fs_data))
	end
	-- set the button label of the main menu based on the given input in the submenu
	fs_data["label"..fields._postfix_] = fs_data["subml"..fields._postfix_.."_label"]
	return fs_data
end


--
-- Operand formspec
--
local function formspec_oprnd(_postfix_, fs_data)
	local oprnd = fs_data["submo".._postfix_.."_oprnd"] or ""
	return "size[6,4]"..
		default.gui_bg..
		default.gui_bg_img..
		default.gui_slots..
		"field[0,0;0,0;_type_;;oprnd]"..
		"field[0,0;0,0;_postfix_;;".._postfix_.."]"..
		"label[0.2,0.3;Operand:]"..
		"textlist[0,0.8;5.6,1.4;oprnd;or,and;"..oprnd.."]"..
		"button[4.5,3;1.5,1;_exit_;ok]"
end

-- evaluate the row operand
local function eval_formspec_oprnd(meta, fs_data, fields, readonly)
	if readonly then return fs_data end
	local oprnd = minetest.explode_textlist_event(fields.oprnd)
	if oprnd.type == "CHG" then
		fs_data["submo"..fields._postfix_.."_oprnd"] = oprnd.index
	end
	if fields._exit_ == nil then
		meta:set_string("formspec", formspec_oprnd(fields._postfix_, fs_data))
	end
	-- set the button label of the main menu based on the given input in the submenu
	fs_data["oprnd"..fields._postfix_] = fs_data["submo"..fields._postfix_.."_oprnd"] == 1 and "or" or "and"
	return fs_data
end

local function formspec_main(state, fs_data, output)
	local tbl = {"size[15,9;true]"..
		default.gui_bg..
		default.gui_bg_img..
		default.gui_slots..
		"field[0,0;0,0;_type_;;main]"..
		"label[0.8,0;label:]label[3.8,0;IF  cond 1:]label[7,0;and/or]label[8.3,0;cond 2:]label[11.7,0;THEN  action:]"}

	for idx = 1,NUM_RULES do
		local ypos = idx * 0.75 - 0.4
		tbl[#tbl+1] = "label[0,"..(0.2+ypos)..";"..idx.."]"
		tbl[#tbl+1] = "button[0.4,"..ypos..";3,1;label"..idx..";"..(fs_data["label"..idx] or "...").."]"
		tbl[#tbl+1] = "button[3.5,"..ypos..";3.4,1;cond1"..idx..";"..(fs_data["cond1"..idx] or "...").."]"
		tbl[#tbl+1] = "button[7,"..  ypos..";1,1;oprnd"..  idx..";"..(fs_data["oprnd"..idx] or "or").."]"
		tbl[#tbl+1] = "button[8,"..  ypos..";3.4,1;cond2"..idx..";"..(fs_data["cond2"..idx] or "...").."]"
		tbl[#tbl+1] = "button[11.5,".. ypos..";3.4,1;actna"..idx..";"..(fs_data["actna"..idx] or "...").."]"
	end
	tbl[#tbl+1] = "image_button[14,8.1;1,1;".. tubelib.state_button(state) ..";button;]"
	tbl[#tbl+1] = "button[10.6,8.2;1.5,1;state;state]"
	tbl[#tbl+1] = "button[12.2,8.2;1.5,1;help;help]"
	tbl[#tbl+1] = "label[0.2,8.4;"..output.."]"
	tbl[#tbl+1] = "field[6.5,8.5;3,1;cmnd;;<cmnd>]"
	tbl[#tbl+1] = "button[9.2,8.2;1,1;ok;OK]"
	return table.concat(tbl)
end

local function eval_formspec_main(meta, fs_data, fields, readonly)
	meta:set_string("fs_old", minetest.serialize(fs_data))
	for idx = 1,NUM_RULES do
		-- eval standard inputs
		if not readonly then
			fs_data["oprnd"..idx] = fields["oprnd"..idx] or fs_data["oprnd"..idx]
		end

		-- eval submenu button events
		if fields["label"..idx] then
			meta:set_string("formspec", formspec_label(idx, fs_data))
		elseif fields["cond1"..idx] then
			meta:set_string("formspec", formspec_cond("1"..idx, fs_data))
		elseif fields["cond2"..idx] then
			meta:set_string("formspec", formspec_cond("2"..idx, fs_data))
		elseif fields["oprnd"..idx] then
			meta:set_string("formspec", formspec_oprnd(idx, fs_data))
		elseif fields["actna"..idx] then
			meta:set_string("formspec", formspec_actn("a"..idx, fs_data))
		end
	end
	return fs_data
end

local function formspec_help(offs)
	return "size[15,9]"..
		default.gui_bg..
		default.gui_bg_img..
		default.gui_slots..
		"field[0,0;0,0;_type_;;help]"..
		"label[0,"..(-offs/50)..";"..sHELP.."]"..
		--"label[0.2,0;test]"..
		"scrollbar[13.5,1;0.5,7;vertical;sb_help;"..offs.."]"..
		"button[13.3,8.2;1.5,1;close;close]"
end

local function background(xpos, ypos, val)
	if val == true then
		return "box["..(xpos-0.1)..",".. ypos..";3.3,0.4;#008000]"
	elseif val == false then
		return "box["..(xpos-0.1)..",".. ypos..";3.3,0.4;#800000]"
	else
		return "box["..(xpos-0.1)..",".. ypos..";3.3,0.4;#202020]"
	end
end

local function formspec_state(meta, fs_data)
	local number = meta:get_string("number")
	local state = meta:get_int("state")
	local tbl = {"size[15,9;true]"..
		default.gui_bg..
		default.gui_bg_img..
		default.gui_slots..
		"field[0,0;0,0;_type_;;state]"..
		"label[0.8,0;label:]label[3.8,0;IF  cond 1:]label[7,0;and/or]label[8.3,0;cond 2:]label[11.7,0;THEN  action:]"}

	if state == tubelib.RUNNING and number then
		local environ = tubelib.get_data(number, "environ")
		local act_gate = tubelib.get_data(number, "act_gate")
		local conds = tubelib.get_data(number, "conds")

		if environ and act_gate and conds then
			for idx = 1,NUM_RULES do
				local ypos = idx * 0.6 + 0.2
				local s1 = fs_data["cond1"..idx] or "    ...    "
				local s2 = fs_data["cond2"..idx] or "    ...    "
				local sa = fs_data["actna"..idx] or "    ...    "
				if conds[idx] == nil then
					tbl[#tbl+1] = background(3.7,  ypos, nil)
					tbl[#tbl+1] = background(8,    ypos, nil)
				else
					tbl[#tbl+1] = background(3.7,  ypos, conds[idx] == 1 or conds[idx] == 3)
					tbl[#tbl+1] = background(8,    ypos, conds[idx] == 2 or conds[idx] == 3)
				end
				tbl[#tbl+1] = background(11.5, ypos, act_gate[idx])
				tbl[#tbl+1] = "label[0,"..  ypos..";"..idx.."]"
				tbl[#tbl+1] = "label[0.5,"..ypos..";"..(fs_data["label"..idx] or "    ...    ").."]"
				tbl[#tbl+1] = "label[3.7,"..  ypos..";"..s1.."]"
				tbl[#tbl+1] = "label[7.2,"..  ypos..";"..(fs_data["oprnd"..idx] or "or").."]"
				tbl[#tbl+1] = "label[8,"..  ypos..";"..s2.."]"
				tbl[#tbl+1] = "label[11.5,".. ypos..";"..sa.."]"
				if act_gate[idx] == true then
					act_gate[idx] = false
				end
			end

			tbl[#tbl+1] = "label[10,7; Seconds: "..(meta:get_int("runtime") or 1).."]"

			tbl[#tbl+1] = "label[0,7;"..output("Inputs", "i(", ")", environ.inputs).."]"
			tbl[#tbl+1] = "label[0,7.6;"..output("Timers", "t", "", environ.timers).."]"
			tbl[#tbl+1] = "label[0,8.2;"..output("Flags", "f", "", environ.flags).."]"

			tbl[#tbl+1] = "label[0,8.8;Hint:]"
			tbl[#tbl+1] = "box[1.3,8.8;6,0.4;#008000]"
			tbl[#tbl+1] = "label[1.4,8.8;condition true / action executed]"
			tbl[#tbl+1] = "box[7.9,8.8;6,0.4;#800000]"
			tbl[#tbl+1] = "label[8,8.8;condition false / action out-of-date]"
		end
	end

	tbl[#tbl+1] = "button[13.3,6.9;1.7,1;update;update]"
	tbl[#tbl+1] = "button[13.3,7.8;1.7,1;close;close]"
	return table.concat(tbl)
end

local function execute(meta, number, debug)
	local rt_rules = tubelib.get_data(number, "rt_rules")
	local environ = tubelib.get_data(number, "environ")
	local act_gate = tubelib.get_data(number, "act_gate")
	local conds = tubelib.get_data(number, "conds")
	if rt_rules and environ and act_gate and conds then
		environ.actions =  {}
		decrement_timers(environ.timers)
		toggle_flag(meta, environ)
		for i,item in ipairs(rt_rules) do
			local c1 = eval_cond(item.cond1, environ)
			local c2 = eval_cond(item.cond2, environ)
			conds[i] = c1 + c2*2
			if c1 + c2 >= item.cond_cnt then
				if act_gate[i] == nil then
					-- execute action
					exec_action(item.actn, environ, number)
					environ.actions[i] = true
					act_gate[i] = true
				end
			else
				act_gate[i] = nil
			end
		end
	end
end

local function check_rules(pos, elapsed)
	if tubelib.data_not_corrupted(pos) then
		--local t = minetest.get_us_time()
		local meta = minetest.get_meta(pos)
		meta:set_int("runtime", (meta:get_int("runtime") or 1) + 1)
		local number = meta:get_string("number")
		local state = meta:get_int("state")
		if state == tubelib.RUNNING and number then
			execute(meta, number, debug)
		end
		--print("time", minetest.get_us_time() - t)
		return true
	end
	return false
end

local function switch_state(pos, state, fs_data)
	local meta = minetest.get_meta(pos)
	local number = meta:get_string("number")
	meta:set_int("state", state)
	meta:set_string("formspec", formspec_main(state, fs_data, sOUTPUT))
	if state == tubelib.RUNNING then
		meta:set_string("infotext", "SmartLine Controller "..number..": running")
		minetest.get_node_timer(pos):start(1)
	else
		meta:set_string("infotext", "SmartLine Controller "..number..": stopped")
		minetest.get_node_timer(pos):stop()
	end
end

local function start_controller(pos, number, fs_data)
	-- delete old data
	tubelib.set_data(number, "environ",  {
		timers = {},
		flags = {},
		inputs = {}
	})
	tubelib.set_data(number, "conds",    {})
	tubelib.set_data(number, "act_gate", {})
	switch_state(pos, tubelib.RUNNING, fs_data)
end

function smartline.stop_controller(pos, fs_data)
	switch_state(pos, tubelib.STOPPED, fs_data)
end

local function formspec2runtime_rule(number, owner, fs_data)
	local rt_rules = {}
	local num2inp = {}
	for idx = 1,NUM_RULES do
		-- valid rule?
		if fs_data["subm1"..idx.."_cond"] and fs_data["subm2"..idx.."_cond"]
		and fs_data["subma"..idx.."_actn"] then
			-- add to list of runtine rules
			local rule = {
				cond_cnt = fs_data["oprnd"..idx] == "and" and 2 or 1,
				cond1 = runtime_data("1"..idx, "cond", fs_data),
				cond2 = runtime_data("2"..idx, "cond", fs_data),
				actn  = runtime_data("a"..idx, "actn", fs_data),
			}
			rule.actn.owner = owner
			table.insert(rt_rules, rule)
		end
	end
	tubelib.set_data(number, "rt_rules", rt_rules)
end


local function get_keys(fs_data)
	-- collect all keys and remove row information
	local keys = {}
	for k,v in pairs(fs_data) do
		local key = string.sub(k,1,5).."*"..string.sub(k, 7)
		if type(v) == 'number' then
			keys[key] = 1  -- default value
		else
			keys[key] = "..."  -- default value
		end
	end
	return keys
end

local function exchange_rules(fs_data, pos1, pos2)
	-- exchange elem by elem
	for k,v in pairs(get_keys(fs_data)) do
		local k1 = string.gsub(k, "*", pos1)
		local k2 = string.gsub(k, "*", pos2)
		local temp = fs_data[k1] or v
		fs_data[k1] = fs_data[k2] or v
		fs_data[k2] = temp
	end
	return fs_data
end

local function copy_rule(fs_data, pos1, pos2)
	-- copy elem by elem
	for k,v in pairs(get_keys(fs_data)) do
		local k1 = string.gsub(k, "*", pos1)
		local k2 = string.gsub(k, "*", pos2)
		fs_data[k2] = fs_data[k1] or v
	end
	return fs_data
end

local function delete_rule(fs_data, pos)
	for k,v in pairs(get_keys(fs_data)) do
		local k1 = string.gsub(k, "*", pos)
		fs_data[k1] = nil
	end
	return fs_data
end

local function edit_command(fs_data, text)
	local cmnd, pos1, pos2 = text:match('^(%S)%s(%d+)%s(%d+)$')
	if pos2 == nil then
		cmnd, pos1 = text:match('^(%S)%s(%d+)$')
	end
	if cmnd and pos1 and pos2 then
		pos1 = math.max(1, math.min(pos1, NUM_RULES))
		pos2 = math.max(1, math.min(pos2, NUM_RULES))

		if cmnd == "x" then
			exchange_rules(fs_data, pos1, pos2)
			return "rows "..pos1.." and "..pos2.." exchanged"
		end
		if cmnd == "c" then
			copy_rule(fs_data, pos1, pos2)
			return "row "..pos1.." copied to "..pos2
		end
	elseif cmnd == "d" and pos1 then
		pos1 = math.max(1, math.min(pos1, NUM_RULES))

		delete_rule(fs_data, pos1)
		return "row "..pos1.." deleted"
	end
	return "Invalid command '"..text.."'"
end

local function on_receive_fields(pos, formname, fields, player)
	local meta = minetest.get_meta(pos)
	local owner = meta:get_string("owner")
	local state = meta:get_int("state")
	if not player or not player:is_player() then
		return
	end
	local fs_data = minetest.deserialize(meta:get_string("fs_data")) or {}
	local output = ""
	local readonly = player:get_player_name() ~= owner

	-- FIRST: test if command entered?
	if fields.ok then
		if not readonly then
			output = edit_command(fs_data, fields.cmnd)
			smartline.stop_controller(pos, fs_data)
			meta:set_string("formspec", formspec_main(tubelib.STOPPED, fs_data, output))
			meta:set_string("fs_data", minetest.serialize(fs_data))
		end
	-- SECOND: eval none edit events (events based in __type__)?
	elseif fields.help then
		meta:set_string("formspec", formspec_help(1))
	elseif fields.state then
		meta:set_string("formspec", formspec_state(meta, fs_data))
	elseif fields.update then
		meta:set_string("formspec", formspec_state(meta, fs_data))
	elseif fields._cancel_ then
		fs_data = minetest.deserialize(meta:get_string("fs_old"))
		meta:set_string("formspec", formspec_main(state, fs_data, sOUTPUT))
	elseif fields.close then
		meta:set_string("formspec", formspec_main(state, fs_data, sOUTPUT))
	elseif fields.sb_help then
		local evt = minetest.explode_scrollbar_event(fields.sb_help)
		if evt.type == "CHG" then
			meta:set_string("formspec", formspec_help(evt.value))
		end
	elseif fields.button then
		if not readonly then
			local number = meta:get_string("number")
			local state = meta:get_int("state")
			if state == tubelib.RUNNING then
				smartline.stop_controller(pos, fs_data)
				meta:set_string("formspec", formspec_main(tubelib.STOPPED, fs_data, sOUTPUT))
			else
				formspec2runtime_rule(number, owner, fs_data)
				start_controller(pos, number, fs_data)
				meta:set_string("formspec", formspec_main(tubelib.RUNNING, fs_data, sOUTPUT))
			end
		end
	-- THIRD: evaluate edit events from sub-menus
	elseif fields._type_ == "main" then
		fs_data = eval_formspec_main(meta, fs_data, fields, readonly)
		meta:set_string("fs_data", minetest.serialize(fs_data))
	elseif fields._type_ == "label" then
		fs_data = eval_formspec_label(meta, fs_data, fields, readonly)
		meta:set_string("fs_data", minetest.serialize(fs_data))
	elseif fields._type_ == "cond" then
		fs_data = eval_formspec_cond(meta, fs_data, fields, readonly)
		meta:set_string("fs_data", minetest.serialize(fs_data))
	elseif fields._type_ == "oprnd" then
		fs_data = eval_formspec_oprnd(meta, fs_data, fields, readonly)
		meta:set_string("fs_data", minetest.serialize(fs_data))
	elseif fields._type_ == "actn" then
		fs_data = eval_formspec_actn(meta, fs_data, fields, readonly)
		meta:set_string("fs_data", minetest.serialize(fs_data))
	elseif fields._type_ == "help" then
		meta:set_string("formspec", formspec_main(state, fs_data, sOUTPUT))
	elseif fields._type_ == "state" then
		meta:set_string("formspec", formspec_main(state, fs_data, sOUTPUT))
	end
	-- FOURTH: back to main menu
	if fields._exit_ then
		meta:set_string("formspec", formspec_main(state, fs_data, sOUTPUT))
--			smartline.stop_controller(pos, fs_data)
--			meta:set_string("fs_data", minetest.serialize(fs_data))
--		end
	end
end

minetest.register_node("smartline:controller", {
	description = "SmartLine Controller",
	inventory_image = "smartline_controller_inventory.png",
	wield_image = "smartline_controller_inventory.png",
	stack_max = 1,
	tiles = {
		-- up, down, right, left, back, front
		"smartline.png",
		"smartline.png",
		"smartline.png",
		"smartline.png",
		"smartline.png",
		"smartline.png^smartline_controller.png",
	},

	drawtype = "nodebox",
	node_box = {
		type = "fixed",
		fixed = {
			{ -6/32, -6/32, 14/32,  6/32,  6/32, 16/32},
		},
	},

	after_place_node = function(pos, placer)
		local meta = minetest.get_meta(pos)
		local number = tubelib.add_node(pos, "smartline:controller")
		local fs_data = {}
		meta:set_string("fs_data", minetest.serialize(fs_data))
		meta:set_string("owner", placer:get_player_name())
		meta:set_string("number", number)
		meta:set_int("state", tubelib.STOPPED)
		meta:set_int("debug", 0)
		meta:set_string("formspec", formspec_main(tubelib.STOPPED, fs_data, sOUTPUT))
		meta:set_string("infotext", "SmartLine Controller "..number..": stopped")
	end,

	on_receive_fields = on_receive_fields,

	on_dig = function(pos, node, puncher, pointed_thing)
		if minetest.is_protected(pos, puncher:get_player_name()) then
			return
		end

		minetest.node_dig(pos, node, puncher, pointed_thing)
		tubelib.remove_node(pos)
	end,

	on_timer = check_rules,

	paramtype = "light",
	sunlight_propagates = true,
	paramtype2 = "facedir",
	groups = {choppy=1, cracky=1, crumbly=1, not_in_creative_inventory=1},
	is_ground_content = false,
	sounds = default.node_sound_stone_defaults(),
	on_blast = function() end,
  drop = "smartline:controller2",
})


minetest.register_craft({
	output = "smartline:controller",
	recipe = {
		{"",         "default:mese_crystal", ""},
		{"dye:blue", "default:copper_ingot", "tubelib:wlanchip"},
		{"",         "default:mese_crystal", ""},
	},
})


local function set_input(meta, payload, val)
	if payload then
		local number = meta:get_string("number")
		local environ = tubelib.get_data(number, "environ") or {}
		if environ.inputs then
			environ.inputs[payload] = val
			tubelib.set_data(number, "environ", environ)
		end
	end
end

tubelib.register_node("smartline:controller", {}, {
	on_recv_message = function(pos, topic, payload)
		local meta = minetest.get_meta(pos)
		if topic == "on" then
			set_input(meta, payload, topic)
		elseif topic == "off" then
			set_input(meta, payload, topic)
		elseif topic == "state" then
			local state = meta:get_int("state")
			return tubelib.statestring(state)
		else
			return "unsupported"
		end
	end,
	on_node_load = function(pos)
		local meta = minetest.get_meta(pos)
		if meta:get_int("state") == tubelib.RUNNING then
			minetest.get_node_timer(pos):start(1)
		end
	end,
})

-- List of Controller actions and conditions is dependent on loaded mods.
-- Therefore, the order of actions and conditions has to be re-assembled each time.
-- last order from last run is stored as meta data
local storage = minetest.get_mod_storage()

local function old_to_new(newTypes, oldTypes)
	local res = {}
	if #oldTypes == 0 then
		return nil
	end
	local new = create_kv_list(newTypes)
	for idx,key in ipairs(oldTypes) do
		res[idx] = new[key] or 1
	end
	return res
end

local function update_node_database(meta)
	local aOldCondTypes = minetest.deserialize(meta:get_string("aCondTypes")) or {}
	local aOldActnTypes = minetest.deserialize(meta:get_string("aActnTypes")) or {}

	local tOld2NewCond = old_to_new(aCondTypes, aOldCondTypes)
	local tOld2NewActn = old_to_new(aActnTypes, aOldActnTypes)

	meta:set_string("aCondTypes", minetest.serialize(aCondTypes))
	meta:set_string("aActnTypes", minetest.serialize(aActnTypes))

	return tOld2NewCond, tOld2NewActn
end

local function maintain_dataset(number)
	local flags = tubelib.get_data(number, "flags")
	if flags ~= nil then
		local timers = tubelib.get_data(number, "timers") or {}
		local inputs = tubelib.get_data(number, "inputs") or {}

		tubelib.set_data(number, "environ", {
			flags = flags,
			timers = timers,
			inputs = inputs,
			vars = {}
		})

		tubelib.set_data(number, "inputs", nil)
		tubelib.set_data(number, "timers", nil)
		tubelib.set_data(number, "flags", nil)
	end
end

--
-- Update formspec data for the case that rules order has changed or new rules were added
--
function smartline.update_fs_data(meta, fs_data)
	local tOld2NewCond, tOld2NewActn = update_node_database(meta)
	if tOld2NewCond and tOld2NewActn then
		-- map from old to new indexes
		for idx = 1,NUM_RULES do
			fs_data["subm1"..idx.."_cond"] = tOld2NewCond[fs_data["subm1"..idx.."_cond"]]
			fs_data["subm2"..idx.."_cond"] = tOld2NewCond[fs_data["subm2"..idx.."_cond"]]
			fs_data["subma"..idx.."_actn"] = tOld2NewActn[fs_data["subma"..idx.."_actn"]]
		end

	end
	return fs_data
end

minetest.register_lbm({
	label = "[SmartLine] Controller update",
	name = "smartline:update",
	nodenames = {"smartline:controller"},
	run_at_every_load = true,
	action = function(pos, node)
		local meta = minetest.get_meta(pos)

		local fs_data = minetest.deserialize(meta:get_string("fs_data"))
		fs_data = smartline.update_fs_data(meta, fs_data)
		meta:set_string("fs_data", minetest.serialize(fs_data))

		local number = meta:get_string("number")
		local owner = meta:get_string("owner")
		formspec2runtime_rule(number, owner, fs_data)

		maintain_dataset(number)
	end
})
