/**
 * If a variable (likely a function parameter) is undefined / wasn't supplied, then
 * coalesce to a different value.
 * @param arg
 * @param defaultToVal
 * @returns {*}
 */
function defaultTo(arg, defaultToVal) {
	return typeof arg !== 'undefined' ? arg : defaultToVal;
}

// See https://stackoverflow.com/questions/6756583/prevent-browser-from-loading-a-drag-and-dropped-file/6756680#6756680
function disableBrowserFilePreview() {
	
	window.addEventListener("dragover", function(e){
		e = e || event;
		e.preventDefault();
	}, false);
	
	window.addEventListener("drop", function(e){
		e = e || event;
		e.preventDefault();
	}, false);
}


/**
 * Implementation of Java's String.hashCode() in JS.
 * http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
 * @param absolute
 * @returns {number}
 */
String.prototype.hashCode = function(absolute){
	
	absolute = defaultTo(absolute, false);
	
	var hash = 0;
	if (this.length === 0) return hash;
	for (let i = 0; i < this.length; i++) {
		let char = this.charCodeAt(i);
		hash = ((hash<<5)-hash)+char;
		hash = hash & hash; // Convert to 32bit integer
	}
	
	if (absolute) {
		hash = Math.abs(hash);
	}
	return hash;
};


/**
 * Recursively build a HTML form compatible key-value set from a potentially multidimensional
 * key-value set
 * @param string name POST key/name.
 * @param mixed val POST value, which may be an object or array.
 * @returns {unresolved}
 */
function buildHTMLFormAttributes(name, val) {
	var postKeyVals = {};
	if (typeof val === 'string') {
		postKeyVals[name] = val;
	} else {
		$.each(val	, function(valEntryName, valEntryVal) {
			$.extend(
				postKeyVals, 
				buildHTMLFormAttributes(
					name+'['+valEntryName+']', 
					valEntryVal
				)
			);
		});
	}

	return postKeyVals;
}

/**
 * Create a deep-clone of a simple object.
 *
 * Should only be used for simple JSON-compliant objects and shouldn't be used for objects with prototypes,
 * functions or circular references.
 * @return Object
 */
function deepClone(yourObj) {
	return JSON.parse(JSON.stringify(yourObj));
}

/**
 * Toggle browser fullscreen, if it's available.
 * Taken from: https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#Toggling_fullscreen_mode
 */
function toggleFullScreen(forceEnable) {

	forceEnable = defaultTo(forceEnable, null);

	var doc = window.document;
	var docEl = doc.documentElement;

	var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen;
	var cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen;

	if (forceEnable === null) {
		forceEnable = !doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement
	}

	if(forceEnable) {
		requestFullScreen.call(docEl);
	} else {
		cancelFullScreen.call(doc);
	}
}

/**
 * Very simple pluralizer.
 */
function pluralize(word, quantity) {
	return quantity + ' ' + (quantity === 1 ? word : word + 's');
}

/**
 *
 * @param {Date} [date]
 */
function getWelcomeMessage(date) {
	date = defaultTo(date, new Date())

	var hours = date.getHours();
	if (hours < 12) {
		return 'Good morning';
	} else if (hours < 17) {
		return 'Good afternoon';
	} else {
		return 'Good evening';
	}
}

function round(num, decimalPlaces) {
	decimalPlaces = defaultTo(decimalPlaces, 2);
	var coefficient = Math.pow(10, decimalPlaces);
 	return (Math.round(num * coefficient) / coefficient);
}


function formatDate(date, incDate, incTime) {

	incDate = defaultTo(incDate, true);
	incTime = defaultTo(incTime, true);

	if (!(date instanceof Date) || (!incDate && !incTime)) {
		return null;
	}

	var opts = {};

	if (incDate) {
		Object.assign(opts, {day: 'numeric', month: 'short', year:'numeric'});
	}
	if (incTime) {
		Object.assign(opts, {hour:'2-digit', minute:'2-digit'});
	}

	return date.toLocaleString('en-GB', opts);
}

/**
 * Convert time string to equivalent total number of seconds. E.g. "16:30" -> 59400. 
 * An invalid time string e.g. "25:30" would be rejected.
 * @param {string} timeString e.g. "16:30", or "4:15" etc.
 * @returns {?int} Total seconds, or NULL if supplied arg is invalid.
 */
function timeToSeconds(timeString) {
	const [hours, minutes] = timeString.split(':').map(val => parseInt(val));
	// Values which are not-a-number will fail this check:
	if (!(hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59)) {
		return null;
	}
	return (hours * 3600) + (minutes * 60);
}


/**
 * Convert HTML metacharacters into to their HTML entities.
 * @param {string} str
 * @return {string}
 */
function escapeHtml(str) {
	var entityMap = {
		'&': '&amp;',
		'<': '&lt;',
		'>': '&gt;',
		'"': '&quot;',
		"'": '&#39;',
		'/': '&#x2F;',
		'`': '&#x60;',
		'=': '&#x3D;'
	};

	return String(str).replace(/[&<>"'`=\/]/g, function(s) {
		return entityMap[s];
	});
}

/**
 * Return string to given length + ellipsis.
 * @todo Enhance to prevent word breaking.
 * @param {string} str
 * @param {number} len
 * @return {string}
 */
function makeNiceString(str, len) {
	if (str.length <= len) {
		return str;
	} else {
		return str.substr(0, len - 3) + '...';
	}
}

/**
 * Parse a PHP or JS JSON encoded DateTime value and return a corresponding JS Date object.
 * @param {(Object|string)} d
 */
function parseDate(d) {
	// Encoded from JS via JSON.stringify
	if (typeof d === 'string') {
		return new Date(d);
	// Encoded from PHP via json_encode:
	} else {
		// return new Date(Date.parse(d.date));
		// Altered for Safari support. See https://stackoverflow.com/questions/5324178
		var arr = d.date.split(/[- :]/);
		return new Date(arr[0], arr[1]-1, arr[2], arr[3], arr[4], arr[5]);
	}

}

/**
 * See https://stackoverflow.com/a/14706877
 * @param {*} val
 * @return {boolean} TRUE if the argument is an Object or an Array.
 */
function isObject(val) {
	var type = typeof val;
	return type === 'function' || type === 'object' && !!val;
}

/**
 * Upper-case the first character in a string.
 * @param {string} str
 * @return {string}
 */
function ucFirst(str) {
	return str.charAt(0).toUpperCase() + str.substring(1);
}

/**
 * Remove HTML from a string.
 * Similar to `strip_tags(..)` in PHP.
 * Source: https://stackoverflow.com/a/47140708
 * @param {string} html e.g. "<p>Hello</p>"
 * @return {string} e.g. "Hello"
 */
function stripTags(html) {
   var doc = new DOMParser().parseFromString(html, 'text/html');
   return doc.body.textContent || "";
}

function setClipboard(text){
	var input = document.createElement('input');
	document.body.appendChild(input);
	input.value = text;
	input.select();
	document.execCommand('copy', false);
	input.remove();
}

function createTooltip(tooltipProps) {
	const {id, heading, content, allowHtml, hideMobile, delayMs, hideOnBlur, autoload, maxWidth} = tooltipProps;

	// Optionally hide on mobile:
	if (isMobileDevice() && hideMobile) {
		return;
	}

	let title = heading ? "<p class='tippy-header'>" + heading + "</p>" : '';
	
	let contentWithTitle = 
		"<div class='tippy-wrapper'>" +
			title +
			"<div class='tippy-inner-content'>" + content + "</div>" +
		"</div>";

	let parent = $("#" + id).parent();
	
	// Don't apply tooltip if the wrapper is tablesorter - this creates duplicates!
	if (parent.hasClass("tablesorter-header-inner")) {
		return;
	}

	// Set tooltip props:
	parent.attr('data-tippy-content', contentWithTitle);
	parent.attr('data-tippy-allowHTML', allowHtml);
	parent.attr('data-tippy-delay', delayMs);
	parent.attr('data-tippy-hideOnClick', hideOnBlur);
	parent.attr('data-tippy-showOnCreate', autoload);
	parent.attr('data-tippy-maxWidth', maxWidth);
}

/**
 * @return {number} Time in milliseconds since initial page load.
 */
function uptime() 
{
	// List of performance types - see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/entryType
	const [currPage] = performance.getEntriesByType("navigation");
	return (performance.now() - currPage.startTime);
}


/**
 * If `arrOrVal` is an array, return it as it is.
 * If `arrOrVal` is anything else, box it inside an array and return that.
 *
 * @param {*|Array} arrOrVal
 */
Array.box = function(arrOrVal) {
	if (Array.isArray(arrOrVal)) {
		return arrOrVal;
	} else {
		return [arrOrVal];
	}
};

// Based on addHours from: https://stackoverflow.com/a/1050782
Date.prototype.addMinutes = function(m) {
   this.setTime(this.getTime() + (m*60*1000));
   return this;
};


/** IE11 Polyfills: **/

/*
  `Element.closest` https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
*/

if (!Element.prototype.matches) {
  Element.prototype.matches = Element.prototype.msMatchesSelector || 
                              Element.prototype.webkitMatchesSelector;
}

if (!Element.prototype.closest) {
  Element.prototype.closest = function(s) {
    var el = this;

    do {
      if (el.matches(s)) return el;
      el = el.parentElement || el.parentNode;
    } while (el !== null && el.nodeType === 1);
    return null;
  };
}

/*
   `NodeList.forEach` https://developer.mozilla.org/en-US/docs/Web/API/NodeList/forEach#Polyfill
 */
if (window.NodeList && !NodeList.prototype.forEach) {
   NodeList.prototype.forEach = Array.prototype.forEach;
}

/* 
   `ChildNode.remove` https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove#Polyfill
 */
(function (arr) {
  arr.forEach(function (item) {
    if (item.hasOwnProperty('remove')) {
      return;
    }
    Object.defineProperty(item, 'remove', {
      configurable: true,
      enumerable: true,
      writable: true,
      value: function remove() {
        if (this.parentNode === null) {
          return;
        }
        this.parentNode.removeChild(this);
      }
    });
  });
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
