/* -*- coding: utf-8 tab-width: 3 -*- */

/****************************************************************************\
* Base functions and PaLeg          fWMKq,           sg                      *
* namespace members                 @N  'H|          W&                      *
*                                   Mj  ;W: ,gwq.s;  M|     d7^"Yb,  ,vgbk,y *
*                                  ,#EMWV',W/` 7MP' :Q;    jML_,jQ' lW`  7P  *
*                                  ;M'    WK  ,jL.  |M\_   VK^""'` :$#omvM'  *
*                                  jM     'QMKV HK  #HKEMX? "QDnsY  ?3xc;,   *
*                                                                  G7,__.J)  *
*                                                                   '"*""'   *
\****************************************************************************/



/*****************************************************************************
 * CLASSES
 */


// PaLeg.Animation ////////////////////////////////////////////////////////////
//
// Varia gli stili CSS degli elementi specificati, simultaneamente, in
// un'animazione.
//
// Normalmente, un elemento viene dato per visibile (display != "none" e
// visibility != "hidden"); questo può essere cambiato con arrSetups.
// Un null nelle posizioni 2 e 3 degli elementi in arrSetups non comporta
// nessun cambiamento; una stringa vuota comporta la rimozione della proprietà
// corrispondente.
//
// [
//   [$("asd"), "visibility", null, "hidden"]
// ],
// [
//   [$("asd"), "top",      3,    10,    "em", "accelerated"],
//   [$("asd"), "opacity",  1.00,  0.50],
//   [$("qwe"), "top",     10,     3,    "em", "linear"],
//   [$("qwe"), "left",    10,     3,    "em", "smoothscroll"]
// ]
//
PaLeg.Animation = function(arrSetups, arrTargets, iDuration, onComplete /*= undefined*/) {
	this._m_arrSetups = arrSetups;
	this._m_arrTargets = arrTargets;
	this._m_iDuration = iDuration;
	this._m_onComplete = onComplete;

	this._m_iActualDuration = iDuration;
	this._onAnimStep = this._onAnimStep.bind(this);

	this._calcSteps();
	if (this._m_arrSetups)
		this._applySetups(2);
	this._onAnimStep();
	this._m_idAnimInterval = window.setInterval(this._onAnimStep, Math.UI_SMOOTH_RATE);
};
PaLeg.Animation.prototype._m_arrSetups = null;
PaLeg.Animation.prototype._m_arrTargets = null;
PaLeg.Animation.prototype._m_cSteps = null;
PaLeg.Animation.prototype._m_iActualDuration = null;
PaLeg.Animation.prototype._m_idAnimInterval = null;
PaLeg.Animation.prototype._m_iDuration = null;
PaLeg.Animation.prototype._m_iStep = 0;
PaLeg.Animation.prototype._m_onComplete = null;


// Ferma l'animazione, permettendo poi di riprenderla con resume().
//
PaLeg.Animation.prototype.pause = function() {
	if (this._m_idAnimInterval) {
		window.clearInterval(this._m_idAnimInterval);
		this._m_idAnimInterval = null;
	}
};


// Riprende l'animazione interrotta da pause().
//
PaLeg.Animation.prototype.resume = function() {
	if (!this._m_idAnimInterval)
		this._m_idAnimInterval = window.setInterval(this._onAnimStep, Math.UI_SMOOTH_RATE);
};


// Cambia la velocità al multiplo specificato.
//
PaLeg.Animation.prototype.setSpeed = function(fRatio) {
	this._m_iActualDuration = this._m_iDuration / fRatio;
	this._calcSteps();
};


// Arresta l'animazione.
//
PaLeg.Animation.prototype.stop = function(bRunOnComplete /*= true*/) {
	if (this._m_idAnimInterval) {
		window.clearInterval(this._m_idAnimInterval);
		this._m_idAnimInterval = null;
	}
	if (this._m_onComplete && (bRunOnComplete === undefined || bRunOnComplete))
		this._m_onComplete();
	this.pause = this.resume = this.stop = Function.Empty;
};


// Standard toString().
//
PaLeg.Animation.prototype.toString = function() {
	return "[object PaLeg.Animation]";
};


// Applica il setup di indice specificato (2 = iniziale, 3 = finale).
//
PaLeg.Animation.prototype._applySetups = function(iSetup) {
	this._m_arrSetups.forEach(function(arrSetup) {
		if (arrSetup[iSetup] !== null)
			if (arrSetup[iSetup] != "")
				arrSetup[0].setStyle(arrSetup[1], arrSetup[iSetup]);
			else
				arrSetup[0].removeStyle(arrSetup[1]);
	});
};


// Prepara le informazioni per le animazioni.
//
PaLeg.Animation.prototype._calcSteps = function() {
	var cNewSteps = Math.ceil(this._m_iActualDuration / Math.UI_SMOOTH_RATE);
	if (this._m_iStep)
		this._m_iStep = Math.round(this._m_iStep * cNewSteps / this._m_cSteps);
	this._m_cSteps = cNewSteps;
	this._m_arrTargets.forEach(function(arrTarget) {
		var arrSteps = Array.fillAnimSteps(arrTarget[5] || "linear", this._m_cSteps),
			 mL = arrTarget[2],
			 mD = (arrTarget[3] - arrTarget[2]);
		for (var i = 0; i < arrSteps.length; ++i)
			arrSteps[i] = mL + arrSteps[i] * mD;
		if (!arrTarget[4])
			arrTarget[4] = "";
		arrTarget[6] = arrSteps;
	}, this);
};


// Compie un passo dell'animazione.
//
PaLeg.Animation.prototype._onAnimStep = function() {
	if (this._m_iStep < this._m_cSteps) {
		var arrTargets = this._m_arrTargets;
		for (var i = 0; i < arrTargets.length; ++i)
			arrTargets[i][0].setStyle(arrTargets[i][1], arrTargets[i][6][this._m_iStep] + arrTargets[i][4]);
		++this._m_iStep;
	} else {
		window.clearInterval(this._m_idAnimInterval);
		this._m_idAnimInterval = null;
		if (this._m_arrSetups)
			this._applySetups(3);
		if (this._m_onComplete)
			this._m_onComplete();
	}
};


// PaLeg.AsyncRequest /////////////////////////////////////////////////////////
//
// Richiesta HTTP asincrona.
//
PaLeg.AsyncRequest = function() {
	this._m_sID = "pl_ar" + (new Date).getTime();
	this._m_aaRequestHeaders = {};
	this._m_oFormElts = null;
	if (!PaLeg.AsyncRequest._abortAll._bListening) {
		PaLeg.AsyncRequest._abortAll._bListening = true;
		// Prova a registrare il gestore che interrompe tutte le richieste.
		// Con FX è necessario, perché altrimenti invoca i gestori delle
		// richieste asincrone come se le richieste fossero fallite.
		// OP non invoca proprio questi gestori, in caso di aggiornamento della
		// pagina.
		window.addEventListener("beforeunload", PaLeg.AsyncRequest._abortAll, false);
		window.addEventListener("unload",       PaLeg.AsyncRequest._abortAll, false);
	}
};
PaLeg.AsyncRequest.STATUS_IDLE     =  0; // Nessuna richiesta attiva.
PaLeg.AsyncRequest.STATUS_PENDING  =  1; // Richiesta in attesa di risposta.
PaLeg.AsyncRequest.STATUS_COMPLETE =  2; // Richiesta completa.
PaLeg.AsyncRequest.STATUS_ABORTED  = -1; // Richiesta interrotta.

PaLeg.AsyncRequest.prototype.status = PaLeg.AsyncRequest.STATUS_IDLE;
PaLeg.AsyncRequest.prototype._m_sID = null;
PaLeg.AsyncRequest.prototype._m_sUrl = null;
PaLeg.AsyncRequest.prototype._m_bForceIFrame = false;
PaLeg.AsyncRequest.prototype._m_onComplete = null;

PaLeg.AsyncRequest.prototype._m_xhr = null;
PaLeg.AsyncRequest.prototype._m_aaRequestHeaders = null;

PaLeg.AsyncRequest.prototype._m_form = null;
PaLeg.AsyncRequest.prototype._m_iframe = null;
PaLeg.AsyncRequest.prototype._m_oFormElts = null;

PaLeg.AsyncRequest.arrActive = [];
PaLeg.AsyncRequest.arrAxProgIDs = ["MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP", "Microsoft.XMLHTTP"];


// Interrompe una richiesta HTTP in corso.
//
PaLeg.AsyncRequest.prototype.abort = function() {
	if (this.status != PaLeg.AsyncRequest.STATUS_PENDING)
		throw new Error("No pending request");
	this.status = PaLeg.AsyncRequest.STATUS_ABORTED;
	if (this._m_xhr)
		this._m_xhr.abort();
	else {
		// Sembra che nessun valore fittizio per src causi un evento onload,
		// quindi il gestore va invocato a mano.
		this._m_iframe.setAttribute("src", "");
		PaLeg.AsyncRequest.arrActive.remove(this);
		if (this._m_onComplete) {
			if (this.debugCallback)
				this._m_onComplete(this);
			else
				try {
					this._m_onComplete(this);
				} catch (x) {
					PaLeg.logException(x, "PaLeg.AsyncRequest.abort$callback", {
						"sUrl": this._m_sUrl
					});
				}
		}
	}
	this._cleanUp();
	return this;
};


// Assegna un elemento o un contenitore di elementi di modulo XHTML, che sarà
// sostituito dal modulo per tutta la durata della comunicazione asincrona
// (implica <iframe>).
//
PaLeg.AsyncRequest.prototype.setFormElements = function(o) {
	if (this.status != PaLeg.AsyncRequest.STATUS_IDLE)
		throw new Error("Request active, cannot prepare for new");
	if (this._m_xhr)
		this._m_xhr = null;
	this._m_bForceIFrame = true;
	this._m_oFormElts = o;
	return this;
};


// Avvia la richiesta asincrona.
//
PaLeg.AsyncRequest.prototype.submit = function(sMethod, sUrl /*= location.href*/, aaParams /*= {}*/, onComplete /*= undefined*/) {
	if (this.status != PaLeg.AsyncRequest.STATUS_IDLE)
		throw new Error("Request active, cannot initiate new");
	var bPost;
	sMethod = sMethod.toUpperCase();
	if (sMethod == "POST")
		bPost = true;
	else if (sMethod == "GET")
		bPost = false;
	else
		throw new Error("Invalid request method");
	if (!sUrl)
		sUrl = location.href.substr(0, location.href.length - location.hash.length);
	if (!aaParams)
		aaParams = {};
	this._m_onComplete = onComplete;

	// Se ancora non ha stabilito cosa usare, prova con XHR.
	if (this._m_bForceIFrame || (!this._m_xhr && !this._createXhr())) {
		// Niente da fare, si va a <iframe>.
		sUrl = sUrl.urlAddQA("forcetext=1");
		this._m_iframe = document.createSubtree(
			"iframe", {
				"id":    "if_" + this._m_sID,
				"style": "display: none;"
			}
		);
		this._m_iframe._ar = this;
		document.getBody().appendChild(this._m_iframe);
		if (this._m_iframe.getAttribute("id") in (document.frames || {}))
			// IE BUG: this._m_iframe.name va riassegnato ora e in questo modo,
			// altrimenti non lo riconosce come target del <form>.
			document.frames[this._m_iframe.getAttribute("id")].name = this._m_iframe.getAttribute("id");

		// Cerca eventuali <input type="file">, che comportano la modifica del
		// Content-Type e del metodo di richiesta.
		if (this._m_oFormElts && (($eN(this._m_oFormElts).elemName == "input" && this._m_oFormElts.getAttribute("type") == "file") || this._m_oFormElts.getDescendants("input", "type", "file").length)) {
			bPost = true;
			sMethod = "POST";
			this._m_aaRequestHeaders["Content-Type"] = "multipart/form-data";
		}
	}

	// Content-Type predefinito, in mancanza di qualsiasi imposizione.
	if (!("Content-Type" in this._m_aaRequestHeaders))
		this._m_aaRequestHeaders["Content-Type"] = "application/x-www-form-urlencoded";

	// Completa l'URL.
	if (location.SID != "")
		if (bPost)
			aaParams["s"] = location.SID.substr(location.SID.indexOf("=") + 1);
		else
			sUrl = sUrl.urlAddQA(location.SID);
	var sParams;
	if (this._m_xhr || !bPost) {
		switch (this._m_aaRequestHeaders["Content-Type"]) {
			case "application/x-www-form-urlencoded":
				sParams = String.urlEncodeQS(aaParams);
				break;
			default:
				sParams = "";
				break;
		}
		if (!bPost && sParams != "") {
			sUrl = sUrl.urlAddQA(sParams);
			sParams = "";
		}
	}
	this._m_sUrl = sUrl;

	// Avvia la connessione.
	if (this._m_xhr) {
		this._m_xhr.open(sMethod, sUrl, true);
		for (var sName in this._m_aaRequestHeaders)
			this._m_xhr.setRequestHeader(sName, this._m_aaRequestHeaders[sName]);
		this._m_xhr.onreadystatechange = this._onXhrReadyStateChange.bind(this);
		this._m_xhr.send(sParams);
		this.status = PaLeg.AsyncRequest.STATUS_PENDING;
	} else {
		if (bPost) {
			this._m_form = document.createSubtree(
				"form", {
					"action":  sUrl,
					"method":  sMethod,
					"enctype": this._m_aaRequestHeaders["Content-Type"],
					"target":  this._m_iframe.getAttribute("id")
				}
			);
			for (var sName in aaParams) {
				var input = document.createElement("input hidden");
				input.setAttribute("name", sName);
				input.value = aaParams[sName];
				this._m_form.appendChild(input);
			}
			if (this._m_oFormElts) {
				this._m_oFormElts.parentNode.replaceChild(this._m_form, this._m_oFormElts);
				this._m_form.appendChild(this._m_oFormElts);
			} else {
				this._m_form.setStyle("display", "none");
				document.getBody().appendChild(this._m_form);
			}
		}
		this._m_iframe.addEventListener("load", this._onIFrameLoad, false);
		if (bPost)
			this._m_form.submit();
		else
			this._m_iframe.setAttribute("src", sUrl);
		// OP BUG: spara un onload a salve, impossibile distinguerlo dal vero.
		// Questo ritardo però è in grado di filtrarlo.
		window.setTimeout((function() {
			this.status = PaLeg.AsyncRequest.STATUS_PENDING;
		}).bind(this), 0);
	}
	PaLeg.AsyncRequest.arrActive.push(this);
	return this;
};


// Standard toString().
//
PaLeg.AsyncRequest.prototype.toString = function() {
	return "[object PaLeg.AsyncRequest]";
};


// Interrompe tutte le richieste in corso.
//
PaLeg.AsyncRequest._abortAll = function() {
	var arrActive = PaLeg.AsyncRequest.arrActive;
	for (var i = 0; i < arrActive.length; ++i)
		arrActive[i].abort();
	PaLeg.AsyncRequest.arrActive = [];
	window.removeEventListener("beforeunload", arguments.callee, false);
	window.removeEventListener("unload",       arguments.callee, false);
};


// Reimposta alcune variabili in seguito al termine dell'esecuzione della
// richiesta.
//
PaLeg.AsyncRequest.prototype._cleanUp = function() {
	if (this._m_xhr)
		// IE BUG: problemi di memoria.
		this._m_xhr.onreadystatechange = Function.Empty;
	else {
		if (this._m_form) {
			if (this._m_oFormElts)
				this._m_form.parentNode.replaceChild(this._m_oFormElts, this._m_form);
			else
				this._m_form.parentNode.removeChild(this._m_form);
			this._m_form = null;
		}
		this._m_iframe.removeEventListener("load", this._onIFrameLoad, false);
		window.setTimeout((function() {
			this._m_iframe.parentNode.removeChild(this._m_iframe);
			this._m_iframe = null;
		}).bind(this), 0);
	}
	this.status = PaLeg.AsyncRequest.STATUS_IDLE;
};


// Cerca di creare un oggetto XHR. Se non ci riesce, degrada all'uso di
// <iframe>.
//
PaLeg.AsyncRequest.prototype._createXhr = function() {
	if (window.XMLHttpRequest) {
		if (this._m_xhr = new XMLHttpRequest)
			return true;
	} else if (window.ActiveXObject)
		for (var i = 0; i < PaLeg.AsyncRequest.arrAxProgIDs.length; ++i)
			try {
				if (this._m_xhr = new ActiveXObject(PaLeg.AsyncRequest.arrAxProgIDs[i]))
					return true;
			} catch (x) {
			}
	this._m_bForceIFrame = true;
	return false;
};


// Gestisce il caricamento della richiesta (solo <iframe>).
//
PaLeg.AsyncRequest.prototype._onIFrameLoad = function(e) {
	if (this._ar.status == PaLeg.AsyncRequest.STATUS_PENDING)
		this._ar._onReady();
};


// Reagisce al completamento della richiesta.
//
PaLeg.AsyncRequest.prototype._onReady = function() {
	PaLeg.AsyncRequest.arrActive.remove(this);
	var iHttpStatus = -1,
		 s = "",
		 sMimeType = "";
	if (this._m_xhr)
		try {
			// FX lancia un'eccezione se c'è stato un errore nella richiesta.
			iHttpStatus = this._m_xhr.status;
			sMimeType = this._m_xhr.getResponseHeader("Content-Type");
			// Se sMimeType è null, questo lancerà un'eccezione.
			sMimeType = sMimeType.substr(0, (sMimeType.indexOf(";") + 1 || sMimeType.length + 1) - 1);
			// Quindi, se è arrivato qui, i precedenti sono tutti andati a buon
			// fine, e può funzionare come previsto.
			s = this._m_xhr.responseText;
			if (this.debugResponse)
				alert(s);
		} catch (x) {
			// Gli errori qui possono capitare per troppi motivi, e nessuno è
			// relativo al server, quindi non invia l'eccezione.
			iHttpStatus = 0;
		}
	else
		try {
			var o = (this._m_iframe.contentDocument || this._m_iframe.contentWindow.document);
			o = o.documentElement;
			if (this.debugResponse)
				alert(o.innerHTML);
			// Salta eventuali tag creati dal browser per visualizzare
			// text/plain (<html><body><pre>).
			while (o.nodeType != document.TEXT_NODE)
				o = o.lastChild;
			// Alcuni browser spezzano nodeValue troppo grandi in segmenti da 4
			// KiB (FX), 32 KiB (OP) o 64 KiB (IE Mac).
			s = "";
			do
				s = o.nodeValue + s;
			while (o = o.previousSibling);
			var arr = s.match(/^Content-Type\s*:\s*([^\r\n;]*)/i);
			if (arr) {
				sMimeType = arr[1];
				s = s.substr(arr[0].length).lTrim();
				iHttpStatus = 200;
			} else
				iHttpStatus = 0;
		} catch (x) {
			PaLeg.logException(x, "PaLeg.AsyncRequest._onReady$iframe", {
				"sUrl":      this._m_sUrl,
				"sMimeType": sMimeType,
				"s":         s
			});
			iHttpStatus = 0;
		}
	if (this._m_onComplete) {
		try {
			// Se lo stato è stato cambiato da qualcos'altro, ora non lo tocca.
			if (this.status == PaLeg.AsyncRequest.STATUS_PENDING)
				this.status = PaLeg.AsyncRequest.STATUS_COMPLETE;
			var oResponse = (iHttpStatus >= 200 && iHttpStatus < 300 ? this._parseResponse(s, sMimeType) : undefined);
			if (typeof(oResponse) == "object" && ("pl_scriptstats" in oResponse) && (PaLeg.User.checkPrivTokens("DBGU") || PaLeg.User.checkPrivTokens("DBGA")))
				PaLeg.AsyncRequest._updateScriptStats(oResponse["pl_scriptstats"]);
		} catch (x) {
			PaLeg.logException(x, "PaLeg.AsyncRequest._onReady$response", {
				"sUrl":        this._m_sUrl,
				"iHttpStatus": iHttpStatus,
				"sMimeType":   sMimeType,
				"s":           s
			});
		}
		if (this.debugCallback)
			this._m_onComplete(this, oResponse);
		else
			try {
				this._m_onComplete(this, oResponse);
			} catch (x) {
				PaLeg.logException(x, "PaLeg.AsyncRequest._onReady$callback", {
					"sUrl":        this._m_sUrl,
					"iHttpStatus": iHttpStatus,
					"s":           s
				});
			}
	}
	this._cleanUp();
};


// Gestisce un cambiamento di stato di completamento (solo XHR).
//
PaLeg.AsyncRequest.prototype._onXhrReadyStateChange = function() {
	if (this._m_xhr.readyState == 4 && this.status != PaLeg.AsyncRequest.STATUS_IDLE)
		this._onReady();
};


// Analizza una risposta testuale, convertendola nel tipo MIME specificato.
//
PaLeg.AsyncRequest.prototype._parseResponse = function(s, sMimeType) {
	switch (sMimeType) {
		case "text/plain":
			return s;

		case "text/tab-separated-values":
			var arr = [],
				 sNL = (s.match(/^[^\r\n]*(\r?\n?)/) || {1: "\r\n"})[1],
				 arrLines = s.split(sNL),
				 sLine;
			do
				sLine = arrLines.shift();
			while (sLine == "");
			var arrProps = sLine.split("\t");
			for (var i = 0; i < arrLines.length; ++i) {
				var arrLine = arrLines[i].split("\t");
				if (arrLine.length >= arrProps.length)
					arr[arr.length] = arrProps.combine(arrLine);
			}
			return arr;

		case "application/json":
			return s.jsonDecode();

		default:
			if (!/^application\/.+\+xml$/.test(sMimeType))
				return undefined;
		case "application/xml":
			if (window.DOMParser)
				return (new DOMParser).parseFromString(s, sMimeType);
			if (window.ActiveXObject) {
				var xd = null;
				try {
					xd = new ActiveXObject("Microsoft.XMLDOM");
				} catch (x) {
				}
				if (xd) {
					xd.async = false;
					xd.loadXML(s);
					return xd;
				}
			}
			if (window.XMLHttpRequest) {
				// Trucco per arrivare al parser XML; funziona almeno con
				// Safari.
				var xhr = new XMLHttpRequest;
				xhr.open("GET", "data:" + sMimeType + ";charset=utf-8," + encodeURIComponent(s), false);
				if (xhr.overrideMimeType)
					xhr.overrideMimeType(sMimeType);
				xhr.send(null);
				return xhr.responseXML;
			}
			// Versione molto misera, può funzionare.
			var div = document.createElement("div");
			div.innerHTML = s;
			div.documentElement = div.firstChild;
			return div;
	}
};


// Aggiorna le statistiche di esecuzione a fondo pagina.
//
PaLeg.AsyncRequest._updateScriptStats = function(aaScriptStats) {
	if (!arguments.callee._oScriptStats && !(arguments.callee._oScriptStats = $("oScriptStats")))
		return;
	var oScriptStats = arguments.callee._oScriptStats,
		 oScriptStats_lxt = oScriptStats.getDescendants("span", "id", "oScriptStats_lxt")[0],
		 oScriptStats_lqc = oScriptStats.getDescendants("span", "id", "oScriptStats_lqc")[0],
		 oScriptStats_lqp = oScriptStats.getDescendants("span", "id", "oScriptStats_lqp")[0];
	var aaTotalStats;
	if (aaTotalStats = arguments.callee._aaTotalStats) {
		var aaLastStats = arguments.callee._aaLastStats;
		aaTotalStats["xt"] += aaLastStats["xt"];
		aaTotalStats["qt"] += aaLastStats["qt"];
		aaTotalStats["qc"] += aaLastStats["qc"];
		oScriptStats_lxt.previousSibling.firstChild.nodeValue = Date.formatDuration(aaTotalStats["xt"] * 1000);
		oScriptStats_lqc.previousSibling.firstChild.nodeValue = aaTotalStats["qc"];
		oScriptStats_lqp.previousSibling.firstChild.nodeValue = Math.round(aaTotalStats["qt"] * 100 / aaTotalStats["xt"]) + "%";
	} else {
		aaTotalStats = arguments.callee._aaTotalStats = oScriptStats.lastChild.firstChild.nodeValue.jsonDecode();
		oScriptStats.removeChild(oScriptStats.lastChild);
		oScriptStats_lxt.appendChild(document.createTextNode(""));
		oScriptStats_lqc.appendChild(document.createTextNode(""));
		oScriptStats_lqp.appendChild(document.createTextNode(""));
	}
	arguments.callee._aaLastStats = aaScriptStats;
	oScriptStats_lxt.firstChild.nodeValue = " + " + Date.formatDuration(aaScriptStats["xt"] * 1000);
	oScriptStats_lqc.firstChild.nodeValue = " + " + aaScriptStats["qc"];
	oScriptStats_lqp.firstChild.nodeValue = " + " + Math.round(aaScriptStats["qt"] * 100 / aaScriptStats["xt"]) + "%";
};


// PaLeg.EventSource //////////////////////////////////////////////////////////
//
// Permette ad un oggetto di sparare eventi come fosse un nodo DOM. Ovviamente
// caratteristiche come la cattura ed il bubbling non sono supportate.
//
PaLeg.EventSource = function() {
};


// Registra un ascoltatore per l'evento.
//
PaLeg.EventSource.prototype.addEventListener =  function(sEventType, fnCallback) {
	sEventType = "on" + sEventType;
	var sHP = "pl_" + sEventType + "_handlersB";
	if (this[sHP]) {
		if (this[sHP].indexOf(fnCallback) == -1)
			this[sHP].push(fnCallback);
	} else {
		var arrHandlers = [];
		if (this[sEventType])
			arrHandlers.push(this[sEventType]);
		arrHandlers.push(fnCallback);
		this[sEventType] = null;
		this[sHP] = arrHandlers;
	}
};


// Smista l'evento.
//
PaLeg.EventSource.prototype.dispatchEvent = function(e) {
	// Gli eventi DOM nativi non danno modo di controllare se è stato chiamato
	// preventDefault(), quindi bisogna sostituirlo con una versione che cambia
	// una variabile.
	if (e.cancelable && !("returnValue" in e)) {
		e.returnValue = true;
		Object.overrideMethod(e, "preventDefault", function() {
			arguments.callee.overriddenMethod();
			this.returnValue = false;
		});
	}

	PaLeg.EventSource._executeHandlers(this["pl_on" + e.type + "_handlersB"], this, e);
	return e.returnValue !== false;
};


// Revoca la registrazione di un ascoltatore per l'evento.
//
PaLeg.EventSource.prototype.removeEventListener = function(sEventType, fnCallback) {
	sEventType = "on" + sEventType;
	var sHP = "pl_" + sEventType + "_handlersB";
	if (this[sHP]) {
		var arrHandlers = this[sHP],
			 i = arrHandlers.indexOf(fnCallback);
		if (i != -1)
			if (arrHandlers.length > 1)
				arrHandlers.removeAt(i);
			else
				if (typeof(this.valueOf) == "function")
					delete this[sHP];
				else
					/* IE BUG: delete on a COM object's member. */
					this[sHP] = undefined;
	}
};


// Standard toString().
//
PaLeg.EventSource.prototype.toString = function() {
	return "[object PaLeg.EventSource]";
};


// Invoca i gestori per un evento, presi dall'array specificato.
//
PaLeg.EventSource._executeHandlers = function(arrHandlers, o, e) {
	var i = 0;
	while (i < arrHandlers.length) {
		var fn = arrHandlers[i];
		fn.call(o, e);
		// Se il gestore si è rimosso da sé, arrHandlers[i] è cambiato e va
		// invocato alla prossima iterazione, quindi non incrementa.
		if (fn == arrHandlers[i])
			++i;
	}
};


// PaLeg.Serializer ///////////////////////////////////////////////////////////
//
// Serializza operazioni che non devono essere eseguite simultaneamente.
//
PaLeg.Serializer = function() {
	this._m_arrQueue = [];
};
PaLeg.Serializer.prototype.busy = false;
PaLeg.Serializer.prototype._m_arrQueue = null;
PaLeg.Serializer.prototype._m_idTimeout = null;


// Annulla tutte le chiamate in attesa.
//
PaLeg.Serializer.prototype.cancelAll = function() {
	if (this._m_arrQueue.length)
		this._m_arrQueue = [];
	if (this._m_idTimeout) {
		window.clearTimeout(this._m_idTimeout);
		this._m_idTimeout = null;
	}
};


// Aggiunge in coda una chiamata a funzione.
//
PaLeg.Serializer.prototype.enqueue = function(fnCallback) {
	this._m_arrQueue.push(fnCallback);
	if (!this.busy) {
		this.busy = true;
		this._m_idTimeout = window.setTimeout(this._run.bind(this), 0);
	}
};


// Standard toString().
//
PaLeg.Serializer.prototype.toString = function() {
	return "[object PaLeg.Serializer]";
};


// Esegue una chiamata in attesa.
//
PaLeg.Serializer.prototype._run = function() {
	this._m_idTimeout = null;
	this._m_arrQueue.shift()();
	if (this._m_arrQueue.length)
		this._m_idTimeout = window.setTimeout(this._run.bind(this), 0);
	else
		this.busy = false;
};


// PaLeg.Sorting //////////////////////////////////////////////////////////////
//
// Classe per metodi di ordinamento e mantenimento di un ordinamento.
//
PaLeg.Sorting = {};


// Restituisce l'indice per l'inserimento ordinato di un elemento.
//
PaLeg.Sorting.insertionIndex = function(mValue, cItems, fnGetItem, fnCompare) {
	var iL = 0,
		 iU = cItems;
	while (iL < iU) {
		var iH = (iL + iU) >> 1;
			 iRes = fnCompare(mValue, fnGetItem(iH));
		if (iRes > 0)
			iL = iH + 1;
		else if (iRes < 0)
			iU = iH;
		else
			return iH;
	}
	return iL;
};


// Ordina gli elementi con un algoritmo generico.
//
PaLeg.Sorting.sort = function(cItems, fnGetItem, fnCompare, fnSetItem) {
	var arr = Array.fill(cItems, fnGetItem);
	arr.sort(fnCompare);
	for (var i = 0; i < cItems; ++i)
		fnSetItem(i, arr[i]);
};


// Ordina gli elementi con un algoritmo stabile.
//
PaLeg.Sorting.stableSort = function(cItems, fnGetItem, fnCompare, fnSetItem) {
	if (cItems > 1) {
		var fnMerge = function(arr, cItems) {
			if (cItems > 2) {
				var cItemsL = cItems >> 1,      arrL = arr.slice(0, cItemsL),
					 cItemsU = cItems - cItemsL, arrU = arr.slice(cItemsL);
				fnMerge(arrL, cItemsL);
				fnMerge(arrU, cItemsU);
				var i = 0, iL = 0, iU = 0;
				while (iL < cItemsL && iU < cItemsU)
					arr[i++] = (fnCompare(arrL[iL], arrU[iU]) <= 0 ? arrL[iL++] : arrU[iU++]);
				while (iL < cItemsL)
					arr[i++] = arrL[iL++];
				while (iU < cItemsU)
					arr[i++] = arrU[iU++];
			} else if (cItems == 2)
				if (fnCompare(arr[0], arr[1]) > 0) {
					var m = arr[0];
					arr[0] = arr[1];
					arr[1] = m;
				}
		};

		var cItemsL = cItems >> 1,      arrL = [],
			 cItemsU = cItems - cItemsL, arrU = [];
		arrL.length = cItemsL;
		arrU.length = cItemsU;
		// Sapendo che cItemsU - cItemsL <= 1, può riempire i due array
		// contemporaneamente, per poi prendere l'ultimo elemento del secondo,
		// se sono dispari.
		for (var i = 0; i < cItemsL; ++i) {
			arrL[i] = fnGetItem(i);
			arrU[i] = fnGetItem(cItemsL + i);
		}
		if (cItemsU > cItemsL)
			arrU[cItemsL] = fnGetItem(cItems - 1);
		fnMerge(arrL, cItemsL);
		fnMerge(arrU, cItemsU);
		var i = 0, iL = 0, iU = 0;
		while (iL < cItemsL && iU < cItemsU)
			fnSetItem(i++, fnCompare(arrL[iL], arrU[iU]) <= 0 ? arrL[iL++] : arrU[iU++]);
		while (iL < cItemsL)
			fnSetItem(i++, arrL[iL++]);
		while (iU < cItemsU)
			fnSetItem(i++, arrU[iU++]);
	}
};


// Standard toString().
//
PaLeg.Sorting.toString = function() {
	return "[object PaLeg.Sorting]";
};


// PaLeg.User /////////////////////////////////////////////////////////////////
//
// Rappresenta l'utente che sta eseguendo la pagina. Alcuni membri sono già
// stati dichiarati prima dell'inclusione di questo file.
//
//PaLeg.User = {};
//PaLeg.User.name = "";
//PaLeg.User._m_sPrivTokens = "";


// Verifica che l'utente abbia i privilegi richiesti.
//
PaLeg.User.checkPrivTokens = function(/*...*/) {
	for (var i = 0; i < arguments.length; ++i)
		if (this._m_sPrivTokens.indexOf(" " + arguments[i] + " ") == -1)
			return false;
	return true;
};


// Standard toString().
//
PaLeg.User.toString = function() {
	return "[object PaLeg.User]";
};



/*****************************************************************************
 * FUNCTIONS
 */


// Aggiunge un modulo PaLeg da inizializzare.
//
PaLeg.addModuleInit = function(fnCallback) {
	PaLeg.addModuleInit._arrModuleInits.push(fnCallback);
};
PaLeg.addModuleInit._arrModuleInits = [];


// Simile a window.alert(), ma permette fra l'altro di selezionare il
// contenuto, visto che è semplice markup.
//
PaLeg.alert = function(sTitle /*= null*/, mMessage) {
	if (mMessage === undefined) {
		mMessage = sTitle;
		sTitle = null;
	}
	var oAlertPopup, oAlert;
	if (oAlertPopup = arguments.callee._oAlertPopup)
		oAlert = arguments.callee._oAlert;
	else {
		oAlert = document.createSubtree(
			"div", {
				"id": "oAlert"
			},
				["hr"],
				["div", {
					"class": "ac"
				},
					["a", {
						"href":  "#"
					},
						"OK"
					]
				]
		);
		document.getBody().appendChild(oAlert);
		oAlertPopup = PaLeg.createInplacePopup("", oAlert);
		arguments.callee._oAlertPopup = oAlertPopup;
		arguments.callee._oAlert      = oAlert;
	}
	oAlertPopup.setPopupTitle(sTitle || "");
	if (mMessage.nodeType !== undefined)
		oAlert.insertBefore(mMessage, oAlert.firstChild);
	else {
		var oMessage = document.createElement("div");
		oAlert.insertBefore(oMessage, oAlert.firstChild);
		var arrLines = mMessage.toString().split("\r\n");
		for (var i = arrLines.length - 1; i >= 0; --i) {
			oMessage.appendChild(document.createTextNode(arrLines[i]));
			if (i > 0)
				oMessage.appendChild(document.createElement("br"));
		}
	}
	PaLeg.popupElement(oAlertPopup);
	var aOK = oAlert.lastChild.firstChild;
	aOK.addEventListener("click", function(e) {
		e.preventDefault();
		this.removeEventListener("click", arguments.callee, false);
		oAlertPopup.dismissPopup();
		oAlert.removeChild(oAlert.firstChild);
	}, false);
	aOK.focus();
};


// Per i tag implementati in JavaScript. Converte un parametro che può essere:
// • un tag contenitore da "raffinare" (elaborare in JavaScript); il tag è
//   specificato in sRefineable
// • un tag creato da zero
// • un array (o simile) di tag, creati da zero o equivalente (presi altrove)
// • una stringa o altro tipo primitivo
//
PaLeg.appendChildAny = function(oParent, mChild, sRefineable /*= undefined*/) {
	if (mChild.nodeType !== undefined)
		if (sRefineable && mChild.nodeType == document.ELEMENT_NODE && $eN(mChild).elemName == sRefineable) {
			// È un nodo da raffinare: sposta tutti gli elementi.
			var oChild;
			while (oChild = mChild.firstChild)
				oParent.appendChild(oChild);
		} else
			// È un nodo DOM, ma non era da raffinare: lo sposta per intero.
			oParent.appendChild(mChild);
	else if (mChild.length !== undefined && mChild.constructor != String)
		// È un array o simile: sposta tutti gli elementi.
		Array.forEach(mChild, function(o) {
			oParent.appendChild(o);
		});
	else
		// Non è un nodo DOM né un array o simile: testo semplice.
		oParent.appendChild(document.createTextNode(mChild.toString()));
};


// Aggiunge un'ombra all'elemento specificato.
//
PaLeg.appendShadow = function(o) {
	if ("Shadow" in PaLeg._aaXJT)
		document.createSubtree.apply(document, [o, null].concat(PaLeg._aaXJT["Shadow"]));
	return o;
};


// Esegue una richiesta asincrona e ne restituisce il risultato alla funzione
// specificata.
//
PaLeg.asyncExec = function(sUrl /*= location.href*/, aaParams /*= {}*/, onComplete /*= null*/, aaOptions /*= {}*/) {
	if (!aaOptions)
		aaOptions = {};
	var ar = new PaLeg.AsyncRequest;
	if (aaOptions.debugResponse)
		ar.debugResponse = true;
	if (aaOptions.debugCallback)
		ar.debugCallback = true;
	ar.submit(
		"POST",
		sUrl,
		aaParams,
		function(ar, oResult) {
			try {
				var sError;
				if (ar.status == PaLeg.AsyncRequest.STATUS_ABORTED)
					sError = null;
				else if (oResult === undefined)
					sError = Loc.PL_LOC_ERR_ASYNC_FAILED;
				else if (("errmsg" in oResult) && oResult["errmsg"] != "")
					sError = oResult["errmsg"];
				else if (("error" in oResult) && oResult["error"])
					sError = Loc.PL_LOC_ERR_ASYNC_FAILED;
				else
					sError = null;
				if (sError !== null && !aaOptions.quiet)
					alert(sError);
				if (onComplete)
					onComplete(sError === null, oResult);
			} catch (x) {
				PaLeg.logException(x, "PaLeg.asyncExec", {
					"oResult": oResult
				});
				throw x;
			}
		}
	);
};


// Invia un <input type="file"> senza ricaricare la pagina.
//
PaLeg.asyncUpload = function(input, sUrl /*= location.href*/, aaParams /*= {}*/, onComplete /*= null*/, aaOptions /*= {}*/) {
	if (!aaOptions)
		aaOptions = {};
	var bi = new PaLeg.BusyIndicator;
	bi.setBuddy(input);
	var ar = new PaLeg.AsyncRequest;
	ar.setFormElements(input);
	if (aaOptions.debugResponse)
		ar.debugResponse = true;
	if (aaOptions.debugCallback)
		ar.debugCallback = true;
	ar.submit(
		"POST",
		sUrl,
		aaParams,
		function(ar, oResult) {
			var xSaved = null;
			try {
				var sError;
				if (ar.status == PaLeg.AsyncRequest.STATUS_ABORTED)
					sError = null;
				else if (oResult === undefined)
					sError = Loc.PL_LOC_ERR_ASYNC_UPLOADFAILED;
				else if (("errmsg" in oResult) && oResult["errmsg"] != "")
					sError = oResult["errmsg"];
				else if (("error" in oResult) && oResult["error"])
					sError = Loc.PL_LOC_ERR_ASYNC_UPLOADFAILED;
				else
					sError = null;
				if (sError !== null && !aaOptions.quiet)
					alert(sError);
				if (onComplete)
					onComplete(sError === null, oResult);
			} catch (x) {
				xSaved = x;
				PaLeg.logException(x, "PaLeg.asyncUpload", {
					"oResult": oResult
				});
			}
			bi.parentNode.removeChild(bi);
			input.value = "";
			input.disable(false);
			if (xSaved)
				throw xSaved;
		}
	);
	input.disable(true);
};


// Per i tag implementati in JavaScript. Converte questo:
//
//   <tag>
//     <subtag class="pl_params">
//       <span title="param1">valore1</span>
//       <span title="param2">valore2 <!-- commento --></span>
//       <span title="param3"><valoretag1>vt1</valoretag1><valoretag2>vt1</valoretag2></span>
//     </subtag>
//     ...
//   </tag>
//
// in:
//
//   parent.param1 = "valore1";
//   parent.param2 = "valore2 ";
//   parent.param3 = [ <valoretag1>vt1</valoretag1>, <valoretag2>vt1</valoretag2> ];
//
PaLeg.convertElementParams = function(oContainer) {
	var o = oContainer.firstChild,
		 oOuterContainer = null,
		 oInnerContainer = null;
	while (o)
		if (o.nodeType == document.ELEMENT_NODE && $(o).isCssClass("pl_params")) {
			if (!oOuterContainer)
				oOuterContainer = o;
			oInnerContainer = o;
			o = o.firstChild;
		} else
			o = o.nextSibling;
	if (oInnerContainer) {
		$(oInnerContainer).getChildren("span").forEach((function(param) {
			var m = "", bText = true;
			for (var oChild = param.firstChild; oChild; oChild = oChild.nextSibling)
				if (oChild.nodeType == document.ELEMENT_NODE)
					if (bText) {
						bText = false;
						m = [oChild];
					} else
						m.push(oChild);
				else if (oChild.nodeType == document.TEXT_NODE && bText)
					m += oChild.nodeValue;
			if (bText)
				oContainer.setAttribute(param.getAttribute("title").toLowerCase(), m);
			else
				oContainer[param.getAttribute("title")] = m;
		}).bind(this));
		oOuterContainer.parentNode.removeChild(oOuterContainer);
	}
	return this;
};


// Crea un popup interno, usando il corrispondente modello XHTML, che
// sostituisce l'elemento specificato.
//
PaLeg.createInplacePopup = function(sTitle, mContents) {
	mContents = $(mContents);
	var oParent = mContents.parentNode,
		 oNextSibling = mContents.nextSibling;
		 oPopup = document.createSubtree.apply(document, PaLeg._aaXJT["InplacePopup"]);
	PaLeg.appendShadow(oPopup);
	oPopup.getDescendants("*", "class", "oPopupTitle")[0].appendChild(document.createTextNode(sTitle));
	(oPopup.isCssClass("oPopupContainer") ? oPopup : oPopup.getDescendants("*", "class", "oPopupContainer")[0]).appendChild(mContents);
	oParent.insertBefore(oPopup, oNextSibling);

	oPopup.setPopupTitle = arguments.callee._setPopupTitle;
	return oPopup;
};


// Assegna un titolo al popup interno.
//
PaLeg.createInplacePopup._setPopupTitle = function(sTitle) {
	this.getDescendants("*", "class", "oPopupTitle")[0].firstChild.nodeValue = sTitle;
};


// Invia il form specificato dopo una breve attesa.
//
function pl_delayedItemSel(form) {
	if (form.idSubmitTimeout)
		window.clearTimeout(form.idSubmitTimeout);
	form.idSubmitTimeout = window.setTimeout(function() {
		form.submit();
	}, Math.UI_SHORT_DELAY);
}


// Rende un elemento riposizionabile dall'utente. Le coordinate minime e
// massime possono essere cambiate anche da fuori, dopo aver chiamato questo
// metodo.
//
PaLeg.dragAllow = function(o, bFade, onDragging, minX, minY, maxX, maxY) {
	if (typeof(minX) != "number") minX = 0;
	if (typeof(minY) != "number") minY = 0;
	if (typeof(maxX) != "number") maxX = 999999999;
	if (typeof(maxY) != "number") maxY = 999999999;
	o.dragMinX = minX;
	o.dragMinY = minY;
	o.dragMaxX = maxX;
	o.dragMaxY = maxY;
	if (!o.draggable) {
		o.onDragMouseDown = function(e) {
			e.stopPropagation();
			e.preventDefault();
			o.dragStartX = e.clientX - parseInt(o.getStyle("left"));
			o.dragStartY = e.clientY - parseInt(o.getStyle("top"));
			if (bFade)
				o.addCssClass("blend_25");
			document.addEventListener("mousemove", o.onDragMouseMove, false);
			document.addEventListener("mouseup",   o.onDragMouseUp,   false);
		};


		o.onDragMouseMove = function(e) {
			e.stopPropagation();
			e.preventDefault();
			o.setStyle("left", Math.min(Math.max(o.dragMinX, e.clientX - o.dragStartX), o.dragMaxX - o.offsetWidth ) + "px");
			o.setStyle("top",  Math.min(Math.max(o.dragMinY, e.clientY - o.dragStartY), o.dragMaxY - o.offsetHeight) + "px");
			if (onDragging)
				onDragging(o);
		};


		o.onDragMouseUp = function(e) {
			e.stopPropagation();
			e.preventDefault();
			/* IE BUG: delete on a COM object's member. */
			o.dragStartX = undefined;
			o.dragStartY = undefined;
			if (bFade)
				o.removeCssClass("blend_25");
			document.removeEventListener("mousemove", o.onDragMouseMove, false);
			document.removeEventListener("mouseup",   o.onDragMouseUp,   false);
		};


		o.draggable = true;
		if (o.getCurrentStyle("position") != "absolute") {
			var xyThis = o.getPosition(o.offsetParent);
			o.setStyle("position", "absolute");
			o.setStyle("left",     xyThis[0] + "px");
			o.setStyle("top",      xyThis[1] + "px");
		}
		if (o.getCurrentStyle("cursor") == "text")
			o.setStyle("cursor", "pointer");
		o.addEventListener("mousedown", o.onDragMouseDown, false);
//		o.addEventListener("drag", Function.False, false);
	}
};


// Restituisce tutta la catena di nodi antenati di this, dal più vicino al
// più remoto.
//
PaLeg.getCommonAncestor = function(oTag1, oTag2) {
	var arrAncestors1 = $(oTag1).getAncestors().reverse(),
		 arrAncestors2 = $(oTag2).getAncestors().reverse(),
		 i = 0,
		 c = Math.min(arrAncestors1.length, arrAncestors2.length);
	while (i < c && arrAncestors1[i] == arrAncestors2[i])
		++i;
	return arrAncestors1[i - 1] || document.getBody();
};


// Esegue l'equivalente di un new su un elemento XHTML già esistente.
//
PaLeg.entangleElement = function(oTag, oClass) {
	for (var sProp in oClass.prototype)
		oTag[sProp] = oClass.prototype[sProp];
	oClass.call(oTag);
	return oTag;
};


/* Write debug messages to a browser-provided facility.
 *
 * String s
 *   Debug message.
 */
PaLeg.log = function(s) {
	if (window.console && window.console.log)
		window.console.log(s);
	if (window.opera)
		window.opera.postError(s);
};


/* Forward information on a JavaScript exception to be logged. For obvious
 * reasons, no code in this function is allowed to let an exception uncaught.
 *
 * var x
 *   Exception or String to be recorded.
 * String sFunction
 *   Function name, if specified.
 * [Object aaContext]
 *   Caller-specified context. It should be an object associating variable
 *   names to their respective values.
 */
PaLeg.logException = function(x, sFunction, aaContext /*= undefined*/) {
	try {
		sException = String.jsonEncode(x);
	} catch (xNested) {
		sException = "{\"JsonEncodeError\":true}";
	}

	if (aaContext !== undefined)
		/* Also send a caller-specified context. */
		try {
			sContext = String.jsonEncode(aaContext);
		} catch (xNested) {
			sContext = "{\"JsonEncodeError\":true}";
		}
	else
		sContext = "";

	try {
		(new PaLeg.AsyncRequest).submit(
			"POST",
			location.RROOTDIR + "pl_ar.php", {
				"a":   "ar_jsehl",
				"url": location.href,
				"fn":  sFunction,
				"x":   sException,
				"ctx": sContext
			}
		);
	} catch (xNested) {
		/* An exception while trying to report an exception. Great. */
	}
}


// Apre l'URL specificato, passandogli come ID la selezione corrente
// dell'elemento accanto all'<a> passato.
//
function pl_openID(a) {
	var sID = $(a).getAncestor("tr").getDescendants("select")[0].value;
	return sID != "" ? window.open(a.getAttribute("href").urlAddQA("id=" + sID), "_blank") : null;
}


// Apre un popup.
//
PaLeg.popup = function(vTarget, iWidth, iHeight) {
	window.open(typeof(vTarget) == "object" ? vTarget.getAttribute("href") : vTarget, "_blank", "width=" + iWidth + ",height=" + iHeight + ",resizable=1,scrollbars=1");
	return false;
};


// Apre il popup del filtro.
//
function pl_popupFilter(a) {
	window.open(a.getAttribute("href") + "&parent=" + encodeURIComponent(location.href), "pl_filterwnd", "width=400,height=150");
	return false;
}


// Mostra l'anteprima PaLegTags per il testo dell'elemento accanto all'<a>
// passato.
//
function pl_popupPLTPreview(a, bMultiline, iWidth, iHeight) {
	return PaLeg.popup(a.getAttribute("href") + "?text=" + encodeURIComponent($(a).getAncestor("tr").getDescendants(bMultiline ? "textarea" : "input")[0].value), iWidth, iHeight);
}


// Simula un popup con un tag posizionato in maniera assoluta. Con oSource =
// null, è molto simile ad alert().
//
// aaOptions:
//   dismissOnClickOut: true | false
//   dismissOnMouseOut: true | false (implica dismissOnClickOut)
//   dismissOnTimeout:  <ms> | false
//   fadeInDuration:    <ms> | true | false (true = predefinito)
//
PaLeg.popupElement = function(o, oSource /*= null*/, aaOptions /*= {}*/, iHDir /*= undefined*/, iVDir /*= undefined*/) {
	if (!aaOptions)
		aaOptions = {};
	if (!oSource) {
		aaOptions.dismissOnMouseOut = false;
		aaOptions.dismissOnClickOut = false;
	} else if (aaOptions.dismissOnMouseOut)
		aaOptions.dismissOnClickOut = true;
	if (aaOptions.fadeInDuration === true)
		aaOptions.fadeInDuration = Math.UI_SHORT_DELAY;
	aaOptions.modal = !oSource;

	if (o.dismissPopup)
		// Chiude il popup prima di mostrarlo di nuovo.
		o.dismissPopup();
	if (aaOptions.modal) {
		// Solo un popup modale per volta.
		if (PaLeg.popupElement.modalActive)
			return false;
		PaLeg.popupElement.modalActive = true;
	}
	++PaLeg.popupElement.active;

	// Prepara la parte grafica.
	PaLeg.positionElement(o, oSource, iHDir, iVDir);
	if (aaOptions.modal) {
		PaLeg.popupElement._oModalBgShade = document.createSubtree(
			"span", {
				"id":    "oModalPopupBgShade",
				"class": "fixedpos blend_75"
			}
		);
		document.getBody().appendChild(PaLeg.popupElement._oModalBgShade);
	}

	// Prepara la parte attiva.
	o.dismissPopup = PaLeg.popupElement._dismissPopup;
	o._onUnpopup   = PaLeg.popupElement._onUnpopup.bind(o);
	o._aaPopupOptions = aaOptions;
	if (aaOptions.dismissOnMouseOut) {
		o.addEventListener("mouseout",  PaLeg.popupElement._onPopupMouseOut,  false);
		o.addEventListener("mouseover", PaLeg.popupElement._onPopupMouseOver, false);
	}
	if (aaOptions.dismissOnTimeout)
		aaOptions.idTimeout = window.setTimeout(o._onUnpopup, aaOptions.dismissOnTimeout);
	if (aaOptions.dismissOnClickOut) {
		o.addEventListener("click", PaLeg.popupElement._onPopupClick, false);
		// Per evitare che l'eventuale click che ha aperto il popup, risalendo
		// fino al <body>, attivi o._onUnpopup().
		window.setTimeout(function() {
			document.documentElement.addEventListener("click", o._onUnpopup, false);
		}, Math.MIN_TIMEOUT);
	}

	// Mostra il popup.
	if (aaOptions.fadeInDuration)
		aaOptions.animation = new PaLeg.Animation(
			[
				[o, "visibility", "visible", null],
				[o, "opacity", null, ""]
			],
			[
				[o, "opacity", 0.00, 1.00]
			],
			aaOptions.fadeInDuration,
			function() {
				delete aaOptions.animation;
			}
		);
	else
		o.setStyle("visibility", "visible");
	return true;
};
PaLeg.popupElement.active = 0;				// != 0 se c'è almeno un elemento-popup attivo.
PaLeg.popupElement.modalActive = false;		// true se c'è un elemento-popup modale attivo.
PaLeg.popupElement._oModalBgShade = null;	// Velo scuro su tutta la pagina, per far risaltare un popup modale.


// Chiude il popup.
//
PaLeg.popupElement._dismissPopup = function() {
	var aaOptions = this._aaPopupOptions;
	if (aaOptions.animation)
		aaOptions.animation.stop();
	if (aaOptions.dismissOnMouseOut) {
		if (aaOptions.idMouseOutTimeout)
			// Un clic prima dello scadere del timer.
			window.clearTimeout(aaOptions.idMouseOutTimeout);
		this.removeEventListener("mouseout",  PaLeg.popupElement._onPopupMouseOut,  false);
		this.removeEventListener("mouseover", PaLeg.popupElement._onPopupMouseOver, false);
	}
	if (aaOptions.dismissOnTimeout)
		window.clearTimeout(aaOptions.idTimeout);
	if (aaOptions.dismissOnClickOut) {
		this.removeEventListener("click", PaLeg.popupElement._onPopupClick, false);
		document.documentElement.removeEventListener("click", this._onUnpopup, false);
	}

	this.setStyle("visibility", "hidden");
	this.setStyle("display",    "none");
	if (aaOptions.modal) {
		this.removeCssClass("fixedpos");
		PaLeg.popupElement._oModalBgShade.parentNode.removeChild(PaLeg.popupElement._oModalBgShade);
		PaLeg.popupElement._oModalBgShade = null;
		PaLeg.popupElement.modalActive = false;
	}

	--PaLeg.popupElement.active;
	/* IE BUG: delete on a COM object's member. */
	this.dismissPopup = undefined;
	this._onUnpopup = undefined;
	this._aaPopupOptions = undefined;
};


// Chiama this.dismissPopup(). Serve a permettere di ridefinire dismissPopup(),
// continuando a permetterne la chiamata come gestore di evento.
//
PaLeg.popupElement._onUnpopup = function() {
	return this.dismissPopup();
};


// Un clic sul popup va bloccato, per non causare la chiusura da clic fuori.
//
PaLeg.popupElement._onPopupClick = function(e) {
	e.stopPropagation();
};


// Attiva il timer per nascondere il popup una volta che il mouse s'è
// allontanato.
//
PaLeg.popupElement._onPopupMouseOut = function(e) {
	this._aaPopupOptions.idMouseOutTimeout = window.setTimeout(this._onUnpopup, Math.UI_SHORT_DELAY);
};


// Annulla il timer innescato da _onPopupMouseOut().
//
PaLeg.popupElement._onPopupMouseOver = function(e) {
	if (this._aaPopupOptions.idMouseOutTimeout) {
		window.clearTimeout(this._aaPopupOptions.idMouseOutTimeout);
		this._aaPopupOptions.idMouseOutTimeout = null;
	}
};


// Apre il popup della versione stampabile selezionata.
//
PaLeg.popupPrintVer = function(sPrintVer) {
	window.open(location.href.replace(/((?:\.php)?(?:[?#]|$))/, "_d_" + sPrintVer + "$1"), "_blank", "width=900,height=700,resizable=1,scrollbars=1,menubar=1");
};


// Colloca un elemento nella posizione desiderata, espressa relativamente ad un
// elemento già esistente o l'intero documento.
// I valori per iHDir e iVDir sono (esempi relativi a iVDir):
//   -2 Allinea il bordo inferiore di o col bordo superiore di oRelTo.
//   -1 Allinea il bordo inferiore di o col bordo inferiore di oRelTo.
//    0 Centra o rispetto a oRelTo.
//   +1 Allinea il bordo superiore di o col bordo superiore di oRelTo.
//   +2 Allinea il bordo superiore di o col bordo inferiore di oRelTo.
// Il risultato può sembrare anomalo se o è più basso/stretto di oRelTo.
//
// Indipendentemente dallo stile iniziale, l'elemento dopo la chiamata ha
// position: absolute o fixed (con class="fixedpos"), display: block e
// visibility: hidden.
//
PaLeg.positionElement = function(o, oRelTo /*= null*/, iHDir /*= undefined*/, iVDir /*= undefined*/) {
	if (oRelTo) {
		$(oRelTo);
		if (iHDir === undefined)
			iHDir = +1;
		if (iVDir === undefined)
			iVDir = +2;
		o.setStyle("position", "absolute");
	} else {
		iHDir = 0;
		iVDir = 0;
		o.removeStyle("position");
		o.addCssClass("fixedpos");
	}
	o.setStyle("left",       "0");
	o.setStyle("top",        "0");
	o.setStyle("visibility", "hidden");
	o.setStyle("display",    "block");
	o.setStyle("z-index",    "100");
	var iDir = [iHDir, iVDir],
		 xyO = [0, 0],
		 cxyO = [o.offsetWidth, o.offsetHeight],
		 xyDoc,
		 cxyDoc,
		 xyRelTo,
		 cxyRelTo;
	if (oRelTo) {
		cxyDoc = [document.documentElement.scrollWidth, document.documentElement.scrollHeight],
		xyRelTo = oRelTo.getPosition(PaLeg.getCommonAncestor(o, oRelTo));
		cxyRelTo = [oRelTo.offsetWidth, oRelTo.offsetHeight];
		if (xyRelTo[2]) {
			o.removeStyle("position");
			o.addCssClass("fixedpos");
			xyDoc = [0, 0];
		} else
			xyDoc = [document.documentElement.scrollLeft, document.documentElement.scrollTop];
	} else {
		xyDoc = [0, 0];
		xyRelTo = xyDoc;
		if (true) {
/*@cc_on
			cxyDoc = [document.documentElement.clientWidth, document.documentElement.clientHeight],
			cxyRelTo = cxyDoc;
		} else {
@*/
			cxyDoc = [document.documentElement.scrollWidth, document.documentElement.scrollHeight],
			cxyRelTo = [window.innerWidth, window.innerHeight];
		}
	}
	for (var i = 0; i <= 1; ++i) {
		if (!cxyO[i])
			// Se non è riuscito a calcolare offset*, prova con lo stile.
			cxyO[i] = parseFloat((o.getCurrentStyle(i == 0 ? "width" : "height").toString().match(/^(.+)px$/) || {1: "0"})[1]);
		switch (iDir[i]) {
			case -2: xyO[i] = xyRelTo[i] - cxyO[i]; break;
			case -1: xyO[i] = xyRelTo[i] - cxyO[i] + cxyRelTo[i]; break;
			case  0: xyO[i] = xyRelTo[i] - Math.round((cxyO[i] - cxyRelTo[i]) * 0.5); break;
			case +1: xyO[i] = xyRelTo[i]; break;
			case +2: xyO[i] = xyRelTo[i] + cxyRelTo[i]; break;
		}
		if (xyRelTo[2]) {
			if (xyO[i] > xyDoc[i] + cxyDoc[i] - cxyO[i])
				xyO[i] = xyDoc[i] + cxyDoc[i] - cxyO[i] - Math.ceil(cxyDoc[i] * 0.02);
			if (xyO[i] < xyDoc[i])
				xyO[i] = xyDoc[i] + Math.ceil(cxyDoc[i] * 0.02);
		}
	}
	o.setStyle("left", xyO[0] + "px");
	o.setStyle("top",  xyO[1] + "px");
};


// Assegna la data corrente al campo specificato.
//
function pl_setToday(sIDInput, sFormat) {
	var input = $(sIDInput);
	input.value = (new Date).format(sFormat);
	input.focus();
}


// Disattiva tutti i pulsanti di tipo submit e reset nel form.
//
PaLeg.submitWithDebouncer = function(form) {
	form.addEventListener("submit", arguments.callee._onSubmit, false);
};
PaLeg.submitWithDebouncer._onSubmit = function(e) {
	var arrButtons = (this.getDescendants("button") || []);
	arrButtons.forEach(function(button) {
		if (button.getAttribute("type") == "submit" && !button.getAttribute("disabled")) {
			button.disable(true);
			button._disabledByDebouncer = true;
		}
	});
	window.setTimeout(function() {
		arrButtons.forEach(function(button) {
			if (button.getAttribute("type") == "submit" && button._disabledByDebouncer) {
				button.disable(false);
				/* IE BUG: delete on a COM object's member. */
				button._disabledByDebouncer = undefined;
			}
		});
		return true;
	}, Math.UI_SHORT_DELAY);
	return true;
};


// Attiva il tasto Invio per l'oggetto specificato.
//
PaLeg.submitWithReturn = function(o, vTarget, bCtrlKey) {
	if (!vTarget && !(vTarget = o.form))
		return false;
	o.pl_submitWithReturnHandler = arguments.callee._onKeyDown.bind(null, vTarget, bCtrlKey);
	o.addEventListener("keydown", o.pl_submitWithReturnHandler, false);
	return true;
};
PaLeg.submitWithReturn._onKeyDown = function(vTarget, bCtrlKey, e) {
	if (e.keyCode == 0x0A /*LF*/ || e.keyCode == 0x0D /*CR*/ && (!bCtrlKey || e.ctrlKey)) {
		e.preventDefault();
		if ($eN(vTarget).elemName == "form") {
			e = document.createEvent("HTMLEvents");
			e.initEvent("submit", true, true);
			if (vTarget.dispatchEvent(e))
				vTarget.submit();
		} else {
			e = document.createEvent("MouseEvents");
			e.initMouseEvent("click", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, undefined);
			vTarget.dispatchEvent(e);
		}
	}
};


// Ponte fra il DOM e XPath (eventualmente implementato a mano).
//
PaLeg._getEltsBy = function(oParent, sElemName, sAttr, vFilter, bChildrenOnly) {
	var arr,
		 bClass = (sAttr == "class");
	if (document.evaluate && (!sAttr || !(vFilter instanceof RegExp) || vFilter.isSimple())) {
		if (sAttr) {
			var bBegin = true,
				 bEnd = true;
			if (vFilter instanceof RegExp) {
				vFilter = vFilter.source;
				if (vFilter.charAt(0) == "^")
					vFilter = vFilter.substr(1);
				else
					bBegin = false;
				if (vFilter.charAt(vFilter.length - 1) == "$")
					vFilter = vFilter.substr(0, vFilter.length - 1);
				else
					bEnd = false;
				vFilter = RegExp.unescape(vFilter);
			}
			if (bBegin)
				if (bEnd)
					vFilter = (bClass ? "[contains(concat(' ', @class, ' '), ' " + vFilter + " ')]"
											: "[@" + sAttr + "='" + vFilter + "']");
				else
					vFilter = (bClass ? "[contains(concat(' ', @class), ' " + vFilter + "')]"
											: "[starts-with(@" + sAttr + ", '" + vFilter + "')]");
			else
				if (bEnd)
					vFilter = (bClass ? "[contains(concat(@class, ' '), '" + vFilter + " ')]"
											: "[ends-with(@" + sAttr + ", '" + vFilter + "')]");
				else
					vFilter =           "[contains(@" + sAttr + ", '" + vFilter + "')]";
		} else
			vFilter = "";
		var q = document.evaluate(
			(bChildrenOnly ? "./html:" : ".//html:") + sElemName + vFilter,
			oParent,
			arguments.callee._prefixToNamespace,
			XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
			null
		);
		arr = [];
		arr.length = q.snapshotLength;
		for (var i = 0; i < arr.length; ++i)
			arr[i] = $(q.snapshotItem(i));
	} else {
		if (sAttr) {
			if (bClass)
				if (vFilter instanceof RegExp) {
					vFilter = vFilter.toString().match(/^\/(.*)\/([a-z]*)$/);
					vFilter = new RegExp("(^|\\s)" + vFilter[1] + "(\\s|$)", vFilter[2]);
				} else
					vFilter = new RegExp("(^|\\s)" + vFilter + "(\\s|$)");

			// Converte vFilter in una funzione.
			vFilter = (function(vFilter) {
				return vFilter instanceof RegExp
					? function(o) {
						return $(o).hasAttribute(sAttr) && vFilter.test(o.getAttribute(sAttr));
					}
					: function(o) {
						return $(o).hasAttribute(sAttr) && o.getAttribute(sAttr) == vFilter;
					};
			})(vFilter);
		} else
			// $() restituisce sempre non-null per non-null, quindi
			// Array.filter() convertirà la HTMLCollection in array.
			vFilter = $;

		if (bChildrenOnly) {
			var bAny = (sElemName == "*");
			arr = [];
			for (var i = 0, o = oParent.firstChild; o; o = o.nextSibling)
				if (o.nodeType == document.ELEMENT_NODE && (bAny || $eN(o).elemName == sElemName) && vFilter(o))
					arr[i++] = o;
		} else {
			arr = null;
			if (oParent == document)
				// Alcuni tag sono già raccolti dal browser in array.
				switch (sElemName) {
					case "a":    arr = document.links; break;
					case "form": arr = document.forms; break;
					case "img":  arr = document.images; break;
				}
			if (arr === null) {
				if (!$eN.noop)
					sElemName = sElemName.toUpperCase();
				arr = oParent.getElementsByTagName(sElemName);
			}
			arr = Array.filter(arr, vFilter);
		}
	}
	return arr;
};


// Converte un namespace in URI.
//
PaLeg._getEltsBy._prefixToNamespace = function(sPrefix) {
	switch (sPrefix) {
		case "html": return "http://www.w3.org/1999/xhtml";
		case "svg":  return "http://www.w3.org/2000/svg";
		case "xml":  return "http://www.w3.org/XML/1998/namespace";
		default:     return sPrefix;
	}
};


// Converte un errore in una pseudo-eccezione da inviare al server. Funziona
// solo in IE, e in FX negli script a livello globale.
//
PaLeg._onError = function(sError, sFileName, iLine) {
	// FX ogni tanto genera questo errore. Probabilmente in seguito prova di
	// nuovo, perché non ne seguono altri.
	if (sError != "Error loading script")
		PaLeg.logException({
			name:       "onerror",
			message:    sError,
			fileName:   sFileName,
			lineNumber: iLine
		}, "window");
	return PaLeg.User.checkPrivTokens("DBGU") || PaLeg.User.checkPrivTokens("DBGA") ? undefined : true;
};
window.onerror = PaLeg._onError;


/* Initializes the page as soon as the DOM tree has been fully loaded.
 */
PaLeg._onInit = function(sSource) {
	if (arguments.callee._bDone)
		return;
	arguments.callee._bDone = true;
//	alert(sSource);

	/* Initializes modules. */
	for (var i = 0; i < PaLeg.addModuleInit._arrModuleInits.length; ++i)
		PaLeg.addModuleInit._arrModuleInits[i]();

	/* Fire the earlyload event immediately. */
	var e = document.createEvent("Events");
	e.initEvent("earlyload", false, false);
	try {
		document.dispatchEvent(e);
	} catch (x) {
		PaLeg.logException(x, "PaLeg._onInit$earlyload", {
			"sSource": sSource
		});
	}

	var onLoad = function() {
		window.onload = Function.Empty;
		var e = document.createEvent("Events");
		e.initEvent("lateload", false, false);
		try {
			document.dispatchEvent(e);
		} catch (x) {
			PaLeg.logException(x, "PaLeg._onInit$lateload", {
				"sSource": sSource
			});
		}
	};
	/* If this has been invoked during an onload event, there's not going to be
	 * a second one, so the lateload event-firing handler must be called
	 * immediately too. */
	if (sSource == "load")
		onLoad();
	else
		window.onload = onLoad;
};
/* Request a call to PaLeg._onInit() in any possible way.
 * Some ideas by Dean Edwards, Matthias Miller, John Resig, Diego Perini. */
/* Warning: the following if implies PaLeg.iefix.js can only be included AFTER
 * this file. */
if (document.addEventListener)
	/* Standard-compliant method. */
	document.addEventListener("DOMContentLoaded", function(e) {
		document.removeEventListener(e.type, arguments.callee, false);
		PaLeg._onInit(e.type);
	}, false);
else {
	if (navigator.vendor == "KDE")
		/* Poll document.readyState. */
		PaLeg._onInit._idKhtmlTimer = window.setInterval(function() {
			if (document.readyState == "loaded" || document.readyState == "complete") {
				window.clearInterval(PaLeg._onInit._idKhtmlTimer);
				PaLeg._onInit("KhtmlTimer");
			}
		}, 20);
	/* The following method exploits the fact that IE's doScroll throws an
	 * exception unless the document tree is fully loaded; so, when that
	 * doesn't happend, the document is ready. */
/*@cc_on
	else
		(function() {
			if (PaLeg._onInit._idIETimer)
				window.clearTimeout(PaLeg._onInit._idIETimer);
			try {
				document.documentElement.doScroll("left");
				PaLeg._onInit("doScroll");
			} catch (x) {
				PaLeg._onInit._idIETimer = setTimeout(arguments.callee, 0);
			}
		})();
@*/
	/* Old-fashioned onload. This is actually late, but it's better than
	 * nothing. */
	window.onload = function(e) {
		window.onload = Function.Empty;
		PaLeg._onInit("load");
	};
}


/* Enables PaLeg functionality for the page.
 */
PaLeg.addModuleInit(function() {
	/* Synthetic invocation of $(); this is necessary since document doesn't
	 * have a nodeName member, nor style, and the following line is the only
	 * one that would apply anyway.
	 * This should actually go in PaLeg.ext.js, but that cannot yet rely on
	 * PaLeg.addModuleInit() as it's loaded first, so this must stay here. */
	Object.merge(document, $._aaMethods);
	$(document.documentElement);

	/* application/xhtml+xml does not mandate implementations provide
	 * document.body; this makes up for it. It cannot be done by simply
	 * assigning document.body = document.getDescendants("body")[0], FX3 won't
	 * like it. */
	document.getBody = function() {
		return document.body || document.getDescendants("body")[0];
	}

	document.getDescendants("select").forEach(function(select) {
		// Supporto per .enableAutoSubmit().
		select.previousSelectedIndex = select.selectedIndex;
		// Applica a tutte le <select>.
		PaLeg.submitWithReturn(select);
	});

	// Applica a tutti i <form>, esclusi quelli asincroni.
	document.getDescendants("form").forEach(function(form) {
		if (!form.isCssClass("pl_form"))
			PaLeg.submitWithDebouncer(form);
	});

	// Applica a tutte le <textarea>.
	document.getDescendants("textarea").forEach(function(textarea) {
		PaLeg.submitWithReturn(textarea, null, true);
	});

	// Supporto per .enableAutoSubmit().
	// document.getDescendants("select") già fatto sopra.
	document.getDescendants("input", "type", "checkbox").forEach(function(chk) {
		chk.previousChecked = chk.checked;
	});
});
