-- Reference
-- ports = get_real_portstates(pos): gets if inputs are powered from outside
-- newport = merge_portstates(state1, state2): just does result = state1 or state2 for every port
-- action_setports(pos, rule, state): activates/deactivates the mesecons according to the portstates (helper for action)
-- action(pos, ports, digiline_msgs): Applies new portstates to a luacontroller at pos, and sends pending digiline messages
-- lc_update(pos): updates the controller at pos by executing the code
-- reset_meta (pos, code, errmsg): performs a software-reset, installs new code and prints error messages
-- reset (pos): performs a hardware reset, turns off all ports
--
-- The Sandbox
-- The whole code of the controller runs in a sandbox,
-- a very restricted environment.
-- However, as this does not prevent you from using e.g. loops,
-- we need to check for these prohibited commands first.
-- Actually the only way to damage the server is to
-- use too much memory from the sandbox.
-- You can add more functions to the environment
-- (see where local env is defined)
-- Something nice to play is is appending minetest.env to it.

local BASENAME = "mesecons_luacontroller:luacontroller"

local rules = {}
rules.a = {x = -1, y = 0, z =  0, name="A"}
rules.b = {x =  0, y = 0, z =  1, name="B"}
rules.c = {x =  1, y = 0, z =  0, name="C"}
rules.d = {x =  0, y = 0, z = -1, name="D"}

------------------
-- Action stuff --
------------------
-- These helpers are required to set the portstates of the luacontroller

function lc_update_real_portstates(pos, rulename, newstate)
	local meta = minetest.get_meta(pos)
	if rulename == nil then
		meta:set_int("real_portstates", 1)
		return
	end
	local n = meta:get_int("real_portstates") - 1
	if n < 0 then
		legacy_update_ports(pos)
		n = meta:get_int("real_portstates") - 1
	end
	local L = {}
	for i = 1, 4 do
		L[i] = n%2
		n = math.floor(n/2)
	end
	if rulename.x == nil then
		for _, rname in ipairs(rulename) do
			local port = ({4, 1, nil, 3, 2})[rname.x+2*rname.z+3]
			L[port] = (newstate == "on") and 1 or 0
		end
	else
		local port = ({4, 1, nil, 3, 2})[rulename.x+2*rulename.z+3]
		L[port] = (newstate == "on") and 1 or 0
	end
	meta:set_int("real_portstates", 1 + L[1] + 2*L[2] + 4*L[3] + 8*L[4])
end

local get_real_portstates = function(pos) -- determine if ports are powered (by itself or from outside)
	local meta = minetest.get_meta(pos)
	local L = {}
	local n = meta:get_int("real_portstates") - 1
	if n < 0 then
		return legacy_update_ports(pos)
	end
	for _, index in ipairs({"a", "b", "c", "d"}) do
		L[index] = ((n%2) == 1)
		n = math.floor(n/2)
	end
	return L
end

local merge_portstates = function (ports, vports)
	local npo = {a=false, b=false, c=false, d=false}
	npo.a = vports.a or ports.a
	npo.b = vports.b or ports.b
	npo.c = vports.c or ports.c
	npo.d = vports.d or ports.d
	return npo
end

local generate_name = function (ports)
	local overwrite = overwrite or {}
	local d = ports.d and 1 or 0
	local c = ports.c and 1 or 0
	local b = ports.b and 1 or 0
	local a = ports.a and 1 or 0
	return BASENAME..d..c..b..a
end

local setport = function (pos, rule, state)
	if state then
		mesecon:receptor_on(pos, {rule})
	else
		mesecon:receptor_off(pos, {rule})
	end
end

local action = function (pos, ports, digiline_msgs)
	local node = minetest.get_node(pos)
	local name = node.name
	local vports = minetest.registered_nodes[name].virtual_portstates
	local newname = generate_name(ports)

	if name ~= newname and vports then
		local rules_on  = {}
		local rules_off = {}

		minetest.swap_node(pos, {name = newname, param2 = node.param2})

		if ports.a ~= vports.a then setport(pos, rules.a, ports.a) end
		if ports.b ~= vports.b then setport(pos, rules.b, ports.b) end
		if ports.c ~= vports.c then setport(pos, rules.c, ports.c) end
		if ports.d ~= vports.d then setport(pos, rules.d, ports.d) end
	end

	if digiline then
		for i = 1, #digiline_msgs do
			digiline:receptor_send(pos, digiline.rules.default,
			    digiline_msgs[i].channel, digiline_msgs[i].msg)
		end
	end
end

--------------------
-- Overheat stuff --
--------------------

local heat = function (meta) -- warm up
	h = meta:get_int("heat")
	if h ~= nil then
		meta:set_int("heat", h + 1)
	end
end

--local cool = function (meta) -- cool down after a while
--	h = meta:get_int("heat")
--	if h ~= nil then
--		meta:set_int("heat", h - 1)
--	end
--end

local overheat = function (meta) -- determine if too hot
	h = meta:get_int("heat")
	if h == nil then return true end -- if nil then overheat
	if h > 40 then 
		return true
	else 
		return false 
	end
end

local overheat_off = function(pos)
	mesecon:receptor_off(pos, mesecon.rules.flat)
end

-------------------
-- Parsing stuff --
-------------------

local code_prohibited = function(code)
	-- Clean code
	local prohibited = {"while", "for", "repeat", "until", "function", "goto"}
	for _, p in ipairs(prohibited) do
		if string.find(code, p) then
			return "Prohibited command: "..p
		end
	end
end

local safe_print = function(param)
	print(dump(param))
end

deep_copy = function(original, visited) --deep copy that removes functions
	visited = visited or {}
	if visited[original] ~= nil then --already visited this node
		return visited[original]
	end
	if type(original) == 'table' then --nested table
		local copy = {}
		visited[original] = copy
		for key, value in next, original, nil do
			copy[deep_copy(key, visited)] = deep_copy(value, visited)
		end
		setmetatable(copy, deep_copy(getmetatable(original), visited))
		return copy
	elseif type(original) == 'function' then --ignore functions
		return nil
	else --by-value type
		return original
	end
end

local safe_serialize = function(value)
	return minetest.serialize(deep_copy(value))
end

local interrupt = function(params)
	lc_update(params.pos, {type="interrupt", iid = params.iid})
end

local getinterrupt = function(pos)
	local interrupt = function (time, iid) -- iid = interrupt id
		if type(time) ~= "number" then return end
		local iid = iid or math.random()
		local meta = minetest.get_meta(pos)
		local interrupts = minetest.deserialize(meta:get_string("lc_interrupts")) or {}
		local found = false
		local search = safe_serialize(iid)
		for _, i in ipairs(interrupts) do
			if safe_serialize(i) == search then
				found = true
				break
			end
		end
		if not found then
			table.insert(interrupts, iid)
			meta:set_string("lc_interrupts", safe_serialize(interrupts))
		end
		minetest.after(time, interrupt, {pos=pos, iid = iid})
	end
	return interrupt
end

local handle_timer = function(pos, elapsed)
	local err = lc_update(pos, {type="timer"})
	if err then print(err) end
	return false
end

local gettimer = function(pos)
	local timer = function (time)
		if type(time) ~= "number" then return end
		local nodetimer = minetest.get_node_timer(pos)
		nodetimer:start(time)
	end
	return timer
end

local create_environment = function(pos, mem, event)
	-- Gather variables for the environment
	local vports = minetest.registered_nodes[minetest.get_node(pos).name].virtual_portstates
	vports = {a = vports.a, b = vports.b, c = vports.c, d = vports.d}
	local rports = get_real_portstates(pos)

	return {
			print = safe_print,
			pin = merge_portstates(vports, rports),
			port = vports,
			interrupt = getinterrupt(pos),
			timer = gettimer(pos),
			digiline_msgs = {},
			digiline_send = function(channel, msg)
				table.insert(lc_digiline_msgs, {["channel"]=channel, ["msg"]=msg})
			end,
			mem = mem,
			tostring = tostring,
			tonumber = tonumber,
			string = {
				byte = string.byte,
				char = string.char,
				find = string.find,
				format = string.format,
				gmatch = string.gmatch,
				gsub = string.gsub,
				len = string.len,
				lower = string.lower,
				upper = string.upper,
				match = string.match,
				rep = string.rep,
				reverse = string.reverse,
				sub = string.sub,
			},
			math = {
				abs = math.abs,
				acos = math.acos,
				asin = math.asin,
				atan = math.atan,
				atan2 = math.atan2,
				ceil = math.ceil,
				cos = math.cos,
				cosh = math.cosh,
				deg = math.deg,
				exp = math.exp,
				floor = math.floor,
				fmod = math.fmod,
				frexp = math.frexp,
				huge = math.huge,
				ldexp = math.ldexp,
				log = math.log,
				log10 = math.log10,
				max = math.max,
				min = math.min,
				modf = math.modf,
				pi = math.pi,
				pow = math.pow,
				rad = math.rad,
				random = math.random,
				sin = math.sin,
				sinh = math.sinh,
				sqrt = math.sqrt,
				tan = math.tan,
				tanh = math.tanh,
			},
			table = {
				insert = table.insert,
				maxn = table.maxn,
				remove = table.remove,
				sort = table.sort
			},
			event = event,
	}
end

local create_sandbox = function (code, env)
	-- Create Sandbox
	if code:byte(1) == 27 then
		return _, "You Hacker You! Don't use binary code!"
	end
	f, msg = loadstring(code)
	if not f then return _, msg end
	setfenv(f, env)
	return f
end

local do_overheat = function (pos, meta)
	-- Overheat protection
	heat(meta)
	--minetest.after(0.5, cool, meta)
	if overheat(meta) then
		local node = minetest.get_node(pos)
		minetest.swap_node(pos, {name = BASENAME.."_burnt", param2 = node.param2})
		minetest.get_meta(pos):set_string("lc_interrupts", "")
		minetest.after(0.2, overheat_off, pos) -- wait for pending operations
		return true
	end
end

local load_memory = function(meta)
	return minetest.deserialize(meta:get_string("lc_memory")) or {}
end

local save_memory = function(meta, mem)
	meta:set_string("lc_memory", safe_serialize(mem))
end

local interrupt_allow = function (meta, event)
	if event.type ~= "interrupt" then return true end

	local interrupts = minetest.deserialize(meta:get_string("lc_interrupts")) or {}
	local search = safe_serialize(event.iid)
	for _, i in ipairs(interrupts) do
		if safe_serialize(i) == search then
			return true
		end
	end

	return false
end

local ports_invalid = function (var)
	if type(var) == "table" then
		return false
	end
	return "The ports you set are invalid"
end

----------------------
-- Parsing function --
----------------------

lc_digiline_msgs = nil
lc_update = function (pos, event)
	local meta = minetest.get_meta(pos)
	if not interrupt_allow(meta, event) then return end
	if do_overheat(pos, meta) then return end

	-- load code & mem from memory
	local mem  = load_memory(meta)
	local code = meta:get_string("code")

	-- make sure code is ok and create environment
	local prohibited = code_prohibited(code)
	if 	prohibited then return prohibited end
	local env = create_environment(pos, mem, event)

	-- create the sandbox and execute code
	lc_digiline_msgs = {}
	local chunk, msg = create_sandbox (code, env)
	if not chunk then return msg end
	local success, msg = pcall(f)
	if not success then return msg end
	if ports_invalid(env.port) then return ports_invalid(env.port) end

	save_memory(meta, mem)

	-- Actually set the ports and send digiline messages
	minetest.after(0, action, pos, env.port, lc_digiline_msgs)
end

local reset_meta = function(pos, code, errmsg)
	local meta = minetest.get_meta(pos)
	meta:set_string("code", code)
	code = minetest.formspec_escape(code or "")
	errmsg = minetest.formspec_escape(errmsg or "")
	meta:set_string("formspec", "size[10,8]"..
		"background[-0.2,-0.25;10.4,8.75;jeija_luac_background.png]"..
		"textarea[0.2,0.6;10.2,5;code;;"..code.."]"..
		"image_button[3.75,6;2.5,1;jeija_luac_runbutton.png;program;]"..
		"image_button_exit[9.72,-0.25;0.425,0.4;jeija_close_window.png;exit;]"..
		"label[0.1,5;"..errmsg.."]")
	meta:set_int("heat", 0)
end

local reset = function (pos)
	minetest.get_meta(pos):set_string("lc_interrupts", "")
	action(pos, {a=false, b=false, c=false, d=false}, {})
end

--        ______
--       |
--       |
--       |        __       ___  _   __         _  _
-- |   | |       |  | |\ |  |  |_| |  | |  |  |_ |_|
-- |___| |______ |__| | \|  |  | \ |__| |_ |_ |_ |\
-- |
-- |
--

-----------------------
-- Node Registration --
-----------------------

local output_rules={}
local input_rules={}

local nodebox = {
		type = "fixed",
		fixed = {
			{ -8/16, -8/16, -8/16, 8/16, -7/16, 8/16 }, -- bottom slab
			{ -5/16, -7/16, -5/16, 5/16, -6/16, 5/16 }, -- circuit board
			{ -3/16, -6/16, -3/16, 3/16, -5/16, 3/16 }, -- IC
		}
	}

local selectionbox = {
		type = "fixed",
		fixed = { -8/16, -8/16, -8/16, 8/16, -5/16, 8/16 },
	}

local digiline = {
	receptor = {},
	effector = {
		action = function (pos, node, channel, msg)
			lc_update (pos, {type = "digiline", channel = channel, msg = msg})
		end
	}
}

for a = 0, 1 do -- 0 = off; 1 = on
for b = 0, 1 do
for c = 0, 1 do
for d = 0, 1 do

local cid = tostring(d)..tostring(c)..tostring(b)..tostring(a)
local nodename = BASENAME..cid
local top = "jeija_luacontroller_top.png"
if a == 1 then
	top = top.."^jeija_luacontroller_LED_A.png"
end
if b == 1 then
	top = top.."^jeija_luacontroller_LED_B.png"
end
if c == 1 then
	top = top.."^jeija_luacontroller_LED_C.png"
end
if d == 1 then
	top = top.."^jeija_luacontroller_LED_D.png"
end

if a + b + c + d ~= 0 then
	groups = {dig_immediate=2, not_in_creative_inventory=1, overheat = 1}
else
	groups = {dig_immediate=2, overheat = 1}
end

output_rules[cid] = {}
input_rules[cid] = {}
if (a == 1) then table.insert(output_rules[cid], rules.a) end
if (b == 1) then table.insert(output_rules[cid], rules.b) end
if (c == 1) then table.insert(output_rules[cid], rules.c) end
if (d == 1) then table.insert(output_rules[cid], rules.d) end

if (a == 0) then table.insert(input_rules[cid], rules.a) end
if (b == 0) then table.insert(input_rules[cid], rules.b) end
if (c == 0) then table.insert(input_rules[cid], rules.c) end
if (d == 0) then table.insert(input_rules[cid], rules.d) end

local mesecons = {
	effector =
	{
		rules = input_rules[cid],
		action_change = function (pos, _, rulename, newstate)
			lc_update_real_portstates(pos, rulename, newstate)
			lc_update(pos, {type=newstate,  pin=rulename})
		end,
	},
	receptor =
	{
		state = mesecon.state.on,
		rules = output_rules[cid]
	}
}

minetest.register_node(nodename, {
	description = "Luacontroller",
	drawtype = "nodebox",
	tiles = {
		top,
		"jeija_microcontroller_bottom.png",
		"jeija_microcontroller_sides.png",
		"jeija_microcontroller_sides.png",
		"jeija_microcontroller_sides.png",
		"jeija_microcontroller_sides.png"
		},

	inventory_image = top,
	paramtype = "light",
	groups = groups,
	drop = BASENAME.."0000",
	sunlight_propagates = true,
	selection_box = selectionbox,
	node_box = nodebox,
	on_construct = reset_meta,
	on_receive_fields = function(pos, formname, fields)
		if not fields.program then
			return
		end
		reset(pos)
		reset_meta(pos, fields.code)
		local err = lc_update(pos, {type="program"})
		if err then print(err) end
		reset_meta(pos, fields.code, err)
	end,
	on_timer = handle_timer,
	sounds = default.node_sound_stone_defaults(),
	mesecons = mesecons,
	digiline = digiline,
	is_luacontroller = true,
	virtual_portstates = {	a = a == 1, -- virtual portstates are
					b = b == 1, -- the ports the the
					c = c == 1, -- controller powers itself
					d = d == 1},-- so those that light up
	after_dig_node = function (pos, node)
		mesecon:receptor_off(pos, output_rules)
	end,
})
end
end
end
end

--overheated luacontroller
minetest.register_node(BASENAME .. "_burnt", {
	drawtype = "nodebox",
	tiles = {
		"jeija_luacontroller_burnt_top.png",
		"jeija_microcontroller_bottom.png",
		"jeija_microcontroller_sides.png",
		"jeija_microcontroller_sides.png",
		"jeija_microcontroller_sides.png",
		"jeija_microcontroller_sides.png"
	},
	inventory_image = "jeija_luacontroller_burnt_top.png",
	paramtype = "light",
	groups = {dig_immediate=2, not_in_creative_inventory=1},
	drop = BASENAME.."0000",
	sunlight_propagates = true,
	selection_box = selectionbox,
	node_box = nodebox,
	on_construct = reset_meta,
	on_receive_fields = function(pos, formname, fields)
		if fields.quit then
			return
		end
		reset(pos)
		reset_meta(pos, fields.code)
		local err = lc_update(pos, {type="program"})
		if err then print(err) end
		reset_meta(pos, fields.code, err)
	end,
	sounds = default.node_sound_stone_defaults(),
	is_luacontroller = true,
	virtual_portstates = {a = false, b = false, c = false, d = false},
})

------------------------
-- Craft Registration --
------------------------

minetest.register_craft({
	output = BASENAME.."0000 2",
	recipe = {
		{'mesecons_materials:silicon', 'mesecons_materials:silicon', 'group:mesecon_conductor_craftable'},
		{'mesecons_materials:silicon', 'mesecons_materials:silicon', 'group:mesecon_conductor_craftable'},
		{'group:mesecon_conductor_craftable', 'group:mesecon_conductor_craftable', ''},
	}
})

