var $ = require('jquery');
var Assert = require('assert');

/**
* Utility functions.
*
* @memberof module:libresignage/util
*/
class Util {
	/**
	* Convert an object into a URL query string.
	* If the passed object is null, an empty string is returned.
	*
	* @param {string[]|null} v An associative array of URL parameters.
	*
	* @return {string} The generated query string.
	*/
	static querify(v) {
		if (v === null) { return ''; }
		return Object.keys(v).map(
			(k) => {
				return encodeURIComponent(k) + '=' +
				encodeURIComponent(v[k]);
			}
		).join('&');
	}

	/**
	* Get the HTTP GET parameters of the current page as an object.
	*
	* @return {object} An object with the GET parameters.
	*/
	static get_GET_parameters() {
		var query_str = window.location.search.substr(1);
		var params_strs = [];
		var params = [];
		var tmp = [];

		if (!query_str.length) {
			return [];
		} else {
			params_strs = query_str.split('&');
		}
		for (var i in params_strs) {
			tmp = params_strs[i].split('=');
			params[decodeURIComponent(tmp[0])] = decodeURIComponent(tmp[1]);
		}
		return params;
	}

	/**
	* Convert a datetime into a unix timestamp in seconds.
	*
	* @param {string} date The date string to use.
	* @param {string} time The time string to use.
	*
	* @return {number} The converted unix timestamp in seconds.
	*
	* @throws {Error} If 'date' or 'time' are empty or null.
	*/
	static datetime_to_tstamp(date, time) {
		var d = null;
		var t = null;

		if (date == null || date.length == 0) {
			throw new Error(`Invalid date string '${date}'.`);
		}
		if (time == null || time.length == 0) {
			throw new Error(`Invalid time '${time}'.`);
		}

		d = date.split('-');
		t = time.split(':');

		if (t.length == 2) { t[2] = '00'; }
		return new Date(
			d[0], '0' + (parseInt(d[1]) - 1).toString(), d[2],
			t[0], t[1], t[2]
		).getTime()/1000;
	}

	/**
	* Convert a unix timestamp in seconds into an array where
	* index 0 is the date string and 1 is the time string.
	*
	* @param {number} tstamp The unix timestamp in seconds.
	*
	* @return {string[]} The datetime array described above.
	*
	* @throws {Error} If 'tstamp' is null.
	*/
	static tstamp_to_datetime(tstamp) {
		var y, m, d, hour, min, sec;
		var ret = [];
		var date = null;

		if (tstamp == null) {
			throw new Error(`Invalid timestamp '${tstamp}'.`);
		}

		date = new Date(tstamp*1000);
		y = date.getFullYear();

		m = date.getMonth() + 1;
		if (m.toString().length == 1) {
			m = '0' + m;
		}
		d = date.getDate();
		if (d.toString().length == 1) {
			d = '0' + d;
		}
		hour = date.getHours();
		if (hour.toString().length == 1) {
			hour = '0' + hour;
		}
		min = date.getMinutes();
		if (min.toString().length == 1) {
			min = '0' + min;
		}
		sec = date.getSeconds();
		if (sec.toString().length == 1) {
			sec = '0' + sec;
		}

		ret[0] = y + '-' + m + '-' + d;
		ret[1] = hour + ':' + min + ':' + sec;

		return ret;
	}

	/**
	* Check whether the keys and values in 'a' also exist in 'E'.
	* if strict == true, keys that exist in 'E' but not in 'a'
	* are considered invalid. The value of 'strict' also applies
	* for checking nested objects in a and E. Using strict == true
	* basically makes this an 'a === E' equality checker.
	*
	* @param {object}  a      The object to check against 'E'.
	* @param {object}  E      The base object to check 'a' against.
	* @param {boolean} strict Perform a strict check (see above)
	*                         (default: false)
	* @return {boolean} True if the 'E' contains 'a' and false otherwise.
	*/
	static object_contains(a, E, strict = false) {
		Assert.ok(a !== null);
		Assert.ok(a === Object(a));
		Assert.ok(E !== null);
		Assert.ok(E === Object(E));

		if (strict && Object.keys(a).length !== Object.keys(E).length) {
			return false;
		}

		for (let k of Object.keys(a)) {
			if (!(k in E)) {
				return false;
			} else {
				if (a[k] === Object(a[k]) || Array.isArray(a[k])) {
					if (!Util.object_contains(a[k], E[k], strict)) {
						return false;
					} else {
						continue;
					}
				} else {
					if (a[k] !== E[k]) {
						return false;
					} else {
						continue;
					}
				}
			}
		}
		return true;
	}

	/**
	* Check whether 'a' is a subset of 'E'.
	*
	* @param {array} a
	* @param {array} E
	*
	* @return {boolean} True if 'E' contains 'a' and false otherwise.
	*/
	static set_contains(a, E) {
		Assert.ok(Array.isArray(a));
		Assert.ok(Array.isArray(E));

		for (let i of a) { if (!E.includes(i)) { return false; } }
		return true;
	}

	/**
	* Check whether the sets 'a' and 'b' are equal.
	*
	* @param {array} a
	* @param {array} b
	*
	* @return True if the sets are equal and false otherwise.
	*/
	static sets_eq(a, b) {
		return Util.set_contains(a, b) && Util.set_contains(b, a);
	}

	/**
	* Make sure multimedia tag memory is released when tags are removed.
	*
	* This function recursively removes all multimedia sources and reloads
	* the multimedia tags before removing them from the DOM.
	*
	* This function implements the best practices from
	* https://html.spec.whatwg.org/multipage/media.html#best-practices-for-authors-using-media-elements
	*
	* @param {HTMLElement} node The DOM element to remove media from.
	*/
	static free_multimedia_memory_recursive(node) {
		node.querySelectorAll('source').forEach(e => e.remove());
		node.querySelectorAll('video, audio').forEach(e => {
			e.pause();
			e.src = "";
			e.load();
			e.remove();
		});
	}

	/**
	* Await a function and catch any errors thrown by it.
	*
	* Errors are printed to the developer console using console.error().
	*
	* @param {function} func    The function to call.
	* @param {object}   thisArg The value of "this" provided to func.
	* @param {...*}     args    The rest of the arguments are passed to func.
	*/
	static async await_and_watch_for_errors(func, thisArg, ...args) {
		try {
			await func.apply(thisArg, args);
		} catch (e) {
			let str = `Caught an unhandled error: ${e} `;

			if (e.fileName) { str += ` in file ${e.fileName}` }
			if (e.lineNumber) { str += `@ ${e.lineNumber}`; }
			if (e.columnNumber) { str += `:${e.columnNumber}`; }
			if (e.stack) { str += `\n\n${e.stack}`; }

			console.error(str);
		}
	}
}
module.exports = Util;
