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

local MAX_PROGRAM_LENGTH = 4096

local tokenize = _G[modname].tokenize
local parse = _G[modname].parse

local node_formspec =
{
	"formspec_version[2]",
	"size[10,13.6]",
	"textarea[2,0.5;6,6;program;" .. S("Program") .. ";${program}]",
	"textarea[2,7;6.5,2;;" .. S("Static error log") .. ";",
	nil,
	"]",
	"button[0.2,1;1.5,0.5;guide;" .. S("Guide") .. "]",
	"button[0.2,1.6;1.5,0.5;check;" .. S("Check") .. "]",
	"button[0.2,2.2;1.5,0.5;save;" .. S("Save") .. "]",
	"button[0.2,2.8;1.5,0.5;cancel;" .. S("Cancel") .. "]",
	"label[0.2,3.8;" .. S("Program tape") .. "]",
	"list[context;main;0.2,4;1,1;]",
	"list[current_player;main;0.1,8.5;8,4;]",
	"listring[]",
}

local function get_node_formspec(errlog)
	--optimally don't hardcode the position
	node_formspec[5] = minetest.formspec_escape(errlog)
	return table.concat(node_formspec)
end

--TODO: make translatable
local guide_formspec =
{
	"formspec_version[2]",
	"size[10,13.6]",
	"textarea[1,1.5;8,12;;",
	S("Coding Table"),
	";",
minetest.formspec_escape([[Coding tables are used to make program tapes for turing machines.
Write your program at the coding table, then put a tape into the the box that sais 'Program tape' and press 'Save'.
You can press 'Check' at the coding table to see if your program is syntactically correct.


Turing machines operate on a tape with numbers. They have a reading and writing head that can manipulate or output the current number. This head can be moved left and right on the tape to select different numbers.

Programs consist of these Commands:
'[' or '(': open loop.
']' or ')': close loop.
	During execution, code in the loop is run if the value at the current head position is not 0 at the start of the loop.

'> [number]': move head right by [number] positions.
'< [number]': move head left by [number] positions.

'+ [number]': add [number] to current position.
'- [number]': subtract [number] from current position.

'.': output value at current head position.
', [number]': write input to current head position.
	If [number] is 1, the machine assumes the input to be 1 if it receives a mesecons signal, 0 otherwise
	If [number] is 2, the machine waits until it receives a mesecons signal.
	If [number] is 3, the input is a random integer in the range [0, current value at head position)
	
'| [number]': wait for [number] steps
In all of the above [number] is optional and is assumed 1 if left out. Only natural numbers (including 0) are supported.

Other characters are ignored. You can use these to comment your code.
]]),
	"]",
}
guide_formspec = table.concat(guide_formspec)

minetest.register_node(modname ..":coding_table",
	{
		description = S("Coding table"),
		drawtype = "mesh",
		mesh = modname .. "_machine.obj",
		tiles = {modname .. "_machine_base_horizontals.png", modname .. "_machine_base.png", "blank.png"},
		groups = {deconstructable = 2},
		
		on_construct = function(pos)
			--initialize meta and formspec and node timer
			local meta = minetest.get_meta(pos)
			meta:set_string("formspec", get_node_formspec(""))
			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_receive_fields = function(pos, formname, fields, sender)
			local meta = minetest.get_meta(pos)
			
			--enforce maximum length of program
			--TODO: let player know that program was cut
			if fields.program and string.len(fields.program) > MAX_PROGRAM_LENGTH
			then
				fields.program = string.sub(fields.program, 1, MAX_PROGRAM_LENGTH)
				
			end
			if fields.check
			then
				do
					local tkns = tokenize(fields.program)
					local ast, errlog = parse(tkns)
					meta:set_string("formspec", get_node_formspec(table.concat(errlog, "\n")))
				end
				collectgarbage()
			elseif fields.cancel
			then
				minetest.close_formspec(sender:get_player_name(), formname)
				return
			elseif fields.guide
			then
				minetest.show_formspec(sender:get_player_name(), modname .. ":guide", guide_formspec)
			elseif fields.save and fields.program
			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
						--check if program is syntactically correct
						local tkns = tokenize(fields.program)
						local ast, errlog = parse(tkns)
						
						if ast
						then
							local smeta = itemstack:get_meta()
							smeta:set_string("program", fields.program)
							inv:set_stack("main", 1, itemstack)
							table.insert(errlog, S("Saved program to tape."))
						else
							table.insert(errlog, S("Could not save program to tape: invalid syntax."))
						end
						meta:set_string("formspec", get_node_formspec(table.concat(errlog, "\n")))
					end
					collectgarbage()
				end
				
			end
			if fields.program
			then
				meta:set_string("program", fields.program)
			end
		end
	})
