--atc.lua
--registers and controls the ATC system

-- Get current translator
local S = advtrains.translate

local atc={}

local eval_conditional

-- ATC persistence table. advtrains.atc is created by init.lua when it loads the save file.
atc.controllers = {}
function atc.load_data(data)
	local temp = data and data.controllers or {}
	--transcode atc controller data to node hashes: table access times for numbers are far less than for strings
	for pts, data in pairs(temp) do
		if type(pts)=="number" then
			pts=minetest.pos_to_string(minetest.get_position_from_hash(pts))
		end
		atc.controllers[pts] = data
	end
end
function atc.save_data()
	return {controllers = atc.controllers}
end
--contents: {command="...", arrowconn=0-15 where arrow points}

--general
function atc.train_set_command(train, command, arrow)
	atc.train_reset_command(train, true)
	train.atc_delay = 0
	train.atc_arrow = arrow
	train.atc_command = command
end

function atc.send_command(pos, par_tid)
	local pts=minetest.pos_to_string(pos)
	if atc.controllers[pts] then
		--atprint("Called send_command at "..pts)
		local train_id = par_tid or advtrains.get_train_at_pos(pos)
		if train_id then
			if advtrains.trains[train_id] then
				--atprint("send_command inside if: "..sid(train_id))
				if atc.controllers[pts].arrowconn then
					atlog("ATC controller at",pts,": This controller had an arrowconn of", atc.controllers[pts].arrowconn, "set. Since this field is now deprecated, it was removed.")
					atc.controllers[pts].arrowconn = nil
				end
				
				local train = advtrains.trains[train_id]
				local index = advtrains.path_lookup(train, pos)
				
				local iconnid = 1
				if index then
					iconnid = train.path_cn[index]
				else
					atwarn("ATC rail at", pos, ": Rail not on train's path! Can't determine arrow direction. Assuming +!")
				end
				
				local command = atc.controllers[pts].command				
				command = eval_conditional(command, iconnid==1, train.velocity)
				if not command then command="" end
				command=string.match(command, "^%s*(.*)$")
				
				if command == "" then
					atprint("Sending ATC Command to", train_id, ": Not modifying, conditional evaluated empty.")
					return true
				end
				
				atc.train_set_command(train, command, iconnid==1)
				atprint("Sending ATC Command to", train_id, ":", command, "iconnid=",iconnid)
				return true
				
			else
				atwarn("ATC rail at", pos, ": Sending command failed: The train",train_id,"does not exist. This seems to be a bug.")
			end
		else
			atwarn("ATC rail at", pos, ": Sending command failed: There's no train at this position. This seems to be a bug.")
			-- huch
			--local train = advtrains.trains[train_id_temp_debug]
			--atlog("Train speed is",train.velocity,", have moved",train.dist_moved_this_step,", lever",train.lever)
			--advtrains.path_print(train, atlog)
			-- TODO track again when ATC bugs occur...
			
		end
	else
		atwarn("ATC rail at", pos, ": Sending command failed: Entry for controller not found.")
		atwarn("ATC rail at", pos, ": Please visit controller and click 'Save'")
	end
	return false
end

-- Resets any ATC commands the train is currently executing, including the target speed (tarvelocity) it is instructed to hold
-- if keep_tarvel is set, does not clear the tarvelocity
function atc.train_reset_command(train, keep_tarvel)
	train.atc_command=nil
	train.atc_delay=nil
	train.atc_brake_target=nil
	train.atc_wait_finish=nil
	train.atc_wait_autocouple=nil
	train.atc_wait_signal=nil
	train.atc_arrow=nil
	if not keep_tarvel then
		train.tarvelocity=nil
	end
end

--nodes
local idxtrans={static=1, mesecon=2, digiline=3}
local apn_func=function(pos)
	-- FIX for long-persisting ndb bug: there's no node in parameter 2 of this function!
	local meta=minetest.get_meta(pos)
	if meta then
		meta:set_string("infotext", S("Unconfigured ATC controller"))
		meta:set_string("formspec", atc.get_atc_controller_formspec(pos, meta))
	end
end

advtrains.atc_function = function(def, preset, suffix, rotation)
		return {
			after_place_node=apn_func,
			after_dig_node=function(pos)
				advtrains.invalidate_all_paths(pos)
				advtrains.ndb.clear(pos)
				local pts=minetest.pos_to_string(pos)
				atc.controllers[pts]=nil
			end,
			on_receive_fields = function(pos, formname, fields, player)
				if advtrains.is_protected(pos, player:get_player_name()) then
					minetest.record_protection_violation(pos, player:get_player_name())
					return
				end
				local meta=minetest.get_meta(pos)
				if meta then
					if not fields.save then 
						return
					end
					--meta:set_string("mode", idxtrans[fields.mode])
					meta:set_string("command", fields.command)
					--meta:set_string("command_on", fields.command_on)
					meta:set_string("channel", fields.channel)
					
					meta:set_string("infotext", S("ATC controller, Command: @1", meta:get_string("command")) )

					meta:set_string("formspec", atc.get_atc_controller_formspec(pos, meta))
					
					local pts=minetest.pos_to_string(pos)
					local _, conns=advtrains.get_rail_info_at(pos, advtrains.all_tracktypes)
					atc.controllers[pts]={command=fields.command}
					if #advtrains.occ.get_trains_at(pos) > 0 then
						atc.send_command(pos)
					end
				end
			end,
			advtrains = {
				on_train_enter = function(pos, train_id)
					atc.send_command(pos, train_id)
				end,
			},
		}
end

function atc.get_atc_controller_formspec(pos, meta)
	local mode=tonumber(meta:get_string("mode")) or 1
	local command=meta:get_string("command")
	local command_on=meta:get_string("command_on")
	local channel=meta:get_string("channel")
	local formspec="size[8,6]"
	--	"dropdown[0,0;3;mode;static,mesecon,digiline;"..mode.."]"
	if mode<3 then
		formspec=formspec
			.."style[command;font=mono]"
			.."field[0.8,1.5;7,1;command;"..S("Command")..";"..minetest.formspec_escape(command).."]"
	else
		formspec=formspec.."field[0.8,1.5;7,1;channel;"..S("Digiline channel")..";"..minetest.formspec_escape(channel).."]"
	end
	return formspec.."button_exit[0.5,4.5;7,1;save;"..S("Save").."]"
end

--from trainlogic.lua train step
local matchptn={
	["SM"]=function(id, train)
		train.tarvelocity=train.max_speed
		return 2
	end,
	["S([0-9]+)"]=function(id, train, match)
		train.tarvelocity=tonumber(match)
		return #match+1
	end,
	["B([0-9]+)"]=function(id, train, match)
		local btar = tonumber(match)
		if train.velocity>btar then
			train.atc_brake_target=btar
			if not train.tarvelocity or train.tarvelocity>btar then
				train.tarvelocity=btar
			end
		else
			-- independent of brake target, must make sure that tarvelocity is not greater than it
			if train.tarvelocity and train.tarvelocity>btar then
				train.tarvelocity=btar
			end
		end
		return #match+1
	end,
	["BB"]=function(id, train)
		train.atc_brake_target = -1
		train.tarvelocity = 0
		return 2
	end,
	["W"]=function(id, train)
		train.atc_wait_finish=true
		return 1
	end,
	["D([0-9]+)"]=function(id, train, match)
		train.atc_delay=tonumber(match)
		return #match+1
	end,
	["R"]=function(id, train)
		if train.velocity<=0 then
			advtrains.invert_train(id)
			advtrains.train_ensure_init(id, train)
			-- no one minds if this failed... this shouldn't even be called without train being initialized...
		else
			atwarn(sid(id), S("ATC Reverse command warning: didn't reverse train, train moving."))
		end
		return 1
	end,
	["O([LRC])"]=function(id, train, match)
		local tt={L=-1, R=1, C=0}
		local arr=train.atc_arrow and 1 or -1
		train.door_open = tt[match]*arr
		return 2
	end,
	["K"] = function(id, train)
		if train.door_open == 0 then
			atwarn(sid(id), S("ATC Kick command warning: doors are closed."))
			return 1
		end
		if train.velocity > 0 then
			atwarn(sid(id), S("ATC Kick command warning: train moving."))
			return 1
		end
		local tp = train.trainparts
		for i=1,#tp do
			local data = advtrains.wagons[tp[i]]
			local obj = advtrains.wagon_objects[tp[i]]
			if data and obj then
				local ent = obj:get_luaentity()
				if ent then
					for seatno,seat in pairs(ent.seats) do
						if data.seatp[seatno] and not ent:is_driver_stand(seat) then
							ent:get_off(seatno)
						end
					end
				end
			end
		end
		return 1
	end,
	["A([01])"]=function(id, train, match)
		if not advtrains.interlocking then return 2 end
		advtrains.interlocking.ars_set_disable(train, match=="0")
		return 2
	end,
	["Cpl"]=function(id, train)
		train.atc_wait_autocouple=true
		return 3
	end,
	["G"]=function(id, train)
		train.atc_wait_signal=true
		return 1
	end,
}

eval_conditional = function(command, arrow, speed)
	--conditional statement?
	local is_cond, cond_applies, compare
	local cond, rest=string.match(command, "^I([%+%-])(.+)$")
	if cond then
		is_cond=true
		if cond=="+" then
			cond_applies=arrow
		end
		if cond=="-" then
			cond_applies=not arrow
		end
	else 
		cond, compare, rest=string.match(command, "^I([<>]=?)([0-9]+)(.+)$")
		if cond and compare then
			is_cond=true
			if cond=="<" then
				cond_applies=speed<tonumber(compare)
			end
			if cond==">" then
				cond_applies=speed>tonumber(compare)
			end
			if cond=="<=" then
				cond_applies=speed<=tonumber(compare)
			end
			if cond==">=" then
				cond_applies=speed>=tonumber(compare)
			end
		end
	end	
	if is_cond then
		atprint("Evaluating if statement: "..command)
		atprint("Cond: "..(cond or "nil"))
		atprint("Applies: "..(cond_applies and "true" or "false"))
		atprint("Rest: "..rest)
		--find end of conditional statement
		local nest, pos, elsepos=0, 1
		while nest>=0 do
			if pos>#rest then
				atwarn(sid(id), S("ATC command syntax error: I statement not closed: @1",command))
				return ""
			end
			local char=string.sub(rest, pos, pos)
			if char=="I" then
				nest=nest+1
			end
			if char==";" then
				nest=nest-1
			end
			if nest==0 and char=="E" then
				elsepos=pos+0
			end
			pos=pos+1
		end
		if not elsepos then elsepos=pos-1 end
		if cond_applies then
			command=string.sub(rest, 1, elsepos-1)..string.sub(rest, pos)
		else
			command=string.sub(rest, elsepos+1, pos-2)..string.sub(rest, pos)
		end
		atprint("Result: "..command)
	end
	return command
end

function atc.execute_atc_command(id, train)
	--strip whitespaces
	local command=string.match(train.atc_command, "^%s*(.*)$")
	
	
	if string.match(command, "^%s*$") then
		train.atc_command=nil
		return
	end

	train.atc_command = eval_conditional(command, train.atc_arrow, train.velocity)
	
	if not train.atc_command then return end
	command=string.match(train.atc_command, "^%s*(.*)$")
	
	if string.match(command, "^%s*$") then
		train.atc_command=nil
		return
	end
	
	for pattern, func in pairs(matchptn) do
		local match=string.match(command, "^"..pattern)
		if match then
			local patlen=func(id, train, match)
			--atdebug("Executing: "..string.sub(command, 1, patlen))
			--atdebug("Train ATC State: tvel=",train.tarvelocity,"brktar=",train.atc_brake_target,"delay=",train.atc_delay,"wfinish=",train.atc_wait_finish,"wacpl=",train.atc_wait_autocouple)

			train.atc_command=string.sub(command, patlen+1)
			if train.atc_delay<=0
				and not train.atc_wait_finish
				and not train.atc_wait_autocouple
				and not train.atc_wait_signal then
				--continue (recursive, cmds shouldn't get too long, and it's a end-recursion.)
				atc.execute_atc_command(id, train)
			end
			return
		end
	end
	atwarn(sid(id), S("ATC command parse error: Unknown command: @1", command))
	atc.train_reset_command(train, true)
end



--move table to desired place
advtrains.atc=atc
