Source: Utils.js

import $ from "jquery";
/*
Last edit date: 05.06.2017
*/
/**
 * A class with static methods used across all Shark components.
 * @constructor
 * @version 0.1.2.36 - 2017.06.05
 */
class Utils {
	constructor() {
		this.__id = "Utils";
	}

	/**
	 * Return a random number between 0 and a max value ora between two values.
	 * @static
	 * @version 0.1.0.1
	 * @param {number} minOrMax - If this is the only passed parameter thant it will be used as "max", and the return number will be between 0 and this number. If the max parameter is passed, then this parameter will be as lower limit.
	 * @param {number} [max] - The higher limit for the generate number.
	 * @returns {number} - The random number generated.
	 */
	static betterRandom (minOrMax, max) {
		let min = parseFloat(minOrMax);
		let returnValue = Math.random();
		max = parseFloat(max);

		if (!max && minOrMax) {
			min = 0;
			max = parseFloat(minOrMax);
		}
		if (!min) {
			min = 0;
		}
		if (!max) {
			max = 1;
		}
		returnValue *= (max - min);
		returnValue += min;

		return returnValue;
	}

	/**
	 * Return a random integer number between 0 and a max value ora between two values. If not integer numbers are passed in, the result will be anyway an integer.
	 * @static
	 * @version 0.1.0.1
	 * @param {number} minOrMax - If this is the only passed parameter thant it will be used as "max", and the return number will be between 0 and this number. If the max parameter is passed, then this parameter will be as lower limit.
	 * @param {number} [max] - The higher limit for the generate number.
	 */
	static betterRandomInt (minOrMax, max) {
		return Math.round(Utils.betterRandom(minOrMax, max));
	}

	/**
	 * Convert a string with camel-cased text to a dash separated words. Each uppercase character will be considered as the word starter and lower-cased.
	 * @static
	 * @version 0.1.0.1
	 * @param {string} inputString - The camelCased string.
	 * @returns {string} - The dash-converted string.
	 */
	static camelToDashed (inputString) {
		let newString = "";
	
		inputString.split("").forEach((currLetter, index) => {
			if (currLetter.toLowerCase() !== currLetter) {
				if (index > 0) {
					newString += "-";
				}
				currLetter = currLetter.toLowerCase();
			}
	
			newString += currLetter;
		});
	
		return newString;
	}

	//Valutare questa:
	// async getHash (m) {
	// 	const msgUint8 = new TextEncoder().encode(m);
	// 	const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);
	// 	const hashArray = Array.from(new Uint8Array(hashBuffer));
	// 	const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
	// 	return hashHex;
	// }

	static computeHash (value) {
		const stringified = Utils.safeStringify(value) || "";
		
		return stringified.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0);
	}

	/**
	 * Convert a string with dash as word separator to a camel-cased text. The first letter after each dash is upper-cased and joined with previous word.
	 * @static
	 * @version 0.1.0.1
	 * @param {string} inputString - The camelCased string.
	 * @returns {string} - The camel-cased string.
	 */
	 static dashedToCamel (inputString) {
		let newString = inputString;
		let dashPos = 0;

		while (dashPos > -1) {
			dashPos = newString.indexOf("-");
			if (dashPos > -1) {
				newString = newString.substr(0, dashPos) + newString.substr(dashPos + 1, 1).toUpperCase() + newString.substr(dashPos + 2);
			}
		}
	
		return newString;
	}

	static extractDifferentProperties (firstObject, secondObject) {
		const returnObject = {};

		for (let prop in firstObject) {
			if (firstObject[prop] && secondObject[prop]) {
				if (firstObject[prop] !== secondObject[prop]) {
					returnObject[prop] = secondObject[prop];
				}
			}
		}

		return returnObject;
	}

	static flatten (objectToFlatten) {
		const result = {}; //Object.create(objectToFlatten);

		for (var key in objectToFlatten) {
			//console.log(key, objectToFlatten[key], typeof objectToFlatten[key]);
			if (typeof objectToFlatten[key] !== 'function') {
				if (typeof objectToFlatten[key] === "object") {
					result[key] = Utils.flatten(objectToFlatten[key]);
				}
				else {
					result[key] = objectToFlatten[key];
				}
			}
		}
		//console.warn("Utils.flatten:", result);
		return result;
	}

	/**
	 * Generate a random GUID
	 * @static
	 * @version 0.1.0.1
	 * @param {string} [prefix] -
	 * @param {string} [suffix] -
	 */
	static generateGUID(prefix = "", suffix = "") {
		var guid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
			var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
			return v.toString(16);
		});
		return prefix + guid.toUpperCase() + suffix;
	}

	/**
	 * Get a property value from an object. The property can be a sub-property of any property of the object. If the path identificates an unexisting property, this function returns undefined.
	 * @static
	 * @version 0.1.0.1
	 * @param {object} object - The object to read from the property value.
	 * @param {string} path - The dot-separated path that identify the route to follow to get to the property to read the value.
	 * @returns {boolean|object|string|array|number|null|undefined} - The value of the property.
	 */
	static getDeepProperty (object, path) {
		if (object) {
			const pathParts = path.split(".");
			// var object = object;

			//Uso "i" più avanti, quindi lo lascio come "var": andrebbe sistemato
			for (var i = 0; i < pathParts.length - 1; i++) {
				var squarePos = pathParts[i].indexOf("[");
				var modelArrayIndex = -1;

				if (squarePos > -1) {
					modelArrayIndex = parseInt(pathParts[i].substring(squarePos + 1, pathParts[i].indexOf("]")));
					pathParts[i] = pathParts[i].substr(0, squarePos);
				}
				if (modelArrayIndex === -1) {
					if (!object[pathParts[i]]) {
						break;
					}
					object = object[pathParts[i]];
				}
				else {
					if (!object[pathParts[i]][modelArrayIndex]) {
						break;
					}
					object = object[pathParts[i]][modelArrayIndex];
				}
			}

			var squarePos = pathParts[i].indexOf("[");
			var modelArrayIndex = -1;

			if (squarePos > -1) {
				modelArrayIndex = parseInt(pathParts[i].substring(squarePos + 1, pathParts[i].indexOf("]")));
				pathParts[i] = pathParts[i].substr(0, squarePos);
			}
			if (modelArrayIndex === -1) {
				return object[pathParts[i]];
			}
			else {
				return object[pathParts[i]][modelArrayIndex];
			}
		}
		else {
			return undefined;
		}
		// Secondo qui: https://jsperf.com/dereference-object-property-path-from-string
		// Questa sotto potrebbe essere una strada migliore
		// function deref(obj, s) {
		// var i = 0;
		// s = s.split('.');
		// while (obj && i < s.length)
		// 	obj = obj[s[i++]];
		// }
	}

	static matchLists (sourceList = [], matchLists = {}, options = {}) {
		//Dave.Info: molto grezza ma fa il suo lavoro.
		sourceList.forEach(currItem => {
			for (const currProp in currItem) {
				if (currProp.substr(-2) === "Guid") {
					const matchListName = currProp.substr(0, currProp.length - 2);
					const matchList = matchLists[matchListName];

					if (matchList) {
						currItem[`__${matchListName}`] = Utils.safp(matchList, "guid", currItem[currProp]);
					}
				}
			}
		});
	}

	/**
	 * Replicate the property from the sourceObject into targetObject, with various options.
	 * @static
	 * @version 0.1.2.36 - 2017.06.05
	 *
	 * @param {object} targetObject - The object that the properties will be copied onto.
	 * @param {object} sourceObject - The object that cointains properties and values to be copied.
	 * @param {object} [options] - An object containing the options.
	 * @param {boolean} [options.createMissingProperties = false] - If true properties that are in the soruceObject but not in the targetObject are created in the targetObject and assigned the value they have in the sourceObject; if false, only properties alredy in the targetObject takes the value they have in the sourceObject.
	 * @param {boolean} [options.matchEmptyParameters = true] - If true a property that is empty in the sourceObject are copied into the targetObject, even if the value in the targetObject is not empty; if false, an empty property in sourceObject will not overwrite any content present in the targetObject.
	 */
	static matchProperties(targetObject, sourceObject, options) {
		let settings = {
			createMissingProperties: false,
			exclude: [],
			// excludeTargetMissingProperties: false,
			matchEmptyParameters: true,
			matchUndefinedParameters: false
		};
		Object.assign(settings, options);
		// $.extend(true, settings, options);
		if (!Array.isArray(settings.exclude)) {
			settings.exclude = [];
		}
		for (let prop in sourceObject) {
			// Dave.ToDo: Qui bisognerà capire se riesco a migliorare la gestione degli Object (magari dopo averli parsati), sostituendo l'extend di jQuery con Object.assign().
			
			// Dave.ToDo: qui si può fare un sacco di lavoro di miglioramento
			const targetPropCanBeWritten = (Object.prototype.hasOwnProperty.call(targetObject, prop) && Object.getOwnPropertyDescriptor(targetObject, prop).writable) || (!Object.prototype.hasOwnProperty.call(targetObject, prop) && settings.createMissingProperties);

			if (targetPropCanBeWritten) {
				if (settings.exclude.length === 0 || settings.exclude.indexOf(prop) === -1) {
					if ((typeof targetObject[prop] !== "undefined" || settings.createMissingProperties) && ((sourceObject[prop] && sourceObject[prop] !== "") || settings.matchEmptyParameters) && ((sourceObject[prop] !== undefined) || settings.matchUndefinedParameters)) {
						// if (sourceObject[prop] && typeof sourceObject[prop].splice === "function") {
						if (Array.isArray(sourceObject[prop])) {
							targetObject[prop] = [...sourceObject[prop]];
							// targetObject[prop] = sourceObject[prop].map(item => item);
						}
						else if ((typeof sourceObject[prop] === "object" || typeof targetObject[prop] === "object") && (sourceObject[prop] !== null && targetObject[prop] !== null)) {
							if (targetObject[prop] && targetObject[prop].constructor.name === "Object" && sourceObject[prop].constructor.name === "Object" && Object.keys(targetObject[prop]).length > 0) {
								Utils.matchProperties(targetObject[prop], sourceObject[prop], options);
							}
							else {
								if (targetObject[prop] && targetObject[prop].constructor.name === "Object" && typeof sourceObject[prop] === "string") {
									targetObject[prop] = $.extend(true, targetObject[prop], Utils.parseJSON(sourceObject[prop], "object"));
								}
								else if (targetObject[prop] && targetObject[prop].constructor.name === "Array" && typeof sourceObject[prop] === "string") {
									targetObject[prop] = Utils.parseJSON(sourceObject[prop], "array");
								}
								else {
									// Per rimpiazzare jQuery serve un deep clone/merge
									// targetObject[prop] = Object.assign({}, sourceObject[prop]);
									targetObject[prop] = $.extend(true, {}, sourceObject[prop]);
								}
							}
						}
						else {
							targetObject[prop] = sourceObject[prop];
						}
					}
				}
			}
			else {
				// Dave questo console.arn rompe le scatole, devo usare il trace
				// console.warn(`Property ${prop} of object "targetObject" is not writable.`, targetObject);
				// Shark.trace(`Property ${prop} of object "targetObject" is not writable.`, targetObject, S);
			}
		}
		/*	for (var prop in sourceObject) {
				if ((typeof targetObject[prop] !== "undefined" || settings.createMissingProperties) && ((sourceObject[prop] && sourceObject[prop] !== "") || settings.matchEmptyParameters)) {
					targetObject[prop] = sourceObject[prop];
				}
			}
			*/
		/*	for (var prop in sourceObject) {
				if ((targetObject[prop] || settings.createMissingProperties) && (sourceObject[prop] !== "" || settings.matchEmptyParameters)) {
					//if (typeof sourceObject[prop] === "object") {
					//	var newObject = {};
					//	Utils.matchProperties(newObject, sourceObject[prop], options);
						
					//	targetObject[prop] = newObject;
					//}
					//else {
						targetObject[prop] = sourceObject[prop];
					//}
				}
			}*/
	}

	static moveCaretToEnd(target) {
		setTimeout(function () {
			if (typeof target.selectionStart === "number") {
				target.selectionStart = target.selectionEnd = target.value.length;
			}
			else if (typeof target.createTextRange !== "undefined") {
				target.focus();
				var range = target.createTextRange();
				range.collapse(false);
				range.select();
			}
		}, 10);
	}

	static parseJSON (data, expectedType) {
		if (typeof data === "string" && data.toLowerCase() === "array") {
			return [];
		}
		else if (data === "" || data === "\"\"") {
			switch (expectedType) {
				case "array":
					return [];
				default:
					return {};
			}
		}
		else {
			if (typeof data === "string") {
				let parsedData;
				
				try {
					parsedData = JSON.parse(data);
				}
				catch (error) {
					console.error("Utisl.parseJSON", {error, data, expectedType});
					parsedData = null;
				}
				if (parsedData === null || parsedData === "") {
					switch (expectedType) {
						case "array":
							return [];
						case "object":
							return {};
						default:
							return parsedData;
					}
				}
				else {
					switch (expectedType) {
						case "array":
							if (typeof parsedData.splice === "function") {
								return parsedData;
							}
							else {
								return [];
							}
						//					case "object":
						//						return {};
						//						break;
						default:
							return parsedData;
					}
				}
			}
			else if (typeof data === "undefined") {
				switch (expectedType) {
					case "array":
						return [];
					default:
						return {};
				}
			}
			else {
				return data;
			}
		}
	}

	static replaceNullValue (objectToParse, replaceVal) {
		if (Array.isArray(objectToParse)) {
			objectToParse.forEach(item => Utils.replaceNullValue(item, replaceVal));
		}
		else {
			for (let currProp in objectToParse) {
				if (typeof objectToParse[currProp] === "undefined" || objectToParse[currProp] === null) {
					objectToParse[currProp] = replaceVal;
				}
			}
		}
	}

	static safeStringify (sourceValue) {
		return JSON.stringify(sourceValue, (key, value) => {
			if (value instanceof Map) {
				value = Array.from(value);
			}
	
			return (key !== "__shguid" && key.substr(0, 2) === "__") ? "" : value
		});
	}

	/**
	 * Shorthand for the [searchArrayForProperty method]{@link Utils.searchArrayForProperty}.
	 *
	 * @borrows searchArrayForProperty as safp
	 * @see Utils.searchArrayForProperty
	 *
	 */
	static safp (array, property, value, searchType) {
		return Utils.searchArrayForProperty(array, property, value, searchType);
	}
	/**
	 * Search an array for an object that has the requested property with the given value. By default if a result is found it stops the loop and returns that item.
	 *<br />If no object matches the requested filter this method returns null.
	 *<br />Since the version 2.0.6 this method uses the faster Array.prototype.find method if is available and the parameters are compatible.
	 *
	 * @returns {object|array|number|null}
	 * @static
	 * @version 2.0.6, 2018.05.31
	 *
	 * @param {array} list - The array of objects to search into.
	 * @param {string} property - The name of the property to search in each object of the array. If a dot separated list (eg. "childs.name") is passed, the value param is compared to the last property of the <i>path</i>; for example if the "property" param is "childs.name", then the value param will compared to the "name" property of the "childs" property of each element in the array.
	 * @param {object} value - The value that will be used to check the given property. It must have same value and same type, since a comparison of type === is made.
	 * @param {boolean|string} [searchType = "first"] - This can be one of "first", "all" or "index".
	 * <br />For compatibilty with previous version of this method, a boolean can be passed and if the value is true the method stops and return on the first occourence and therefore return a single object (like it was "first"), if is false, the method will return an array of object that match the filter (like it was "all").
	 * <br />If searchType is set to "all" or false, the method will return an array even if it matches only one object.
	 *
	 */
	static searchArrayForProperty (list, property, value, searchType) {
		if (!list) {
			console.warn("Error, cant search on a null list.");
			return;
		}
		switch (typeof searchType) {
			case "undefined":
				searchType = "first";
				break;
			case "boolean":
				searchType = searchType ? "first" : "all";
				break;
		}
		var propList;
		var returnList = [];
		if (property.indexOf(".") > -1) {
			propList = property.split(".");
		}
		else {
			propList = [property];
		}
		var propListLength = propList.length;
		//Dave.ToDo: Qui, nei casi permessi (solo find first) provo ad usare il Array.find, è molto più veloce. Da capire se va e in quali altri casi posso usarlo.
		//Non è vero, è più lento...
		// if (Array.prototype.find && propListLength === 1) {
		// 	console.log("******** Utils.SAFP sta usando il find");
		// 	var result = list.find(function (item) {return item[property] === value});
		// 	return typeof result === "undefined" ? null : result;
		// }
		var listLength = list.length;
		for (var i = 0; i < listLength; i++) {
			var currValue = list[i][propList[0]];
			if (currValue) {
				for (var p = 1; p < propListLength; p++) {
					if (!currValue[propList[p]]) {
						break;
					}
					currValue = currValue[propList[p]];
					//console.info(currValue);
				}
			}
			//Dave.ToDo: Should implement something like "indexAll" to have all indexes of found items.
			if (currValue === value) {
				switch (searchType) {
					case "all":
						returnList.push(list[i]);
						break;
					case "first":
						return list[i];
					case "index":
						return i;
				}
			}
		}
		//Dave.ToDo: Potremmo valutare che se la property richiesta è senza "." faccio il giro diverso... devo fare un controllo sui tempi con tipo 1.000.000 di righe
		/*	return null;
		
			for (var i = 0; i < array.length; i++) {
				if (array[i][property] === value) {
					if (returnFirstOnly) {
						return array[i];
					}
					else {
						returnList.push(array[i]);
					}
				}
			}*/
		if (returnList.length > 0) {
			return returnList;
		}
		else {
			return null;
		}
	}

	/**
	 * Assign a property to an object. The property can be a sub-property of any property of the object. If the path parameters indicates a sub-property of a property that does not exists, the property and the sub-property will be both created.
	 * @static
	 * @version 0.1.0.1
	 * @param {object} object - The object on whom the property will be created or assigned a value.
	 * @param {string} path -
	 */
	static setDeepProperty (object, path, value, depth = 0) {
		const pathParts = path.split(".");
		// depth = depth || 0;

		if (!object[pathParts[depth]]) {
			object[pathParts[depth]] = {};
		}
		if (depth < pathParts.length - 1) {
			Utils.setDeepProperty(object[pathParts[depth]], path, value, depth + 1);
		}
		else {
			//Dave.Warn: Qui sarebbe il caso di fare attenzione che se il tipo che mi viene passato è diverso da quello presente dovrei dare un warn (se imposto a "true" un oggetto complesso rompo tutto)
			object[pathParts[depth]] = value;
		}

		return object;
	}

	static trace (content) {
		try {
			if (AppData.consoleDebugActive) {
				console.debug(content);
				//console.debug(arguments.callee.caller);
			}
		}
		catch (err) {
			alert(content);
		}
	}

	static toCamelCase (value) {
		return value.slice(0, 1).toLowerCase() + value.slice(1);
	}

	static toTitleCase (value, forceLowerCase = true) {
		return value.slice(0, 1).toUpperCase() + (forceLowerCase ? value.slice(1).toLowerCase() : value.substr(1));
	}

	/**
	 * Return a string of <code>length</code> characters representing the given number with leading zeros.
	 * @static
	 * @version 1.0.2 - 2019.04.23
	 * @param {number} number - 
	 * @param {number} length - 
	 * @returns {string} - The string representing the passed number with leading zeros.
	 */
	static zeroFill (number, length) {
		let numberAsString = number.toString();

		if (numberAsString.length < length) {
			return numberAsString.padStart(length, "0")
		}
		return numberAsString;
	}
}


export { Utils };