
(doc=>{
	Array.prototype.unique = function() { return this.filter((v, i, a) => a.indexOf(v) === i); };
	Array.prototype.validUnique = function(fn) {
		return this.filter((v, i, a) => (fn ? fn(v) : v) && a.indexOf(v) === i);
	};

	Date.defineRelativeTimeFormat = config => relativeTime = config;

	let formats = {
		LT   : {hour: 'numeric', minute: 'numeric'},
		L    : {},
		LL   : {year: 'numeric', month: 'short', day: 'numeric'},
		LLL  : {year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric'},
		LLLL : {weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric'},
		'd M': {day: '2-digit', month: 'short'}
	},
	phpFormats = {
		// Day
		d: {day: '2-digit'},
		D: {weekday: 'short'},
		j: {day: 'numeric'},
		l: {weekday: 'long'},
		z: {},
		// Month
		F: {month: 'long'},
		m: {month: '2-digit'},
		M: {month: 'short'},
		n: {month: 'numeric'},
		Y: {year: 'numeric'},
		y: {year: '2-digit'},
		// Time
		A: {hour12: true},
		G: {hour: 'numeric'},
		H: {hour: '2-digit'},
		i: {minute: '2-digit'},
		s: {second: '2-digit'},
		u: {fractionalSecondDigits: 3},
		Z: {timeZone: 'UTC'}
	},
	relativeTime = {
		// see /snappymail/v/0.0.0/app/localization/relativetimeformat/
	},
	pad2 = v => 10 > v ? '0' + v : v;

	// Format momentjs/PHP date formats to Intl.DateTimeFormat
	// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat
	Date.prototype.format = function (options, UTC) {
		if (typeof options == 'string') {
			if ('Y-m-d\\TH:i:s' == options) {
				return this.getFullYear() + '-' + pad2(1 + this.getMonth()) + '-' + pad2(this.getDate())
					+ 'T' + pad2(this.getHours()) + ':' + pad2(this.getMinutes()) + ':' + pad2(this.getSeconds());
			}
			if (formats[options]) {
				options = formats[options];
			} else {
				let o, s = options + (UTC?'Z':''), i = s.length;
				console.log('Date.format('+s+')');
				options = {};
				while (i--) {
					o = phpFormats[s[i]] || phpFormats[s[i].toUpperCase()];
					o && Object.entries(o).forEach(([k,v])=>options[k]=v);
				}
				formats[s] = options;
			}
		}
		return new Intl.DateTimeFormat(doc.documentElement.lang, options).format(this);
	};

	// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat
	Date.prototype.fromNow = function() {
		let unit = 'second',
			value = Math.round((this.getTime() - Date.now()) / 1000),
			t = [[60,'minute'],[3600,'hour'],[86400,'day'],[2628000,'month'],[31536000,'year']],
			i = 5,
			abs = Math.abs(value);
		while (i--) {
			if (t[i][0] <= abs) {
				value = Math.round(value / t[i][0]);
				unit = t[i][1];
				break;
			}
		}
		if (Intl.RelativeTimeFormat) {
			let rtf = new Intl.RelativeTimeFormat(doc.documentElement.lang);
			return rtf.format(value, unit);
		}
		abs = Math.abs(value);
		let rtf = relativeTime.long[unit][0 > value ? 'past' : 'future'],
			plural = relativeTime.plural(abs);
		return (rtf[plural] || rtf).replace('{0}', abs);
	};

	Element.prototype.closestWithin = function(selector, parent) {
		const el = this.closest(selector);
		return (el && el !== parent && parent.contains(el)) ? el : null;
	};

	Element.fromHTML = string => {
		const template = doc.createElement('template');
		template.innerHTML = string.trim();
		return template.content.firstChild;
	};

	/**
	 * Every time the function is executed,
	 * it will delay the execution with the given amount of milliseconds.
	 */
	if (!Function.prototype.debounce) {
		Function.prototype.debounce = function(ms) {
			let func = this, timer;
			return function(...args) {
				timer && clearTimeout(timer);
				timer = setTimeout(()=>{
					func.apply(this, args)
					timer = 0;
				}, ms);
			};
		};
	}

	/**
	 * No matter how many times the event is executed,
	 * the function will be executed only once, after the given amount of milliseconds.
	 */
	if (!Function.prototype.throttle) {
		Function.prototype.throttle = function(ms) {
			let func = this, timer;
			return function(...args) {
				if (!timer) {
					timer = setTimeout(()=>{
						func.apply(this, args)
						timer = 0;
					}, ms);
				}
			};
		};
	}

})(document);
