Source: Shark.js

import Mustache from "mustache";
import Jaw from "./Jaw";
import { MustacheRenderer } from "./renderers/MustacheRenderer";
// import SmartModel from "./SmartModel";
import { Utils } from "./Utils";

import $ from "jquery";
import SmartModel from "./SmartModel";
import VirtualDOMRenderer from "./renderers/VirtualDOMRenderer";
import { v4 as uuidv4 } from 'uuid';

/**
 * Represents the Shark main class. It's has only static methods and should be considered as a Singleton.
 * @constructor
 * @version 0.7.1.56, 2018.04.13
 */
 class Shark {
	// static TRACE_INFO = "info";
	// static TRACE_LOG = "log";
	// static TRACE_NONE = "none";
	// static TRACE_WARN = "warn";
	
	// static __store = {};
	// static __stores = [];//For future implementantion of multiple stores
	// static __views = {};
	
	// static requestedView = "";
	
	
	constructor () {
	}
	
	/**
	 * @param {object} [options] - An object containing the options for initializing.
	 * @param {object} [options.commonEndPointsBase = ""] - DEPRECATED and REMOVED The "base" parte of the URL for the remote request. Use this value only if you have common destination for all your API call.
	 * Otehrwise you can set different URL for each call.
	 * @param {object} [options.labels = []] - An array of objects containing a list of labels to be used in conjunction with localization features.
	 * @param {integer} [options.remoteCallTimeOutDuration = 15000] - The number of millisecods that Shark should wait befor abort remote call.
	 * @param {object} [options.dataManager = DataManager] - DEPRECATED and REMOVED The Class to be used to manage the remote request. Not yet fully implemented.
	 * @param {object} [options.templates = {}] - An object containing html templates as strings. (documentation has to be written about creating templates object)
	 * @param {object} [options.templateRenderer = MustacheRenderer] - The Class responsible for rendering Views on screen. Must be a SharRenderer sub-class. Not yet fully implemented.
	 * @param {boolean} [forceReinit = false] - If true process initialize step even if the initialization phase has alredy been completed.
	 */
	static init (options, forceReinit) {
		Object.defineProperty(this, "$store", {
			configurable: false,
			enumerable: true,
			value: {},
			writable: false,
		});

		// Dave.Warn: Tutta la gestione dei template a livello Shark deve sparire, i template stanno nelle View!
		if (options.templates) {
			console.warn("WARNING!!!! You are using 'templates' option that will be removed");
		}
		if (options.commonEndPointsBase) {
			console.warn("WARNING!!!! You are using 'commonEndPointsBase' option that has been removed");
		}
		if (options.dataManager) {
			console.warn("WARNING!!!! You are using 'dataManager' option that has been removed");
		}
		Shark.trace("Shark.init, Shark.isInitialized: " + Shark.isInitialized + ", forceReinit: " + forceReinit, Shark.TRACE_INFO);

		if (!Shark.isInitialized || forceReinit) {
			let settings = {
				enableMainStoreAutoRender: false,
				enableJawStoreAutoRender: false,
				enableSmartModelSetUpdatesView: false,
				enableJawsCompatibilityMode: false,
				labels: [],
				preventDeepLinking: false,
				remoteCallTimeOutDuration: 15000,
				routingMethod: "hash",//optional, can be "url"
				templates: typeof Templates !== "undefined" ? Templates : {},
				templateRenderer: typeof MustacheRenderer !== "undefined" ? new MustacheRenderer() : null,
			};

			Object.assign(settings, options);

			Shark.activeFeatures.moment = typeof moment !== "undefined";
			Shark.settings = settings;
			Shark.labels = settings.labels;

			Shark.templates = settings.templates;
			Shark.templates.set = function (template) {
				if (template.hasOwnProperty("set")) {
					Shark.trace("You cant add a template with name 'set' to Shark.templates", Shark.TRACE_ERROR);
					delete template.set;
				}
				Object.assign(this, template);
			};

			Shark.templateRenderer = settings.templateRenderer;

			Shark.__extractTemplateMap(Shark.templates);

			Object.values(Shark.__components).forEach(item => {
				Shark.trace("Calling appInit on components", Shark.TRACE_LOG);
				if (typeof item.appInit === "function") {
					item.appInit.call(item);
				}
			});

			Object.values(Shark.__controllers).forEach(item => {
				Shark.trace("Calling appInit on controllers", Shark.TRACE_LOG);
				if (typeof item.appInit === "function") {
					item.appInit.call(item);
				}
			});

			Object.values(Shark.__views).forEach(item => {
				Shark.trace("Calling appInit on views", Shark.TRACE_LOG);
				if (typeof item.appInit === "function") {
					item.appInit.call(item);
				}
			});

			Shark.isInitialized = true;
		}

		if (!Shark.settings.preventDeepLinking) {
			Shark.parseRequestedViewFromLocation();

			if (!Shark.__pageOpening) {
				Shark.trace("Page required from URL", Shark.requestedView, Shark.TRACE_INFO);
				//Shark.openPage(Shark.requestedView, {isAutoOpen: true});
			}

			window.addEventListener("popstate", (event) => {
				console.log(`%cSi è verificato l'evento ${event.type}, dovrei gestirlo.`, "border: 1px solid orange; color: violet; padding: 5px;", {event});
				Shark.trace("event: popstate, location: " + document.location + ", state: " + JSON.stringify(event.state), event, Shark.TRACE_LOG);

				if (event.state && !Shark.__pageOpening) {
					Shark.requestedView = event.state.jawId;
					
					//Dave.ToDo: This is the same stuff, should be an external function, leaving here only the "requestedView" extraction
					//This Check is wrong: requestdPage could be "activePage.route"
					if (Shark.requestedView !== Shark.activePage.className) {
						Shark.trace("Page requested from URL (routing: url)", Shark.requestedView, Shark.TRACE_INFO);
						Shark.openPage(Shark.requestedView, {
							isAutoOpen: true
						});
					}
				}
			});

			window.addEventListener("pushState", (event) => {
				console.log(`%cSi è verificato l'evento ${event.type}, dovrei gestirlo.`, "border: 1px solid orange; color: violet; padding: 5px;", {event});
			});

			window.addEventListener("replaceState", (event) => {
				console.log(`%cSi è verificato l'evento ${event.type}, dovrei gestirlo.`, "border: 1px solid orange; color: violet; padding: 5px;", {event});
			});
			
			// if (Shark.settings.routingMethod === "url" && typeof history.pushState === "function") {
			// }
			// else {
			if (Shark.settings.routingMethod === "hash" || typeof history.pushState !== "function") {
				window.addEventListener("hashchange", function () {
					Shark.trace("event: hashchange, location: " + document.location + ", state: " + JSON.stringify(event.state), event, Shark.TRACE_LOG);
					
					if (document.location.hash.length > 1 && !Shark.__pageOpening) {
						Shark.parseRequestedViewFromLocation();
						// var requestedView = document.location.hash;
						// Shark.requestedView = requestedView.splice(1);
						
						//Dave.ToDo: This is the same stuff, should be an external function, leaving here only the "requestedView" extraction
						if (Shark.requestedView !== Shark.activePage.className) {
							Shark.trace("Page requested from URL (routing: hash)", Shark.requestedView, Shark.TRACE_INFO);
							Shark.openPage(Shark.requestedView, { isAutoOpen: true });
						}
					}
				});
			}
		}
	}

	static parseRequestedViewFromLocation () {
		const requestedView = ["url", "pathname"].includes(Shark.settings.routingMethod) ? document.location.pathname : document.location.hash;

		if (requestedView !== "") {
			Shark.requestedView = requestedView.slice(1);
		}
		else {
			Shark.requestedView = "";
		}
		// return Shark.requestedView;
	}
}


Shark.TRACE_ERROR = "error";
Shark.TRACE_INFO = "info";
Shark.TRACE_LOG = "log";
Shark.TRACE_NONE = "none";
Shark.TRACE_WARN = "warn";

Shark.__store = {};
Shark.__stores = [];//For future implementantion of multiple stores
Shark.__views = {};

Shark.requestedView = "";



Shark.activeFeatures = {
	moment: false
};

Shark.activePage = {id: ""};
Shark.appData = {};
Shark.appDataItems = {};
Shark.history = {content: [], currentPos: -1};
Shark.isInitialized = false;
Shark.labels = {};
Shark.languagesPath = "";
Shark.models = {};
Shark.pages = {};
Shark.requiredPage = "";
Shark.templates = {};
Shark.traceLevel = Shark.TRACE_WARN;

Shark.__appDataMap = [];
Shark.__autoBoundProperties = [];
Shark.__components = {};
Shark.__controllers = {};
Shark.__globalListeners = [];
Shark.__internalData = {
	waitBoxOpenCount: 0
};
Shark.__internalSettings = {};
Shark.__listeners = {
	viewToModelBinds: {},
	viewToModelBinds2: {},
};
Shark.__modelProcessors = {};
Shark.__models = {};
Shark.__templateMap = {};
// Shark.__store = {};
// Shark.__stores = [];//For future implementantion of multiple stores
// Shark.__views = {};


// Shark.__addGlobalListener = function (listenerData) {
// 	"use strict";

// 	if (!Utils.safp(Shark.__globalListeners, "name", listenerData.name)) {
// 		Shark.__globalListeners.push(listenerData);
// 	}
// };

Shark.__bindViewToModel = function (containerCSSSelector) {
	if (typeof containerCSSSelector === "undefined" || containerCSSSelector.trim() === "") {
		containerCSSSelector = "body";
	}

	const referenceModels = document.querySelector(containerCSSSelector);

	// if (containerCSSSelector !== "") {
		// referenceModels = $(containerCSSSelector);
	// }
	// else {
	// 	referenceModels = $("body");
	// }

	const referenceModelAttributeName = "sh-model";
	const $containerDOM = referenceModels.querySelectorAll("[data-" + referenceModelAttributeName + "]");
	// var $containerDOM = referenceModels.find("[data-" + referenceModelAttributeName + "]");

	// Tutto questo va sostituito con le funzioni native!
	$($containerDOM).each(function (index, parentItem) {
		// var referenceModelPath = $(parentItem).data(referenceModelAttributeName);
		var referenceModelPath = parentItem.dataset[referenceModelAttributeName];
		// var originalReferenceModelPath = referenceModelPath;
		//var referenceModelPropAttributeName = "sh-model-model";

		$(parentItem).find("[data-sh-prop]").each(function (childIndex, childItem) {//[data-sh-model-prop] Is deprecated //.add($(parentItem).find("[data-sh-model-prop]"))
			var $child = $(childItem);
			// var referenceModelField = $(childItem).data("sh-prop");// || $(childItem).data("sh-model-prop"); //Deprecated the "-model" variant
			var referenceModelField = childItem.dataset.shProp; //Dave.Warn: Dovunque passo al dataset devo camelare!!!
			// var referenceEvent = $(childItem).data("sh-trigger");//  || $(childItem).data("sh-model-trigger");//data-sh-trigger="@child.click //Deprecated the "-model" variant
			var referenceEvent = childItem.dataset.shTrigger; //Dave.Warn: Dovunque passo al dataset devo camelare!!!
			var eventTypeList = "input";//C'era anche "change" ma lo dispaccia due volte!
			var shGUID;
			var $eventTargets = $child;

			//Dave.ToDo: Qui è tutto da verificare e modificare
			// switch ($child[0].nodeName.toLowerCase()) {
			switch (childItem.nodeName.toLowerCase()) {
				case "input":
					// switch ($child[0].type.toLowerCase()) {
					switch (childItem.type.toLowerCase()) {
						case "checkbox":
						case "radio":
							eventTypeList = "change";
							break;
					}
					break;
			}

			var propertyData = Shark.__extractPropertyAttributes(referenceModelField);
			var referenceModelField = propertyData.propertyName;
			var propertyAttributes = propertyData.attributes;

			//Dave: Occhio, ho rimosso "updateDisplay" c'è il potenziale per rompere tutto!
			if (propertyAttributes.indexOf("dynamic") > -1) {
				Shark.trace("%c" + referenceModelField + " è dinamico!!!", "background-color: red; color: white; padding: 5px;", Shark.TRACE_LOG);

				var referenceModel = Shark.getReferenceModel(referenceModelPath);

				if (referenceModel) {
					$(referenceModel).on(referenceModelField + "Changed", function (event) {
						if (Shark.activePage) {
							Shark.activePage.updateDisplay(referenceModelPath, event);
						}
					});
				}
			}

			if (referenceEvent !== undefined && referenceEvent !== null) {
				switch (referenceEvent.substr(0, referenceEvent.indexOf("."))) {
					case "@child":
						$eventTargets = $child.children();
						eventTypeList = referenceEvent.substr(referenceEvent.indexOf(".") + 1);
						break;
					default:
						eventTypeList = referenceEvent;
						break;
				}
			}

			//Dave.Warn: Qui è tutto da capire come deve essere gestito il giro per evitare assegnazioni multiple..
			// e sarebbe da usare il dataset

			//DAve: Comunque questa cosa non ha senso se non sto usando VDOM tanto ad ogni passaggio sono tutti nuovi.
			let newGUID = Shark.__generateGUID("SHG");
			if (!$eventTargets.data("shGUID")) {
				$eventTargets.data("shGUID", newGUID);
			}

			if (!$eventTargets[0].dataset.shGuid || $eventTargets[0].dataset.shGuid === "") {
				$eventTargets[0].dataset.shGuid = newGUID;
			}

			//Dave: questo è un grosso paaso avanti nella gestione dei listener, ma va fatta una grossa pulizia.
			const viewBindName = "document__" + eventTypeList + "__" + childItem.dataset.shProp;
			
			if (!Shark.__listeners.viewToModelBinds2[viewBindName]) {
				const eventHandlerFunction = function (event) {
					//Dave: Resta ancora da trovare un modo "automatico" di gestire i sub-models
					// console.count(childItem.dataset.shProp + " > " + eventTypeList);
					// console.count("childItem.id > " + childItem.id);
					// $eventTargets.on(eventTypeList, function (event) {

					var $eventTarget = $(event.target);
					const eventTargetNode = $eventTarget[0];
	
					const parentNodeWithModel = $eventTarget.closest("[data-sh-model]");
					const referenceModelPath = parentNodeWithModel.data(referenceModelAttributeName);
					const referenceModel = Shark.getReferenceModel(referenceModelPath);
	
					if (referenceModel) {
						//var newValue = $eventTarget.data("sh-model-value") !== undefined ? $eventTarget.data("sh-model-value") : $eventTarget.val(); //  <li class="list-group-item" data-sh-value="56987">Cras justo odio</li>
						var $modelValueContainer = $eventTarget.closest("[data-sh-value]") || $eventTarget.closest("[data-sh-model-value]");
						var newValue;
						var propertyForValue = $eventTarget.data("sh-property-for-value") || $eventTarget.data("sh-value-from-property");//Da deprecare
						
						//Dave.Warn: Qui è tutto da risistemare.
						if (propertyForValue !== undefined) {
							switch ($eventTarget[0].nodeName.toLowerCase()) {
								case "input":
									switch ($eventTarget[0].type.toLowerCase()) {
										case "checkbox":
										case "radio":
											newValue = $eventTarget.prop(propertyForValue);
											break;
										default:
											newValue = $modelValueContainer.data("sh-value") !== undefined ? $modelValueContainer.data("sh-value") : ($modelValueContainer.data("sh-model-value") !== undefined ? $modelValueContainer.data("sh-model-value") : $eventTarget.val());
											break;
									}
									break;
								default:
									newValue = $modelValueContainer.data("sh-value") !== undefined ? $modelValueContainer.data("sh-value") : ($modelValueContainer.data("sh-model-value") !== undefined ? $modelValueContainer.data("sh-model-value") : $eventTarget.val());
									break;
							}
						}
						else {
							newValue = $modelValueContainer.data("sh-value") !== undefined ? $modelValueContainer.data("sh-value") : ($modelValueContainer.data("sh-model-value") !== undefined ? $modelValueContainer.data("sh-model-value") : $eventTarget.val());
						}
	
						if ($eventTarget[0].nodeName.toLowerCase() === "input") {
							if ($eventTarget[0].type.toLowerCase() === "number") {
								newValue = parseFloat(newValue);
							}
						}
	
						referenceModel.set(referenceModelField, newValue, eventTargetNode);
						Shark.trace("Aggiorno il valore", referenceModelPath, referenceModelField, referenceModel, referenceModel[referenceModelField], "log");
					}
				};

				//Dave: questa variante fa saltare la gestione degli @child...
				$(document).on(eventTypeList, `[data-sh-prop="${childItem.dataset.shProp}"]`, eventHandlerFunction);
				
				Shark.__listeners.viewToModelBinds2[viewBindName] = eventHandlerFunction;
				Shark.activePage.__viewBinds2[viewBindName] = {handler: eventHandlerFunction, target: "DOM", type: "autoListener"};
			}

			Shark.activePage.__viewBinds[$child.data("shGUID")] = {handler: "currHandler", target: "DOM", type: "autoListener"};
			Shark.__listeners.viewToModelBinds[$child.data("shGUID")] = Shark.activePage.__viewBinds[$child.data("shGUID")];

			//Dave.Warn: Qui è tutto da capire come deve essere gestito il giro per evitare assegnazioni multiple..
			if (!Shark.__listeners.viewToModelBinds[referenceModelPath]) {
				Shark.__listeners.viewToModelBinds[referenceModelPath] = {};
			}
			if (!Shark.activePage.__viewBinds[referenceModelPath]) {
				Shark.activePage.__viewBinds[referenceModelPath] = {};
			}

			Shark.activePage.__viewBinds[referenceModelPath][referenceModelField] = {eventTypeList: eventTypeList, target: referenceModelField, type: "autoListener"};
			Shark.__listeners.viewToModelBinds[referenceModelPath][referenceModelField] = Shark.activePage.__viewBinds[referenceModelPath][referenceModelField];
		});
	});
};

Shark.__evaluateDynamicBindings = function () {
	// __boundProperties
}

Shark.__evaluateReferenceValues = function (options) {
	var defaultSettings = {
		type: "render",
		data: {
			containerSelector: "body"
		}
	};
	var settings = $.extend(true, defaultSettings, options);

	switch (settings.type) {
		case "render":
			$(settings.data.containerSelector).find("[data-sh-prop]").each(function (index, item) {
				var $item = $(item);
				// var propName = $item.data("sh-prop");
				var propName = item.dataset.shProp; //Dave.Warn: Dovunque passo al dataset devo camelare!!!
				// var referenceModelPath = $item.closest("[data-sh-model]").data("sh-model");
				//Dave: Occhio che per usare il dataset devo essere certo che l'oggetto esista.
				const referenceModelNode = item.closest("[data-sh-model]")
				var referenceModelPath = referenceModelNode ? referenceModelNode.dataset.shModel : ""; //Dave.Warn: Dovunque passo al dataset devo camelare!!!

				//Dave.Info: Could happend that there is some child with sh-prop given but without sh-model as a parent. We check that the refereceModelPath is found
				if (referenceModelPath) {
					//Dave.Warn: Occhio che questo è replicato, dovrebbe diventare una funzione
//					var squarePos = referenceModelPath.indexOf("[");
//					var modelArrayIndex = -1;
//
//					if (squarePos > -1) {
//						modelArrayIndex = referenceModelPath.substring(squarePos + 1, referenceModelPath.indexOf("]"));
//						referenceModelPath = referenceModelPath.substr(0, squarePos);
//					}
//
//					//var referenceModel = Shark.getReferenceModel(referenceModelPath);
//					var referenceModel;
//					if (modelArrayIndex === -1) {
//						referenceModel = Shark.getReferenceModel(referenceModelPath);
//					}
//					else {
//						referenceModel = Shark.getReferenceModel(referenceModelPath)[modelArrayIndex];
//					}
					
					//if (referenceModel.substr(0, 12) === "@renderItems") {
					var referenceModel = Shark.getReferenceModel(referenceModelPath); //Dave: Questa cosa non deve essere rifatta per ogni prop.. non possiamo ottimizzare prima raccogliendo?
					var referenceValue = Utils.getDeepProperty(referenceModel, propName);
					// var propertyForValue = $item.data("sh-property-for-value") || $item.data("sh-value-from-property"); //"sh-value-from-property" Andrà deprecato
					var propertyForValue = item.dataset.shPropertyForValue || item.dataset.shValueFromProperty; //"sh-value-from-property" Andrà deprecato //Dave.Warn: Dovunque passo al dataset devo camelare!!!

					//Dave.Warn: Qui è tutto da risistemare.
					if (propertyForValue !== undefined) {
						// switch ($item[0].nodeName.toLowerCase()) {
						switch (item.nodeName.toLowerCase()) {
							case "input":
								// switch ($item[0].type.toLowerCase()) {
								switch (item.type.toLowerCase()) {
									case "checkbox":
									case "radio":
										$item.prop(propertyForValue, referenceValue);
										break;
									default:
										$item.val(referenceValue);
										break;
								}
								break;
							default:
								$item.val(referenceValue);
								break;
						}
					}
					else {
						//Dave.Warn Dave.ToDo: Occhio che qui ci sono da gestire un sacco di casi, mi sa...
						// switch ($item[0].nodeName.toLowerCase()) {
						switch (item.nodeName.toLowerCase()) {
							case "input":
								// switch ($item[0].type.toLowerCase()) {
								switch (item.type.toLowerCase()) {
									case "checkbox":
									case "radio":
										// var referenceActiveValue = $item.closest("[data-sh-active-value]").data("sh-active-value");
										const referenceValueParent = item.closest("[data-sh-active-value]");
										const referenceActiveValue = referenceValueParent ? referenceValueParent.dataset.shActiveValue : undefined;
										const itemValue = $item.val();

										$item.prop("checked", ((itemValue == referenceValue ||
											(typeof referenceValue !== "undefined" && (referenceValue !== null && typeof referenceValue.toString === "function" && itemValue.toString() === referenceValue.toString()))) ||
											// (typeof referenceValue !== "undefined" && referenceValue !== null && typeof referenceValue.splice === "function" && referenceValue.indexOf($item.val()) > -1) ||
											(typeof referenceValue !== "undefined" && referenceValue !== null && Array.isArray(referenceValue) && (referenceValue.indexOf(itemValue) > -1 || referenceValue.indexOf(parseFloat(itemValue)) > -1)) ||
											(typeof referenceValue !== "undefined" && referenceValue !== null && referenceValue == referenceActiveValue)
										// $item.prop("checked", (($item.val() == referenceValue ||
										// 	(typeof referenceValue !== "undefined" && $item.val().toString() === referenceValue.toString())) ||
										// 	(typeof referenceValue !== "undefined" && typeof referenceValue.splice === "function" && referenceValue.indexOf($item.val()) > -1) ||
										// 	referenceValue == referenceActiveValue
										));
										break;
									default:
										$item.val(referenceValue);
										break;
								}
								break;
							default:
								$item.val(referenceValue);
								break;
						}
					}
				}
			});
			break;
		case "renderItem":
			console.error("You shouldn't be here!!");
			// $(settings.data.containerSelector).find("[data-sh-reference-value]").each(function (index, item) {
			// 	var $item = $(item);
			// 	var referenceModel = $item.data("sh-reference-value");

			// 	if (referenceModel.substr(0, 12) === "@renderItems") {
			// 		var referenceValue = referenceModel.substr(referenceModel.indexOf(".") + 1);

			// 		$item.val(options.data.renderItems[referenceValue]);
			// 	}
			// });
			break;
	}
};

Shark.__extractPropertyAttributes = function (propertyName) {
	if (propertyName) {
		var propertyAttributes = [];
		var attributesStart = propertyName.indexOf("@");
		var trimmedPropertyName = propertyName;

		if (attributesStart > -1) {
			trimmedPropertyName = propertyName.substr(0, attributesStart);
			propertyAttributes = propertyName.substr(attributesStart + 1).split("@");
		}

		return {propertyName: trimmedPropertyName, attributes: propertyAttributes};
	}
	
	return {propertyName: "", attributes: ""};
};

Shark.__extractTemplateMap = function (source, prefix) {
	"use strict";

	prefix = prefix + "." || "";
	var strings = [];

	for (var currProp in source) {
		if (typeof source[currProp].length === "undefined") {
			strings = strings.concat(Shark.__extractTemplateMap(source[currProp], currProp));
		}
		else {
			strings.push(prefix + currProp);
		}
	}
	
	if (prefix === "undefined.") {
		strings.forEach(function (item) {
			Shark.__templateMap[item] = Shark.get(item);
		});
	}
	
	return strings;
}

Shark.__generateGUID = function (prefix, suffix) {
	"use strict";
	prefix = prefix || "";
	suffix = 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;
};

Shark.generateUUID = () => {
	return uuidv4();
}

Shark.__shouldTrace = function (level) {
	"use strict";

	var shouldTrace = false;

	if (Shark.traceLevel === Shark.TRACE_NONE) {
		return false;
	}

	if (Shark.traceLevel === level) {
		return true;
	}

	if (Shark.traceLevel === Shark.TRACE_LOG) {
		return true;
	}

	if (Shark.traceLevel === Shark.TRACE_INFO && [Shark.TRACE_ERROR, Shark.TRACE_INFO, Shark.TRACE_WARN].indexOf(level) > -1) {
		return true;
	}

	if (Shark.traceLevel === Shark.TRACE_WARN && [Shark.TRACE_ERROR, Shark.TRACE_WARN].indexOf(level) > -1) {
		return true;
	}

	if (Shark.traceLevel === Shark.TRACE_ERROR && [Shark.TRACE_ERROR].indexOf(level) > -1) {
		return true;
	}

	return shouldTrace;
};

//Dave.ToDo: Should be deprecated, is not Shark that should manage labels and i18n
// Shark.addLabels = function (langFile, container, successCallBack, failCallBack) {
// 	"use strict";

// 	$.getJSON(Shark.languagesPath + langFile).done(function (data) {
// 		console.info(data);

// 		$.extend(true, Shark.labels[container], data);
		
// 		if (successCallBack) {
// 			successCallBack(data, langFile, container);
// 		}
// 	})
// 	.fail(function(jqxhr, textStatus, error) {
// 		if (failCallBack) {
// 			failCallBack(langFile, container, jqxhr, textStatus, error);
// 		}
// 	});
// };


//Dave.ToDo: I think this should be deprecated.
/**
 * Adds a model processors function. The model processors are methods identified by a name that must contain the model full path (eg. "Shark.__store.currentUser") and the name of a property of that model (eg. "userName") joined by an underscore.
 * <br />This method will be automatically called whenever an input on the page, with the right "sh" data tags, triggers it's "change" or "input" event.
 * <br />The default behaviour is that the content of the field became the value of the property, but if a model processor function exists, it's invoked and then you can change the default behaviour.
 *
 * @param {string} methodName - The name of the new method. It must contain the model full path, an underscore and the name of a property of that model (eg. "Shark.__store.currentUser_userName").
 * @param {function} methodBody - The function that will be executed when the method will be called.
 * @param {object} [options] - An object containing the options. (not used)
 */
Shark.addModelProcessor = function (methodName, methodBody, options) {
	Shark.__modelProcessors[methodName] = methodBody;
};

Shark.bindViewToModel = function (containerCSSSelector) {
	Shark.__bindViewToModel(containerCSSSelector);
};

Shark.createComponent = function (name) {
	var newJaw = new Jaw(name);
	
	newJaw.className = name;

	return newJaw;
};

Shark.createJaw = function (jawName, jawDefinition) {
	var newJaw = new Jaw(jawName, jawDefinition);// Object.create(Jaw);
	//newJaw.className = jawName;

	return newJaw;
};

Shark.registerModel = function (modelName, modelClass) {
	if (!Shark.__models[modelName]) {
		// modelClass.prototype = new SmartModel();
		modelClass.prototype.__modelName = modelName;
		Shark.__models[modelName] = modelClass;

		return Shark.__models[modelName];
	}
	else {
		throw (new Error("Model alredy declared"));
	}
};

Shark.createModel = function (modelName, modelData) {
	if (!Shark.models[modelName]) {
		Shark.trace(`%cThe "createModel" Shark method is deprecated, you should create your models creating a class that extends the SmartModel class.`, "font-size: 16px; color: red;", Shark.TRACE_ERROR);
		//new Function("return function (call) { return function " + modelName + " () { return call(this, arguments) }; };")())(Function.apply.bind(fn)
		//Shark.models[modelName] = new Function("return function (call) { return function " + modelName + " () { return call(this, arguments) }; };")();
		
		//Dave.Warn: Tutta la roba qui sotto la sto mettendo anche nel getModelInstance per poter gestire anche gli oggetti "generici" ma furbi come ogni Model di Shark. Si risolverà quando il baseObject sarà una realtà.
		Shark.models[modelName] = function (data, options) {
			this.__modelStructure = modelData;

			//for (var prop in this.__modelStructure) {
				//this[prop] = this.__modelStructure[prop];
				Utils.matchProperties(this, this.__modelStructure, {createMissingProperties: true, matchEmptyParameters: true});
			//}

			if (data) {
				var parseOptions = $.extend(true, {createMissingProperties: true, matchEmptyParameters: true}, options);

				this.parse(data, parseOptions);
			}
		};

		Shark.models[modelName].prototype.__isDirty = false;
		Shark.models[modelName].prototype.__dirtyProperties = [];
		Shark.models[modelName].prototype.__dirtyProperties2 = [];
		Shark.models[modelName].prototype.__modelName = modelName;

		Shark.models[modelName].prototype.__clean = function (properties) {
			"use strict";

			if (!properties) {
				this.__dirtyProperties = [];
				this.__dirtyProperties2 = [];
			}
			else {
				this.__dirtyProperties = this.__dirtyProperties.filter(function (item) {
					return properties.indexOf(item) === -1;
				});
				//		this.__dirtyProperties2 = this.__dirtyProperties.filter(function (item) {
				//			return properties.indexOf(item) === -1;
				//		});
			}

			this.__isDirty = this.__dirtyProperties.length > 0;

			return this.__dirtyProperties;
		};

		//DAVE TO DO Dovrei stringifiare le cose che lo necessitano
		Shark.models[modelName].prototype.__collectData = function (options) {
			var collectedData = {};

			for (var prop in this) {
				if (typeof this[prop] !== "function" && prop.substr(0, 2) !== "__" && prop.substr(0, 6) !== "jQuery") {
					collectedData[prop] = this[prop];
				}
			}

			return collectedData;
		};

		Shark.models[modelName].prototype.on = function (eventName, eventHandler) {
			return $(this).on(eventName, eventHandler);
		};

		Shark.models[modelName].prototype.clean = function (properties) {
			return this.__clean(properties);
		};

		Shark.models[modelName].prototype.emit = function (eventName, eventData) {
			"use strict";

			return $(this).trigger(eventName, eventData);
		};

		Shark.models[modelName].prototype.__parse = function (data, options) {
			Utils.matchProperties(this, data, options);
		};

		Shark.models[modelName].prototype.__restore = function (properties) {

			if (!properties) {
				this.__dirtyProperties2.forEach(function (item) {
					//Dave.Warn: Valutare se usare il set (che triggera tutto a cascata) o invece impostare i valori e basta.
					this.set(item.property, item.originalValue);
				}, this);

				this.__dirtyProperties2 = [];
				this.__dirtyProperties = [];
				this.__isDirty = false;
				//				this.__clean();
			}
		};

		Shark.models[modelName].prototype.__toJSON = function () {
			var tmp = {};

			for (var key in this) {
				if (typeof this[key] !== 'function') {
					tmp[key] = this[key];
				}
			}

			return JSON.stringify(tmp);
		};

		Shark.models[modelName].prototype.collectData = function (options) {
			return this.__collectData(options);
		};

		Shark.models[modelName].prototype.parse = function (data, options) {
			this.__parse(data, options);
		};

		Shark.models[modelName].prototype.restore = function (properties) {
			this.__restore(properties);
		};

		Shark.models[modelName].prototype.set = function (name, value) {
			let actualValue = Utils.getDeepProperty(this, name);

			switch (typeof actualValue) {
				case "number":
					Shark.trace(`%cChiamato il metodo set e la propietà è di tipo ${typeof actualValue}`, "font-size: 16px; color: red;", Shark.TRACE_LOG);
					value = parseFloat(value);
					break;
				case "object":
					if (Array.isArray(actualValue)) {
						actualValue = Array.from(actualValue);

						let pos = actualValue.indexOf(value);

						if (pos > -1) {
							actualValue.splice(pos, 1);
						}
						else {
							actualValue.push(value);
						}
						value = actualValue;
						//Utils.setDeepProperty(this, `__${name}`, actualValue.join(","));
						Shark.trace(`%cChiamato il metodo set e la propietà è di tipo ${typeof actualValue} ed è un array`, "font-size: 16px; color: red;", Shark.TRACE_LOG);
					}
					else if (Shark.activeFeatures.moment && actualValue !== null && actualValue !== undefined && (actualValue._isAMomentObject || actualValue instanceof moment)) {
						Shark.trace(`%cChiamato il metodo set e la propietà è di tipo ${typeof actualValue} ed è un moment`, "font-size: 16px; color: red;", Shark.TRACE_LOG);
						value = moment(value);
					}
					else {
						Shark.trace(`%cChiamato il metodo set e la propietà è di tipo ${typeof actualValue}`, "font-size: 16px; color: red;", Shark.TRACE_LOG);
					}
					break;
				case "string":
					Shark.trace(`%cChiamato il metodo set e la propietà è di tipo ${typeof actualValue}`, "font-size: 16px; color: red;", Shark.TRACE_LOG);
					if (typeof value.toString === "function") {
						value = value.toString();
					}
					break;
			}

			var eventData = { modelName: modelName, newValue: value, oldValue: Utils.getDeepProperty(this, name), property: name };
			Utils.setDeepProperty(this, name, value);

			if (eventData.oldValue !== eventData.newValue) {
				if (this.__dirtyProperties.indexOf(name) === -1) {
					this.__dirtyProperties2.push({ property: name, originalValue: eventData.oldValue });
					this.__dirtyProperties.push(name);
				}
				this.__isDirty = true;

				//Dave.Warn: using jQuery.trigger() does not require this check because jQuery itself have same behavior when emitting the event
				// if (typeof this.onDataChanged === "function") {
					// this.onDataChanged.call(this, eventData);
				// }
				this.emit("dataChanged", eventData);
				this.emit(name + "Changed", eventData);
			}
		};

		//Dave.Warn: Ma questa cosa qui sarà all'origine del problema dei valori tipo array che si smontano nel prototype?
		if (modelData) {
			for (var prop in modelData) {
				Shark.models[modelName].prototype[prop] = modelData[prop];
			}
		}

		return Shark.models[modelName];
	}
	else {
		throw(new Error("Model alredy declared"));
	}

	return true;//new Shark.models[modelName](modelData);
};

/**
 * Retrieve values from the main store. It supports object deconstruction
 * 
 * @param {string|array} itemName - 
 * @param {string} forcedType - Deprecated
 */
Shark.get = function (itemName, forcedType) {
	//Dave.ToDo: Qui devo gestire la restituzione di pezzi di DOM (per ora con jQuery) ma, probabilmente, anche di Models, template, ecc...
	//Dave: Ma non sono proprio sicuro di quanto scritto sopra!
	// No no... meglio avere un metodo dedicato per le altre cose, get lavora solo sullo store.

	if (itemName === undefined) {
		// Destructuring!!
		return Shark.__store;
	}
	else if (Array.isArray(itemName)) {
		const returnValues = [];

		itemName.forEach(item => {
			const returnValue = Shark.__store[item];
			returnValues.push(returnValue);
		});

		return returnValues;
	}
	else {
		//Dave: 12/06/2021 Ho invertito la priorità: se esiste la proprietà nello store restituisco quella prima di cercare con jQuery.. tanto jQuery verrà deprecato, Shark.get NON deve restituire DOM
		returnValue = Shark.__store[itemName];
		
		// Dave.Warn: questo potrebbe rompere cose: se il valore è "null" lo devo restituire, perché posso impostare un null... undefined magari ok... ma tanto quando Shark.get leggerà solo dallo store il problema non ci sarà più
		// if (returnValue !== null && returnValue !== undefined) {
		if (returnValue !== undefined) {
			return returnValue;
		}
		
		var returnValue = $(itemName);

		if (returnValue.length > 0) {
			return returnValue;
		}
	
		// returnValue = Shark.getTemplate(itemName);
	
		// if (returnValue !== "") {
		// 	return returnValue;
		// }
	}

	// Dave: Questo dovrebbe essere undefined...
	return null;
};

/**
 * Return an instance of the requested model, filled with the passed data if present.
 *
 * @return {object}
 * @static
 * @version 1.1.0.10 - 2017.07.28
 *
 * @param {string} modelNameOrClass - The name of the model to instantiate. It must exist: prior of this call you must have called the [createModel method]{@link Shark#createModel} withe the same model name and a model definition.
 * @param {object} [modelData = undefined] - An object containing the data to use to fill the new instance. (it just call the instance.parse method)
 */
Shark.getInstance = function (modelNameOrClass, modelData) {
	let newInstance;

	if (typeof modelNameOrClass === "string") {
		if (Shark.__models[modelNameOrClass]) {
			newInstance = new Shark.__models[modelNameOrClass](modelData);
			newInstance.__dirtyProperties = [];
			//newInstance.__shguid = Utils.generateGUID();

			return newInstance;
		}
		else if (modelNameOrClass === "__smart__") {
			newInstance = new SmartModel();

			if (modelData) {
				if (modelData.hasOwnProperty("__dirtyProperties")) {
					Shark.trace("Your data model have a '__dirtyProperties' property that will be removed.", Shark.TRACE_WARN);
				}
				delete modelData.__dirtyProperties;
				
				if (modelData.hasOwnProperty("__export")) {
					Shark.trace("Your data model have a '__export' property that will be removed.", Shark.TRACE_WARN);
				}
				delete modelData.__export;

				if (modelData.hasOwnProperty("__import")) {
					Shark.trace("Your data model have a '__import' property that will be removed.", Shark.TRACE_WARN);
				}
				delete modelData.__import;

				if (modelData.hasOwnProperty("__isDirty")) {
					Shark.trace("Your data model have a '__isDirty' property that will be removed.", Shark.TRACE_WARN);
				}
				delete modelData.__isDirty;

				if (modelData.hasOwnProperty("__shguid")) {
					Shark.trace("Your data model have a '__shguid' property that will be removed.", Shark.TRACE_WARN);
				}
				delete modelData.__shguid;
				
				if (modelData.hasOwnProperty("__toJSON")) {
					Shark.trace("Your data model have a '__toJSON' property that will be removed.", Shark.TRACE_WARN);
				}
				delete modelData.__toJSON;
				
				newInstance.import(modelData, {createMissingProperties: true, matchEmptyParameters: true});
			}
			return newInstance;
		}
		else if (Shark.models[modelNameOrClass]) {//Deprecato!!!!
			newInstance = new Shark.models[modelNameOrClass](modelData);

			//Dave.Info: To avoid sharing of prototype property between model instances, each <i>non-basic</i> type property is replicated in the instance with the default value.
			//		for (var prop in newInstance) {
			//			if (!newInstance.hasOwnProperty(prop) && newInstance[prop] !== null) {
			//				if (typeof newInstance[prop].splice === "function") {
			//					newInstance[prop] = Shark.models[modelName].prototype[prop].map(function (item) {return item});
			//				}
			//				else if (typeof newInstance[prop] === "object") {
			//					newInstance[prop] = $.extend(true, {}, Shark.models[modelName].prototype[prop]);
			//				}
			//			}
			//		}
			newInstance.__dirtyProperties = [];
			newInstance.__dirtyProperties2 = [];
			//newInstance.__shguid = Utils.generateGUID();

			return newInstance;
		}
	}
	else if (modelNameOrClass === null) {
		// .warn("YOU SHOULD NOT USE 'null' AS Model Name, use '__smart__' instead to have a generic SmartModel. This feature is deprecated and will be removed");
		//Dave.ToDo: Siamo sulla buona strada, ma devo gestire anche le dirty properties.
		newInstance = Object.create({});

		newInstance.__shguid = Shark.generateUUID(); //Utils.generateGUID();
		newInstance.__isDirty = false;
		newInstance.__dirtyProperties = [];
		newInstance.__dirtyProperties2 = [];

		newInstance.__collectData = function () {
			var collectedData = {};

			for (var prop in this) {
				if (typeof this[prop] !== "function" && prop.substr(0, 2) !== "__" && prop.substr(0, 6) !== "jQuery") {
					collectedData[prop] = this[prop];
				}
			}

			return collectedData;
		};

		newInstance.__parse = function (data, options) {
			Utils.matchProperties(this, data, options);
		};

		newInstance.__toJSON = function() {
			var tmp = {};

			for (var key in this) {
				if (typeof this[key] !== 'function') {
					tmp[key] = this[key];
				}
			}

			return JSON.stringify(tmp);
		};

		newInstance.collectData = function (options) {
			return this.__collectData(options);
		};

		newInstance.on = function (eventName, eventHandler) {
			return $(this).on(eventName, eventHandler);
		};

		newInstance.parse = function (data, options) {
			this.__parse(data, options);
		};

		newInstance.set = function (name, value) {
			let actualValue = Utils.getDeepProperty(this, name);

			switch (typeof actualValue) {
				case "number":
					Shark.trace(`%cChiamato il metodo set e la propietà è di tipo ${typeof actualValue}`, "font-size: 16px; color: red;", Shark.TRACE_LOG);
					value = parseFloat(value);
					break;
				case "object":
					if (Array.isArray(actualValue)) {
						actualValue = Array.from(actualValue);

						let pos = actualValue.indexOf(value);

						if (pos > -1) {
							actualValue.splice(pos, 1);
						}
						else {
							actualValue.push(value);
						}
						value = actualValue;
						//Utils.setDeepProperty(this, `__${name}`, actualValue.join(","));
						Shark.trace(`%cChiamato il metodo set e la propietà è di tipo ${typeof actualValue} ed è un array`, "font-size: 16px; color: red;", Shark.TRACE_LOG);
					}
					else if (Shark.activeFeatures.moment && actualValue !== null && actualValue !== undefined && (actualValue._isAMomentObject || actualValue instanceof moment)) {
						Shark.trace(`%cChiamato il metodo set e la propietà è di tipo ${typeof actualValue} ed è un moment`, "font-size: 16px; color: red;", Shark.TRACE_LOG);
						value = moment(value);
					}
					else {
						Shark.trace(`%cChiamato il metodo set e la propietà è di tipo ${typeof actualValue}`, "font-size: 16px; color: red;", Shark.TRACE_LOG);
					}
					break;
				case "string":
					Shark.trace(`%cChiamato il metodo set e la propietà è di tipo ${typeof actualValue}`, "font-size: 16px; color: red;", Shark.TRACE_LOG);
					if (typeof value.toString === "function") {
						value = value.toString();
					}
					break;
			}

			var eventData = { modelName: modelNameOrClass, newValue: value, oldValue: Utils.getDeepProperty(this, name), property: name };
			Utils.setDeepProperty(this, name, value);

			if (eventData.oldValue !== eventData.newValue) {
				if (this.__dirtyProperties.indexOf(name) === -1) {
					this.__dirtyProperties2.push({ property: name, originalValue: eventData.oldValue });
					this.__dirtyProperties.push(name);
				}
				this.__isDirty = true;

				if (typeof this.onDataChanged === "function") {
					this.onDataChanged.call(this, eventData);
				}
				$(this).trigger("dataChanged", eventData);
				$(this).trigger(name + "Changed", eventData);
			}
		};

		newInstance.toJSON = function () {
			return this.__toJSON();
		};

		if (modelData) {
			if (modelData.hasOwnProperty("__collectData")) {
				Shark.trace("Your data model have a '__collectData' property that will be removed.", Shark.TRACE_WARN);
			}
			delete modelData.__collectData;
			
			if (modelData.hasOwnProperty("__dirtyProperties")) {
				Shark.trace("Your data model have a '__dirtyProperties' property that will be removed.", Shark.TRACE_WARN);
			}
			delete modelData.__dirtyProperties;
			
			if (modelData.hasOwnProperty("__isDirty")) {
				Shark.trace("Your data model have a '__isDirty' property that will be removed.", Shark.TRACE_WARN);
			}
			delete modelData.__isDirty;
			
			if (modelData.hasOwnProperty("__parse")) {
				Shark.trace("Your data model have a '__parse' property that will be removed.", Shark.TRACE_WARN);
			}
			delete modelData.__parse;

			if (modelData.hasOwnProperty("__shguid")) {
				Shark.trace("Your data model have a '__shguid' property that will be removed.", Shark.TRACE_WARN);
			}
			delete modelData.__shguid;
			
			if (modelData.hasOwnProperty("__toJSON")) {
				Shark.trace("Your data model have a '__toJSON' property that will be removed.", Shark.TRACE_WARN);
			}
			delete modelData.__toJSON;
			
			newInstance.parse(modelData, {createMissingProperties: true, matchEmptyParameters: true});
		}

		return newInstance;
	}
	else if (modelNameOrClass instanceof Object && modelNameOrClass.prototype instanceof SmartModel) {
		newInstance = new modelNameOrClass(modelData);
		return newInstance;
	}
	else {
		throw(new Error("Missing Model '" + modelNameOrClass + "'. Please specify '__smart__' or 'null' as model name if you want to create a generic SmartModel."));
	}
};

Shark.getInstanceList = (modelNameOrClass, dataList) => {
	return dataList.map(item => Shark.getInstance(modelNameOrClass, item));
};

//Dave.ToDo: Should be deprecated, is not Shark that should manage labels and i18n
// Shark.getLabel = function (labelCode) {
// 	if (Shark.labels.labels[labelCode]) {
// 		return Shark.labels.labels[labelCode];
// 	}
// 	else {
// 		return "";
// 	}
// };

// /**
//  * Return an instance of the requested model, filled with the passed data if present.
//  *
//  * @return {object}
//  * @static
//  * @version 1.1.0.10 - 2017.07.28
//  *
//  * @param {string} modelName - The name of the model to instantiate. It must exist: prior of this call you must have called the [createModel method]{@link Shark#createModel} withe the same model name and a model definition.
//  * @param {object} [modelData = undefined] - An object containing the data to use to fill the new instance. (it just call the instance.parse method)
//  */
// Shark.getModelInstance = function (modelName, modelData) {
// 	return Shark.getInstance(modelName, modelData);
// };

Shark.getReferenceModel = function (path) {
	if (Utils.getDeepProperty(Shark.__store, path)) {
		referenceModel = Utils.getDeepProperty(Shark.__store, path);
	}
	else {
		var pathParts = path.split(".");
		var referenceModel = window;

		for (var i = 0; i < pathParts.length; i++) {
			if (!referenceModel[pathParts[i]]) {
				break;
			}
			referenceModel = referenceModel[pathParts[i]];
		}
	}

	if (referenceModel !== window) {
		return referenceModel;
	}
	else {
		return null;
	}
};

// Serve per i sub template!!
Shark.getTemplate = function (route) {
	if (Shark.__templateMap && Shark.__templateMap[route] !== undefined) {
		return Shark.__templateMap[route];
	}

	var routeStep = route.split(".");
	var currTemplateObject = Shark.templates;

	Shark.trace("Shark.getTemplate: ", routeStep);

	for (var i = 0; i < routeStep.length; i++) {
		var currStep = routeStep[i];

		currTemplateObject = currTemplateObject[currStep];
		
		if (!currTemplateObject) {
			return "";
		}
	}

	return currTemplateObject;
};

Shark.history.append = function (currJaw) {
	"use strict";
	
	if (Shark.history.currentPos === Shark.history.content.length - 1) {
		Shark.history.currentPos = Shark.history.content.length;
		Shark.history.content.push(currJaw);
	}
	else {
		Shark.history.content = Shark.history.content.slice(0, Shark.history.currentPos + 1);
		Shark.history.currentPos = Shark.history.content.length;
		Shark.history.content.push(currJaw);
	}
};

Shark.history.goNext = function () {
	"use strict";
	
	if (Shark.history.currentPos < Shark.history.content.length + 1) {
		Shark.history.currentPos++;
		Shark.openPage(Shark.history.content[Shark.history.currentPos], {saveInHistory: false});
	}
	else {
		Shark.trace("No next page in history", Shark.TRACE_WARN);
	}
};

Shark.history.goPrevious = function () {
	"use strict";
	
	if (Shark.history.currentPos > 0) {
		Shark.history.currentPos--;
		Shark.openPage(Shark.history.content[Shark.history.currentPos], {saveInHistory: false});
	}
	else {
		Shark.trace("No previos page in history", Shark.TRACE_WARN);
	}
};

// /**
//  *
//  *
//  * @param {object} [options] - An object containing the options for initializing.
//  * @param {object} [options.commonEndPointsBase = ""] - DEPRECATED and REMOVED The "base" parte of the URL for the remote request. Use this value only if you have common destination for all your API call.
//  * Otehrwise you can set different URL for each call.
//  * @param {object} [options.labels = []] - An array of objects containing a list of labels to be used in conjunction with localization features.
//  * @param {integer} [options.remoteCallTimeOutDuration = 15000] - The number of millisecods that Shark should wait befor abort remote call.
//  * @param {object} [options.dataManager = DataManager] - DEPRECATED and REMOVED The Class to be used to manage the remote request. Not yet fully implemented.
//  * @param {object} [options.templates = {}] - An object containing html templates as strings. (documentation has to be written about creating templates object)
//  * @param {object} [options.templateRenderer = MustacheRenderer] - The Class responsible for rendering Views on screen. Must be a SharRenderer sub-class. Not yet fully implemented.
//  * @param {boolean} [forceReinit = false] - If true process initialize step even if the initialization phase has alredy been completed.
//  */
// Shark.init = function (options, forceReinit) {
// 	// Dave.Warn: Tutta la gestione dei template a livello Shark deve sparire, i template stanno nelle View!
// 	if (options.templates) {
// 		console.warn("WARNING!!!! You are using 'templates' option that will be removed");
// 	}
// 	if (options.commonEndPointsBase) {
// 		console.warn("WARNING!!!! You are using 'commonEndPointsBase' option that has been removed");
// 	}
// 	if (options.dataManager) {
// 		console.warn("WARNING!!!! You are using 'dataManager' option that has been removed");
// 	}
// 	Shark.trace("Shark.init, Shark.isInitialized: " + Shark.isInitialized + ", forceReinit: " + forceReinit, Shark.TRACE_INFO);

// 	if (!Shark.isInitialized || forceReinit) {
// 		var settings = {
// 			// commonEndPointsBase: "",
// 			// dataManager: DataManager,
// 			enableMainStoreAutoRender: false,
// 			enableJawStoreAutoRender: false,
// 			enableSmartModelSetUpdatesView: false,
// 			enableJawsCompatibilityMode: false,
// 			labels: [],
// 			preventDeepLinking: false,
// 			remoteCallTimeOutDuration: 15000,
// 			routingMethod: "hash",//optional, can be "url"
// 			templates: typeof Templates !== "undefined" ? Templates : {},
// 			templateRenderer: typeof MustacheRenderer !== "undefined" ? new MustacheRenderer() : null,
// 		};

// 		// $.extend(true, settings, options);
// 		Object.assign(settings, options);

// 		Shark.activeFeatures.moment = typeof moment !== "undefined";
// 		Shark.settings = settings;
// 		Shark.labels = settings.labels;

// 		Shark.templates = settings.templates;
// 		Shark.templates.set = function (template) {
// 			if (template.hasOwnProperty("set")) {
// 				Shark.trace("You cant add a template with name 'set' to Shark.templates", Shark.TRACE_ERROR);
// 				delete template.set;
// 			}
// 			 Object.assign(this, template);
// 		};

// 		Shark.templateRenderer = settings.templateRenderer;

// 		Shark.__extractTemplateMap(Shark.templates);

// 		Object.values(Shark.__components).forEach(item => {
// 			Shark.trace("Provo a chiamare appInit sui __components", Shark.TRACE_LOG);
// 			if (typeof item.appInit === "function") {
// 				item.appInit.call(item);
// 			}
// 		});

// 		Object.values(Shark.__controllers).forEach(item => {
// 			Shark.trace("Provo a chiamare appInit sui __controllers", Shark.TRACE_LOG);
// 			if (typeof item.appInit === "function") {
// 				item.appInit.call(item);
// 			}
// 		});

// 		Object.values(Shark.__views).forEach(item => {
// 			Shark.trace("Provo a chiamare appInit sulle __views", Shark.TRACE_LOG);
// 			if (typeof item.appInit === "function") {
// 				item.appInit.call(item);
// 			}
// 		});

// 		Shark.isInitialized = true;
// 	}

// 	if (!Shark.settings.preventDeepLinking) {
// 		var requiredPage = document.location.hash;

// 		if (requiredPage !== "" && !Shark.__pageOpening) {
// 			Shark.requiredPage = requiredPage.substr(1);
// 			Shark.trace("Page required from URL", Shark.requiredPage, Shark.TRACE_INFO);
// 			//Shark.openPage(Shark.requiredPage, {isAutoOpen: true});
// 		}

// 		window.addEventListener("popstate", (event) => {
// 			console.log(`%cSi è verificato l'evento ${event.type}, dovrei gestirlo.`, "border: 1px solid orange; color: violet; padding: 5px;", {event});
// 			Shark.trace("event: popstate, location: " + document.location + ", state: " + JSON.stringify(event.state), event, Shark.TRACE_LOG);

// 			if (event.state && !Shark.__pageOpening) {
// 				Shark.requiredPage = event.state.jawId;
				
// 				//Dave.ToDo: This is the same stuff, should be an external function, leaving here only the "requiredPage" extraction
// 				if (Shark.requiredPage !== Shark.activePage.className) {
// 					Shark.trace("Page requested from URL (routing: url)", Shark.requiredPage, Shark.TRACE_INFO);
// 					Shark.openPage(Shark.requiredPage, {
// 						isAutoOpen: true
// 					});
// 				}
// 			}
// 		});
// 		window.addEventListener("pushState", (event) => {
// 			console.log(`%cSi è verificato l'evento ${event.type}, dovrei gestirlo.`, "border: 1px solid orange; color: violet; padding: 5px;", {event});
// 		});
// 		window.addEventListener("replaceState", (event) => {
// 			console.log(`%cSi è verificato l'evento ${event.type}, dovrei gestirlo.`, "border: 1px solid orange; color: violet; padding: 5px;", {event});
// 		});
		
// 		// if (Shark.settings.routingMethod === "url" && typeof history.pushState === "function") {
// 		// }
// 		// else {
// 		if (Shark.settings.routingMethod === "hash" || typeof history.pushState !== "function") {
// 			window.addEventListener("hashchange", function () {
// 				Shark.trace("event: hashchange, location: " + document.location + ", state: " + JSON.stringify(event.state), event, Shark.TRACE_LOG);

// 				if (document.location.hash.length > 1 && !Shark.__pageOpening) {
// 					var requiredPage = document.location.hash;
// 					Shark.requiredPage = requiredPage.substr(1);
					
// 					//Dave.ToDo: This is the same stuff, should be an external function, leaving here only the "requiredPage" extraction
// 					if (Shark.requiredPage !== Shark.activePage.className) {
// 						Shark.trace("Page requested from URL (routing: hash)", Shark.requiredPage, Shark.TRACE_INFO);
// 						Shark.openPage(Shark.requiredPage, { isAutoOpen: true });
// 					}
// 				}
// 			});
// 		}
// 	}
// };

/**
 * Open the requested View showing it on the screen
 *
 * @param {(string|Jaw)} pageIdOrJaw - The view to register.
 */

Shark.openPage = function (pageIdOrJaw, options) {
	Shark.__pageOpening = true;

	//Dave.ToDo: Qui c'è da gestire bene il "beforeUnload" e di conseguenza la rimozione dei contenuti di currentOpenOptions.
	if (Shark.activePage.id !== "") {
		Shark.activePage.__activeComponents.forEach(function (item) {
			if (typeof item.beforeUnload === "function") {
				item.beforeUnload.call(item, options);
			}
		});

		if (typeof Shark.activePage.beforeUnload === "function") {
			Shark.activePage.beforeUnload.call(Shark.activePage);
		}
		Shark.activePage.smartUnlisten();
		Shark.activePage.currentOpenOptions = null;
	}

	var currJaw;

	var settings = {
		dataType: "",
		delayRender: false,
		renderOptions: {
			forceCleanRender: true,
		},
		saveInHistory: true,
		urlSearchParams: "",
	};

	$.extend(true, settings, options);

	if (typeof pageIdOrJaw === "string") {
		Shark.trace("Shark.openPage is string: " + (typeof pageIdOrJaw === "string") + ", Shark.__views[pageIdOrJaw]: ", pageIdOrJaw, Shark.__views[pageIdOrJaw]);

		if (Shark.__views[pageIdOrJaw]) {
			currJaw = Shark.__views[pageIdOrJaw];
		}
		else if (Shark.settings.routingMethod === "url") {
			//Dave:ToDo: This should be a find.
			for (let currView in Shark.__views) {
				if (Shark.__views[currView].$mainRoute === pageIdOrJaw || Shark.__views[currView].route === pageIdOrJaw) {
					currJaw = Shark.__views[currView];
					break;
				}
			}
		}
		// else if (Shark.pages[pageIdOrJaw]) {
		// 	currJaw = Shark.pages[pageIdOrJaw];
		// }
		// else {
		// 	currJaw = new Jaw(pageIdOrJaw);
		// 	/*var pageData;
			
		// 	if (dataType === "") {
		// 		pageData = pageIdOrJaw;
		// 	}
		// 	else {
		// 		pageData = pageIdOrJaw + "_" + dataType;
		// 	}*/
	
		// 	currJaw.templateRoute = "pages." + currJaw.id;
		// }
	}
	else {
		currJaw = pageIdOrJaw;
	}

	currJaw.currentOpenOptions = settings;
	Shark.activePage = currJaw;

	if (typeof currJaw.init === "function" && !currJaw.isInitialized) {
		currJaw.init.call(currJaw, settings);
		currJaw.isInitialized = true;

		//If there is components, there must be an init method, so this is ok here:
		currJaw._initComponents();
	}

	if (typeof currJaw.beforeAutoOpen === "function" && settings.isAutoOpen) {
		var returnSettings = currJaw.beforeAutoOpen.call(currJaw, settings);

		if (returnSettings) {
			settings = returnSettings;
		}
	}

	//Dave: Qui bisogna usare il metodo Shark.setURLParams!!!!!
	Shark.setURLSearchParams(settings.urlSearchParams, false);

	//Dave.Warn: Sposto queste funzioni dopo l'impostazione dell'URL, perché nel before load devo poter accedere ai parametri dell'url.
	if (typeof currJaw.beforeLoad === "function") {
		currJaw.beforeLoad.call(currJaw, settings);
	}

	currJaw.__activeComponents.forEach(function (item) {
		if (typeof item.beforeLoad === "function") {
			item.beforeLoad.call(item, settings);
		}
	});

	// setting.renderOptions.forceCleanRender = true;

	if (Shark.templateRenderer instanceof VirtualDOMRenderer) {
		Shark.renderAsync(currJaw, settings)
		.then(() => {
			// Questo dovrebbe farlo al render, ma per ora lo fa solo all'openPage. (parlo del cambio di titolo in base a "__htmlTitle")
			let htmlTitle = currJaw.get("__htmlTitle") || Shark.get("__htmlTitle");
			if (typeof htmlTitle === "string" && htmlTitle !== "") {
				document.title = htmlTitle;
			}
		
			if (settings.saveInHistory) {
				Shark.history.append(currJaw);
			}
		
			currJaw.currentOpenOptions.renderOptions.forceCleanRender = false;
			Shark.__pageOpening = false;

			if (typeof currJaw.afterLoad === "function") {
				currJaw.afterLoad.call(currJaw, settings);
			}
	
			currJaw.__activeComponents.forEach(function (item) {
				if (typeof item.afterLoad === "function") {
					item.afterLoad.call(item, settings);
				}
			});
		});
	}
	else {
		Shark.render(currJaw, settings);

		// Questo dovrebbe farlo al render, ma per ora lo fa solo all'openPage. (parlo del cambio di titolo in base a "__htmlTitle")
		let htmlTitle = currJaw.get("__htmlTitle") || Shark.get("__htmlTitle");
		if (typeof htmlTitle === "string" && htmlTitle !== "") {
			document.title = htmlTitle;
		}
	
		if (settings.saveInHistory) {
			Shark.history.append(currJaw);
		}
	
		currJaw.currentOpenOptions.renderOptions.forceCleanRender = false;
		Shark.__pageOpening = false;

		if (typeof currJaw.afterLoad === "function") {
			currJaw.afterLoad.call(currJaw, settings);
		}

		currJaw.__activeComponents.forEach(function (item) {
			if (typeof item.afterLoad === "function") {
				item.afterLoad.call(item, settings);
			}
		});
	}
};

Shark.activateView = function (pageIdOrJaw, options) {
	return Shark.openPage(pageIdOrJaw, options);
};

Shark.registerComponent = function (jaw) {
	if (jaw.type !== "" && jaw.type !== "component" && !Shark.settings.enableJawsCompatibilityMode) {
		throw new Error(`You can't register a ${jaw.type} as a Component. (see "enableJawsCompatibilityMode" Shark setting.)`);
	}
	//Dave: this is need for compatibility with old project that create component without extending Component class. Will be deprecated and removed
	if (jaw.type === "") {
		console.warn("Warning! You should extend the Component base class to create a component. Creation of component by 'registerComponent' method is deprecated and will be removed.");
		jaw.type = "component";
	}

	if (!Shark.__components[jaw.className]) {
		Shark.__components[jaw.className] = jaw;

		if (typeof jaw.onRegister === "function") {
			jaw.onRegister.call(jaw);
		}
	}
};

Shark.registerController = function (jaw) {
	if (jaw.type !== "" && jaw.type !== "controller" && !Shark.settings.enableJawsCompatibilityMode) {
		throw new Error(`You can't register a ${jaw.type} as a Controller. (see "enableJawsCompatibilityMode" Shark setting.)`);
	}
	//Dave: this is need for compatibility with old project that create controller without extending Controller class. Will be deprecated and removed
	if (jaw.type === "") {
		console.warn("Warning! You should extend the Controller base class to create a controller. Creation of controller by 'registerController' method is deprecated and will be removed.");
		jaw.type = "controller";
	}

	if (!Shark.__controllers[jaw.className]) {
		Shark.__controllers[jaw.className] = jaw;

		if (typeof jaw.onRegister === "function") {
			jaw.onRegister.call(jaw);
		}
	}
};

/**
 * Register a view that can be used withe the openPage method.
 *
 * @param {Jaw} jaw - The view to register.
 */
Shark.registerView = function (jaw) {
	if (jaw.type !== "" && jaw.type !== "view" && !Shark.settings.enableJawsCompatibilityMode) {
		throw new Error(`You can't register a ${jaw.type} as a View. (see "enableJawsCompatibilityMode" Shark setting.)`);
	}
	//Dave: this is need for compatibility with old project that create view without extending View class. Will be deprecated and removed
	if (jaw.type === "") {
		console.warn("Warning! You should extend the View base class to create a view. Creation of view by 'registerView' method is deprecated and will be removed.");
		jaw.type = "view";
	}

	if (!Shark.__views[jaw.className]) {
		Shark.__views[jaw.className] = jaw;
		
		if (typeof jaw.onRegister === "function") {
			jaw.onRegister.call();
		}
	}
};

Shark.render = function (currJaw, options) {
	//Dave: Da queste parti dovrei anche gestire il fatto che, se sono durante il "beforeLoad", eventuali Shark.set o this.set non devono triggerare l'autorender
	if (Shark.__renderRunning) {
		Shark.trace("Autorender should be triggered but there is another render running.", Shark.TRACE_INFO);
		Shark.__renderPending = true;
	}
	else {
		Shark.__renderRunning = true;

		const rand = Math.random();
		Shark.trace("Start render: " + rand, Shark.TRACE_LOG);

		let modelBindNeeded = true;

		if (currJaw === undefined) {
			// Shark.activePage.render();
			// return Shark.render(this, completeOptions);
			var completeOptions = $.extend(true, Shark.activePage.currentOpenOptions, options);
			options = completeOptions;
			currJaw = Shark.activePage;

			if (Shark.templateRenderer instanceof VirtualDOMRenderer) {
				modelBindNeeded = false;
			}
			// return;
		}

		var renderOptions = options.renderOptions;

		//Dave.Warn: ma sarà giusto che stia qui per prima??
		if (Shark.activePage && typeof Shark.activePage.smartUnlisten === "function") {
			Shark.activePage.smartUnlisten();
		}

		if (typeof Shark.beforeRenderEachPage === "function") {
			Shark.beforeRenderEachPage(currJaw, options);
		}

		if (currJaw && typeof currJaw.beforeRender === "function") {
			currJaw.beforeRender.call(currJaw, options);
		}

		if (currJaw.__activeComponents) {
			currJaw.__activeComponents.forEach(function (item) {
				if (typeof item.beforeRender === "function") {
					item.beforeRender.call(item, options);
				}
			});
		}

		if (currJaw && typeof currJaw.afterComponentsPreRender === "function") {
			currJaw.afterComponentsPreRender.call(currJaw, options);
		}

		var settings = {
	//		container: "#mainContainer"
		};

		$.extend(true, settings, renderOptions);

		if ($(settings.container).length === 0 /*|| currJaw.isOuterContainer*/) {//isOuterContainer is deprecated and should be removed
			settings.container = "body";
		}

		if (currJaw && currJaw.id !== "" && Shark.templateRenderer !== null) {
			Shark.templateRenderer.render(currJaw, settings);

			$("body").attr("data-page-id", currJaw.id);
			$(settings.container).attr("data-page-id", currJaw.id);
		}

		Shark.__evaluateReferenceValues();
		Shark.__evaluateDynamicBindings();

		if (modelBindNeeded) {
			//Dave: Questa è una schifezza madornale, questa cosa va completamente rivista
			Shark.bindViewToModel();
		}

		if (currJaw && typeof currJaw.afterRender === "function") {
			currJaw.afterRender.call(currJaw, options);
		}

		if (currJaw.__activeComponents) {
			currJaw.__activeComponents.forEach(function (item) {
				if (typeof item.afterRender === "function") {
					item.afterRender.call(item, options);
				}
			});
		}

		if (currJaw && typeof currJaw.afterComponentsPostRender === "function") {
			currJaw.afterComponentsPostRender.call(currJaw, options);
		}

		if (typeof Shark.afterRenderEachPage === "function") {
			Shark.afterRenderEachPage(currJaw, options);
		}

		if (Shark.activePage && typeof Shark.activePage.smartListen === "function") {
			Shark.activePage.smartListen();
		}

		Shark.trace("End render: " + rand, Shark.TRACE_LOG);
		Shark.__renderRunning = false;
		if (Shark.__renderPending) {
			Shark.trace("There was a render pending, I'ill do it now.", Shark.TRACE_INFO);
			Shark.__renderPending = false;
			Shark.render();
		}
	}
};

Shark.renderAsync = function (currJaw, options) {
	let returnPromise;
	// return Promise ((resolve, rejects) => {
		let modelBindNeeded = true;

		if (currJaw === undefined) {
			// Shark.activePage.render();
			// return Shark.render(this, completeOptions);
			var completeOptions = $.extend(true, Shark.activePage.currentOpenOptions, options);
			options = completeOptions;
			currJaw = Shark.activePage;

			if (Shark.templateRenderer instanceof VirtualDOMRenderer) {
				modelBindNeeded = false;
			}
			// return;
		}

		var renderOptions = options.renderOptions;

		//Dave.Warn: ma sarà giusto che stia qui per prima??
		Shark.activePage.smartUnlisten();

		if (typeof Shark.beforeRenderEachPage === "function") {
			Shark.beforeRenderEachPage(currJaw, options);
		}

		if (typeof currJaw.beforeRender === "function") {
			currJaw.beforeRender.call(currJaw, options);
		}

		currJaw.__activeComponents.forEach(function (item) {
			if (typeof item.beforeRender === "function") {
				item.beforeRender.call(item, options);
			}
		});

		if (typeof currJaw.afterComponentsPreRender === "function") {
			currJaw.afterComponentsPreRender.call(currJaw, options);
		}

		var settings = {
	//		container: "#mainContainer"
		};

		$.extend(true, settings, renderOptions);

		if ($(settings.container).length === 0 /*|| currJaw.isOuterContainer*/) {//isOuterContainer is deprecated and should be removed
			settings.container = "body";
		}

		if (Shark.templateRenderer !== null) {
			returnPromise = Shark.templateRenderer.render(currJaw, settings)
			.then(() => {


				$("body").attr("data-page-id", currJaw.id);
				$(settings.container).attr("data-page-id", currJaw.id);

				
				Shark.__evaluateReferenceValues();
				Shark.__evaluateDynamicBindings();

				if (modelBindNeeded) {
					//Dave: Questa è una schifezza madornale, questa cosa va completamente rivista
					Shark.bindViewToModel();
				}

				if (typeof currJaw.afterRender === "function") {
					currJaw.afterRender.call(currJaw, options);
				}

				currJaw.__activeComponents.forEach(function (item) {
					if (typeof item.afterRender === "function") {
						item.afterRender.call(item, options);
					}
				});

				if (typeof currJaw.afterComponentsPostRender === "function") {
					currJaw.afterComponentsPostRender.call(currJaw, options);
				}

				if (typeof Shark.afterRenderEachPage === "function") {
					Shark.afterRenderEachPage(currJaw, options);
				}

				Shark.activePage.smartListen();
			})

		}

	// })
	return returnPromise;
};

Shark.__singleSet = (name, value) => {
	console.log("%csingleSet", "background-color: #dddddd; boder: 2px solid lime; color: purple; padding: 5px 10px;", name, value);
	if (!Shark.$store.hasOwnProperty(name)) {
		console.log("%cCreo la property", "background-color: #dddddd; boder: 2px solid lime; color: purple; padding: 5px 10px;");

		Object.defineProperty(Shark.$store, name, {
			configurable: true,
			enumerable: true,
			get () {
				return Shark.__store[name];
			},
			set (value) {
				//Dave: Questa roba qui sotto è da sistemare, ma ha il potenziale per risolvere per sempre l'autorender.
				//Questo deve tenere conto del tipo di dato, ad esempio se è un array non serve a niente... vale solo coi basic types
				// L'idea dell'observator è ottima:
				//https://www.wintellect.com/how-to-implement-the-observer-pattern-with-objects-and-arrays-in-pure-javascript/
				const oldValue = Shark.__store[name];
				const newHash = Utils.computeHash(value);
				let valueChanged = newHash !== Utils.computeHash(oldValue);

				if (typeof value === "object") {
					if (value.__sh_hash__ != null) {
						valueChanged = value.__sh_hash__ !== newHash;
						console.log(`%cHo un vecchio hash (sulla property)!!!${value.__sh_hash__}, ${newHash}`, `background-color: ${valueChanged ? "yellow" : "green"}; color: ${valueChanged ? "red" : "white"}; font-weight: bold; padding: 3px 6px;`);
					}
					value.__sh_hash__ = newHash;
				}

				Shark.__store[name] = value;

				if (Shark.settings.enableMainStoreAutoRender && valueChanged) {
					Shark.trace("%c SharkMVC: AUTORENDER TRIGGERED from single property", "background-color: red; color: white; font-weight: bold;", {name, value}, Shark.TRACE_INFO);
					Shark.render();
					Shark.trace("%c SharkMVC: AUTORENDER COMPLETED from single property", "background-color: red; color: white; font-weight: bold;", Shark.TRACE_INFO);
				}

				console.log("%c scrivo la property valueChanged: " + valueChanged, "background-color: #ff99dd; boder: 2px solid lime; color: purple; padding: 5px 10px;", name, value);
			},
		});
	}

	let oldValue = Shark.__store[name];
	let oldValueHash = oldValue;
	let newValueHash = "";
	let valueChanged = oldValueHash !== newValueHash;

	if (oldValue !== undefined && value !== undefined) {
		try {
			oldValueHash = Utils.computeHash(oldValue);
			newValueHash = Utils.computeHash(value);
		}
		catch (err) {
			oldValueHash = oldValue;
			newValueHash = value;
		}

		valueChanged = oldValueHash !== newValueHash

		//Dave: Questa roba qui sotto è da sistemare, ma ha il potenziale per risolvere per sempre l'autorender.
		if (typeof value === "object") {
			if (value.__sh_hash__ != null) {
				valueChanged = value.__sh_hash__ !== newValueHash;
				console.log(`%cHo un vecchio hash (sul set)!!!${value.__sh_hash__}, ${newValueHash}`, `background-color: ${valueChanged ? "yellow" : "green"}; color: ${valueChanged ? "red" : "white"}; font-weight: bold; padding: 3px 6px;`);
			}
			value.__sh_hash__ = newValueHash;
		}
	}

	Shark.__store[name] = value;

	if (valueChanged) {
		console.log("%cValueChangend", "background-color: #dddddd; boder: 2px solid lime; color: purple; padding: 5px 10px;", true);
		return true;
	}
	else {
		console.log("%cValueChangend", "background-color: #dddddd; boder: 2px solid lime; color: purple; padding: 5px 10px;", false);
		return false;
	}
}

//Dave: Devo fare in modo che se il "set" avviene in metodi specifici (tipo il beforeRender) non si triggeri il rerender.
//Una roba tipo che attivo una "__isRendering" che blocca tutto.
Shark.set = function (nameOrObject, value) {
	//Dave.Warn: Deve accettare anche oggetti per set multipli
	let requireRender = false;
	let changeCount = 0;
	let autoBoundCount = 0;
	const autoRenderTriggerer = [];

	if (Array.isArray(nameOrObject)) {
		const loopLength = nameOrObject.length;
	
		for (let i = 0; i < loopLength; i++) {
			let currElement = nameOrObject[i];
			const valueChanged = Shark.__singleSet(currElement.name, currElement.value);

			// let oldValue = Shark.__store[currElement.name];
			// console.time("string valueHash");
			// let oldValueHash = oldValue;
			// let newValueHash = "";

			// if (oldValue !== undefined && currElement.value !== undefined) {
			// 	try {
			// 		oldValueHash = Utils.computeHash(oldValue);
			// 		newValueHash = Utils.computeHash(currElement.value);
			// 	}
			// 	catch (err) {
			// 		oldValueHash = oldValue;
			// 		newValueHash = currElement.value;
			// 	}
			// }
			// console.timeEnd("string valueHash");
		
			// if (oldValue !== currElement.value) {
			if (valueChanged) {
			// if (oldValueHash !== newValueHash) {
				// Shark.__store[currElement.name] = currElement.value;
				changeCount++;
				// autoBoundCount += Shark.__autoBoundProperties.indexOf(currElement.name) > -1
				if (Shark.activePage.id !== "") {
					autoBoundCount += Shark.activePage.__autoBoundProperties.indexOf(currElement.name) > -1;
					autoRenderTriggerer.push(currElement.name);
				}
			}
		}
	}
	else if (typeof nameOrObject === "object") {
		for (let currProp in nameOrObject) {
			const valueChanged = Shark.__singleSet(currProp, nameOrObject[currProp]);

			// let oldValue = Shark.__store[currProp];
			// let oldValueHash = oldValue;
			// let newValueHash = "";

			// if (oldValue !== undefined && nameOrObject[currProp] !== undefined) {
			// 	try {
			// 		oldValueHash = Utils.computeHash(oldValue);
			// 		newValueHash = Utils.computeHash(nameOrObject[currProp]);
			// 	}
			// 	catch (err) {
			// 		oldValueHash = oldValue;
			// 		newValueHash = nameOrObject[currProp];
			// 	}
			// }

			// if (oldValue !== nameOrObject[currProp]) {
			//Dave: Probabilmente dovrei comunque storare l'oggetto, e poi valutare il cambio... se no ci sono problemi se per qualche motivo il cambio non si triggera
			if (valueChanged) {
				// Shark.__store[currProp] = nameOrObject[currProp];
				changeCount++;
				// autoBoundCount += Shark.__autoBoundProperties.indexOf(currProp) > -1
				if (Shark.activePage.id !== "") {
					autoBoundCount += Shark.activePage.__autoBoundProperties.indexOf(currProp) > -1
					autoRenderTriggerer.push(currProp);
				}
			}
		}
	}
	else if (typeof nameOrObject === "string") {
		const valueChanged = Shark.__singleSet(nameOrObject, value);

		// let oldValue = Shark.__store[nameOrObject];
		// console.time("string valueHash");
		// let oldValueHash = oldValue;
		// let newValueHash = "";

		// if (oldValue !== undefined && value !== undefined) {
		// 	try {
		// 		oldValueHash = Utils.computeHash(oldValue);
		// 		newValueHash = Utils.computeHash(value);
		// 	}
		// 	catch (err) {
		// 		oldValueHash = oldValue;
		// 		newValueHash = value;
		// 	}
		// }
		// console.timeEnd("string valueHash");

		// if (oldValue !== value) {
		if (valueChanged) {
			// if (oldValueHash !== newValueHash) {
			// Shark.__store[nameOrObject] = value;
			changeCount++;
			// autoBoundCount += Shark.__autoBoundProperties.indexOf(nameOrObject) > -1
			if (Shark.activePage.id !== "") {
				autoBoundCount += Shark.activePage.__autoBoundProperties.indexOf(nameOrObject) > -1
				autoRenderTriggerer.push(nameOrObject);
			}
		}
	}
	else {
		console.error("Shark.set: This kind of data could not be used.");
	}	

	if (Shark.__dynamicSections) {
		if (Shark.__dynamicSections[nameOrObject]) {
			Shark.__dynamicSections[nameOrObject].forEach(item => {
				const container = document.querySelector(`[data-sh-rguid="${item.rGuid}"]`);
				
				if (container) {
					const renderStore = Object.assign({}, Shark.__store);
					const output = Mustache.render(item.templateContent, renderStore);

					container.innerHTML = output;
					Shark.trace("Dovrei aggiornare queste sezioni dinamiche", item, "log");
					// .log("Dovrei aggiornare queste sezioni dinamiche", item, renderStore, container, output);
				}
			});
		}
	}

	const autoBoundListSectionParsed = [];

	if (Shark.activePage.__autoBoundListSection) {
		Shark.activePage.__autoBoundListSection.forEach(section => {
			// if (section.propertyName === nameOrObject) {
			if (autoRenderTriggerer.indexOf(section.propertyName) > -1) {
				// .log("%c Avrei voluto renderizzare " + section.propertyName, "background-color: yellow; color: red; font-weight: bold;", nameOrObject);

				const renderStore = Object.assign({}, Shark.__store, Shark.activePage.__store);
				const output = Mustache.render(`{{#${section.propertyName}}}${section.template}{{/${section.propertyName}}}`, renderStore);
				const container = document.querySelector(`shark-list[property="${section.propertyName}"]`);

				if (container) {
					const replaceContainer = container.cloneNode();
					replaceContainer.innerHTML = output;
					container.replaceWith(replaceContainer);
					autoBoundListSectionParsed.push(section.propertyName);
				}
			}
		});

		if (autoBoundListSectionParsed.length > 0) {
			console.log("%c AUTORENDER TRIGGERED 1 ", "background-color: yellow; color: red; font-weight: bold;", {nameOrObject, value, autoRenderTriggerer, autoBoundListSectionParsed});
			Shark.activePage.smartUnlisten();
			Shark.activePage.smartListen();
			Shark.__evaluateReferenceValues();
			Shark.__evaluateDynamicBindings();
			Shark.bindViewToModel();
		}
	}
	
	const notParsedAutoSections = autoRenderTriggerer.filter(x => !autoBoundListSectionParsed.includes(x));
	
	if (Shark.settings.enableMainStoreAutoRender && changeCount > 0 && autoBoundCount > 0 && notParsedAutoSections.length > 0) {
		Shark.trace(`%c SharkMVC: AUTORENDER TRIGGERED (${changeCount})`, "background-color: red; color: white; font-weight: bold;", {nameOrObject, value}, Shark.TRACE_INFO);
		Shark.render();
		Shark.trace("%c SharkMVC: AUTORENDER COMPLETED ", "background-color: red; color: white; font-weight: bold;", Shark.TRACE_INFO);
	}

	return Shark.__store[nameOrObject];
};

//Dave: Questa non serve solo a settare i searchParams, il nome è sbagliato
Shark.setURLSearchParams = (urlSearchParams = null, replace = false) => {
	//Dave: Devo gestire la possibilità che urlSearchParams sia un oggetto, nativo o URLSearchParams
	const currJaw = Shark.activePage;

	if (currJaw) {
		const stateObj = {
			jawId: currJaw.className,
			urlSearchParams,
		};

		const destinationURL = new URL(window.location);
		const destinationViewURL = currJaw.$mainRoute || currJaw.route || currJaw.className;

		if (urlSearchParams === null) {
			destinationURL.searchParams.forEach((val, key) => destinationURL.searchParams.delete(key));
		}
		else if (typeof urlSearchParams === "string" && urlSearchParams !== "") {
			const urlParamsList = urlSearchParams.split("&");

			urlParamsList.forEach(param => {
				const params = param.split("=");
				destinationURL.searchParams.set(params[0], params[1]);
			});
		}
		else if (typeof urlSearchParams === "object") {
			for (let param in urlSearchParams) {
				destinationURL.searchParams.set(param, urlSearchParams[param]);
			};
		}

		if (Shark.settings.routingMethod === "url" && typeof history.pushState === "function") {
			destinationURL.pathname = destinationURL.pathname.substr(0, destinationURL.pathname.lastIndexOf("/")) + "/" + destinationViewURL;

			if (replace) {
				// history.replaceState(stateObj, currJaw.className, (currJaw.route || currJaw.className) + (urlSearchParams !== "" ? "?" + urlSearchParams : ""));
				history.replaceState(stateObj, currJaw.className, destinationURL);
			}
			else {
				// history.pushState(stateObj, currJaw.className, (currJaw.route || currJaw.className) + (urlSearchParams !== "" ? "?" + urlSearchParams : ""));
				history.pushState(stateObj, currJaw.className, destinationURL);
			}
		}
		else {
			if (typeof history.pushState === "function") {
				destinationURL.hash = destinationViewURL;
		
				if (replace) {
					// history.replaceState(stateObj, currJaw.className, (urlSearchParams !== "" ? "?" + urlSearchParams : "/") + "#" + currJaw.className);
					history.replaceState(stateObj, currJaw.className, destinationURL);
				}
				else {
					// history.pushState(stateObj, currJaw.className, (urlSearchParams !== "" ? "?" + urlSearchParams : "/") + "#" + currJaw.className);
					history.pushState(stateObj, currJaw.className, destinationURL);
				}
			}
			else {
				document.location.hash = currJaw.className;
			}
	//		document.location.search = settings.urlSearchParams !== "" ? "?" + settings.urlSearchParams : "";
		}
	}
}


//Dave: no more needed since Shark is no more responsible for remote data loading
// Shark.stopRemoteCallTimeout = function () {
// 	if (Shark.__internalSettings.remoteCallTimeOutId) {
// 		clearTimeout(Shark.__internalSettings.remoteCallTimeOutId);
// 	}
// };

Shark.trace = function() {
	const content = Array.from(arguments);
	let level = content[content.length - 1];
	let addFormatting = false;
	const baseFormat = " display: inline-block; font-weight: bold; padding: 2px 5px;";

	if ([Shark.TRACE_ERROR, Shark.TRACE_LOG, Shark.TRACE_INFO, Shark.TRACE_WARN].indexOf(level) === -1) {
		level = Shark.TRACE_LOG;
	}
	else {
		content.pop();
	}

	if (typeof content[0] !== "string" || content[0].indexOf("%c") === -1) {
		addFormatting = true;
	}

	if (console && Shark.__shouldTrace(level)) {
		switch (level) {
			case Shark.TRACE_ERROR:
				if (addFormatting) {
					content.unshift("background-color: #ff0000; color: #ffffff;" + baseFormat);// border: 1px solid #ff0000;
					content.unshift("background-color: #96b7c0; color: #151515;" + baseFormat);// border: 1px solid #ff0000;
					content.unshift("%cSharkMVC%c" + level);
				}
				console.error(...content);
				break;
			case Shark.TRACE_INFO:
				if (addFormatting) {
					content.unshift("background-color: #0000ff; color: #ffffff;" + baseFormat);// border: 1px solid #0000ff;
					content.unshift("background-color: #96b7c0; color: #151515;" + baseFormat);// border: 1px solid #0000ff;
					content.unshift("%cSharkMVC%c" + level);
				}
				console.info(...content);
				break;
			case Shark.TRACE_LOG:
				if (addFormatting) {
					content.unshift("background-color: #aaaaaa; color: #000000;" + baseFormat);// border: 1px solid #aaaaaa;
					content.unshift("background-color: #96b7c0; color: #151515;" + baseFormat);// border: 1px solid #aaaaaa;
					content.unshift("%cSharkMVC%c" + level);
				}
				console.log(...content);
				break;
			case Shark.TRACE_WARN:
				if (addFormatting) {
					content.unshift("background-color: #ffaa00; color: #000000;" + baseFormat);// border: 1px solid #ffaa00;
					content.unshift("background-color: #96b7c0; color: #151515;" + baseFormat);// border: 1px solid #ffaa00;
					content.unshift("%cSharkMVC%c" + level);
				}
				console.warn(...content);
				break;
		}
	}
};

export { Shark };