require "Debug_1";
require "Commodity_1";

local callbacks = {};

local function append(tab1, tab2)
	local n = tab1.n or #tab1;
	local m = tab2.n or #tab2;
	for i = 1, m do
		tab1[n + i] = tab2[i];
	end;
	tab1.n = n + m;
end;

local function genWrapper(name, ...)
	local moreArgs = table.pack(...);
	local wrapper = function(...)
		local results = {};
		local functions = callbacks[name];
		if type(functions) == "table" then
			local args = table.pack(...);
			append(args, moreArgs);
			for i, f in ipairs(functions) do
				if i > 1 then
					local t = type(f);
					if t == "function" then
						local r = table.pack(f(table.unpack(args, 1, args.n)));
						append(results, r);
					else
						error("Callback.addCallback.<anon>(): value at index "..tostring(i).." is a "..t..", but not a function.");
					end;
				end;
			end;
		else
			error("Callback.addCallback.<anon>(): callbacks[\""..tostring(name).."\"] returned a "..type(functions)..", but not a table.");
		end;
		return table.unpack(results, 1, results.n);
	end;
	return wrapper;
end;

local function addCallback(name, f, ...)
	if type(name) ~= "string" then
		error("Callback.addCallback(): first argument must be a string, not a "..type(name));
	elseif f and type(f) ~= "function" then
		error("Callback.addCallback(): second argument must be a function, not a "..type(f));
	end;
	local functions = callbacks[name];
	if functions then
		table.insert(functions, f);
	else
		local previous = rawget(_G, name);
		rawset(_G, name, nil);
		local wrapper = genWrapper(name, ...);
		callbacks[name] = previous and { wrapper, previous, f } or { wrapper, f };
	end;
end;

local function doRegister(ID, isSignal, f, ...)
	local states = table.pack(...);
	local result;
	if type(isSignal) ~= "boolean" then
		result = EEPRegisterSignal(ID);
		isSignal = result == 1;
		if not isSignal then
			result = EEPRegisterSwitch(ID);
		end;
	elseif isSignal then
		result = EEPRegisterSignal(ID);
	else
		result = EEPRegisterSwitch(ID);
	end;
	if result ~= 1 then
		return false;
	end;
	local g;
	if #states == 0 then
		g = function(state)
				  return f(state, ID);
				end;
	else
		g = function(state)
					for i, v in ipairs(states) do
						if v == state then
							return f(state, ID);
						end;
					end;
				end;
	end;
	addCallback(string.format("EEPOn%s_%i", isSignal and "Signal" or "Switch", ID), g);
	return true;
end;

local function registerSignal(ID, f, ...)
	return doRegister(ID, true, f, ...);
end;

local function registerSwitch(ID, f, ...)
	return doRegister(ID, false, f, ...);
end;

local function register(ID, f, ...)
	return doRegister(ID, nil, f, ...);
end;

local function registerTrainCoupling(f)
	addCallback("EEPOnTrainCoupling", f);
end;

local function registerTrainSplitting(f)
	addCallback("EEPOnTrainLooseCoupling", f);
end;

local function registerExitTrainyard(f)
	addCallback("EEPOnTrainExitTrainyard", f);
end;

setmetatable(_G, {
	__index = function(tbl, key)
		local t = callbacks[key];
		if t then
			return t[1];
		else
			-- Bennys Code-Zeile
			local p, message = load(key);
			if p then
				local f = function(param)
					local savedTrainName = trainName;
					trainName = param; -- global trainName !!!
					p();
					trainName = savedTrainName;
				end;
				rawset(_G, key, f);
				return f;
			end;
			return rawget(_G, key);
		end;
	end;
	__newindex = function(tbl, key, value)
		if callbacks[key] then
			error(key .. " is protected", 2);
		else
			rawset(tbl, key, value);
		end;
	end;
});

Callback = {
	_VERSION = "1.0";

	-- Funktion addCallback(name, f, ...)
	-- Bedeutung:			registriert die Funktion <f> unter dem Namen <name>.
	--								Die Funktion wird mit den angegebenen Argumenten (...) aufgerufen.
	-- name:					Name der Funktion, z.B. "EEPOnTrainCoupling"
	-- f:							Auszuführende Funktion, wenn das Ereignis eintritt
	-- Rückgabewert:	keiner
	addCallback = addCallback;

	-- Funktion registerSignal(ID, f, ...)
	-- Bedeutung:			registriert die Funktion <f> für das Signal <ID>.
	--								Falls weitere (ganzzahlige) Argumente (...) übergeben wurden, wird die Funktion nur aufgerufen,
	--								wenn das Signal auf einen der angegebenen Zustände wechselt.
	-- ID:						ID des Signals
	-- f:							Auszuführende Funktion bei Wechsel des Signalbilds
	-- Rückgabewert:	true bei Erfolg, ansonsten existiert das Signal wahrscheinlich nicht.
	registerSignal = registerSignal;

	-- Funktion registerSwitch(ID, f, ...)
	-- Bedeutung:			registriert die Funktion <f> für die Weiche <ID>.
	--								Falls weitere (ganzzahlige) Argumente (...) übergeben wurden, wird die Funktion nur aufgerufen,
	--								wenn die Weiche auf einen der angegebenen Zustände wechselt.
	-- ID:						ID der Weiche
	-- f:							Auszuführende Funktion beim Legen der Weiche
	-- Rückgabewert:	true bei Erfolg, ansonsten existiert die Weiche wahrscheinlich nicht.
	registerSwitch = registerSwitch;

	-- Funktion register(ID, f, ...)
	-- Bedeutung:			registriert die Funktion <f> für das Signal oder die Weiche mit der <ID>.
	--								Falls weitere (ganzzahlige) Argumente (...) übergeben wurden, wird die Funktion nur aufgerufen,
	--								wenn das Signal oder die Weiche auf einen der angegebenen Zustände wechselt.
	-- ID:						ID des Signals bzw. der Weiche
	-- f:							Auszuführende Funktion bei Wechsel des Signalbilds / der Weichenstellung
	-- Rückgabewert:	true bei Erfolg, ansonsten existieren wahrscheinlich weder ein Signal noch eine Weiche mit der angegebenen ID
	register = register;

	-- Funktion registerTrainCoupling(f)
	-- Bedeutung:			Convenience-Methode, ruft addCallback("EEPOnTrainCoupling", f) auf
	-- f:							Auszuführende Funktion beim Kuppeln zweier Züge
	-- Rückgabewert:	keiner
	registerTrainCoupling = registerTrainCoupling;

	-- Funktion registerTrainSplitting(f)
	-- Bedeutung:			Convenience-Methode, ruft addCallback("EEPOnTrainLooseCoupling", f) auf
	-- f:							Auszuführende Funktion beim Trennen eines Zuges
	-- Rückgabewert:	keiner
	registerTrainSplitting = registerTrainSplitting;

	-- Funktion registerExitTrainyard(f)
	-- Bedeutung:			Convenience-Methode, ruft addCallback("EEPOnTrainExitTrainyard", f) auf
	-- f:							Auszuführende Funktion, wenn ein Zug ein virtuelles Depot verlässt
	-- Rückgabewert:	keiner
	registerExitTrainyard = registerExitTrainyard;
};

setmetatable(Callback, {
  __call = function(name, ...) return Callback.addCallback(...); end;
});

-- Backward compatibility:
_G.Register = Callback;
_G.register = Callback.register;

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

return Callback;
