--[[

	Signs Bot
	=========

	Copyright (C) 2019 Joachim Stolberg

	GPL v3
	See LICENSE.txt for more information
	
	Signs Bot: Robot command interpreter

]]--

-- for lazy programmers
local S = function(pos) if pos then return minetest.pos_to_string(pos) end end
local P = minetest.string_to_pos
local M = minetest.get_meta

-- Load support for intllib.
local MP = minetest.get_modpath("signs_bot")
local I,_ = dofile(MP.."/intllib.lua")
local ci = dofile(MP.."/interpreter.lua")

local lib = signs_bot.lib

-- Possible command results
signs_bot.BUSY = ci.BUSY
signs_bot.DONE = ci.DONE
signs_bot.NEW = ci.NEW
signs_bot.ERROR = ci.ERROR
signs_bot.TURN_OFF = ci.TURN_OFF

local tCommands = {}
local SortedKeys = {}
local SortedMods = {}
local tMods = {}
local ExpensiveCmnds = {}

function signs_bot.steps(mem, first, last)
	if not mem.steps then
		mem.steps = first - 1
	end
	mem.steps = mem.steps + 1
	if mem.steps >= last then
		mem.steps = nil
		return ci.DONE, last
	end
	return ci.BUSY, mem.steps
end

--
-- Command register API function
--
function signs_bot.register_botcommand(name, def)
	tCommands[name] = def
	tCommands[name].name = name
	if def.expensive then
		ExpensiveCmnds[name] = true
	end
	if not SortedKeys[def.mod] then
		SortedKeys[def.mod] = {}
		SortedMods[#SortedMods+1] = def.mod
		tMods[#tMods+1] = def.mod
	end
	local idx = #SortedKeys[def.mod] + 1
	SortedKeys[def.mod][idx] = name
	if def.num_param and def.cmnd then
		ci.register_command(name, def.num_param, def.cmnd, def.check)
	end
end

function signs_bot.get_commands()
	local tbl = {}
	for _,mod in ipairs(SortedMods) do
		tbl[#tbl+1] = mod.." "..I("commands:")
		for _,cmnd in ipairs(SortedKeys[mod]) do
			local item = tCommands[cmnd]
			tbl[#tbl+1] = "    "..item.name.." "..item.params
		end
	end
	return tbl
end	

function signs_bot.get_help_text(cmnd)
	if cmnd then
		cmnd = unpack(string.split(cmnd, " "))
		local item = tCommands[cmnd]
		if item then
			return item.description
		end
	end
	return I("unknown command")
end	
	
function signs_bot.check_commands(pos, text)
	return ci.check_script(text)
end

--
-- Bot Commands
--

local function check_sign(pos, mem)
	local meta = M(pos)
	local cmnd = meta:get_string("signs_bot_cmnd")
	if cmnd ~= "" then  -- command block?
		if meta:get_int("err_code") ~= 0 then -- code not valid?
			return false
		end
		
		local node = lib.get_node_lvm(pos)
		-- correct sign direction?
		if mem.robot_param2 == node.param2 then
			return true
		end
		-- special sign node?
		if node.name == "signs_bot:bot_flap" or node.name == "signs_bot:box" then
			return true
		end
	end
	return false
end

-- Function returns 2 values:
--  - true if a sensor could be available, else false
--  - the sign pos or nil
local function scan_surrounding(mem)
	local pos1 = lib.next_pos(mem.robot_pos, mem.robot_param2)
	if check_sign(pos1, mem) then
		return true, pos1
	end
	local pos2 = {x=pos1.x, y=pos1.y+1, z=pos1.z}
	if check_sign(pos2, mem) then
		return true, pos2
	end
	if minetest.find_node_near(mem.robot_pos, 1, {
			"signs_bot:box", "signs_bot:bot_sensor"}) then  -- something around?
		return true
	end
	return false
end

local function activate_sensor(pos, param2)
	local pos1 = lib.next_pos(pos, param2)
	local node = lib.get_node_lvm(pos1)
	if node.name == "signs_bot:bot_sensor" then
		node.name = "signs_bot:bot_sensor_on"
		minetest.swap_node(pos1, node)
		local ndef = minetest.registered_nodes[node.name]
		if ndef and ndef.after_place_node then
			ndef.after_place_node(pos1)
		end
	end
end

local function bot_error(base_pos, mem, err)
	minetest.sound_play('signs_bot_error', {pos = base_pos})
	minetest.sound_play('signs_bot_error', {pos = mem.robot_pos})
	print(err)
	signs_bot.infotext(base_pos, err)
	mem.error = true
	return false
end

local function power_consumption(mem, cmnd)
	if mem.capa then
		if ExpensiveCmnds[cmnd] then
			mem.capa = mem.capa - 2
		else
			mem.capa = mem.capa - 1
		end
		return mem.capa > 0
	end
	return true
end

function signs_bot.run_next_command(base_pos, mem)
	local res, err = ci.run_script(base_pos, mem)
	if res == ci.ERROR then
		return bot_error(base_pos, mem, err)
	elseif res == ci.EXIT then
		signs_bot.stop_robot(base_pos, mem)
		return false
	end
	if not power_consumption(mem) then 
		signs_bot.stop_robot(base_pos, mem)
		mem.bot_state = "nopower"
		return bot_error(base_pos, mem, "No power")
	end
	return true
end

function signs_bot.reset(base_pos, mem)
	ci.reset_script(base_pos, mem)
end

signs_bot.register_botcommand("repeat", {
	mod = "core",
	params = "<num>",	
	description = I("start of a 'repeat..end' block"),
})	

signs_bot.register_botcommand("end", {
	mod = "core",
	params = "",	
	description = I("end command of a 'repeat..end' block"),
})	

signs_bot.register_botcommand("call", {
	mod = "core",
	params = "<label>",	
	description = I("call a subroutine (with 'return' statement)"),
})	

signs_bot.register_botcommand("return", {
	mod = "core",
	params = "",	
	description = I("return from a subroutine"),
})	

signs_bot.register_botcommand("jump", {
	mod = "core",
	params = "<label>",	
	description = I("jump to a label"),
})	

local function move(mem, any_sensor)
	local new_pos = signs_bot.move_robot(mem)
	if new_pos then  -- not blocked?
		mem.robot_pos = new_pos
		if any_sensor then
			activate_sensor(mem.robot_pos, (mem.robot_param2 + 1) % 4)
			activate_sensor(mem.robot_pos, (mem.robot_param2 + 3) % 4)
		end
	elseif mem.capa then
		mem.capa = mem.capa + 1
	end
end

signs_bot.register_botcommand("move", {
	mod = "move",
	params = "<steps>",	
	num_param = 1,
	description = I([[Move the robot 1..999 steps forward
without paying attention to any signs.
Up and down movements also become
counted as steps.]]),
	check = function(steps)
		steps = tonumber(steps) or 1
		return steps > 0 and steps < 1000
	end,
	cmnd = function(base_pos, mem, steps)
		steps = tonumber(steps) or 1
		local res, idx = signs_bot.steps(mem, 1, steps)
		move(mem, scan_surrounding(mem))
		return res
	end,
})

signs_bot.register_botcommand("cond_move", {
	mod = "move",
	params = "",
	num_param = 0,
	description = I([[Walk until a sign or obstacle is
reached. Then continue with the next command.
When a sign has been reached, 
the current program is ended
and the bot executes the
new program from the sign]]),
	cmnd = function(base_pos, mem)
		local any_sensor, sign_pos = scan_surrounding(mem)
		if not sign_pos then
			move(mem, any_sensor)
			return ci.BUSY
		else
			mem.script = M(sign_pos):get_string("signs_bot_cmnd").."\ncond_move"
			return ci.NEW
		end
	end,
})