require "Commodity_1";
require "Scheduler_3";
require "Speed_1";
local P = require "Persistence_3";

local statBranches = {};
local dynBranches = P["Crossing$Branches"] or {};

local function define(branchName, speed, giveWayBranchNames, ...) -- ... is a sequence of triples (trackID, switchID, switchState)
	assertParam(1, false, "string");
	assertParam(2, false, "number");
	assertParam(3, false, "string", "table");
	giveWay = {};
	if type(giveWayBranchNames) == "table" then
		for i, v in ipairs(giveWayBranchNames) do
			if type(v) == "string" then
				table.insert(giveWay, v);
			else
				error("giveWayBranchNames["..i.."] is not a string "..tostring(v));
			end;
		end;
	else
		giveWay[1] = giveWayBranchNames;
	end;
	local switchInfos = {...};
	local switches = {};
	if #switchInfos % 3 == 0 then
		for i = 1, #switchInfos, 3 do
			local trackID, switchID, switchState = switchInfos[i], switchInfos[i + 1], switchInfos[i + 2];
			if type(trackID) ~= "number" or trackID <= 0 then
				error("trackID "..tostring(trackID).." is not a positive number");
			elseif type(switchID) ~= "number" or switchID <= 0 then
				error("switchID "..tostring(switchID).." is not a positive number");
			elseif type(switchState) ~= "number" or switchState <= 0 then
				error("switchState "..tostring(switchState).." is not a positive number");
			else
				table.insert(switches, {trackID = trackID, switchID = switchID, switchState = switchState});
			end;
		end;
	else
		error("Crossing.define(): switch information must be sets of 3 values each.");
	end;
	switches.speed = speed;
	switches.giveWay = giveWay;
	statBranches[branchName] = switches;
end;

local function mustGiveWay(branchName)
	for _, giveWayBranchName in ipairs(statBranches[branchName].giveWay) do
		if dynBranches[giveWayBranchName] and next(dynBranches[giveWayBranchName]) then
			return giveWayBranchName;
		end;
	end;
	return nil;
end;

local function entering(branchName, ...)
	assertParam(1, false, "string");
	local candidates = {...};
	if #candidates > 0 then
		table.insert(candidates, 1, branchName);
		local i = 1;
		local s = 0;
		local n = 0;
		while i <= #candidates do
			local t = type(candidates[i]);
			if t ~= "string" then
				error("Crossing.entering: string parameter expected, but "..t.." found.");
			elseif i < #candidates and type(candidates[i + 1]) == "number" then
				s = s + candidates[i + 1];
			else
				table.insert(candidates, i + 1, 0);
				n = n + 1;
			end;
			i = i + 2;
		end;
		if n > 0 then s = (1.0 - s) / n; end;
		local r = math.random();
		for i = 2, #candidates, 2 do
			branchName = candidates[i - 1];
			r = r - (candidates[i] == 0.0 and s or candidates[i]);
			if r < 0 then
				break;
			end;
		end;
	end;
	Debug.printf("Crossing", "\"%s\" enters \"%s\"", trainName, branchName);
	if not statBranches[branchName] then
		error("Crossing.entering: branchName has not been defined");
	end;
	local trains = dynBranches[branchName];
	if not trains then
		trains = {};
		dynBranches[branchName] = trains;
	end;
	trains[trainName] = 0;
	P["Crossing$Branches"] = dynBranches;
end;

local function stopOrGo(distance)
	assertParam(1, false, "number");
	local branchName;
	for branch, trains in pairs(dynBranches) do
		if trains[trainName] then
			branchName = branch;
			break;
		end;
	end;
	local ok, speed = EEPGetTrainSpeed(trainName);
	if not ok then
		error("Crossing.stopOrGo(): failed to get speed of \""..trainName.."\"");
	elseif not branchName then
		error("Crossing.stopOrGo(): \""..trainName.."\" not found on any branch");
	else
		local speedSign;
		if mustGiveWay(branchName) then
			speedSign = speed > 0 and 1 or -1;
			speed = 0;
		else
			speedSign = 0;
			speed = statBranches[branchName].speed;
		end;
		Speed.change(trainName, speed, distance);
		dynBranches[branchName][trainName] = speedSign;
		P["Crossing$Branches"] = dynBranches;
	end;
end;

local function leaving(...) -- ... is a sequence of branchNames or a final speed
	for _, branchName in ipairs({...}) do
		-- Debug.printf("Crossing", "Crossing.leaving(): testing branch %s", branchName);
		local trains = dynBranches[branchName];
		if trains ~= nil and trains[trainName] ~= nil then
			trains[trainName] = nil;
			Debug.printf("Crossing", "Crossing.leaving(): removed %s from %s", trainName, branchName);
			Speed.cancel(trainName); -- just in case ...
		end;
	end;
	P["Crossing$Branches"] = dynBranches;
end;

local function getSwitchInfos(branchName, trackID)
	for i, switchInfo in ipairs(statBranches[branchName]) do
		if trackID == switchInfo.trackID then
			return switchInfo.switchID, switchInfo.switchState;
		end;
	end;
	return nil, nil;
end;

local function checkBranches() -- to be called on a regular basis by EEPMain() or an appropriate scheduler
	-- for all branches
	--   for all registered trains
	--     restart stopped trains if possible
	--     check if it is the last train on a switch and set the switch appropriate
	-- if all trains are stopped, select one to restart
	local anyChanges = false;
	local distances = {};
	local switches = {};
	local runningCount = 0;
	local stoppedCount = 0;
	local rnd = math.random();
	local candidateTrainName = nil;
	local candidateSpeed = 0.0;
	for branchName, trains in pairs(dynBranches) do
		for trainName, speedSign in pairs(trains) do
			if speedSign ~= 0 then -- train is stopped or stopping
				if mustGiveWay(branchName) then
					stoppedCount = stoppedCount + 1;
					if rnd <= 1.0 / stoppedCount then
						candidateTrainName = trainName;
						candidateSpeed = statBranches[branchName].speed * speedSign;
					end;
				else -- starting over
					local speed = statBranches[branchName].speed * speedSign;
					if Speed.cancel(trainName, speed) then
						Debug.printf("Crossing", "Crossing.checkBranches(): Restart \"%s\" to %.1f km/h", trainName, speed);
					end;
					trains[trainName] = 0;
					anyChanges = true;
					runningCount = runningCount + 1;
					speedSign = 0; -- train is no longer stopped nor stopping
				end;
			else
				runningCount = runningCount + 1;
			end;
			local rsName = EEPGetRollingstockItemName(trainName, 0);
			local ok, trackID, pos = EEPRollingstockGetTrack(rsName);
			if ok then
				local switchID, switchState = getSwitchInfos(branchName, trackID);
				if switchID and (not distances[switchID] or distances[switchID] < pos) then
					if speedSign == 0 then -- train is running, so switch must be checked
						switches[switchID] = switchState;
					else
						switches[switchID] = true;
					end;
					distances[switchID] = pos;
				end;
			else
				error(string.format("EEPRollingstockGetTrack() failed, branchName = \"%s\", trainName = \"%s\", rsName = \"%s\"",
					branchName, trainName, rsName));
			end;
		end;
	end;
	for switchID, switchState in pairs(switches) do
		if type(switchState) == "number" and EEPGetSwitch(switchID) ~= switchState then
			if EEPSetSwitch(switchID, switchState, 1) == 1 then
				Debug.printf("Crossing", "Crossing.checkBranches(): Switch %d set to %d", switchID, switchState);
			else
				error(string.format("Failed to set switch %d to %d", switchID, switchState));
			end;
		end;
	end;
	if runningCount == 0 and candidateTrainName and candidateSpeed then
		if EEPSetTrainSpeed(candidateTrainName, candidateSpeed) then
			Debug.printf("Crossing", "Crossing.checkBranches(): No train running, so restart %s (%f km/h)",
				candidateTrainName, candidateSpeed);
		else
			error(string.format("No train running, but failed to set speed for %s to %f", candidateTrainName, candidateSpeed));
		end;
	end;
	if anyChanges then
		P["Crossing$Branches"] = dynBranches;
	end;
	return true;
end;

Scheduler.addRequest(checkBranches, -1);

Crossing = {
	_VERSION = "2.0";
	
	-- Funktion define(branchName, speed, giveWayBranchNames, ...)
	-- Bedeutung: 					definiert einen einzelnen Abzweig in einer (Straßen-) Kreuzung
	-- branchName:					ein Name für den Abzweig, der Name muss in der ganzen Anlage eindeutig sein.
	-- speed:								maximale Geschwindigkeit beim Befahren des Abzweigs
	-- giveWayBranchNames:	eine Tabelle von Abzweignamen, die gegenüber dem aktuellen Abzweig
	--                      Vorfahrt haben, indiziert mit 1, 2, ...
	-- trackID:             ID des Splines einer Weiche
	-- switchID:            ID derselben Weiche
	-- switchState:         Stellung der Weiche
	-- Die letzten drei Parameter werden so oft wiederholt, wie es Weichen in diesem Abzweig gibt.
	define = define;

	-- Funktion entering(branchName, ...)
	-- Bedeutung:		Der auslösende Zug (Variable trainName) betritt eine Abzweigung.
	--							Der auslösende Kontaktpunkt muss soweit VOR der Abzweigung liegen, dass ein
	--							nachrangiger Verkehrsteilnehmer den auslösenden früh genug "sieht" und bremsen kann.
	--							Die Parameter sind die Bezeichner der Abzweigungen und ihre Wahrscheinlichkeit,
	--							von denen zufällig einer ausgewählt wird.
	--							Im einfachsten Fall legt "entering('Nord->Ost')" die Abzweigung direkt fest.
	--							"entering('A', 0.5, 'B', 0.3, 'C', 'D')" wählt in 50% der Fälle die Abzweigung 'A',
	--							in 30% die Abzweigung 'B'; der Rest verteilt sich gleichmäßig auf 'C' und 'D'.
	-- WICHTIG:     Jedes Verlassen einer Verzweigung muss die Funktion leaving (s.u.) aufrufen.
	-- branchName:	Name des Abzweigs, in den der Zug eintritt.
	-- ...:					Eintrittswahrscheinlichkeit für die letzte Abzweigung, oder nächste Abzweigung, u.s.w.
	entering = entering;
	
	-- Funktion stopOrGo(distance)
	-- Bedeutung:		Ab dieser Stelle ändert sich die Geschwindigkeit. Ist ein vorfahrtberechtigtes
	--							Fahrzeug in der Kreuzung, stoppt das auslösende Fahrzeug. Andernfalls ändert es
	--							seine Geschwindigkeit entsprechend der Definition der Abzweigung.
	-- distance:		Entfernung, bis das Fahrzeug stehen soll, oder seine neue Geschw. erreicht haben soll.
	stopOrGo = stopOrGo;
	
	-- Funktion leaving(branchName, ...)
	-- Bedeutung:		entfernt den auslösenden Zug (Variable trainName) aus allen angegebenen Abzweigen
	-- branchName:	Name des Abzweigs, aus dem der Zug entfernt werden soll.
	-- Es können beliebig viele Abzweige (durch Kommata getrennt) angegeben werden.
	leaving = leaving;
}

print("Crossing Version " .. Crossing._VERSION);

return Crossing;