class Request {
	static get POST() { return "POST"; }
	static get GET() { return "GET"; }
	static get PUT() { return "PUT"; }
	static get PATCH() { return "PATCH"; }
	static get DELETE() { return "DELETE"; }

	/**
	 * Tries to start the next request in the queue if there is one.
	 * If a request is already running, nothing is done.
	 * This function is to be called on completion of a request and on queueing of a new request only.
	 * Do not call this function from anywhere else (it's pointless)!
	 */
	static performNextRequest() {
		if (Request.requestRunning || !Request.queue || Request.queue.length < 1) {
			return;
		}

		Request.requestRunning = true;

		var nextRequest = Request.queue[0];

		if (!nextRequest.data) {
			nextRequest.data = { };
		}

		nextRequest.data.session = Storage.get("session");

		var urlParameters = "";
		var stringBody = null;

		if (nextRequest.requestType == "GET") {
			//Serialize the data as a URL parameter string.
			for (var key in nextRequest.data) {
				if (urlParameters != "") {
					urlParameters += "&";
				}

				urlParameters += key + "=" + encodeURI(nextRequest.data[key]);
			}
		}
		else {
			stringBody = JSON.stringify(nextRequest.data);
		}

		//Add the URL parameters.
		//There might be already some URL parameters in the URL itself.
		var url = nextRequest.url;
		if (url.indexOf("?") == -1) {
			url += "?" + urlParameters;
		}
		else {
			url += "&" + urlParameters;
		}

		Request.currentRequest = new XMLHttpRequest();
		Request.currentRequest.open(nextRequest.requestType, url);
		Request.currentRequest.setRequestHeader("Content-Type", "application/json");
		Request.currentRequest.onload = Request.onRequestReturned;
		Request.currentRequest.onerror = Request.onRequestError;
		Request.currentRequest.send(stringBody);

		Request.requestTimeout = setTimeout(Request.onRequestTimeout, nextRequest.timeout);
	}

	static onRequestTimeout() {
		Request.currentRequest.abort();

		//Call the error callback on a timeout.
		var currentRequestInfo = Request.queue[0];
		currentRequestInfo.error.bind(currentRequestInfo)(Request.currentRequest, "request_timeout", {
			code: "request_timeout",
			message: "The request did not return within the timeout of " + (currentRequestInfo.timeout / 1000) + " seconds."
		});
	}

	static onRequestError(event) {
		//Clear the timeout when a request error occurred.
		clearTimeout(Request.requestTimeout);

		//Call the error callback on a networking error.
		var currentRequestInfo = Request.queue[0];
		currentRequestInfo.error.bind(currentRequestInfo)(Request.currentRequest, "request_error", {
			code: "request_error",
			message: "The request could not be performed. Are you properly connected to the internet?"
		});
	}

	static onRequestReturned(event) {
		//Clear the timeout when a response was got.
		clearTimeout(Request.requestTimeout);

		var currentRequestInfo = Request.queue[0];

		//Call the error callback on a non-2XX status code.
		if (Request.currentRequest.status > 299) {
			currentRequestInfo.error.bind(currentRequestInfo)(Request.currentRequest, "http_error", {
				code: Request.currentRequest.status,
				message: Request.currentRequest.statusText
			});

			return;
		}

		//Call the error callback when the body cannot be parsed from JSON.
		var responseData = null;
		try {
			responseData = JSON.parse(Request.currentRequest.responseText);
		}
		catch (exception) {
			currentRequestInfo.error.bind(currentRequestInfo)(Request.currentRequest, "parse_error", {
				code: "parse_error",
				message: exception.message
			});

			return;
		}

		//Call the error callback when the response itself contains a standard error object.
		if (responseData.error) {
			var error = responseData.error;

			//Show the login view controller of the application if the error is "invalid_session" or "not_logged_in".
			//We do not finish the request here because we will retry it afterwards.
			if (error.code == "invalid_session" || error.code == "not_logged_in") {
				var loginController = UIKit.getViewControllerById("login-view-controller");
				UIKit.getSceneWithViewController(loginController).showWithViewController(loginController);

				return;
			}

			//Finish the request if the error states that the credentials are invalid.
			//This is the only case in which we properly finish a failed request.
			if (error.code == "invalid_credentials") {
				if (currentRequestInfo.errorCallback) {
					currentRequestInfo.errorCallback(Request.currentRequest, "application_error", error);
				}

				currentRequestInfo.finish();

				return;
			}

			//Call the request instance's error function.
			currentRequestInfo.error.bind(currentRequestInfo)(Request.currentRequest, "application_error", error);

			return;
		}

		//Otherwise call the success callback.
		currentRequestInfo.response.bind(currentRequestInfo)(responseData);
	}

	/**
	 * Runs the first request in the queue again.
	 */
	static tryAgainWithSession() {
		if (!Request.requestRunning) {
			return;
		}

		Request.requestRunning = false;
		Request.performNextRequest();
	}

	/**
	 * Called upon completion of a request.
	 * Only after calling this request, a new one may be processed.
	 */
	static finishRequest() {
		Request.queue.splice(0, 1);
		Request.requestRunning = false;

		Request.performNextRequest();
	}

	constructor(url, type = Request.GET, callback = null, errorCallback = null) {
		this.url = url;
		this.requestType = type;
		this.callback = callback;
		this.errorCallback = errorCallback;
		this.ready = true;
		this.timeout = 30000;
	}

	/**
	 * Adds this request to the global request queue and tries to start the next request in the queue.
	 * There is an option to make this request the first in the queue, but this is only allowed for the login request.
	 */
	send(data = { }, logInRequest = false) {
		this.ready = false;

		if (!Request.queue) {
			Request.queue = [ ];
		}
		this.data = data;

		if (Request.queue.indexOf(this) != -1) {
			console.error("Trying to perform a request to " + this.url + " more than once at a time. This is not permitted. Wait until the request did return or create a new one instead.");
			return;
		}

		if (logInRequest) {
			Request.queue.splice(0, 0, this);
			Request.requestRunning = false;
		}
		else {
			Request.queue.push(this);
		}

		Request.performNextRequest();
	}

	response(responseData) {
		if (responseData.session) {
			Storage.set("session", responseData.session);
		}

		//Finish the request before the callback is called.
		//This is because the callback might want to perform the same request again.
		this.finish();

		if (this.callback) {
			this.callback(responseData.response);
		}
	}

	error(request, status, error) {
		if (this.errorCallback) {
			this.errorCallback(request, status, error);
		}

		this.finish();
	}

	finish() {
		this.ready = true;
		Request.finishRequest();
	}
}