--Environment Code
-------------------------------------------------------------------------------------
-- Yard Information
if event.init then
	S.yards = {
		--[[  yard info template
			yard_id = {
			yard_name = string,
			active_indicator_pos = POS(),
			dir_indicator_pos = POS(),
			error_indicator_pos = POS(),
			headshunt_max = number,
			notify = empty table,
			notify_pos = pos,

			-- reserved vars --
			last_id = string  (the id of the last train that entered the yard last)
			arrival_length = number
			arrival_time = string (RWT)
			departure_length = number or "?" (set when train arrives, until train departs)
			departure_time = string (RWT)
			rts = bool (whether the entering train will exit the way it came in)
		},
		]]--
		TY = {
			yard_name = "Trisiston",
			active_indicator_pos = POS(-4025,14,-2659),
			dir_indicator_pos = POS(-4025,12,-2665),
			error_indicator_pos = POS(-4025,13,-2671),
			headshunt_max = 5,
			notify = {},
			notify_pos = POS(-4023,12,-2660),
			arrival_time = rwt.now(),
			departure_time = rwt.now()
		},
		BY = {
			yard_name = "Banach",
			active_indicator_pos = POS(-2002,3,-1099),
			dir_indicator_pos = POS(-2009,3,-1101),
			error_indicator_pos = POS(-1999,3,-1099),
			headshunt_max = 5,
			notify = {},
			notify_pos = POS(-2004,2,-1101),
			arrival_time = rwt.now(),
			departure_time = rwt.now()
		},
		ARC = {
			yard_name = "Arcadius",
			active_indicator_pos = POS(-1952,16,840),
			dir_indicator_pos = POS(-1950,16,840),
			error_indicator_pos = POS(-1948,16,840),
			headshunt_max = 5,
			notify = {},
			notify_pos = POS(-1946,16,840),
			arrival_time = rwt.now(),
			departure_time = rwt.now()
		},
		IP = {
			yard_name = "Ipswich",
			active_indicator_pos = POS(1179,16,3848),
			dir_indicator_pos = POS(1177,16,3855),
			error_indicator_pos = POS(1179,16,3850),
			headshunt_max = 5,
			notify = {},
			notify_pos = POS(1178,16,3851),
			arrival_time = rwt.now(),
			departure_time = rwt.now()
		},
		CAN = {
			yard_name = "Cannery",
			active_indicator_pos = POS(-594,26,2486),
			dir_indicator_pos = POS(-594,26,2484),
			error_indicator_pos = POS(-594,26,2482),
			headshunt_max = 2,
			--notify = {},
			--notify_pos = POS(-594,26,2485),
			arrival_time = rwt.now(),
			departure_time = rwt.now()
		}
		-- HY = {
			-- active_indicator_pos = POS(-4025,14,-2659),
			-- dir_indicator_pos = POS(-4025,13,-2665),
			-- error_indicator_pos = POS(-4025,13,-2671),
		-- },
	}
	S.known_trains = {
		['120684'] =  "LHF #1", --Maverick2797
		['249165'] =  "LHF #2", --Maverick2797
		['368003'] =  "ARC-BY", --Maverick2797
		['588750'] =  "MMF-TY", --survivalg/erstazi
		['879136'] =  "ERS-SNS", --erstazi
		['543381'] =  "WOA-IP", --Maverick2797
		['834721'] =  "S27-ARC", --Maverick2797
		['513598'] =  "S27EX-ARC",  --Maverick2797
		['590988'] =  "CAN-ARC", --Maverick2797
	}
end
------------------------------------------------------------------------------------
-- Utility Functions
F.indicator = function(indicator,set)
	if set ~= nil then
		if type(set) == string then
			setstate(indicator,set)
		else
			setstate(indicator,(set and "on") or "off")
		end
	end
	return (getstate(indicator) == "on") or false
end

F.get_rc_safe = function()
	return get_rc() or ""
end

F.has_rc = function(query,rc_list) -- query = string, single entry
	if not atc_id then return false end
	if rc_list == "" or query == nil or query=="" then return false end
	if not rc_list then rc_list = F.get_rc_safe() end
	for word in rc_list:gmatch("[^%s]+") do
		if word == query then return true end
	end
	return false
end

F.has_rc_match = function(query,rc_list) -- query = pattern string, single entry
	if not atc_id then return false end
	if rc_list == "" or query == nil or query=="" then return false end
	if not rc_list then rc_list = F.get_rc_safe() end

	local rc = {}
	for v in rc_list:gmatch("("..query..")") do
		table.insert(rc,v)
	end

	if rc[1] == true then
		return true, rc
	else
		return nil
	end
end

F.add_rc = function(rc_list) -- rc_list = string or table, eg: {"rc1","rc2"} OR "rc1 rc2"
	if not atc_id then return false end
	if type(rc_list) == "table" then
		rc_list = table.concat(rc_list," ")
	end
	set_rc(F.get_rc_safe().." "..rc_list)
	return true
end

F.remove_rc = function(rc_list,arrow_mode) -- rc_list = string eg: "rc1 rc2 rc3" OR table eg: {"rc1","rc2","rc3"}
	-- Arrow Modes:
	-- true: with arrow direction
	-- false: against arrow direction
	-- nil: ignores arrow direction

	if not atc_id then return false end
	if not rc_list then return false end

	if (arrow_mode == nil) or (atc_arrow == arrow_mode) then
		-- prep rc_list to useable format
		local rc_remove = {}
		if type(rc_list) == "string" then
			for word in rc_list:gmatch("[^%s]+") do
				rc_remove[word] = true
			end
		elseif type(rc_list) == "table" then
			for _,word in pairs(rc_list) do
				rc_remove[word] = true
			end
		end

		-- remove codes from train's rc
		local rc = F.get_rc_safe()
		local reinsert = {}
		for token in rc:gmatch("[^%s]+") do
			if not rc_remove[token] then
				table.insert(reinsert,token)
			end
		end
		-- insert new string to train's rc
		set_rc(table.concat(reinsert," "))
	end
	return reinsert
end

F.remove_rc_match = function(rc_list) -- rc_list = pattern string, single entry, eg: "rc_%d+"
	if not atc_id then return false end
	if not rc_list then return false end
	local rm = {}
	for v in F.get_rc_safe():gmatch("("..rc_list..")") do
		table.insert(rm,v)
	end
	F.remove_rc(rm)
	return rm
end

----------------------------------------------------------------------------------------------
-- Trackside Functions
-- this_dir = points towards Origin/Junction Yard
F.yard_arrival = function(yard_id,this_dir, force_rts) -- arrow points towards yard
	local yard = S.yards[yard_id] --yard ref

	if F.has_rc(yard_id.."_NOSHUNT") then return end

	local function enter_yard()
		if not atc_id then
			F.indicator(yard.error_indicator_pos,true)
			return
		end
		F.indicator(yard.dir_indicator_pos,this_dir)
		F.indicator(yard.active_indicator_pos,true)
		F.add_rc({yard_id.."_ARRIVE"})
		local rts = false
		if force_rts then -- yard is designated as a terminus yard. all trains MUST rts
			F.add_rc(yard_id.."_RTS")
			rts = true
		elseif F.has_rc(yard_id.."_RTS") then --save the RTS flag as it's removed during the arrival procedure
			F.add_rc({yard_id.."_HAS_RTS"})
			rts = true
		end
		atc_set_ars_disable(false)
		atc_send("S6")

		S.yards[yard_id].last_id = atc_id
		S.yards[yard_id].arrival_length = train_length()
		S.yards[yard_id].arrival_time = rwt.now()
		S.yards[yard_id].departure_length = "?"
		S.yards[yard_id].departure_time = rwt.now()
		S.yards[yard_id].rts = rts

		if S.print_debug then
			print(rwt.to_string(rwt.now()))
			print("YARD "..yard_id..": Train "..atc_id.." ("..(S.known_trains[atc_id] or "Unknown")..") enters from the "..tostring(this_dir).." direction and will exit in the "..tostring(rts).." direction")
			print("YARD "..yard_id..": Length "..train_length())
		end
		return
	end

	__approach_callback_mode = 1
	if event.approach and not event.has_entered then
		atc_set_ars_disable(true)
		atc_set_lzb_tsr(1)
		return
	end

	if event.train and atc_arrow then
		if F.indicator(yard.active_indicator_pos) then
			if S.print_debug then
				print(rwt.to_string(rwt.now()))
				print("YARD "..yard_id..": Train "..atc_id.." ("..(S.known_trains[atc_id] or "Unknown")..") has arrived from the "..tostring(this_dir).." direction and has to wait for the yard to deactivate.")
			end
			schedule_in(";10","recheck")
			return
		else
			enter_yard()
			return true
		end
	end

	if event.schedule then
		if F.indicator(yard.active_indicator_pos) then
			schedule_in(";10","recheck")
			return
		else
			enter_yard()
			return true
		end
	end
end

F.classification = function(yard_id, this_dir) -- arrow points towards headshunt
	local yard = S.yards[yard_id] --yard ref

	if not F.indicator(yard.active_indicator_pos) then return end
	if F.has_rc(yard_id.."_NOSHUNT") then return end

	-- this_dir == true for north end, false for south end
	if F.indicator(yard.active_indicator_pos) then
		if atc_arrow then -- loco is at working end
			F.remove_rc({yard_id.."_PICKUP"})
			if F.has_rc(yard_id.."_ARRIVE") and F.indicator(yard.dir_indicator_pos) == this_dir then --first pass, prep train for working
				F.remove_rc({yard_id.."_AROUND"})
				if not F.get_rc_safe():match(yard_id.."_LOCOS_%d+") or not F.get_rc_safe():match(yard_id.."_WAGONS_%d+") then
					local full_length = train_length()
					split_off_locomotive("A0B0")
					local loco_count = train_length()
					local wagon_count = 0
					if full_length > loco_count then
						wagon_count = full_length - loco_count
					end
					F.add_rc({yard_id.."_LOCOS_"..loco_count, yard_id.."_WAGONS_"..wagon_count})
					--[[
					train will end up going through headshunt no matter what
					there shouldn't be any trains entering that pass straight through the yard
					any that do should run under the _NOSHUNT rc
					]]--
					if not F.has_rc(yard_id.."_RTS") then
						F.add_rc({yard_id.."_HEADSHUNT",yard_id.."_PICKUP"})
					else
						F.indicator(yard.dir_indicator_pos,not F.indicator(yard.dir_indicator_pos))
						F.remove_rc({yard_id.."_RTS"})
						F.add_rc({yard_id.."_AROUND"}) -- send loco around to the other end
					end
					atc_set_ars_disable(false)
					return -- train has departed for headshunt/around
				end --else train has arrived from wagon count and headshunt/around
				F.remove_rc({yard_id.."_ARRIVE"})
			end
			if this_dir == F.indicator(yard.dir_indicator_pos) then --train has bounced and is ready to classify wagon(s)
				local locos = tonumber(F.get_rc_safe():match(yard_id.."_LOCOS_(%d+)")) or 1 --saved loco count
				local wagons = tonumber(F.get_rc_safe():match(yard_id.."_WAGONS_(%d+)")) or 0 -- wagon_count from last classification split
				local lane = split_at_fc("A0B0",yard.headshunt_max) -- where to classify this rake -- headshunt length
				local this_rake = train_length() - locos
				if lane ~= "" then
					F.add_rc({yard_id.."_CLASS_"..lane})
				end
				if this_rake == wagons then -- mark for last_classification
					F.add_rc({yard_id.."_LAST_CLASS"})
				end
				F.remove_rc_match(yard_id.."_WAGONS_%d+")
				F.add_rc({yard_id.."_HEADSHUNT",yard_id.."_WAGONS_"..wagons-this_rake}) -- subtract these wagons from the overall wagon count
				atc_set_ars_disable(false)
				set_autocouple()
			else --bounce train back towards working end
				atc_set_ars_disable(true)
				atc_send("S0WRD1S3")
				unset_autocouple()
			end
		else -- train entering from the far end. set autocouple so it pushes all the way through to the bounce
			atc_set_ars_disable(true)
			set_autocouple()
		end
	end
end

F.headshunt_yard = function(yard_id,this_dir) -- arrow points toward yard
	local yard = S.yards[yard_id] --yard ref

	__approach_callback_mode = 1
	if event.approach and not event.has_entered then
		atc_set_ars_disable(true)
		atc_set_lzb_tsr(1)
		return
	end

	if not F.indicator(yard.active_indicator_pos) then return end
	if F.has_rc(yard_id.."_NOSHUNT") then return end

	if event.train then
		if not atc_arrow then --train has entered headshunt from yard. bounce or depart
			atc_set_ars_disable(true)
			F.remove_rc({yard_id.."_HEADSHUNT"})
			if not F.has_rc(yard_id.."_FINAL_COLLECT") then
				if not F.has_rc(yard_id.."_DEPART") then -- MOST TRAINS. only set when train wants to depart, otherwise train will start the reverse countdown
					schedule_in(";01",atc_id)
					return
				else
					if get_line() ~= "LHF" then -- LHF trains will always only collect the * lane for that direction. This code is for other trains
						local collect = F.get_rc_safe():match(yard_id.."_COLLECT_(%S+)") -- will only match to the first _COLLECT RC to avoid excessively long trains in the headshunt
						if collect then --train needs to collect a rake from the yard before departing
							F.add_rc({yard_id.."_CLASS_"..collect,yard_id.."_FINAL_COLLECT"})
							schedule_in(";01",atc_id)
							return
						end
					else -- CLASS_* should never be set in ARS, instead being left to the * route
						F.add_rc({yard_id.."_FINAL_COLLECT", yard_id.."_CLASS_*"})
						schedule_in(";01",atc_id)
						return
					end
				end

				 --else let train proceed forward to the exit controller without a rake
			end --else train has already collected rake. let it exit yard
		else --train has bounced
			if F.has_rc(yard_id.."_AROUND") then
				if this_dir == F.indicator(yard.dir_indicator_pos) then --send train to Classification
					F.remove_rc({yard_id.."_AROUND"})
					F.add_rc({yard_id.."_PICKUP"})
					atc_set_ars_disable(false)
					return
				end --if at wrong end, do nothing. train will follow <yard_id>_AROUND ARS to relevant headshunt
			end -- if doesn't have <yard_id>_AROUND then let ARS classify the train

			atc_set_ars_disable(false)
		end
		return
	end

	if event.schedule then
		if not atc_id then --bounce the train that just passed
			atc_send_to_train(event.msg,"B0WRD1A1S3")
		else
			if atc_id == event.msg then --train hasn't fully passed yet, wait a bit longer
				schedule_in(";01",atc_id)
			else -- somehow another train crossed the controller before the first one returned. Unlikely but possible
				atc_send_to_train(event.msg,"BBOL")
				atc_send("BBOL")
				F.indicator(yard.error_indicator_pos,true)
			end
		end
		return
	end
end

F.headshunt_exit = function(yard_id,this_dir) -- arrow points out of yard
	local yard = S.yards[yard_id] --yard ref

	__approach_callback_mode = 1
	if event.approach and not event.has_entered then
		atc_set_ars_disable(true)
		atc_set_lzb_tsr(1)
		return
	end

	if not F.indicator(yard.active_indicator_pos) then return end
	if F.has_rc(yard_id.."_NOSHUNT") then return end

	if event.train then
		if atc_arrow and (F.indicator(yard.dir_indicator_pos) == this_dir) then
			if F.has_rc(yard_id.."_DEPART") then
				if S.print_debug then
					print(rwt.to_string(rwt.now()))
					print("YARD "..yard_id..": Train "..atc_id.." ("..(S.known_trains[atc_id] or "Unknown")..") has a length of "..train_length())
					print("YARD "..yard_id..": It will depart in the "..tostring(this_dir).." direction.")
				end
				S.yards[yard_id].departure_length = train_length()
				unset_autocouple()
				F.remove_rc({yard_id.."_FINAL_COLLECT",yard_id.."_DEPART"})
				F.remove_rc_match(yard_id.."_LOCOS_%d+")
				F.remove_rc_match(yard_id.."_WAGONS_%d+")

				if yard.notify_pos then
					interrupt_pos(yard.notify_pos,"notify")
				end

				if F.has_rc(yard_id.."_HAS_RTS") then --reinsert the RTS flag to fully return RC list to starting
					F.remove_rc(yard_id.."_HAS_RTS")
					F.add_rc({yard_id.."_RTS"})
				end
				step_fc()
				atc_set_ars_disable(false)
				atc_send("S6D10SM")
				schedule_in(";05","deactivate_check") -- wait a little to ensure train is clear from headshunt to deactivate yard
				return
			end -- if not _DEPART then let the train pass as extra space for the bounce back to the yard
		else
			atc_send("BBOL")
			F.indicator(yard.error_indicator_pos,true)
		end
	end

	if event.schedule then
		if event.msg == "deactivate_check" then
			if not atc_id then
				F.indicator(yard.active_indicator_pos,false)
				S.yards[yard_id].departure_time = rwt.now()
				if S.print_debug then
					print(rwt.to_string(rwt.now()))
					print("YARD "..yard_id..": yard Disabled")
				end
			else
				schedule_in(";05","deactivate_check")
				return
			end
		end
	end
end

F.lane_EOL = function(yard_id,this_dir) -- arrow points towards headshunt
	local yard = S.yards[yard_id] --yard ref

	if not F.indicator(yard.active_indicator_pos) then return end
	if F.has_rc(yard_id.."_NOSHUNT") then return end

	if atc_arrow then
		if F.indicator(yard.dir_indicator_pos) == this_dir then --train has bounced and needs to leave the rake or depart with it
			if F.has_rc(yard_id.."_LAST_CLASS") then -- this is the last clasification move
				F.remove_rc({yard_id.."_LAST_CLASS"})
				F.add_rc({yard_id.."_DEPART"})
				split_off_locomotive("A0B0")
			elseif not F.has_rc(yard_id.."_FINAL_COLLECT") then
				-- disconnect loco and return to pickup
				split_off_locomotive("A0B0")
				F.add_rc({yard_id.."_PICKUP"})
			end --if has FINAL_COLLECT then don't split off the loco. take the full rake
			F.add_rc({yard_id.."_HEADSHUNT"})
		else --train needs to bounce
			atc_send("S0WRD1S4")
		end
	else
		if F.indicator(yard.dir_indicator_pos) == this_dir then
			local fc = F.get_rc_safe():match(yard_id.."_CLASS_(%S+)")
			F.remove_rc_match(yard_id.."_CLASS_%S+")
			if not yard.notify or not yard.notify_pos then return end
			if not fc then return end
			if fc == "*" then return end
			if F.has_rc(yard_id.."_COLLECT_"..fc) then return end
			S.yards[yard_id].notify[fc] = true
		else --this should never come into play as it means the train has entered from the wrong end somehow
			atc_send("BBOL") --stop the train and open the doors (if available) to signify assistance required
			F.indicator(yard.error_indicator_pos,true)
		end
	end
end