// URL Serialization/Restoration implementation for JavaX
// Require: prototype.js
// Require: types.js
// Require: aserializer.js
// Bugs: Grep TODOs, NOTEs, FIXMEs in the code

// Known issues:
// undefined & null are serialized in the same way (asymmetricity)

// ------------------------------------------------------------
// Helper class for serialization handling
// ------------------------------------------------------------
var URLContextEntry = Class.create({
	initialize: function(name, status) {
		this.name_ = name;
		this.status_ = status;
	},
	Name: function() { return this.name_; },
	SetName: function(name) { this.name_ = name; },
	Status: function() { return this.status_; },
	SetStatus: function(status) { this.status_ = status; },
	Print: function() { return "N:"+this.name_+",S:"+this.status_+"; "; }
});

function URLRestore(sBuffer) {
	var oRestorer = new AURLRestorer(sBuffer);
	return oRestorer.Restore();
}

function URLRestoreV(sBuffer) {
	var sVBuffer = "Value:"+sBuffer+"&";
	return URLRestore(sVBuffer);
}

function URLRestoreVS(sBuffer) {
	var sVSBuffer = "Value:Structure:"+sBuffer+"&&";
	return URLRestore(sVSBuffer);
}

function URLSerialize(oObj) {
	var oSerializer = new AURLSerializer();
	oSerializer.Serialize(oObj);
	return oSerializer.Buffer();
}

function URLSerializeV(oObj) {
	var sV = URLSerialize(oObj);
	// Cut 'Value:' and '&'
	return sV.substr(6, sV.length-7);
}

function URLSerializeVS(oObj) {
	var sVS = URLSerialize(oObj);
	// Cut 'Value:Structure:' and '&&'
	return sVS.substr(16, sVS.length-16-2);
}

String.prototype.Value = function() {
	return this;
}

// ------------------------------------------------------------
// Restorer class
// ------------------------------------------------------------
var AURLRestorer = Class.create({
initialize: function(buffer) {
	this.buffer_ = (buffer)?buffer:"";
	this.pos_ = 0;
	this.context_ = new Array();
	
	this.BEGIN = 0;
	this.BEFOREKEY = 1;
	this.AFTERKEY = 2;
	this.END = 3;
	this.BEGIN_ANONYM = 4;
	this.END_ANONYM = 5;
},
sh: function(len) {
	this.pos_ += len;
},
setEnd: function() {
	var oBack = null;
	if (this.context_.length != 0) oBack = this.context_.last();
	if (oBack && oBack.Name() == "Value" && oBack.Status() == this.BEGIN) oBack.SetStatus(this.END);
	else if(oBack && oBack.Name() == "Value" && oBack.Status() == this.BEGIN_ANONYM) oBack.SetStatus(this.END_ANONYM);
},
invalidContext: function(sTrace, typeName) {
	alert(sTrace+": Invalid restoring context for the type '"+typeName+
		"'.\nBuffer is '"+this.buffer_+"'.\n Passed part: '"+this.buffer_.substr(0, this.pos_));
},
BeginType: function(typeName) {
	var oBack = null;
	if (this.context_.length != 0) oBack = this.context_.last();
	if (oBack) {
		if (typeName == "Value") {
			if (oBack.Name() == "Structure" && oBack.Status() == this.AFTERKEY) {
				oBack.SetStatus(this.BEFOREKEY);
				this.context_.push(new URLContextEntry(typeName, this.BEGIN_ANONYM));
				return;
			} else if (oBack.Name() == "Array") {
				this.context_.push(new URLContextEntry(typeName, this.BEGIN_ANONYM));
				return;
			}
		}
	}
	var len = typeName.length;
	if (len < 1 || this.buffer_.substr(this.pos_, len) != typeName) {
		this.invalidContext("BeginType", typeName);
		return;
	}
	this.sh(len+1);
	this.context_.push(new URLContextEntry(typeName, this.BEGIN));
	if (typeName == "Structure") {
		this.context_.last().SetStatus(this.BEFOREKEY);
	}
},
EndType: function(typeName) {
	if (this.context_.length == 0) {
		this.invalidContext("EndType:begin", typeName);
		return;
	}
	var oBack = this.context_.last();
	if (typeName == "Value") {
		if( oBack.Status() == this.END ) {
			/*if( this.buffer_.charAt(this.pos_)=='&' )*/
			this.sh(1);
			this.context_.pop();
			return;
		}
		else if( oBack.Status() == this.END_ANONYM ) {
			this.context_.pop();
			return;
		}
	}
	if (this.pos_ < this.buffer_.length && this.buffer_.charAt(this.pos_) != '&') {
		this.invalidContext("EndType:end", typeName);
		return;
	}
	this.sh(1);
	this.context_.pop();
	this.setEnd();
},
GetType: function() {
	this.bNull = false;
	if (this.pos_ >= this.buffer_.length) return "";
	if (this.buffer_.charAt(this.pos_) == '&') return "";
	var oBack = null;
	if (this.context_.length != 0) oBack = this.context_.last();
	if (oBack) {
		if (oBack.Name() == "Structure" && oBack.Status() == this.BEFOREKEY) {
			return "string";
		} else if (oBack.Name() == "Structure" && oBack.Status() == this.AFTERKEY) {
			return "Value";
		} else if (oBack.Name() == "Array") {
			return "Value";
		} else if (oBack.Name() == "Value" && (oBack.Status()==this.END || oBack.Status()==this.END_ANONYM )) {
			return "";
		}
	}
	var nP = this.buffer_.indexOf(':', this.pos_);
	if (nP < 0) return "";
	var typeName = this.buffer_.substr(this.pos_, nP-this.pos_);
	if (typeName == "i") return "int";
	if (typeName == "l") return "int64_t";
	if (typeName == "s") return "string";
	if (typeName == "b") return "base64";
	if (typeName == "null") {
		this.bNull = true;
		return "";
	}
	if (typeName == "d") return "double";
	return typeName;
},
restoreSimpleType: function(T) {
	var TC = T + ":";
	if (this.buffer_.substr(this.pos_, TC.length) != TC) {
		this.invalidContext("restoreSimpleType:type", typeName);
		return null;
	}
	var nE = this.buffer_.indexOf('&', this.pos_);
	if (nE < 0) {
		this.invalidContext("restoreSimpleType:end", typeName);
		return null;
	}
	var nB = this.pos_+TC.length;
	var sVal = this.buffer_.substr(nB,nE-nB);
	var oVal = null;
	if (T == "i") oVal = new Int(sVal);
	else if (T == "d") {
		// TODO: proper handling of NaN value
		if (sVal == "NaN") oVal = NaN;
		else oVal = new Double(sVal);
	}
	else if (T == "l") oVal = new Int64(sVal);
	else if (T == "null") {
		// default value
	}
	else if (T == "b") {
		oVal=new Blob(sVal);
	} else {
		throw new Error("Unknown simple type! ");
	}
	this.sh(nE-nB+TC.length+1);
	this.setEnd();
	return oVal;
},
RestoreInt: function() {
	return this.restoreSimpleType("i");
},
RestoreInt64: function() {
	return this.restoreSimpleType("l");
},
RestoreDouble: function() {
	return this.restoreSimpleType("d");
},
RestoreBool: function() {
	this.BeginType("bool");
	var v=this.RestoreInt().Value();
	this.EndType("bool");
	if (v==0) return false;
	else if (v==1) return true;
	else throw new Error("Invalid boolean value. ");
},
RestoreBinary: function() {
	return this.restoreSimpleType("b");
},
RestoreNull: function() {
	return this.restoreSimpleType("null");
},
RestoreString: function() {
	var nE = -1;
	var nB = this.pos_;
	var oBack = null;
	if (this.context_.length != 0) oBack = this.context_.last();
	if (oBack && oBack.Name() == "Structure" && oBack.Status() == this.BEFOREKEY) {
		nE = this.buffer_.indexOf('=', nB);
		oBack.SetStatus(this.AFTERKEY);
	} else {
		if (this.buffer_.substr(nB, 2) != "s:") {
			this.invalidContext("RestoreString:begin", "string");
			return null;
		}
		nB += 2;
		// TODO: BAD: this changes stream state, however, restoration can fail after that
		this.sh(2);
		nE = this.buffer_.indexOf('&', nB);
	}
	if (nE < 0) {
		this.invalidContext("RestoreString:end", "string");
		return null;
	}
	var sVal = this.urlUnescape(this.buffer_.substr(nB, nE-nB));
	this.sh(nE-nB + 1); // string length + '=' or '&'
	this.setEnd();
	return sVal;
},
RestoreCompound: function() {
	var oObj = null;
	this.BeginType("Value");
	var typeName = this.GetType();
	if (typeName == "Structure") {
		oObj = new Object();
		this.BeginType("Structure");
		while (this.GetType() == "string") {
			var sKey = this.RestoreString();
			//this.BeginType("Value");
			oObj[sKey] = this.Restore();
			//this.EndType("Value");
		}
		this.EndType("Structure");
	} else if (typeName == "Array") {
		oObj = new Array();
		this.BeginType("Array");
		while (this.GetType() == "Value") {
			//this.BeginType("Value");
			oObj.push(this.Restore());
			//this.EndType("Value");
		}
		this.EndType("Array");
	} else if (!typeName) {
		if (this.bNull) this.RestoreNull();
		// for empty Values
		oObj = null;
	} else {
		oObj = this.Restore();
	}
	this.EndType("Value");
	return oObj;
},
Restore: function() {
	return RestorerIF.restore(this);
},
Buffer: function() {
	return this.buffer_;
},
urlUnescape: function(sVal) {
	if (!sVal) return "";
	return sVal.replace(/\\a/g, "&")
		.replace(/\\d/g, "#")
		.replace(/\\w/g, " ")
		.replace(/\\p/g, "%")
		.replace(/\\b/g, "`")
		.replace(/\\s/g, "\\");
}
});

// ------------------------------------------------------------
// Serializer class
// ------------------------------------------------------------
var AURLSerializer = Class.create({
initialize: function() {
	this.buffer_ = "";
	this.context_ = Array();
	
	this.BEGIN = 0;
	this.BEFOREKEY = 1;
	this.AFTERKEY = 2;
	this.END = 3;
},
Buffer: function() {
	return this.buffer_;
},
setEnd: function() {
	var oBack = null;
	if (this.context_.length != 0) oBack = this.context_.last();
	if (oBack && oBack.Name() == "Value" && oBack.Status() == this.BEGIN) oBack.SetStatus(this.END);
},
invalidContext: function(sTrace, typeName) {
	alert(sTrace+": Invalid restoring context for the type '"+typeName+
		"'.\nBuffer is '"+this.buffer_+"'.\n Passed part: '"+this.buffer_.substr(0, this.pos_));
},
BeginType: function (typeName) {
	var oBack = null;
	if (this.context_.length != 0) oBack = this.context_.last();
	if (oBack) {
		if (typeName == "Value") {
			if (oBack.Name() == "Structure" && oBack.Status() == this.AFTERKEY) {
				oBack.SetStatus(this.BEFOREKEY);
				// can be optimized...
				this.context_.push(new URLContextEntry(typeName, this.BEGIN));
				return;
			} else if (oBack.Name() == "Array") {
				this.context_.push(new URLContextEntry(typeName, this.BEGIN));
				return;
			}
		}
	}
	this.buffer_ += typeName;
	this.buffer_ += ":";
	oBack = new URLContextEntry(typeName, this.BEGIN);
	this.context_.push(oBack);
	if (typeName == "Structure") {
		oBack.SetStatus(this.BEFOREKEY);
	}
},
EndType: function (typeName) {
	if (!this.context_.length) {
		this.invalidContext("EndType", typeName);
		return;
	}

	var oBackStatus = this.context_.last().Status();
	this.context_.pop();
	var back = this.context_.last();
	
	if (typeName != "Value" || oBackStatus != this.END || !back ||
		(back.Name() != "Array" & back.Name() != "Structure"))
	{
		this.buffer_ += "&";
		this.setEnd();
	}
},
PrintContext: function() {
	var c = "";
	for (var i = 0; i < this.context_.length; i++) {
		c += this.context_[i].Print();
	}
	LogL(c);
},
SerializeObject: function(oObj) {
	var prevName = (this.context_.length ? this.context_.last().Name() : "");
	var inCompound = (prevName == "Array" || prevName == "Structure");

	if (!inCompound) this.BeginType("Value");

	var nCount = 0;
	for (var sKey in oObj) {
		if (typeof(oObj[sKey]) == "function") continue;
		++nCount;
	}
	if (nCount) {
		this.BeginType("Structure");
		for (var sKey in oObj) {
			if (typeof(oObj[sKey]) == "function") continue;
			var oItem = oObj[sKey];
			// put key
			this.Serialize(String(sKey));
			// put value as Value
			if (typeof(oItem) != 'undefined' && oItem !== null) { // NULL-FIX
				this.Serialize(oItem, true);
			}
			else {
				this.buffer_ += 'null:&'; // NULL-FIX
			}
			this.context_.last().SetStatus( this.BEFOREKEY );
		}
		this.EndType("Structure");
	} else {
		this.buffer_ += 'null:&'; // NULL-FIX
	}

	if (!inCompound) this.EndType("Value");
},
SerializeArray: function(oObj) {
	var prevName = (this.context_.length ? this.context_.last().Name() : "");
	var inCompound = (prevName == "Array" || prevName == "Structure");

	if (!inCompound) this.BeginType("Value");

	this.BeginType("Array");
	for (var i = 0; i < oObj.length; i++) {
		var oItem = oObj[i];
		// put value as Value
		if (typeof(oItem) != 'undefined' && oItem !== null) { // NULL-FIX
			this.Serialize(oItem, true);
		}
		else {
			this.buffer_ += 'null:&';
		}
	}
	this.EndType("Array");

	if (!inCompound) this.EndType("Value");
},
SerializeString: function(oObj) {
	var oBack = null;
	if (this.context_.length != 0) oBack = this.context_.last();
	if (oBack && oBack.Name() == "Structure" && oBack.Status() == this.BEFOREKEY) {
		this.buffer_ += String(oObj);
		this.buffer_ += "=";
		oBack.SetStatus(this.AFTERKEY);
	} else {
		this.buffer_ += "s:";
		this.buffer_ += String(oObj)
				.replace(/\\/g, "\\s")
				.replace(/&/g, "\\a")
				.replace(/#/g, "\\d")
				.replace(/ /g, "\\w")
				.replace(/%/g, "\\p")
				.replace(/`/g, "\\b");
		this.buffer_ += "&";
	}
	this.setEnd();
},
SerializeDouble: function(oObj) {
	this.buffer_ += "d:";
	if (oObj == NaN) this.buffer_ += "NaN";
	else this.buffer_ += String(oObj);
	this.buffer_ += "&";
	this.setEnd();
},
SerializeBool: function(oObj) {
	this.BeginType("bool");
	var iv=(oObj==true) ? 1 : 0;
	this.Serialize(iv);
	this.EndType("bool");
},
SerializeInt: function(oObj) {
	this.buffer_ += "i:";
	this.buffer_ += String(oObj);
	this.buffer_ += "&";
	this.setEnd();
},
SerializeInt64: function(oObj) {
	this.buffer_ += "l:";
	this.buffer_ += oObj.toString();
	this.buffer_ += "&";
	this.setEnd();
},
SerializeBinary: function(oObj) {
	this.buffer_ += "b:";
	this.buffer_ += Encoder.Base64Encode(Encoder.UTF8Encode(oObj));
	this.buffer_ += "&";
	this.setEnd();
},
// generic serialization interface
Serialize: function(oObj, bValueElement) {
	SerializerIF.serialize(this, oObj, bValueElement);
}
});

