/*
  Author: Simon Weijgers <simon.weijgers@hotlizard.net>
  Purpose:
 	- toSWONString serializes javascript datatypes to SWON (Simon Weijgers Object Notation) - very similar to JSON (JavaScript Object Notation)
 		(* only a subset of datatypes are supported:  
 			arrays, objects, numbers (except for NaN, Infinity), booleans, nulls, oh and, strings, of course, oh and dates as well
 	- parseSWONString deserializes SWON text back to javascript data types
*/

// Example:
// 	toSWONString([function () {}(),null,1,2,3,{a:5,b:[null, new Date]},4]) -> "[Empty,Null,1,2,3,{'a':5,'b':[Null,D20070710T12:09:04]},4]"
function toSWONString(o)
{
	var parts, i, key;
	
	if (o === true) {
		return "True";
	} else if (o === false) {
		return "False";
	} else if (typeof(o) === "string" || o instanceof String) {
		return "'" + o.replace(/'/g, "''") + "'";
	} else if (typeof(o) === "number" || o instanceof Number) {
		if (o===Infinity || o===-Infinity || isNaN(o)) {
			throw "swon.js: toSWONString: can't serialize Infinity/-Infinity/NaN";
		} else {
			return "" + o;
		}
	} else if (o instanceof Date) {
		return "D" +
				o.getFullYear() +
				((o.getMonth()+1)<10?'0'+(o.getMonth()+1):''+(o.getMonth()+1)) +
				(o.getDate()<10?'0'+o.getDate():''+o.getDate()) + 'T' +
				(o.getHours()<10?'0'+o.getHours():''+o.getHours()) + ':' +
				(o.getMinutes()<10?'0'+o.getMinutes():''+o.getMinutes()) + ':' +
				(o.getSeconds()<10?'0'+o.getSeconds():''+o.getSeconds());
	} else if (o instanceof Array) {
		parts = [];
		
		for (i=0; i<o.length; i++) {
			parts.push(toSWONString(o[i]));
		}
		
		return "[" + parts.join(',') + "]";
	} else if (o instanceof Object) {
		parts = [];
		
		for (key in o) {
			if (o.hasOwnProperty(key)) {
				parts.push(toSWONString(key) + ':' + toSWONString(o[key]));
			}
		}
		
		return "{" + parts.join(',') + "}";
	} else if (typeof(o) === "undefined") {
		return "Empty";
	} else if (o === null) {
		return "Null";
	} else {
		throw "swon.js: ToSWONString: encountered value with a data type I don't know how to serialize";
	}
}

var parseSWONString = function () {  // translated from vbscript (fnSWON.asp/Class SWONParser)
	var mlngCharIndex;
	var mstrSWONString;
		
	var len = function (pstr) {
		return pstr.length;
	};
	
	var mid = function (pstr, startidx, len) {
		if (typeof(len)==='undefined') {
			return pstr.substr(startidx-1);
		} else {
			return pstr.substr(startidx-1, len);
		}
	};
	
	var uCase = function (pstr) {
		return pstr.toUpperCase();
	};

	var lCase = function (pstr) {
		return pstr.toLowerCase();
	};
	
	var split = function (pstr, delimiter) {
		return pstr.split(delimiter);
	};
				
	var isEOS = function () {
		return mlngCharIndex > len(mstrSWONString);
	};
	
	var getCurrentChar = function () {
		if (isEOS()) {
			throw "swon.js:ParseSWONString:ParseError: unexpected end of string";
		}
		return mid(mstrSWONString, mlngCharIndex, 1);
	};
	
	var getCharsAndAdvanceIdx = function (n) {
		var str =  mid(mstrSWONString, mlngCharIndex, n);
		if (len(str) < n) {
			throw "swon.js:ParseSWONString:ParseError: unexpected end of string";
		}
		
		mlngCharIndex = mlngCharIndex + n;
		
		return str;
	};
	
	var advanceCharIdx = function () {
		mlngCharIndex = mlngCharIndex + 1;
	};
	
	var skipWhitespace  = function () {
		if (isEOS()) {
			return;
		}
	
		var re = /\s/;
		while (re.test(getCurrentChar())) {
			advanceCharIdx();
			if (isEOS()) {
				return;
			}
		}
	};
	
	var parseActual; // defined later on -- here to shut up jslint
	
	var parseEmpty  = function () {
		var origidx = mlngCharIndex;
		var strToken = getCharsAndAdvanceIdx(5);
		if (lCase(strToken) === "empty") {
			return (function () { return; })();
		} else {
			throw "swon.js:ParseSWONString:ParseError: unexpected token '" + strToken + "' at character " + origidx;
		}
	};

	var parseNull = function () {
		var origidx = mlngCharIndex;
		var strToken = getCharsAndAdvanceIdx(4);
		if (lCase(strToken) === "null") {
			return null;
		} else {
			throw "swon.js:ParseSWONString:ParseError: unexpected token '" + strToken + "' at character " + origidx;
		}
	};
	
	var parseNumber = function () {
		var origidx = mlngCharIndex;

		var strNumber = "";
		if (getCurrentChar() === "-") {
			strNumber += "-";
			advanceCharIdx();
		}
		
		var blnSeenDot = false;
		while (/^\d+$/.test(getCurrentChar()) || (getCurrentChar() === "." && !blnSeenDot)) {
			if (getCurrentChar() === ".") {
				blnSeenDot = true;
			}
			strNumber += getCurrentChar();
			advanceCharIdx();
			if (isEOS()) {
				break;
			}
		}
		
		var blnSeenE = false;
		if (!isEOS()) {
			if (lCase(getCurrentChar()) === "e") {
				blnSeenE = true;
				strNumber += getCurrentChar();
				advanceCharIdx();
				
				if (getCurrentChar() === "-") {
					strNumber += getCurrentChar();
					advanceCharIdx();
				}
				
				while (/^\d+$/.test(getCurrentChar())) {
					strNumber += getCurrentChar();
					advanceCharIdx();
					if (isEOS()) {
						break;
					}
				}
			}
		}
		
		if (blnSeenDot || blnSeenE) {
			try {
				return parseFloat(strNumber, 10);
			} catch (e) {
				throw "swon.js:ParseSWONString:ParseError: invalid floating point number '" + strNumber + "' at character " + origidx;
			}
		} else {
			try {
				return parseInt(strNumber, 10);
			} catch (e) {
				throw "swon.js:ParseSWONString:ParseError: error parsing number '" + strNumber + "' at character " + origidx + ": " + e;
			}
		}
	};
	
	var parseDate = function () {
		advanceCharIdx();
		
		var origidx = mlngCharIndex;
		
		var strYear = getCharsAndAdvanceIdx(4);
		var strMonth = getCharsAndAdvanceIdx(2);
		var strDay = getCharsAndAdvanceIdx(2);
		var strHour = "00";
		var strMinute = "00";
		var strSecond = "00";
		
		var strTime;
		var arrParts;
		
		var blnHasTimePart = false;
		if (!isEOS()) {
			if (getCurrentChar() === "T") {
				advanceCharIdx();
				blnHasTimePart = true;
				
				strTime = getCharsAndAdvanceIdx(8);
				arrParts = split(strTime, ":");
				if (arrParts.length === 0) {
					throw "swon.js:ParseSWONString:ParseError: syntax error parsing time '" + strTime + "' near character " + origidx;
				}
				if (arrParts.length !== 3) {
					throw "swon.js:ParseSWONString:ParseError: syntax error parsing time '" + strTime + "' near character " + origidx;
				}
				
				strHour = arrParts[0];
				strMinute = arrParts[1];
				strSecond = arrParts[2];
			}
		}

		if (!(len(strYear) === 4 && /^\d+$/.test(strYear))) {
			throw "swon.js:ParseSWONString:ParseError: invalid date syntax at character " + origidx;
		}
		if (!(len(strMonth) === 2 && /^\d+$/.test(strMonth))) {
			throw "swon.js:ParseSWONString:ParseError: invalid date syntax at character " + origidx;
		}
		if (!(len(strDay) === 2 && /^\d+$/.test(strDay))) {
			throw "swon.js:ParseSWONString:ParseError: invalid date syntax at character " + origidx;
		}
		if (!(len(strHour) === 2 && /^\d+$/.test(strHour))) {
			throw "swon.js:ParseSWONString:ParseError: invalid date syntax at character " + origidx;
		}
		if (!(len(strMinute) === 2 && /^\d+$/.test(strMinute))) {
			throw "swon.js:ParseSWONString:ParseError: invalid date syntax at character " + origidx;
		}
		if (!(len(strSecond) === 2 && /^\d+$/.test(strSecond))) {
			throw "swon.js:ParseSWONString:ParseError: invalid date syntax at character " + origidx;
		}
		
		var rc;
		
		try {
			 rc = new Date(parseInt(strYear, 10), 
							parseInt(strMonth, 10)-1, 
							parseInt(strDay, 10), 
							parseInt(strHour, 10), 
							parseInt(strMinute, 10), 
							parseInt(strSecond, 10));
		} catch (e) {
			throw "swon.js:ParseSWONString:ParseError: invalid date at character " + origidx;
		}
		
		return rc;
	};
	
	var parseBoolean  = function () {
		var origidx = mlngCharIndex;
	
		var strExpectedToken;
		if (lCase(getCurrentChar()) === "t") {
			strExpectedToken = "true";
		} else {
			strExpectedToken = "false";
		}
		
		var strToken = getCharsAndAdvanceIdx(len(strExpectedToken));
		if (lCase(strToken) === strExpectedToken) {
			if (strExpectedToken === "true") {
				return true;
			} else {
				return false;
			}
		} else {
			throw "swon.js:ParseSWONString:ParseError: unexpected token '" + strToken + "' at character " + origidx;
		}
	};
	
	var parseString  = function () {
		advanceCharIdx();
		
		var thestr = "";
	
		while (true) {
			if (getCurrentChar() === "'") {
				advanceCharIdx();
				if (isEOS()) {
					return thestr;
				} else {
					if (getCurrentChar() === "'") {
						thestr += "'";
						advanceCharIdx();
					} else {
						return thestr;
					}
				}
			} else {
				thestr += getCurrentChar();
				advanceCharIdx();
			}
		}
		
		return thestr;
	};
	
	var parseArray = function () {
		advanceCharIdx();
		
		skipWhitespace();

		if (getCurrentChar() === "]") {
			advanceCharIdx();
			return [];
		}
	
		var thearr = [];
		
		while (true) {
			thearr.push(parseActual(true));
			
			skipWhitespace();
			
			if (getCurrentChar() === "]") {
				advanceCharIdx();
				return thearr;
			}
			
			if (getCurrentChar() === ",") {
				advanceCharIdx();
			} else {
				throw "swon.js:ParseSWONString:ParseError: unexpected token '" + getCurrentChar() + "' at character " + mlngCharIndex;
			}
		}
		
		return thearr;
	};
	
	var parseDictionary = function () {
		advanceCharIdx();
		
		skipWhitespace();

		if (getCurrentChar() === "}") {
			advanceCharIdx();
			return {};
		}
	
		var thedict = {};
		
		var origidx, strKey;
		
		while (true) {
			origidx = mlngCharIndex;
			strKey = parseActual(true);
					
			skipWhitespace();
				
			if (getCurrentChar() !== ":") {
				throw "swon.js:ParseSWONString:ParseError: unexpected token '" + getCurrentChar() + "' at character " + mlngCharIndex;
			}
			advanceCharIdx();
			
			thedict[strKey] = parseActual(true);
	
			skipWhitespace();
			
			if (getCurrentChar() === "}") {
				advanceCharIdx();
				return thedict;
			}
			
			if (getCurrentChar() === ",") {
				advanceCharIdx();
			} else {
				throw "swon.js:ParseSWONString:ParseError: unexpected token '" + getCurrentChar() + "' at character " + mlngCharIndex;
			}
		}
		
		return thedict;
	};
	
	parseActual = function (pblnIgnoreTrailingCharacters) {
		skipWhitespace();

		var rc;
		
		if (uCase(getCurrentChar()) === 'E') {
			rc = parseEmpty();
		} else if (uCase(getCurrentChar()) === 'N') {
			rc = parseNull();
		} else if (/^\d+$/.test(getCurrentChar()) || getCurrentChar() === '-' || getCurrentChar() === '.') {
			rc = parseNumber();
		} else if (uCase(getCurrentChar()) === 'D') {
			rc = parseDate();
		} else if (uCase(getCurrentChar()) === 'T' || uCase(getCurrentChar()) === 'F') {
			rc = parseBoolean();
		} else if (uCase(getCurrentChar()) === "'") {
			rc = parseString();
		} else if (uCase(getCurrentChar()) === "[") {
			rc = parseArray();
		} else if (uCase(getCurrentChar()) === "{") {
			rc = parseDictionary();
		} else {
			throw "swon.js:ParseSWONString:ParseError: unexpected character '" + getCurrentChar() + "' at character " + mlngCharIndex;
		}
		
		if (isEOS()) {
			return rc;
		}
		
		if (!pblnIgnoreTrailingCharacters) {
			skipWhitespace();
			// shouldn't find anything other than whitespace at this point if we do, throw error
			if (!isEOS()) {
				throw "swon.js:ParseSWONString:ParseError: unexpected character '" + getCurrentChar() + "' at character " + mlngCharIndex;
			}
		}
		
		return rc;
	};
		
	return function (pstr) {
		mstrSWONString = '' + pstr;
		mlngCharIndex = 1;

		return parseActual(false);			
	};
}();
