/**
 * Copyright (c) 2007, Softamis, http://soft-amis.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Author: Alexey Luchkovsky
 * E-mail: jsoner@soft-amis.com
 *
 * Version: 1.24 alfa
 * Last modified: 06/06/2007
 */

/**
 * @fileoverview
 *
 * Main purpose of serializer create string representation of JavaSript object
 * so well to be able to restore corresponding object instance by this string.
 * It's allowed client JavaSript object to be transfered by network
 * on server and conversely.
 *
 * Limitation:
 * <ul>
 *  <li>object properties which are XML and DOM nodes are transient,
 *  in other words, there are excluded from traversing.
 * </ul>
 *
 * Sulution:
 * <ul>
 * <li>Uses node lookup instead forward link on node.
 * <li>Restore nodes by particular custom logic
 * </ul>
 *
 * Serializer is easier, realy only two methods are used:
 * <code>serialize(anObject)</code> is a method which is used to serialize object.
 * <code>deserialize(aString)</code> is a method which is used to deserialize object.
 *
 * Serializer is flexible, it contains functionality allowed to register
 * custom serializer/deserializer as a function which aware to create string
 * representation for data with corresponding type or create instance
 * of object by its string representation.
 *
 * Author: Alexey Luchkovsky
 * E-mail: jsoner@soft-amis.com
 */

var SERIALIZER =
{
  version: 1.24
};

/**
 * Defines set of JavaScript reserved words.
 * http://www.quackit.com/javascript/javascript_reserved_words.cfm
 */
SERIALIZER.RESERVED_WORDS = new KeySet( "abstract", "as", "boolean", "break", "byte", "case", "catch", "char",
			                                  "class", "continue", "const", "debugger", "default", "delete", "do",
																 			  "double", "else", "enum", "export", "extends", "false", "final", "finally",
				                                "float", "for", "function", "goto", "if", "implements", "import", "in",
				                                "instanceof", "int", "interface", "is", "long", "let", "namespace", "native",
				                                "new", "null", "package", "private", "prototype", "protected", "public", "return",
			                                  "short", "static", "super", "switch", "synchronized", "this", "throw", "throws",
																			  "transient", "true", "try", "typeof", "use", "var", "void", "volatile",
																			  "while", "with");

/**
 * Defines a map, which used to cache default object instances.
 * The cache structure is: {String}object type - {Object}object instance.
 */
SERIALIZER.fDefaults = new HashMap();

/**
 * Creates a Walker.
 * Walker extends Jsoner and used to traverse JavaScript objects.
 * @constructor
 */
function Walker()
{
	var self = JSINER.extend(this, Jsoner);
	self.isWalkNode = function(aName, aValue)
	{
		return COMMONS.isObject(aValue) && isNaN(aValue.nodeType);
	};
	return self;
}

/**
 * Redefines logger.
 */
Walker.prototype.fLogger = new Logger("Serializer.Walker");

/**
 * Converts property name to attribute name.
 * Corresponding to JSON specification property name always should be wrapped in brackets,
 * to serialize JavaScript object it not needs always.
 *
 * @param {String} The object property name.
 * @return {String} The corresponding to property attribute name.
 */
Walker.prototype.getAttrName = function(aName)
{
	var result = String(aName);
	if ( SERIALIZER.RESERVED_WORDS.isContains(result) || result.indexOf(' ') >= 0 || result.indexOf('.') >=0 )
	{
		result = '"' + result + '"';
	}
	return result;
};

/**
 * Indicates that the object property should be ignored.
 * Overrides Jsoner method to skip nodes and include object methods to walker.
 * @return {Boolean} If so it returns true, otherwise it returns false.
 *
 * @param {String} The property name.
 * @param The property value.
 * @param The parent object.
 */
Walker.prototype.isMute = function(aName, aValue, aParent)
{
	var result = aName === Jsoner.MAGIC_HASH_CODE || !aParent.hasOwnProperty(aName) ||
	             (COMMONS.isObject(aValue) && !isNaN(aValue.nodeType));
	return result;
};

/**
 * Returns default instance of the object.
 * Checks if object instance already was created,
 * if it is not true, creates new instance by object
 * constructor and them puts the instance in cache.
 *
 * @return Returns default instance of the object.
 * @param  The object.
 */
Walker.prototype.getDefaultInstance = function(anObject)
{
	var type = JSINER.getType(anObject);
	var result = SERIALIZER.fDefaults.get(type);
	if (COMMONS.isUndefined(result))
	{
		result = anObject;
		if (COMMONS.isObject(anObject))
		{
			try
			{
				var cons = JSINER.getConstructor(anObject);
				result = new cons();

				SERIALIZER.fDefaults.put(type, result);
			}
			catch(ex)
			{
				this.fLogger.warning("getDefaultInstance, unable to create new instance:" + type, ex);
			}
		}
	}
	return result;
};

/**
 * Indicates that the object property already presents
 * by defaults in object instance and property value is equals with default value.
 * @return {Boolean} If so it returns true, otherwise it returns false.
 *
 * @param The default instance of the object.
 * @param {String} The path to obtain object property.
 * @param The property value.
 */
Walker.prototype.isDefaultProperty = function(aDefault, aName, aValue)
{
	var result = false;
	if ( COMMONS.isDefined(aDefault) )
	{
		try
		{
			var value = this.getValue(aDefault, aName);
			result = this.isEquals(aValue, value);
		}
		catch(ex)
		{
			this.fLogger.warning("isDefaultProperty, unable to get property:" + aName, ex);
		}
	}
	return result;
};

/**
 * Creates default instance of an array;
 */
Walker.prototype.array = [];

/**
 * Indicates that the argument is a pure array.
 * @return {Boolean} If so it returns true, otherwise it returns false.
 *
 * @param The value to be checked.
 */
Walker.prototype.isPureArray = function(anObject)
{
  var result = COMMONS.isArray(anObject);
	if ( result )
	{
		var value;
		for (var name in anObject)
		{
			if ( name != Jsoner.MAGIC_HASH_CODE )
			{
				value = anObject[name];
				if ( isNaN(Number(name)) && !this.isDefaultProperty(this.array, name, value) )
				{
					result = false;
					break;
				}
			}
		}
	}
	return result;
};

/**
  * Collects the object attributes to array as pair name-value
  * <samp>{name:name, value:value}</samp>.
  * Overrides Jsoner method to skip default object properties.
  * @return {Array} An array of attributes.
  *
  * @param {Array} The path as array to walked node.
  * @param The node value.
  * @param The top level object.
  */
Walker.prototype.collectAttributes = function(aPath, aValue, anObject)
{
	var result = [];
	var value;
	var property;

	if ( COMMONS.isDefined(aValue) )
	{
		var def = this.getDefaultInstance(anObject);
		var path = aPath.join('.');
		for (var name in aValue)
		{
			try
			{
				value = aValue[name];
				if ( !this.isMute(name, value, aValue) && this.isAttribute(name, value) )
				{
					property = path.length > 0 ? path + "." + name : name;
					if ( !this.isDefaultProperty(def, property, value) )
					{
						result.push({name:name, value:value});
					}
				}
			}
			catch(ex)
			{
				this.fLogger.error("collectAttributes, unable to collect attribute:" + name, ex);
			}
		}
	}
	return result;
};

/**
  * Collects the object children to array as pair name-value
  * <samp>{name:name, value:value}</samp>.
  * Overrides Jsoner method to skip default object properties.
  * @return {Array} An array of node children.
  *
  * @param {Array} The path as array to walked node.
  * @param The node value.
  * @param The top level object.
  *
  * @see #isDefaultProperty.
  */
Walker.prototype.collectChildren = function(aPath, aValue, anObject)
{
	var value;
	var property;
	var result = [];
	if ( !this.isPureArray(aValue) )
	{
		var def = this.getDefaultInstance(anObject);
		var path = aPath.join('.');
		for (var name in aValue)
		{
			try
			{
				value = aValue[name];
				if ( !this.isMute(name, value, aValue) && !this.isAttribute(name, value) )
				{
					property = path.length > 0 ? path + "." + name : name;
					if ( !this.isDefaultProperty(def, property, value) )
					{
						result.push( {name:name, value:value} );
					}
				}
			}
			catch(ex)
			{
				this.fLogger.warning( "collectChildren, unable to collect child:" + name, ex);
			}
		}
	}
	return result;
};

/********************************************/

/**
 * Creates JavaScript object serializer.
 * @constructor
 * Main purpose of serializer create string representation of JavaSript object
 * so well to be able to restore corresponding object instance by this string.
 * It's allowed client JavaSript object to be transfered by network
 * on server and conversely.
 *
 * Limitation:
 * <ul>
 *  <li>object properties which are XML and DOM nodes are transient,
 *  in other words, there are excluded from traversing.
 * </ul>
 *
 * Sulution:
 * <ul>
 * <li>Uses node lookup instead forward link on node.
 * <li>Restore nodes by particular custom logic
 * </ul>
 *
 * Serializer is easier, realy only two methods are used:
 * <code>serialize(anObject)</code> is a method which is used to serialize object.
 * <code>deserialize(aString)</code> is a method which is used to deserialize object.
 *
 * Serializer is flexible, it contains functionality allowed to register
 * custom serializer/deserializer as a function which aware to create string
 * representation for data with corresponding type or create instance
 * of object by its string representation.
 */
function Serializer(aPettyPrint)
{
	function ValueWalker()
	{
		return JSINER.extend(this, Walker);
	}

	ValueWalker.prototype.getDefaultInstance = function(anObject)
	{
		var def = ValueWalker.superClass.getDefaultInstance.call(this, anObject.value);
		return { value:def };
	};

  this.fSerializers   = new HashMap();
	this.fDeserializers = new HashMap();

	this.fPettyPrint = aPettyPrint;

	this.fWalker = new ValueWalker();
	this.fWalker.isWalkArray = function(aName, aValue)
	{
		return false;
	};

	this.fCrossLinker = new ValueWalker();
  this.initProcessors();
}

Serializer.FIELD_TYPE   = "type";
Serializer.FIELD_STREAM = "data";

Serializer.DECODE_TABLE   = {'\b': '\\b',	'\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"',	'\\': '\\\\'};
Serializer.REGEXP_TEST    = /["\\\x00-\x1f]/;
Serializer.REGEXP_REPLACE = /([\x00-\x1f\\"])/g;

/**
 * Predefined object method name to externalize object.
 */
Serializer.SERIALIZE_METHOD  = "toJSONString";

/**
 * Predefined object method name to restore externalized object.
 */
Serializer.DESERIALIZE_METHOD = "stringToJSON";

/**
 * Defines Serializer logger.
 */
Serializer.prototype.fLogger = new Logger("Serializer");

/**
 * Registers serializer.
 * A serializer is a function which aware to create
 * string representation for an object with corresponding type.
 *
 * @param {String} The object type.
 * @param {Function} The custom serializer.
 */
Serializer.prototype.registerSerializer = function(aType, aSerializer)
{
	if ( COMMONS.isFunction(aSerializer) )
	{
		this.fSerializers.put(aType, aSerializer);
	}
	else
	{
		this.fLogger.warning( "registerSerializer, illegal argument type:" + aSerializer);
	}
};

/**
 * Registers deserializer.
 * A deserializer is a function which aware to create
 * instance of the object by its string representation.
 *
 * @param {String} The object type.
 * @param {Function} The custom deserializer.
 */
Serializer.prototype.registerDeserializer = function(aType, aDeserializer)
{
	if ( COMMONS.isFunction(aDeserializer) )
	{
		this.fDeserializers.put(aType, aDeserializer);
	}
	else
	{
		this.fLogger.warning( "registerSerializer, illegal argument type:" + aDeserializer);
	}
};

/**
 * Registers predefined serializers and deserializers.
 *
 * @see #registerSerializer
 * @see #registerDeserializer
 */
Serializer.prototype.initProcessors = function()
{
	this.registerSerializer("object", this.serializeObject);
	this.registerDeserializer("object", this.deserializeObject );
	this.registerSerializer("string", this.serializeString );
	this.registerSerializer("function", this.serializeFunction );
	this.registerSerializer("Date", this.serializeDate);
	this.registerDeserializer("Date", this.deserializeDate);
	this.registerSerializer("RegExp", this.serializeRegexp);
};

/**
 * Obtains object serializer.
 * Checks predefined object method to externalize object
 * <code>object.toJSONString()</code>,
 * if it can't be resolved uses map to get serializer
 * by object type. If noting is found, returns default serializer.
 *
 * @return {Function} Returns corresponding serializer.
 *
 * @param The object used to obtain serializer.
 */
Serializer.prototype.getSerializer = function(anObject)
{
	function defaultSerializer(anObject)
	{
		return String(anObject);
	}

	var result = null;
	if ( COMMONS.isDefined(anObject) )
	{
		result = anObject[Serializer.SERIALIZE_METHOD];
		if ( !COMMONS.isFunction(result) )
		{
			result = this.fSerializers.get( JSINER.getType(anObject) );
			if ( !COMMONS.isFunction(result) )
			{
				result = this.fSerializers.get( typeof(anObject) );
			}
		}
	}
	if (!COMMONS.isFunction(result))
	{
		result = defaultSerializer;
	}
	return result;
};

/**
 * Obtains object deserializer.
 * Checks predefined object method to restore object
 * from externalize form: "stringToJSON",
 * if it can't be resolved, gets deserializer by object type.
 * If noting is found returns default deserializer.
 *
 * @return {Function} Returns corresponding deserializer.
 * @param The object used to obtain deserializer.
 */
Serializer.prototype.getDeserializer = function(anObject)
{
	var result = COMMONS.isDefined(anObject) ? anObject[Serializer.DESERIALIZE_METHOD] : null;
	if ( !COMMONS.isFunction(result) && COMMONS.isDefined(anObject) )
	{
		var type = anObject[Serializer.FIELD_TYPE];
		if ( COMMONS.isDefined(type) )
		{
			result = this.fDeserializers.get(type);
		}
	}
	if (!COMMONS.isFunction(result))
	{
		result = this.fDeserializers.get(typeof(anObject));
		if (!COMMONS.isFunction(result))
		{
			result = COMMONS.proxy;
		}
	}
	return result;
};

/**
 * Date serializer.
 * @return {String} Returns string representation of the Date.
 * @param {Date} The object used to create string representation of Date.
 *
 * @see #deserializeDate
 */
Serializer.prototype.serializeDate = function(aDate)
{
	var time = aDate.getTime();
	aDate.time = aDate.getTime();

	var result = this.serializeObject(aDate);

  aDate.time = undefined;
	delete aDate["time"];
	return result;
};

/**
 * Date deserializer.
 * @param {Object} The initial object that represents the Date.
 * @return {Date} Returns new Date by its initial representation.
 *
 * @see #serializeDate
 */
Serializer.prototype.deserializeDate = function(aString)
{
  var result = this.deserializeObject(aString);
	if ( COMMONS.isDefined(result.time) )
	{
		result.setTime(result.time);
		result.time = undefined;
		delete result["time"];
	}
	return result;
};

/**
 * RegExp serializer.
 *
 * @return {String} Returns string representation of the RegExp.
 * @param {RegExp} The object used to create string representation.
 */
Serializer.prototype.serializeRegexp = function(aRegExp)
{
  return aRegExp.toString();
};

/**
 * Function serializer.
 *
 * @return {String} Returns string representation of the Function.
 * @param {Function} The object used to create string representation.
 */
Serializer.prototype.serializeFunction = function(aFunc)
{
  var result = aFunc.toString();
	var ind1 = result.indexOf(' ')+ 1;
	var ind2 = result.indexOf('(');
	if ( ind1 < ind2)
	{
	  result = result.substring(ind1, ind2);
  }
  else
  {
    this.fLogger.warning("serializeFunction, anonymous function: " + result);
	}
	return result;
};

/**
 * String serializer.
 * Code from http://www.json.org/json.js (2007-03-01) was used.
 *
 * @return {String} Returns string representation of the String.
 * @param {String} The object used to create string representation.
 */
Serializer.prototype.serializeString = function(aString)
{
	if ( Serializer.REGEXP_TEST.test(aString) )
	{
		aString = aString.replace(Serializer.REGEXP_REPLACE, function(a, b)
		{
			var c = Serializer.DECODE_TABLE[b];
			if ( COMMONS.isDefined(c) )
			{
				return c;
			}
			c = b.charCodeAt();
			return '\\u00' + Math.floor(c/16).toString(16) + (c%16).toString(16);
		});
	}
	return '"' + aString + '"';
};

/**
 * Object serializer.
 *
 * @return {String} Returns string representation of the object.
 * @param {Object} The object used to create string representation.
 *
 * @see #deserializeObject
 */
Serializer.prototype.serializeObject = function(anObject)
{
	var result= "";
  var stack = [];
	var serializer = this;
	var pettyPrint = this.fPettyPrint;

	this.fWalker.jsonTreeWalker( { value:anObject }, function(aPath, aValue, anAttributes, aType)
	{
	  var value;
		var func;
		var b = false;

	  if ( aType === Jsoner.JSON_NODE_START || aType === Jsoner.JSON_NODE_LEAF )
	  {
		  if (aPath.length > 1 )
		  {
			  if (result.charAt(result.length - 1) !== '{' )
			  {
				  result += pettyPrint ? ",\n" : ",";
			  }
        result += this.getAttrName( this.getLastProperty(aPath) ) + ":";
		  }

		  if ( this.isPureArray(aValue) )
		  {
			  result += "[";
			  for (var i = 0; i < aValue.length; i++)
			  {
				  func = serializer.getSerializer(aValue[i]);
				  if (COMMONS.isFunction(func))
				  {
						if (b)
						{
							result += ",";
						}
					  result += func.call(serializer, aValue[i]);
					  b = true;
				  }
			  }
			  stack.push("]");
		  }
		  else
			{
				var type = JSINER.getType(aValue);
				if (type === "Object")
				{
					result += "{";
          stack.push("}");
				}
				else
				{
					result += '{"' + Serializer.FIELD_TYPE + '":"' + type + '",';
					result += '"' + Serializer.FIELD_STREAM + '":' + (pettyPrint ? "\n{" : ' {');
					stack.push( pettyPrint ? "}\n}":  "}}");
				}

				for (var i = 0; i < anAttributes.length; i++)
				{
					value = anAttributes[i].value;

					func = serializer.getSerializer(value);
					if (COMMONS.isFunction(func))
					{
						value = func.call(serializer, value);
						if (b)
						{
							result += pettyPrint ? ",\n" : ",";
						}
						result += this.getAttrName(anAttributes[i].name) + ':' + value;
						b = true;
					}
				}
			}
		}

	  if ( aType === Jsoner.JSON_NODE_END || aType === Jsoner.JSON_NODE_LEAF )
	  {
		  result += stack.pop();
	  }
		return true;
	});

	return result;
};

/**
 * Object deserializer.
 * @param {Object} The initial object.
 * @return {Date} Returns new object instance by its initial representation.
 *
 * @see #serializeObject
 */
Serializer.prototype.deserializeObject = function(anObject)
{
	var result = anObject;
	var value;
	var func;
	if ( COMMONS.isObject(anObject) )
	{
		var type = anObject[ Serializer.FIELD_TYPE ];
		if ( COMMONS.isString(type) )
		{
			try
			{
				var cons = JSINER.getConstructor(type);
				result =  new cons();

				var data = anObject[ Serializer.FIELD_STREAM ];
				if ( COMMONS.isObject(data) )
				{
					for (var name in data)
					{
					  if ( data.hasOwnProperty(name) )
					  {
							value = anObject.data[name];
							func = this.getDeserializer(value);
							if (COMMONS.isFunction(func))
							{
								result[name] = func.call(this, value);
							}
						}
					}
				}
			}
			catch(ex)
			{
				this.fLogger.error("deSerialize error " + type, ex);
			}
		}
		else
		{
			for (var name in anObject)
			{
				if ( anObject.hasOwnProperty(name) )
				{
					value = anObject[name];
					func = this.getDeserializer(value);
					if (COMMONS.isFunction(func))
					{
						result[name] = func.call(this, value);
					}
				}
			}
		}
	}
	return result;
};

/**
 * Base serialize method.
 * Selects corresponding serializer by given object and
 * use them creates string representation of the object.
 *
 * @return {String} Returns string representation of the object.
 * @param {Object} The object used to create string representation.
 */
Serializer.prototype.serialize = function(anObject)
{
	var result = "undefined";
  var func = this.getSerializer(anObject);

	if ( COMMONS.isDefined(func) )
	{
		try
		{
			result = func.call(this, anObject);
			
			var pettyPrint = this.fPettyPrint;
			this.fCrossLinker.jsonPathEvaluator( {value:anObject}, function(aPath, aValue, aType)
			{
				if ( aType === Jsoner.JSON_NODE_CROSS_LINKED )
				{
					result += pettyPrint ? ";\n" : ";";
					var value = aValue.substring(6 + Jsoner.CROSS_LINK_PREFIX.length); // "value." length = 6
					result += Jsoner.CROSS_LINK_PREFIX + "."+ aPath.substring(6) + "=" + (value.length > 0 ? + "result." + value : "result");
				}
				return true;
			});
		}
		catch(ex)
		{
			this.fLogger.error("serialize error", ex);
		}
		result += ";"
	}
	else
	{
	  this.fLogger.error("serialize, corresponding serializer not found:" + anObject );
	}
	return result;
};

/**
 * Base deserialize method.
 * Selects corresponding deserializer by string representation
 * of the object and creates new object instance.
 *
 * @param {String} The string representation of the Object.
 * @return {Date} Returns new object instance by its string representation.
 *
 */
Serializer.prototype.deserialize = function(aString)
{
	var result = undefined;
	if ( COMMONS.isString(aString) )
	{
		var object = aString;
		var crossLink = null;

		var index = aString.indexOf(Jsoner.CROSS_LINK_PREFIX);
    if (index > 0)
    {
	    object = aString.substring(0, index);
	    crossLink = aString.substring(index).replace(new RegExp(Jsoner.CROSS_LINK_PREFIX, "g"), "result");
	  }
		try
		{
			var $json = undefined;
			eval( "$json =" + object);
			if ( $json !== undefined )
			{
				var func = this.getDeserializer($json);
				if ( COMMONS.isDefined(func) )
				{
					result = func.call(this, $json);
				}
			}

			if ( crossLink !== null )
			{
				try
				{
					eval(crossLink);
				}
				catch(ex)
				{
					this.fLogger.error("deSerialize, unable to deserialize cross links:" + crossLink, ex)
				}
			}
		}
		catch(ex)
		{
			this.fLogger.error("deSerialize error", ex)
		}
	}
	else
	{
		this.fLogger.error("deSerialize error, illegal argument type:" + aString )
	}
	return result;
};
