local modname = minetest.get_current_modname()
local S = minetest.get_translator(modname)

local MACHINE_TICK_INTERVAL = 0.5
local MAX_MEMORY_RANGE = 512
local tokenize = _G[modname].tokenize
local parse = _G[modname].parse
local compute_step = _G[modname].compute_step
local node_formspec =
{
	"size[8,7.5]",
	"label[2,0.5;" .. S("Program tape") .. "]",
	"list[context;main;2,1;1,1;]",
	nil,
	nil,
	"list[current_player;main;0,3.5;8,4;]",
	"listring[]",
}
local function get_node_formspec(running)
	node_formspec[4] = "label[4,0.5;" .. S("Current status:@n@1", running and S("running") or S("halted")) .. "]"
	node_formspec[5] = "button[4,1.3;1.7,1;" .. (running and "halt;" or "run;") ..
		(running and S("Halt") or S("Run")) .. "]"
	return table.concat(node_formspec)
end

local function halt(pos, meta)
	mesecon.receptor_off(pos)
	meta:set_string("formspec", get_node_formspec(false))
end

minetest.register_node(modname .. ":machine",
	{
		description = S("Turing machine"),
		drawtype = "mesh",
		mesh = modname .. "_machine.obj",
		tiles =
		{
			modname .. "_machine_base_horizontals.png",
			modname .. "_machine_base.png",
			modname .. "_machine_taperolls.png",
			modname .. "_machine_tapes.png"
		},
		groups = {deconstructable = 2},
		
		on_construct = function(pos)
			local meta = minetest.get_meta(pos)
			meta:set_string("formspec", get_node_formspec(false))
			local inv = meta:get_inventory()
			inv:set_size("main", 1)
		end,
		
		can_dig = function(pos)
			local inv = minetest.get_inventory({type = "node", pos = pos})
			return inv:is_empty("main")
		end,
		
		on_timer = function(pos, elapsed)
			local oc = os.clock()
			--TODO: do program caching stuff
			local meta = minetest.get_meta(pos)
			
			--fetch data from meta
			local ast = minetest.deserialize(meta:get_string("program"))
			local env = minetest.deserialize(meta:get_string("env"))
			local pc = minetest.deserialize(meta:get_string("pcs"))
			local instr = ast
			
			--process one step of the program
			--advance program counter
			pc[#pc] = pc[#pc] + 1
			--go to the loop we're in and find the current instruction
			for i, v in ipairs(pc)
			do
				instr = instr[v]
			end
			local powered = meta:get_int("mesecon_powered")
			local restart, output = compute_step(env, instr, pc, powered)
			if output == "wait"
			then
				if powered == 0
				then
					--save pc so we can continue after receiving a signal
					meta:set_string("pcs",minetest.serialize(pc))
					
					meta:set_int("wait_for_input", 1)
					return false
				end
			elseif output
			then
				if output > 0
				then
					mesecon.receptor_on(pos)
				else
					mesecon.receptor_off(pos)
				end
			end
			if not restart
			then
				halt(pos, meta)
				return false
			end
			--keep memory consumption in check
			if math.abs(env.headpos) > MAX_MEMORY_RANGE
			then
				halt(pos, meta)
				return false --TODO: show some sign of error
			end
			
			restart = restart * MACHINE_TICK_INTERVAL
			
			--writeback
			meta:set_string("pcs",minetest.serialize(pc))
			meta:set_string("env", minetest.serialize(env))
			--minetest.chat_send_all(os.clock() - oc)
			
			--we don't use auto restart by returning true so we can implement
			--the | (wait) instruction
			local timer = minetest.get_node_timer(pos)
			timer:start(restart)
			return false
		end,
		
		on_receive_fields = function(pos, formname, fields, sender)
			local meta = minetest.get_meta(pos)
			local timer = minetest.get_node_timer(pos)
			local timer_started = timer:is_started()
			
			if fields.run and not timer_started
			then
				local inv = meta:get_inventory()
				local itemstack = inv:get_stack("main", 1)
				if not itemstack:is_empty() and itemstack:get_name() == modname .. ":tape"
				then
					do
						--parse program
						local imeta = itemstack:get_meta()
						local program = imeta:get_string("program")
						local tkns = tokenize(program)
						local ast = parse(tkns)
						if ast
						then
							meta:set_string("pcs",minetest.serialize({0})) --initialize program counter
							meta:set_string("program", minetest.serialize(ast)) --write program into meta
							meta:set_string("env", minetest.serialize({headpos = 1})) --write tape into meta
						end
					end
					collectgarbage()
					--start node timer
					local timer = minetest.get_node_timer(pos)
					timer:start(MACHINE_TICK_INTERVAL)
					meta:set_string("formspec", get_node_formspec(true))
				end		
			elseif fields.halt and timer_started
			then
				--stop node timer
				timer:stop()
				halt(pos, meta)
			end
		end,
		
		on_metadata_inventory_take = function(pos, listname, index, stack, player)
			local timer = minetest.get_node_timer(pos)
			timer:stop()
			local meta = minetest.get_meta(pos)
			halt(pos, meta)
		end,
		
		mesecons =
		{
			receptor =
			{
				state = mesecon.state.off
			},
			effector =
			{
				action_on = function(pos, node)
					local meta = minetest.get_meta(pos)
					meta:set_int("mesecon_powered", 1)
					
					if meta:get_int("wait_for_input") == 1
					then
						meta:set_int("wait_for_input", 0)
						local def = minetest.registered_nodes[node.name]
						def.on_timer(pos, 0)
					end
				end,
				action_off = function(pos, node)
					local meta = minetest.get_meta(pos)
					meta:set_int("mesecon_powered", 0)
				end,
			},
		},
	})
