require "Debug_1";
local P = require "Persistence_3";

local loadstring = loadstring or load;

local tickNr;
local requests = {};
local now;

-- performance functions:

local tickPerformance = { firstTick = 0; maxTime = 0; sumTime = 0; }

local function measurePerformance(duration)
	if tickPerformance.firstTick == 0 then
		tickPerformance.firstTick = tickNr;
	else
		if not tickPerformance.minTime or tickPerformance.minTime > duration then
			tickPerformance.minTime = duration;
		end;
		if tickPerformance.maxTime < duration then
			tickPerformance.maxTime = duration;
		end;
		tickPerformance.sumTime = tickPerformance.sumTime + duration;
	end;
end;

local function showPerformance()
	return string.format("min = %0.3f, max = %0.3f, avg = %0.3f", tickPerformance.minTime or 0, tickPerformance.maxTime, tickPerformance.sumTime / (tickNr - tickPerformance.firstTick + 1));
end;

-- local functions:

local function createID()
	local maxID = 0;
	for i = 1, #requests do
		local ID = requests[i].ID;
		if ID and ID > maxID then
			maxID = ID;
		end;
	end;
	return maxID + 1;
end;

local function getRequestByID(ID)
	for i = 1, #requests do
		local r = requests[i];
		if r.ID == ID then
			return r;
		end;
	end;
	return nil;
end;

local function compile(what, ID, fieldName)
	local text, code, errormsg;
	if type(what) == "function" then
		text = string.dump(what);
		code = what;
	elseif type(what) == "string" then
		code, errormsg = loadstring(what);
		if not code then
			print(string.format("Request ID=%d: %s compiling %s \"%s\"",
				ID, errormsg, fieldName, what));
		else
			text = what;
		end;
	elseif what then
		print(string.format("Request ID=%d: %s is neither string nor function",
			ID, fieldName));
	end;
	return text, code;
end;

local function secureCall(ID, functionName, functionCode, ...)
	local success, value = pcall(functionCode, ...);
	if not success then
		print("Request ID=", ID, " function ", functionName, "(", ..., ") failed: ", value);
	end;
	return success, value;
end;

local function doAddRequest(ID, action, repetitions)
	local r = {};
	r.ID = ID;
	r.action, r._action = compile(action, ID, "action");
	if not repetitions or type(repetitions) == "number" then
		r.repetitions = repetitions;
	else
		print(string.format("Request ID=%d: repetitions(%s) is not numeric",
			ID, tostring(repetitions)));
	end;
	if (action and not r._action) or
		(repetitions and not r.repetitions) then
		Debug.print("Scheduler", "Request not accepted, see error(s) above.");
		return 0;
	else
		r.repetitions = repetitions or 0;
		table.insert(requests, r);
		return #requests;
	end;
end;

local function doRemoveRequest(index)
	local r = table.remove(requests, index);
--~ 	if r then
--~ 		Debug.printf("Scheduler", "Request ID=%s, action=\"%s\" gelöscht.",
--~ 			r.ID, r.action);
--~ 	else
--~ 		Debug.printf("Scheduler", "Request mit Index %d nicht gelöscht.", index);
--~ 	end;
end;

local function saveRequests(fromIndex, toIndex)
	for i = fromIndex, toIndex do
		local r = requests[i];
		if not r or r.repetitions < 0 then
			P["Sched$r"..i] = nil;
		else
			P["Sched$r"..i] = r;
		end;
	end;
	Debug.printf("Scheduler", "saved requests %d to %d", fromIndex, toIndex);
end;

local function advanceTime()
	tickNr = (tickNr or 0) + 1;
	P["Sched$tickNr"] = tickNr;
	local n = EEPTimeH * 3600 + EEPTimeM * 60 + EEPTimeS;
	if not now then
		now = n;
	else
		n = n + math.floor(now / 86400) * 86400;
		if n > now then
			now = n;
		elseif n < now - 43200 then -- next day
			Debug.printf("Scheduler", "Scheduler.advanceTime: now = %d, n = %d => next day, now = %d", now, n, n + 86400);
			now = n + 86400;
		else
			now = now + 0.19;
		end;
	end;
	P["Sched$now"] = now;
end;

-- exported functions:

local function getTickNr()
	return tickNr;
end;

local function addRequest(action, repetitions)
	local ID = createID();
	local index = doAddRequest(ID, action, repetitions);
	if index > 0 then
		saveRequests(1, #requests);
		return ID;
	else
		return nil;
	end;
end;

local function removeRequest(ID)
	local firstIndex2save = nil;
	local requestCount = #requests;
	local i = 1;
	while i <= #requests do
		if requests[i].ID == ID then
			doRemoveRequest(i);
			if not firstIndex2save then
				firstIndex2save = i;
			end;
		else
			i = i + 1;
		end;
	end;
	if firstIndex2save then
		saveRequests(firstIndex2save, requestCount);
		return true;
	else
		return false;
	end;
end;

local function requestSignal(signalID, value, checkSignalID, checkValue)
	if checkSignalID == nil then
		checkSignalID = signalID;
		checkValue = value;
	end;
	if type(signalID) ~= "number" or type(value) ~= "number" or type(checkSignalID) ~= "number" or type(checkValue) ~= "number" then
		print("Scheduler.requstSignal: all parameters must be numbers");
		return nil;
	else
		Debug.printf("Scheduler", "Requesting signal %d to state %d ...", signalID, value);
		local code = string.format(
			"if EEPGetSignal(%d) == %d then return true; else EEPSetSignal(%d, %d, 1); return false; end",
			checkSignalID, checkValue, signalID, value);
		return addRequest(code, 1);
	end;
end;

local function getNow()
	return now;
end;

local function executeAt(when, action)
	local t = type(when);
	if t == "string" then
		local h, m, s = string.match(when, "(%d+):?(%d*):?([%d.]*)");
		h = tonumber(h);
		m = tonumber(m);
		s = tonumber(s);
		when = (h or 0) * 3600 + (m or 0) * 60 + (s or 0);
	elseif t ~= "number" then
		print("Scheduler.executeAfter: 'when' must be a valid time (as a string or a number)!");
		return nil;
	end;
	if type(action) ~= "string" then
		print("Scheduler.executeAt: 'action' must be a compilable string!");
		return nil;
	end;
	if when < now then
		when = math.floor(now / 86400) * 86400 + when;
		if when < now then
			when = when + 86400;
		end;
	end;
	local code = string.format("if Scheduler.getNow() > %f then\n%s;\nreturn true;\nelse\nreturn false;\nend", when, action);
	Debug.printf("Scheduler", "Scheduler.executeAt: '%s' will be called in %f seconds\n", action, when - now);
	return addRequest(code, 1);
end;

local function executeAfter(delay, action)
	if not delay or type(delay) ~= "number" or delay < 0 then
		print("Scheduler.executeAfter: 'delay' must be a positive number!");
		return nil;
	else
		return executeAt(now + delay, action);
	end;
end;

local function hasActionContaining(substring)
	for i = 1, #requests do
		r = requests[i];
		if r and r.ID and string.find(r.action, substring, 1, true) then
			return true;
		end;
	end;
	return false;
end;

local function showRequests(doClear)
	if doClear == true then
		clearlog();
	else
		print();
	end;
	local anythingPrinted = false;
	local r, line;
	local n = math.floor(now);
	line = string.format("now = %d (day %d, %02d:%02d:%02d)",
		n, math.floor(n / 86400), math.floor((n % 86400) / 3600), math.floor((n % 3600) / 60), n % 60);
	print(line);
	for i = 1, #requests do
		r = requests[i];
		if r and r.ID then
			if not anythingPrinted then
				line = string.format("%-5s %-5s %s", "ID", "Wdh.", "Aktion");
				print(line);
				anythingPrinted = true;
			end;
			line = string.format("%-5d %-5d %s", r.ID, r.repetitions, r.action);
			line = string.gsub(line, "\n", " ");
			print(line);
		end;
	end;
	if not anythingPrinted then
		print("keine Requests im Scheduler");
	end;
end;

local function removeAllRequests()
	requests = {};
	saveRequests(1, 1000);
end;

-- Initialize module

now = P["Sched$now"];
tickNr = P["Sched$tickNr"];
if not now or not tickNr then
	advanceTime();
end;

for i = 1, 1000 do
	local r = P["Sched$r" .. i];
	if r then
		doAddRequest(r.ID, r.action, r.repetitions);
	end;
end;
if Debug.isSet("Scheduler") then showRequests(); end;

local function tick()
	local startTime, endTime;
	if Debug.isSet("Scheduler") then
		startTime = os.clock();
		if tickNr % 600 == 0 then
			Debug.printf("Scheduler", "tick(%d) with %d request(s), Performance: %s", tickNr, #requests, showPerformance());
		end;
	end;
	advanceTime();
	local firstIndex2save, requestCount = 0, #requests;
	local r, success, result;
	local i = 1;
	while i <= #requests do
		r = requests[i];
		-- Debug.printf("Scheduler", "%f: considering request %d of %d, ID = %d, action = \"%s\"", getNow(), i, #requests, r.ID, r.action);
		if type(r) == "table" then
			local requestCountBefore = #requests;
			success, result = secureCall(r.ID, r.action, r._action);
			if success and result then
				Debug.printf("Scheduler", "Request ID=%d, action=\"%s\" erfolgreich ausgeführt",
					r.ID, r.action);
			end;
			local requestCountAfter = #requests;
			if requestCountAfter > requestCountBefore then
				requestCount = requestCount + requestCountAfter - requestCountBefore;
			end;
			if success and result and r.repetitions > 0 then
				if firstIndex2save == 0 then
					firstIndex2save = i;
				end;
				if r.repetitions == 1 then
					doRemoveRequest(i);
					i = i - 1;
				else
					r.repetitions = r.repetitions - 1;
				end;
				Debug.printf("Scheduler", "indices to save: %d to %d", firstIndex2save, requestCount);
			end;
		end;
		i = i + 1;
	end;
	if firstIndex2save > 0 then
		saveRequests(firstIndex2save, requestCount);
	end;
	if Debug.isSet("Scheduler") then
		endTime = os.clock();
		measurePerformance(endTime - startTime);
	end;
	return 1;
end;

local oldEEPMain = EEPMain;
if type(oldEEPMain) == "function" then
	EEPMain = function()
		tick();
		return oldEEPMain();
	end;
else
	EEPMain = tick;
end;

Scheduler = {
	_VERSION = "3.0";

	-- Funktion getTickNr()
	-- Bedeutung:	gibt die aktuelle tickNr zurück
	-- Rückgabe:	tickNr als number
	getTickNr = getTickNr;

	-- Funktion getNow()
	-- Bedeutung:	gibt die aktuelle Uhrzeit zurück (als Sekunden seit Beginn
	--				der Epoche, kann Nachkommastellen enthalten)
	-- Rückgabe:	now als number (mit Nachkommastellen)
	getNow = getNow;

	-- Funktion addRequest(action, repetitions)
	-- Bedeutung:	erzeugt einen Request und speichert diesen im Scheduler
	-- Beispiel:  Scheduler.addRequest("print('Hallo Welt'); return true", 3) -- !!! das RETURN ist WICHTIG!!!
	-- action:		die auszuführende Aktion als String (oder Funktion)
	--				die Funktion darf keine lokalen oder Up-Values verwenden
	--				der String (oder der Dump der Funktion) müssen gespeichert
	--				werden können (Stringlänge deutlich unter 1000 Zeichen!)
	--				Die action erhält keine Parameter und muss einen boolean
	--				zurückgeben; true bedeutet, dass die Aktion erfolgreich war.
	--				Bei Erfolg wird die Anzahl der verbleibenden Ausführungen
	--				um 1 reduziert.
	-- repetitions:	> 0: Anzahl der Ausführungen
	--              = 0: unendliche Ausführungen
	--				< 0: unendliche Ausführungen, keine Speicherung (Annahme,
	--					 dass das LUA-Skript der Anlage diese Requests
	--					 wiederherstellt).
	-- Rückgabe:	eine numerische ID des Requests
	addRequest = addRequest;

	-- Funktion removeRequest(requestID)
	-- Bedeutung:	entfernt den Request mit der angegebenen ID aus dem Scheduler,
	--				so dass dieser Request nicht mehr aufgerufen wird.
	-- requestID:	ID des Requests wie von addRequest zurückgegeben.
	-- Rückgabe:	true, wenn ein Request mit der ID existierte, sonst false
	removeRequest = removeRequest;

	-- Funktion requestSignal(signalID, value, checkSignalID, checkValue)
	-- Bedeutung:	fügt einen Signal-Request hinzu. Dieser bleibt solange im
	--				Scheduler, bis er erfüllt ist.
	--				Hiermit kann insbesondere eine Fahrstraße angefordert werden.
	--              Das Schalten des Signals (der Fahrstraße) wird normalerweise
	--              an der signalID und ihrem value geprüft. Es kann aber auch
	--              durch den 3. und 4. Parameter mit einem anderen Signal
	--				verbunden werden.
	-- signalID:	ID des zu stellenden Signals
	-- value:		Wert, auf den das Signal zu stellen ist
	-- Rückgabe:	ID des Requests bei Erfolg, sonst nil
	requestSignal = requestSignal;

	-- Funktion function executeAt(when, action)
	-- Bedeutung:	fügt die angebenene Aktion zum gewünschten Zeitpunkt aus
	-- when:		Zeitpunkt als Zeichenkette im Format dd[:dd[:dd[.sss]]] oder
	--				Number als Sekunden seit Beginn der Epoche
	-- action:		LUA-Fragment (muss eine Zeichenkette sein)
	-- Rückgabe:	ID des Requests bei Erfolg, sonst nil
	executeAt = executeAt;

	-- Funktion function executeAfter(delay, action)
	-- Bedeutung:	fügt die angebenene Aktion nach einer Verzögerung aus
	-- delay:		Verzögerung in Sekunden (als positive Number!)
	-- action:		LUA-Fragment (muss eine Zeichenkette sein)
	-- Rückgabe:	ID des Requests bei Erfolg, sonst nil
	executeAfter = executeAfter;
	
	-- Funktion function hasActionContaining(substring)
	-- Bedeutung:	sucht, ob es einen request gibt, dessen Aktion die angegebene Zeichenkette enthält
	-- substring:	zu suchende Zeichenkette (textuelle Suche, kein regulärer Ausdruck!)
	-- Rückgabe:	true, wenn ein solcher Request existiert, andernfalls false
	hasActionContaining = hasActionContaining;

	-- Funktion showRequests(doClear)
	-- Bedeutung:	zeigt alle Requests des Schedulers an
	-- doClear:     boolean	true, wenn die Logs zuvor gelöscht werden sollen
	-- Rückgabe:	string
	showRequests = showRequests;

	-- Funktion showPerformance()
	-- Bedeutung:	gibt Performance-Informationen als String zurück
	-- Rückgabe:	string
	showPerformance = showPerformance;

	-- Funktion removeAllRequests()
	-- Bedeutung:	entfernt alle Requests
	-- Rückgabe:	keiner
	removeAllRequests = removeAllRequests
}

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

return Scheduler;
