var Preloader = /** @constructor */ function() {

	var DOWNLOAD_ATTEMPTS_MAX = 4;
	var progressFunc = null;
	var lastProgress = { loaded: 0, total: 0 };

	var loadingFiles = {};
	this.preloadedFiles = [];

	function loadXHR(resolve, reject, file, tracker) {
		var xhr = new XMLHttpRequest;
		xhr.open('GET', file);
		if (!file.endsWith('.js')) {
			xhr.responseType = 'arraybuffer';
		}
		['loadstart', 'progress', 'load', 'error', 'abort'].forEach(function(ev) {
			xhr.addEventListener(ev, onXHREvent.bind(xhr, resolve, reject, file, tracker));
		});
		xhr.send();
	}

	function onXHREvent(resolve, reject, file, tracker, ev) {

		if (this.status >= 400) {

			if (this.status < 500 || ++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) {
				reject(new Error("Failed loading file '" + file + "': " + this.statusText));
				this.abort();
				return;
			} else {
				setTimeout(loadXHR.bind(null, resolve, reject, file, tracker), 1000);
			}
		}

		switch (ev.type) {
			case 'loadstart':
				if (tracker[file] === undefined) {
					tracker[file] = {
						total: ev.total,
						loaded: ev.loaded,
						attempts: 0,
						final: false,
					};
				}
				break;

			case 'progress':
				tracker[file].loaded = ev.loaded;
				tracker[file].total = ev.total;
				break;

			case 'load':
				tracker[file].final = true;
				resolve(this);
				break;

			case 'error':
				if (++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) {
					tracker[file].final = true;
					reject(new Error("Failed loading file '" + file + "'"));
				} else {
					setTimeout(loadXHR.bind(null, resolve, reject, file, tracker), 1000);
				}
				break;

			case 'abort':
				tracker[file].final = true;
				reject(new Error("Loading file '" + file + "' was aborted."));
				break;
		}
	}

	this.loadPromise = function(file) {
		return new Promise(function(resolve, reject) {
			loadXHR(resolve, reject, file, loadingFiles);
		});
	}

	this.preload = function(pathOrBuffer, destPath) {
		if (pathOrBuffer instanceof ArrayBuffer) {
			pathOrBuffer = new Uint8Array(pathOrBuffer);
		} else if (ArrayBuffer.isView(pathOrBuffer)) {
			pathOrBuffer = new Uint8Array(pathOrBuffer.buffer);
		}
		if (pathOrBuffer instanceof Uint8Array) {
			this.preloadedFiles.push({
				path: destPath,
				buffer: pathOrBuffer
			});
			return Promise.resolve();
		} else if (typeof pathOrBuffer === 'string') {
			var me = this;
			return this.loadPromise(pathOrBuffer).then(function(xhr) {
				me.preloadedFiles.push({
					path: destPath || pathOrBuffer,
					buffer: xhr.response
				});
				return Promise.resolve();
			});
		} else {
			throw Promise.reject("Invalid object for preloading");
		}
	};

	var animateProgress = function() {

		var loaded = 0;
		var total = 0;
		var totalIsValid = true;
		var progressIsFinal = true;

		Object.keys(loadingFiles).forEach(function(file) {
			const stat = loadingFiles[file];
			if (!stat.final) {
				progressIsFinal = false;
			}
			if (!totalIsValid || stat.total === 0) {
				totalIsValid = false;
				total = 0;
			} else {
				total += stat.total;
			}
			loaded += stat.loaded;
		});
		if (loaded !== lastProgress.loaded || total !== lastProgress.total) {
			lastProgress.loaded = loaded;
			lastProgress.total = total;
			if (typeof progressFunc === 'function')
				progressFunc(loaded, total);
		}
		if (!progressIsFinal)
			requestAnimationFrame(animateProgress);
	}
	this.animateProgress = animateProgress; // Also exposed to start it.

	this.setProgressFunc = function(callback) {
		progressFunc = callback;
	}
};