class DataManager {

	constructor() {
		//The fetch timer interval in milliseconds.
		this.fetchInterval = 7000;

		//The retry timeout for when a request fails.
		this.failedRequestRetryTimeout = 5000;

		//A flag that is true as long as there is a fetch or store request running.
		//The fetch timer will skip an interval if there is still a request running.
		this.isRequestRunning = false;

		this.listeners = { };
		this.storeErrorListeners = { };
		this.storedCallbacks = { };

		//The object that contains all data that has been loaded from the server.
		//This is up to date all the time.
		this.localData = { };

		//Time in milliseconds since 1970-01-01 that indicates when the last updates were fetched from the server.
		this.lastUpdate = 0;

		this.loadDataRequest = new Request(TimeCards.REQUEST_URI + "DataManager/Fetch", "POST", this.loadedData.bind(this), this.loadDataError.bind(this));

		//Will contain the data that should be stored. Will be processed one after the other.
		this.storeQueue = [ ];

		this.storeDataRequest = new Request(TimeCards.REQUEST_URI + "DataManager/Store", "POST", this.storedData.bind(this), this.storeDataError.bind(this));

		this.fetchTimer = setInterval(this.loadData.bind(this), this.fetchInterval);
		this.loadData();
	}

	addDataListener(type, filters, callback, errorCallback = null, performInitialCalls = true, initialCallOnNull = false) {
		if (!(type in this.listeners)) {
			this.listeners[type] = [ ];
		}

		this.listeners[type].push({
			filters,
			callback,
			errorCallback,
			initialCallOnNull
		});

		//Call the new listener with the already loaded data, but only if there is already a local data object for the given type.
		if (performInitialCalls) {
			if (type in this.localData) {
				//Call the listeners after the current call stack has finished.
				setTimeout(function () {
					let foundEntity = false;
					for (var key in this.localData[type]) {
						var entity = this.localData[type][key];

						//Call the callback if the conditions match the entity.
						if (!filters || this.matchesFilters(entity, filters)) {
							try {
								foundEntity = true;
								callback(key, entity);
								break;
							}
							catch (exception) {
								console.error("An error occurred while calling an initial callback (i.e. when adding a new listener) of the DataManager:");
								console.error(exception);
							}
						}
					}

					if (!foundEntity) {
						//Call callback if entity is not loaded and caller requests us to do so in this case.
						setTimeout(function () {
							try {
								callback(key, null);
							}
							catch (exception) {
								console.error("An error occurred while calling an initial callback (i.e. when adding a new listener) of the DataManager:");
								console.error(exception);
							}
						}.bind(this), 0);
					}
				}.bind(this), 0);
			}
			else if (initialCallOnNull) {
				//Call callback if entity is not loaded and caller requests us to do so in this case.
				setTimeout(function () {
					try {
						callback(key, null);
					}
					catch (exception) {
						console.error("An error occurred while calling an initial callback (i.e. when adding a new listener) of the DataManager:");
						console.error(exception);
					}
				}.bind(this), 0);
			}
		}
	}

	/**
	 * Does the same as addDataListener, but without performing any calls for already existing entities.
	 */
	putDataListener(type, filters, callback, errorCallback = null) {
		this.addDataListener(type, filters, callback, errorCallback, false);
	}

	/**
	 * Removes the given data listener for the given type.
	 * The error callback will be removed as well.
	 * Will do nothing if the listener is not found.
	 * @param type The type on which the listener was set.
	 * @param callback The callback to remove from the list.
	 */
	removeDataListener(type, callback) {
		if (!(type in this.listeners)) {
			return;
		}

		//Iterate over the listeners for the given type and find the ones to remove. (Yes, there might be multiple)
		for (var i = this.listeners[type].length - 1; i >= 0; i--) {
			if (this.listeners[type][i].callback == callback) {
				this.listeners[type].splice(i, 1);
			}
		}
	}

	/**
	 * Adds an error listener for the store function for the given type.
	 * @param type The type to add the listener for.
	 * @param errorCallback The callback function for the listener.
	 */
	addStoreErrorListener(type, errorCallback) {
		if (!(type in this.storeErrorListeners)) {
			this.storeErrorListeners[type] = [ ];
		}

		this.storeErrorListeners[type].push(errorCallback);
	}

	/**
	 * Removes the given error listener from the store function for the given type.
	 * @param type The type to remove the listener from.
	 * @param errorCallback The callback function to remove.
	 */
	removeStoreErrorListener(type, errorCallback) {
		if (!(type in this.storeErrorListeners)) {
			console.warn("Trying to remove a store error listener for a type that does not have any store error listeners.");
			return;
		}

		var index = this.storeErrorListeners[type].indexOf(errorCallback);

		if (index == -1) {
			console.warn("Trying to remove a store error listener that is not in the list of listeners.");
			return;
		}

		this.storeErrorListeners[type].splice(index, 1);
	}

	loadData() {
		//Do not fetch the data if there is already a request running.
		if (this.isRequestRunning) {
			console.info("Skipping a fetch interval because a previous fetch or store request has not yet returned.");
			return;
		}

		//Set the loading flag.
		this.setRequestRunning(true, false);

		//Send the fetch request.
		this.loadDataRequest.send({
			since_date: this.lastUpdate
		});

		//Store the time when the fetch attempt started.
		//When the fetch is successful, this time is used for the next request.
		//EDIT: No longer used because now the server dictated the next update time.
		//this.fetchAttemptTime = new Date().getTime();
	}

	loadDataError(request, status, error) {
		for (var type in this.listeners) {
			var listeners = this.listeners[type];

			for (var i = 0; i < listeners.length; i++) {
				if (listeners[i].errorCallback) {
					listeners[i].errorCallback();
				}
			}
		}

		if (error && (error.code == "request_timeout" || error.code == "request_error")) {
			this.showingLoadingView = true;

			//Get the loading view controller.
			if (!this.loadingViewController) {
				this.loadingViewController = UIKit.getViewControllerById("loading-view-controller");
			}

			UIKit.getActiveViewController().presentModalViewController(this.loadingViewController);
		}

		//Retry the request after a timeout.
		//Only retry if it is not a programmatical error on the server side.
		//Also retry if it was a request timeout error.
		if (typeof error != "object" || !error.code || status === "request_timeout") {
			console.info("Retrying the fetch request after " + this.failedRequestRetryTimeout + " milliseconds ...");
			setTimeout(function() {
				this.setRequestRunning(false);
				this.loadData();
			}.bind(this), this.failedRequestRetryTimeout);
		}
		else {
			console.info("We do not retry the failed fetch request because it is a programmatical issue.");

			//Reset the loading flag.
			this.setRequestRunning(false);

			//Perform the next store request if there is one.
			if (this.storeQueue.length > 0) {
				this.storeNext();
			}
		}
	}

	loadedData(remoteData) {
		//Reset the loading flag.
		this.setRequestRunning(false);

		if (this.showingLoadingView) {
			this.showingLoadingView = false;
			this.loadingViewController.dismissModally();
		}

		//Update the last update time.
		//We use the time that the server recommends.
		this.lastUpdate = remoteData.meta.since_date;

		//Use the actual data from now on.
		remoteData = remoteData.data;

		//Immediately process the next store request if there is one in the queue.
		if (this.storeQueue.length > 0) {
			this.storeNext();
		}

		//An array containing the local data copies after they are replaced by the new remote entities.
		//We keep the old entities to call the listeners for them as well.
		var oldLocalData = { };

		//Determine the sorting order of all remote datasets.
		var orderedKeys = { };
		for (var type in remoteData) {
			//Sort the data if a sorting is specified for this type.
			var sorting = this.getSortingForType(type);
			if (sorting) {
				//This array will contain the keys in the correct order.
				orderedKeys[type] = [ ];

				//Go through all entities to sort their keys in.
				for (var key in remoteData[type]) {
					var entity = remoteData[type][key];

					//Go through all other entities to find the next later one that is the closest to this one.
					var closestLaterEntity = null;
					var closestLaterEntityKey = null;
					for (var i = 0; i < orderedKeys[type].length; i++) {
						var comparingKey = orderedKeys[type][i];
						//Skip this entity if it is the one we are currently sorting in.
						if (comparingKey == key) {
							continue;
						}

						var comparingEntity = remoteData[type][comparingKey];

						//Only compare if entities are not null.
						if (comparingEntity && entity && this.compare(comparingEntity, entity, sorting) > 0) {
							if (!closestLaterEntity || this.compare(closestLaterEntity, comparingEntity, sorting) >= 0) {
								closestLaterEntity = comparingEntity;
								closestLaterEntityKey = comparingKey;
							}
						}
					}

					//Insert the entity before the closest later one if there is one. Otherwise, just add it at the end of the keys array.
					if (closestLaterEntityKey) {
						orderedKeys[type].splice(orderedKeys[type].indexOf(closestLaterEntityKey), 0, key);
					}
					else {
						orderedKeys[type].push(key);
					}
				}
			}
			else {
				//Just use the original keys array from the object if no sorting is necessary.
				orderedKeys[type] = Object.keys(remoteData[type]);
			}
		}

		//Iterate over the data to first identify deleted entities and then also added or changed ones.
		//The local data array is updated as well.
		for (var type in remoteData) {
			//Create the local array if it does not exist yet.
			if (!(type in this.localData)) {
				this.localData[type] = { };
			}
			//Same for the old local data object.
			if (!(type in oldLocalData)) {
				oldLocalData[type] = { };
			}

			//Get the remote dataset for this type.
			var remoteTypeData = remoteData[type];

			//Iterate over the remote entities for this type to find any new, changed or deleted entity.
			for (var i = 0; i < orderedKeys[type].length; i++) {
				var key = orderedKeys[type][i];
				var remoteEntity = remoteTypeData[key];

				//Treat the entity as deleted if it is null.
				if (remoteEntity === null) {
					this.deleteLocally(type, key);
				}
				else {
					//Preserve the existing entity if present.
					if (key in this.localData[type]) {
						oldLocalData[type][key] = this.localData[type][key];
					}

					//Add or update the changed entity.
					this.localData[type][key] = remoteEntity;
				}
			}
		}

		//Call the added or updated listeners in a second step.
		for (var type in remoteData) {
			//Get the list of listeners to this type or use an empty array if there are none.
			var typeListeners = (type in this.listeners) ? this.listeners[type] : [ ];

			//Get the remote dataset for this type.
			var remoteTypeData = remoteData[type];

			//Iterate over the remote entities for this type to find any new, changed or deleted entity.
			for (var i = 0; i < orderedKeys[type].length; i++) {
				var key = orderedKeys[type][i];
				var remoteEntity = remoteTypeData[key];

				//If the entity is not a deleted one ...
				if (remoteEntity !== null) {
					//Call the next store listener for this entity.
					if (type in this.storedCallbacks && this.storedCallbacks[type].length > 0) {
						//Call the callback and then remove it from the list.
						this.storedCallbacks[type][0](key, remoteEntity);
						this.storedCallbacks[type].splice(0, 1);
					}

					//Call the callbacks if their conditions match the entity.
					for (var j = 0; j < typeListeners.length; j++) {
						var listener = typeListeners[j];

						//Get the old local entity if there was one.
						var oldLocalEntity = null;
						if (key in oldLocalData[type]) {
							oldLocalEntity = oldLocalData[type][key];
						}

						//Call the added or updated callbacks.
						var remoteEntityMatchesFilters = !listener.filters ? true : this.matchesFilters(remoteEntity, listener.filters);
						if (remoteEntityMatchesFilters) {
							try {
								listener.callback(key, remoteEntity, this.sortIn(type, remoteEntity, this.getSortingForType(type)));
							}
							catch (exception) {
								console.error("An error occurred while calling an update callback of the DataManager:");
								console.error(exception);
							}
						}

						//Call the callbacks as if the entity was deleted if the old entity matched the filters before the update, but not after it.
						if (listener.filters && oldLocalEntity && this.matchesFilters(oldLocalEntity, listener.filters) && !remoteEntityMatchesFilters) {
							try {
								listener.callback(key, null);
							}
							catch (exception) {
								console.error("An error occurred while calling a callback of the DataManager for an entity that no longer matches the filters of the listener:");
								console.error(exception);
							}
						}
					}
				}
			}
		}
	}

	/**
	 * Registers the given sorting methods for the given type.
	 * @param type The type to register the sorting methods for.
	 * @param sorting An array containing sorting methods or one single sorting method object.
	 */
	setSortingForType(type, sorting) {
		if (!this.sortingsByType) {
			this.sortingsByType = { };
		}

		this.sortingsByType[type] = sorting;
	}

	/**
	 * Gets the preset sorting methods for the given type.
	 * @param type The type for which to get the sorting methods.
	 * @return An array containing all sorting methods for the given type or null if no sorting methods were provided for the given type.
	 */
	getSortingForType(type) {
		if (!(type in this.sortingsByType)) {
			return null;
		}

		return this.sortingsByType[type];
	}

	/**
	 * Determines where in the given type's dataset the given entity should be sorted in with the given sorting method.
	 * If the given entity is determined to be the last in the dataset according to the sorting method, null is returned.
	 * This method will not actually put the entity inside the dataset.
	 * The sorting parameter requires an array containing objects with the keys "field" and "direction", with the latter being either "ASC" or "DESC".
	 * The direction property may be omitted. In this case, "ASC" is used.
	 * @param type The type of the given entity.
	 * @param entity The entity to sort into the dataset of the given type.
	 * @param sorting A sorting method array or a single sorting method object. If omitted, the sorting of the given type is fetched.
	 * @return The key of the entity before which the given entity should be sorted in or null if the entity should be sorted in in the last position.
	 */
	sortIn(type, entity, sorting = null) {
		if (!sorting) {
			sorting = this.getSortingForType(type);
		}

		if (!sorting) {
			return null;
		}

		//Encapsule the sorting object into an array if it was provided as an object.
		if (!Array.isArray(sorting)) {
			sorting = [ sorting ];
		}

		//Return null if the dataset was not yet loaded.
		if (!(type in this.localData)) {
			console.warn("Trying to sort an object into the dataset for \"" + type + "\", but the dataset for this type was not yet loaded.");
			return null;
		}

		//Go through all existing entities in the dataset.
		//We skip those that come later than the given one, but we remember which one was the closest after the given entity.
		var closestLaterEntity = null;
		var closestLaterEntityKey = null;
		for (var key in this.localData[type]) {
			var checkingEntity = this.localData[type][key];

			if (this.compare(checkingEntity, entity, sorting) > 0) {
				//Use this entity as the new closest later entity if it comes before the current closest later entity.
				if (!closestLaterEntity || this.compare(closestLaterEntity, checkingEntity, sorting) >= 0) {
					closestLaterEntity = checkingEntity;
					closestLaterEntityKey = key;
				}
			}
		}

		return closestLaterEntityKey;
	}

	/**
	 * Compares the two given entities with the given sorting method(s).
	 * @param entityA The first entity.
	 * @param entityB The second entity.
	 * @param sorting A sorting method array or a single sorting method object.
	 * @return 1 if entityA comes after entityB, -1 if entityA comes before entityB, 0 if the two entities are indifferent.
	 */
	compare(entityA, entityB, sorting) {
		//Encapsule the sorting object into an array if it was provided as an object.
		if (!Array.isArray(sorting)) {
			sorting = [ sorting ];
		}

		//Go through the sorting method objects and compare the two entities accordingly.
		//We break out of the loop as soon as the first difference has been found.
		//Therefore, the second, third, etc. sorting method object will not be relevant if the first one already determines a difference.
		for (var i = 0; i < sorting.length; i++) {
			var fieldInformation = sorting[i];

			var valueA = entityA[fieldInformation.field];
			var valueB = entityB[fieldInformation.field];

			//We always work with uppercase strings for sorting.
			var stringReplacements = {
				"Ä": "AE",
				"Ü": "UE",
				"Ö": "OE",
				"É": "E",
				"È": "E",
				"À": "A",
				"Å": "A",
				"Ñ": "N",
				"ç": "C"
			};
			if (typeof valueA == "string") {
				valueA = valueA.toUpperCase();
				for (var original in stringReplacements) {
					valueA = valueA.replaceAll(original, stringReplacements[original]);
				}
			}
			if (typeof valueB == "string") {
				valueB = valueB.toUpperCase();
				for (var original in stringReplacements) {
					valueB = valueB.replaceAll(original, stringReplacements[original]);
				}
			}

			//Skip to the next sorting method if the field of this method is equal in both entites.
			if (valueA == valueB) {
				continue;
			}

			//Return the difference direction of the two entities.
			var sortingInverter = (("direction" in fieldInformation) && fieldInformation.direction == "DESC") ? -1 : 1;
			return sortingInverter * (valueA > valueB ? 1 : -1);
		}

		//Return 0 if none of the sorting methods recognised a difference.
		return 0;
	}

	/**
	 * Setter for isRequestRunning.
	 * Will also show or hide the visible loading inditation for the user.
	 */
	setRequestRunning(running, loadingCursor = true) {
		if (!this.isRequestRunning && running && loadingCursor) {
			document.body.classList.add("loading");
		}
		else if (this.isRequestRunning && !running && loadingCursor) {
			document.body.classList.remove("loading");
		}

		this.isRequestRunning = running;
	}

	/**
	 * Checks whether the given entity matches the given filters or not.
	 * It is possible to provide multiple filter groups (an array containing multiple filter objects).
	 * It this is done, the groups are treated like OR, whle the filters inside each filter objects are treated like AND.
	 * You can think of filter groups like individual options. Therefore the OR.
	 * @param entity The entitiy to check.
	 * @param filters The filters to apply to the entity. Either one filter object or an array of filter objects.
	 * @return true if the entity matches, false otherwise.
	 */
	matchesFilters(entity, filters) {
		//First put the filters into an array if it is a simple object, i.e. just one single filter group.
		var filtersArray = null;
		if (!Array.isArray(filters)) {
			filtersArray = [ filters ];
		}
		else {
			filtersArray = filters;
		}



		//Iterate over the filter objects. As soon as the first filter group returns true, we return true here.
		for (var i = 0; i < filtersArray.length; i++) {
			if (this.matchesFilterGroup(entity, filtersArray[i])) {
				return true;
			}
		}

		//If we arrive here, none of the filter groups matched.
		return false;
	}

	matchesFilterGroup(entity, filters) {
		//Go through the filters, check the conditions and return false upon the first mismatch.
		for (var fieldKey in filters) {
			//Convert the abbreviated filters to proper ones.
			if ((typeof filters[fieldKey]) != "object" || filters[fieldKey] === null) {
				filters[fieldKey] = {
					cond: "=",
					value: filters[fieldKey]
				};
			}

			var condition = filters[fieldKey];

			//Remove the §'s from the field key. It is possible to insert § in the key to allow for multiple identical keys in the same object.
			var fieldName = fieldKey.replaceAll("§", "");

			//Skip the condition if the entity does not contain such a field.
			if (!(fieldName in entity)) {
				continue;
			}

			//Get the value to check.
			var value = entity[fieldName];

			if (condition.cond == "=" && value != condition.value) {
				return false;
			}
			else if (condition.cond == "!=" && value == condition.value) {
				return false;
			}
			else if (condition.cond == ">" && value <= condition.value) {
				return false;
			}
			else if (condition.cond == "<" && value >= condition.value) {
				return false;
			}
			else if (condition.cond == ">=" && value < condition.value) {
				return false;
			}
			else if (condition.cond == "<=" && value > condition.value) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Returns the entity with the given primary key for the given type.
	 * @param type The type of the desired entity.
	 * @param key The primary key to search for.
	 * @return The found entity object or null if the entity was not found.
	 */
	getEntity(type, key) {
		if (!(type in this.localData)) {
			console.warn("Trying to get the entity for the key " + key + " for the type " + type + ", but the dataset for this type was not yet loaded.");
			return null;
		}

		if (!(key in this.localData[type])) {
			console.warn("Trying to get the entity for the key " + key + " for the type " + type + ", but there is no such entity loaded locally.");
			return null;
		}

		return this.localData[type][key];
	}

	/**
	 * Returns the loaded entities for the given type.
	 * It is possible to filter the results.
	 * @param type The type of the entities.
	 * @param filters A standard filters array.
	 * @return The loaded entity objects or an empty object if the dataset was not yet loaded or no entities match the given filters.
	 */
	getEntities(type, filters = null) {
		if (!(type in this.localData)) {
			console.warn("Trying to get the entities for the type " + type + ", but the dataset for this type was not yet loaded.");
			return { };
		}

		var dataToGet = { };

		if (filters) {
			for (var key in this.localData[type]) {
				if (this.matchesFilters(this.localData[type][key], filters)) {
					dataToGet[key] = this.localData[type][key];
				}
			}
		}
		else {
			dataToGet = this.localData[type];
		}

		return dataToGet;
	}

	/**
	 * Stores the given entity under the given key for the given type.
	 * @param type The type of the entity.
	 * @param key The key to store the entity with OR the entity itself if it is a new entity.
	 * @param entity The entity to store OR -1 if the entity was passed as the second parameter. Pass null here to delete the entity.
	 */
	store(type, key, entity = -1, storedCallback = null) {
		//Parameter switching for overload.
		if (typeof entity === "function") {
			storedCallback = entity;
			entity = -1;
		}
		if (entity === -1) {
			entity = key;
			key = null;
		}

		//Store the stored callback if one is given.
		if (!(type in this.storedCallbacks)) {
			this.storedCallbacks[type] = [ ];
		}
		if (storedCallback) {
			this.storedCallbacks[type].push(storedCallback);
		}

		//Build the request data.
		var requestData = {
			type,
			key,
			entity
		};

		//Add the request to the queue.
		this.storeQueue.push(requestData);

		//Immediately start the request if there is no other request running at this time.
		if (!this.isRequestRunning) {
			this.storeNext();
		}
		else {
			console.info("Waiting for the previous request to return before storing " + requestData.type + "#" + requestData.key + ".");
		}
	}

	/**
	 * Deletes the entity for the given key for the given type.
	 * Will actually just call store() with null as the entity. This will trigger the delete action on the server.
	 * @param type The type of the entity to delete.
	 * @param key The primary key value of the entity to delete.
	 */
	delete(type, key) {
		this.store(type, key, null);
	}

	/**
	 * Removes the entitiy with the given key for the given type from the local dataset only.
	 * The deletion will not be propagated to the server.
	 * Therefore, this function must only be used for cases where the server would not normally return that entity.
	 * @param type The type of the entity to delete.
	 * @param key The primary key value of the entity to delete.
	 */
	deleteLocally(type, key) {
		if (!(type in this.localData)) {
			console.warn("Trying to delete " + type + "#" + key + ", but there is no dataset for that type.");
			return;
		}

		//Do nothing if there is no corresponding local entity.
		//This might be the case when an entity is deleted immediately after being created and thus this client has never seen that entity.
		if (!(key in this.localData[type])) {
			console.warn("Trying to delete " + type + "#" + key + ", but there is no such entity.");
			return;
		}

		//Get the local entity.
		var localEntity = this.localData[type][key];

		//Get the list of listeners to this type or use an empty array if there are none.
		var typeListeners = (type in this.listeners) ? this.listeners[type] : [ ];

		//Call the callbacks for the delete event if their conditions match the entity.
		for (var j = typeListeners.length - 1; j >= 0; j--) {
			var listener = typeListeners[j];

			if (!listener.filters || this.matchesFilters(localEntity, listener.filters)) {
				try {
					listener.callback(key, null);
				}
				catch (exception) {
					console.error("An error occurred while calling a delete callback of the DataManager:");
					console.error(exception);
				}
			}
		}

		//Remove the local entity.
		delete this.localData[type][key];
	}

	/**
	 * Clears the request queue so that the next store request will be performed immediately.
	 * This is only used by the logout action.
	 * @param forLoginRequest Pass true if you want to perform a login or logout request afterwards. By that, the next request will skip the request queue.
	 */
	clearQueue(forLoginRequest = false) {
		this.storeQueue = [ ];
		this.isRequestRunning = false;

		this.awaitingLoginRequest = forLoginRequest;
	}

	/**
	 * Performs the next store request in the queue.
	 * Will do nothing if the queue is empty.
	 */
	storeNext() {
		//Do nothing if the queue is empty or there is already a request running.
		if (this.storeQueue.length == 0) {
			console.warn("Calling storeNext() in the DataManager even though there is nothing in the queue to store. This should never happen!");
			return;
		}
		if (this.isRequestRunning) {
			console.warn("Calling storeNext() in the DataManager even though there is already a request running. This should never happen!");
			return;
		}

		//Set the loading flag.
		this.setRequestRunning(true);

		//Get the request data from the queue.
		var requestData = this.storeQueue[0];

		//Only demand the newest updates if this is the last request in the queue.
		if (this.storeQueue.length == 1) {
			requestData.get_updates_since = this.lastUpdate;

			//Reset the fetch timer.
			clearInterval(this.fetchTimer);
			this.fetchTimer = setInterval(this.loadData.bind(this), this.fetchInterval);
		}

		//Prevent the update if this is an attempt to delete an entity that does not exist locally.
		//This may happen when the data has been deleted just before the last fetch request has returned.
		if (!requestData.entity && !this.getEntity(requestData.type, requestData.key)) {
			console.info("Skipping a delete request because the corresponding entity (" + requestData.type + "#" + requestData.key + ") does not exist. It might have already been deleted.");

			//Remove this skipped request.
			this.storeQueue.splice(0, 1);

			//Move to the next store request if there is another one left.
			if (this.storeQueue.length > 0) {
				this.storeNext();
			}
			else {
				//Otherwise just reset the loading flag and call it a day.
				this.setRequestRunning(false);
			}

			return;
		}

		//Send the request.
		this.storeDataRequest.send(requestData, (this.awaitingLoginRequest ? true : false));

		this.awaitingLoginRequest = false;
	}

	storedData(remoteData) {
		//Do nothing if the queue has been reset in the meantime.
		if (this.storeQueue.length == 0) {
			return;
		}

		//Remove the request from the queue.
		this.storeQueue.splice(0, 1);

		//Reset the loading flag.
		this.setRequestRunning(false);

		//Perform the next request if there is one.
		if (this.storeQueue.length > 0) {
			this.storeNext();
		}
		else if (remoteData !== true) {
			//Otherwise pass on the received data to the loadedData() function because it contains the updated entities.
			this.loadedData(remoteData);
		}
		else {
			console.warn("The response of the store request is just true even though this was the last request in the queue.");
		}
	}

	storeDataError(request, status, error) {
		//Call the store error listeners.
		for (var type in this.storeErrorListeners) {
			var listeners = this.storeErrorListeners[type];

			for (var i = 0; i < listeners.length; i++) {
				try {
					listeners[i](this.storeQueue[0].key, this.storeQueue[0].entity, error);
				}
				catch (exception) {
					console.error("An error occurred while calling a store data error callback of the DataManager:");
					console.error(exception);
				}
			}
		}

		//Display an error message.
		new ErrorDialog(error).show();

		//Get current store data.
		var storeData = this.storeQueue[0];

		//Try the request again after a timeout.
		//Only retry if it is not a programmatical error on the server side.
		//Also retry if it was a request timeout error.
		if (typeof error != "object" || !error.code || status === "request_timeout") {
			console.info("Retrying the store request for " + storeData.type + "#" + storeData.key + " after " + this.failedRequestRetryTimeout + " milliseconds ...");
			setTimeout(function() {
				this.setRequestRunning(false);
				this.storeNext();
			}.bind(this), this.failedRequestRetryTimeout);
		}
		else {
			console.info("We do not retry the failed store request because it is a programmatical issue.");

			//Remove store listener if not retrying.
			if (storeData.type in this.storedCallbacks && this.storedCallbacks[storeData.type].length > 0) {
				this.storedCallbacks[storeData.type].splice(0, 1);
			}

			//Remove the request from the queue.
			this.storeQueue.splice(0, 1);

			//Reset the loading flag.
			this.setRequestRunning(false);

			//Perform the next request if there is one.
			if (this.storeQueue.length > 0) {
				this.storeNext();
			}
		}
	}

}

//TODO: Localize this.
DataManager.UNKNOWN_LABEL_GENERIC = "(Unknown)";