// SpryJSONDataSet.js - version 0.3 - Spry Pre-Release 1.5
//
// Copyright (c) 2007. Adobe Systems Incorporated.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//   * Redistributions of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//   * Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//   * Neither the name of Adobe Systems Incorporated nor the names of its
//     contributors may be used to endorse or promote products derived from this
//     software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

Spry.Data.JSONDataSet = function(dataSetURL, dataSetOptions)
{
	// Call the constructor for our HTTPSourceDataSet base class so that
	// our base class properties get defined.

	this.path = "";
	this.pathIsObjectOfArrays = false;
	this.doc = null;
	this.subPaths = [];
	this.useParser = false;
	this.preparseFunc = null;

	Spry.Data.HTTPSourceDataSet.call(this, dataSetURL, dataSetOptions);

	// Callers are allowed to pass either a string, an object or an array of
	// strings and/or objects for the 'subPaths' option, so make sure we normalize
	// the subPaths value to be an array.

	var jwType = typeof this.subPaths;
	if (jwType == "string" || (jwType == "object" && this.subPaths.constructor != Array))
		this.subPaths = [ this.subPaths ];
}; // End of Spry.Data.JSONDataSet() constructor.

Spry.Data.JSONDataSet.prototype = new Spry.Data.HTTPSourceDataSet();
Spry.Data.JSONDataSet.prototype.constructor = Spry.Data.JSONDataSet;

// Override the inherited version of getDataRefStrings() with our
// own version that returns the strings memebers we maintain that
// may have data references in them.

Spry.Data.JSONDataSet.prototype.getDataRefStrings = function()
{
	var strArr = [];
	if (this.url) strArr.push(this.url);
	if (this.path) strArr.push(this.path);
	if (this.requestInfo && this.requestInfo.postData) strArr.push(this.requestInfo.postData);
	return strArr;
};

Spry.Data.JSONDataSet.prototype.getDocument = function() { return this.doc; };
Spry.Data.JSONDataSet.prototype.getPath = function() { return this.path; };
Spry.Data.JSONDataSet.prototype.setPath = function(path)
{
	if (this.path != path)
	{
		this.path = path;
		if (this.dataWasLoaded && this.doc)
		{
			this.notifyObservers("onPreLoad");
			this.setDataFromDoc(this.doc);
		}
	}
};

// A recursive method that returns all objects that match the given object path.

Spry.Data.JSONDataSet.getMatchingObjects = function(path, jsonObj)
{
	var results = [];

	if (path && jsonObj)
	{
		var prop = "";
		var leftOverPath = "";

		var offset = path.search(/\./);
		if (offset != -1)
		{
			prop = path.substring(0, offset);
			leftOverPath = path.substring(offset + 1);
		}
		else
			prop = path;

		var matches = [];

		if (prop && typeof jsonObj == "object")
		{
			var obj = jsonObj[prop];
			var objType = typeof obj;
			if (objType != undefined && objType != null)
			{
				if (obj && objType == "object" && obj.constructor == Array)
					matches = matches.concat(obj);
				else
					matches.push(obj);
			}
		}

		var numMatches = matches.length;
		if (leftOverPath)
		{
			for (var i = 0; i < numMatches; i++)
				results = results.concat(Spry.Data.JSONDataSet.getMatchingObjects(leftOverPath, matches[i]));
		}
		else
			results = matches;
	}

	return results;
};

// Flatten the specified object into a row object that can be added
// to a record set.

Spry.Data.JSONDataSet.flattenObject = function(obj, basicColumnName)
{
	// If obj is a basic type (null, string, boolean, or number), we need
	// to store it under a column name in our row object. If the caller supplied
	// a column name, use that, if not use our default "column0".

	var basicName = basicColumnName ? basicColumnName : "column0";

	// If obj is an object, copy its properties into our row object. If obj
	// is a basic type, then store it in the row under the column name specified
	// by basicName.

	var row = new Object;
	var objType = typeof obj;
	if (objType == "object")
		Spry.Data.JSONDataSet.copyProps(row, obj);
	else
		row[basicName] = obj;

	// Make sure we note the original JSON object we used to create
	// this row. It may be needed if we need to flatten other sub-paths.

	row.ds_JSONObject = obj;
	return row;
};

// Utility routine for copying properties from one object to another.

Spry.Data.JSONDataSet.copyProps = function(dstObj, srcObj, suppressObjProps)
{
	if (srcObj && dstObj)
	{
		for (var prop in srcObj)
		{
			if (suppressObjProps && typeof srcObj[prop] == "object")
				continue;
			dstObj[prop] = srcObj[prop];
		}
	}
	return dstObj;
};

// Given an object created from JSON data, and an object path, find all the
// matching objects and flatten them into rows of data.

Spry.Data.JSONDataSet.flattenDataIntoRecordSet = function(jsonObj, path, pathIsObjectOfArrays)
{
	var rs = new Object;
	rs.data = [];
	rs.dataHash = {};

	if (!path)
		path = "";

	var obj = jsonObj;
	var objType = typeof obj;
	var basicColName = "";

	// Handle the basic non-object data types.

	if (objType != "object" || !obj)
	{
		// JSON has a null data type which we translate
		// into a data set with no rows. All other data types
		// translate into a data set with a single row with a
		// column named "column0" which contains the actual
		// data.

		if (obj != null)
		{
			var row = new Object;
			row.column0 = obj;
			row.ds_RowID = 0;
			rs.data.push(row);
			rs.dataHash[row.ds_RowID] = row;
		}
		return rs;
	}

	var matches = [];

	if (obj.constructor == Array)
	{
		var arrLen = obj.length;

		// We have a top-level array. If the array is empty,
		// then there's nothing for us to do.

		if (arrLen < 1)
			return rs;

		// XXX: We are making a big assumption here that all of the
		// elements within the array are of the same type!
		//
		// If the elements are of a basic data type, we create
		// a row for each element and add it as a row to the data set.

		var eleType = typeof obj[0];

		if (eleType != "object")
		{
			for (var i = 0; i < arrLen; i++)
			{
				var row = new Object;
				row.column0 = obj[i];
				row.ds_RowID = i;
				rs.data.push(row);
				rs.dataHash[row.ds_RowID] = row;
			}
			return rs;
		}
		
		// The elements within the array are objects.
		//
		// XXX: If they are arrays, bail, because we don't handle
		// arrays within arrays right now!

		if (obj[0].constructor == Array)
			return rs;

		// We have an array of objects. If we have a path, use it
		// to fetch the data the user is interested in from each element
		// in the array and append the results to our matches array.
		// If no path was specified, just add the element to the matches
		// array.

		if (path)
		{
			for (var i = 0; i < arrLen; i++)
				matches = matches.concat(Spry.Data.JSONDataSet.getMatchingObjects(path, obj[i]));
		}
		else
		{
			for (var i = 0; i < arrLen; i++)
				matches.push(obj[i]);
		}
	}
	else
	{
		// We have a top-level object. If the user has specified a path,
		// use it to extract out the data they are interested in. If no
		// path was specified, then just copy 
		
		if (path)
			matches = Spry.Data.JSONDataSet.getMatchingObjects(path, obj);
		else
			matches.push(obj);
	}

	var numMatches = matches.length;
	if (path && numMatches >= 1 && typeof matches[0] != "object")
		basicColName = path.replace(/.*\./, "");

	if (!pathIsObjectOfArrays)
	{
		for (var i = 0; i < numMatches; i++)
		{
			var row = Spry.Data.JSONDataSet.flattenObject(matches[i], basicColName, pathIsObjectOfArrays);
			row.ds_RowID = i;
			rs.dataHash[i] = row;
			rs.data.push(row);
		}
	}
	else
	{
		// Each object that was matched contains properties that are the column names
		// of our rows. The value of each property is an array of values for that column. This
		// means the data for each row is inverted and spread across multiple arrays. We expect arrays of
		// objects, so run through all of the arrays and build up row objects and add them
		// to our record set.

		for (var i = 0; i < numMatches; i++)
		{
			var obj = matches[i];
			var colNames = [];
			var maxNumRows = 0;
			for (var propName in obj)
			{
				var prop = obj[propName];
				var propyType = typeof prop;
				if (propyType == 'object' && prop.constructor == Array)
				{
					colNames.push(propName);
					maxNumRows = Math.max(maxNumRows, obj[propName].length);
				}
			}

			var numColNames = colNames.length;
			for (var j = 0; j < maxNumRows; j++)
			{
				var row = new Object;
				for (var k = 0; k < numColNames; k++)
				{
					var colName = colNames[k];
					row[colName] = obj[colName][j];
				}
				row.ds_RowID = i;
				rs.dataHash[i] = row;
				rs.data.push(row);
			}
		}
	}

	return rs;
};

// For each JSON object used to create the rows in the specified recordset,
// find the data the matches the specified subPaths, flatten them, and append
// them to the rows of the record set.

Spry.Data.JSONDataSet.prototype.flattenSubPaths = function(rs, subPaths)
{
	if (!rs || !subPaths)
		return;

	var numSubPaths = subPaths.length;
	if (numSubPaths < 1)
		return;

	var data = rs.data;
	var dataHash = {};

	// Convert all of the templated subPaths to object paths with real values.
	// We also need a "cleaned" version of the object path which contains no
	// expressions in it, so that we can pre-pend it to the column names
	// of any nested data we find.

	var pathArray = [];
	var cleanedPathArray = [];
	var isObjectOfArraysArr = [];

	for (var i = 0; i < numSubPaths; i++)
	{
		// The elements of the subPaths array can be path strings,
		// or objects that describe a path with nested sub-paths below
		// it, so make sure we properly extract out the object path to use.

		var subPath = subPaths[i];
		if (typeof subPath == "object")
		{
			isObjectOfArraysArr[i] = subPath.pathIsObjectOfArrays;
			subPath = subPath.path;
		}
		if (!subPath)
			subPath = "";

		// Convert any data references in the object path to real values!

		pathArray[i] = Spry.Data.Region.processDataRefString(null, subPath, this.dataSetsForDataRefStrings);

		// Create a clean version of the object path by stripping out any
		// expressions it may contain.

		cleanedPathArray[i] = pathArray[i].replace(/\[.*\]/g, "");
	}

	// For each row of the base record set passed in, generate a flattened
	// recordset from each subPath, and then join the results with the base
	// row. The row from the base data set will be duplicated to match the
	// number of rows matched by the subPath. The results are then merged.

	var row;
	var numRows = data.length;
	var newData = [];

	// Iterate over each row of the base record set.

	for (var i = 0; i < numRows; i++)
	{
		row = data[i];
		var newRows = [ row ];

		// Iterate over every subPath passed into this function.

		for (var j = 0; j < numSubPaths; j++)
		{
			// Search for all nodes that match the given path underneath
			// the JSON Object for the base row and flatten the data into
			// a tabular recordset structure.

			var newRS = Spry.Data.JSONDataSet.flattenDataIntoRecordSet(row.ds_JSONObject, pathArray[j], isObjectOfArraysArr[j]);

			// If this subPath has additional subPaths beneath it,
			// flatten and join them with the recordset we just created.

			if (newRS && newRS.data && newRS.data.length)
			{
				if (typeof subPaths[j] == "object" && subPaths[j].subPaths)
				{
					// The subPaths property can be either an object path string,
					// an Object describing a subPath and paths beneath it,
					// or an Array of object path strings or objects. We need to
					// normalize these variations into an array to simplify
					// our processing.
	
					var sp = subPaths[j].subPaths;
					spType = typeof sp;
					if (spType == "string")
						sp = [ sp ];
					else if (spType == "object" && spType.constructor == Object)
						sp = [ sp ];
	
					// Now that we have a normalized array of sub paths, flatten
					// them and join them to the recordSet we just calculated.
	
					this.flattenSubPaths(newRS, sp);
				}
	
				var newRSData = newRS.data;
				var numRSRows = newRSData.length;
	
				var cleanedPath = cleanedPathArray[j] + ".";
				
				var numNewRows = newRows.length;
				var joinedRows = [];
	
				// Iterate over all rows in our newRows array. Note that the
				// contents of newRows changes after the execution of this
				// loop, allowing us to perform more joins when more than
				// one subPath is specified.
	
				for (var k = 0; k < numNewRows; k++)
				{
					var newRow = newRows[k];
	
					// Iterate over all rows in the record set generated
					// from the current subPath. We are going to create
					// m*n rows for the joined table, where m is the number
					// of rows in the newRows array, and n is the number of
					// rows in the current subPath recordset.
	
					for (var l = 0; l < numRSRows; l++)
					{
						// Create a new row that will house the join result.
	
						var newRowObj = new Object;
						var newRSRow = newRSData[l];
	
						// Copy the data from the current row of the record set
						// into our new row object, but make sure to store the
						// data in columns that have the subPath prepended to
						// it so that it doesn't collide with any columns from
						// the newRows row data.
	
						// Copy the props to the new object using the new property name.
						for (var prop in newRSRow)	
						{
							// The new propery name will have the subPath used prepended to it.
							var newPropName = cleanedPath + prop;
	
							// We need to handle the case where the property name of the object matched
							// by the object path has a value. In that specific case, the name of the
							// property should be the cleanedPath itself. For example:
							//
							//	{
							//		"employees":
							//			{
							//				"employee":
							//					[
							//						"Bob",
							//						"Joe"
							//					]
							//			}
							//	}
							//
							// Object Path: employees.employee
							//
							// The property name that contains "Bob" and "Joe" will be "employee".
							// So in our new row, we need to call this column "employees.employee"
							// instead of "employees.employee.employee" which would be incorrect.
	
							if (cleanedPath == prop || cleanedPath.search(new RegExp("\\." + prop + "\\.$")) != -1)
								newPropName = cleanedPathArray[j];
	
							// Copy the props to the new object using the new property name.

							newRowObj[newPropName] = newRSRow[prop];
						}
	
						// Now copy the columns from the newRow into our row
						// object.

						Spry.Data.JSONDataSet.copyProps(newRowObj, newRow);
	
						// Now add this row to the array that tracks all of the new
						// rows we've just created.
	
						joinedRows.push(newRowObj);
					}
				}

				// Set the newRows array equal to our joinedRows we just created,
				// so that when we flatten the data for the next subPath, it gets
				// joined with our new set of rows.

				newRows = joinedRows;
			}
		}

		newData = newData.concat(newRows);
	}

	// Now that we have a new set of joined rows, we need to run through
	// all of the rows and make sure they all have a unique row ID and
	// rebuild our dataHash.

	data = newData;
	numRows = data.length;

	for (i = 0; i < numRows; i++)
	{
		row = data[i];
		row.ds_RowID = i;
		dataHash[row.ds_RowID] = row;
	}

	// We're all done, so stuff the new data and dataHash
	// back into the base recordSet.

	rs.data = data;
	rs.dataHash = dataHash;
};

Spry.Data.JSONDataSet.prototype.parseJSON = function(str, filter)
{
	// The implementation of this JSON Parser is from the json.js (2007-03-20)
	// reference implementation from json.org. It was modified to accept the
	// JSON string as an arg, and throw a generic Error.
	//
	// Parsing happens in three stages. In the first stage, we run the text against
	// a regular expression which looks for non-JSON characters. We are especially
	// concerned with '()' and 'new' because they can cause invocation, and '='
	// because it can cause mutation. But just to be safe, we will reject all
	// unexpected characters.

	try
	{
		if (/^("(\\.|[^"\\\n\r])*?"|[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t])+?$/.test(str))
		{
			// In the second stage we use the eval function to compile the text into a
			// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
			// in JavaScript: it can begin a block or an object literal. We wrap the text
			// in parens to eliminate the ambiguity.

			var j = eval('(' + str + ')');

			// In the optional third stage, we recursively walk the new structure, passing
			// each name/value pair to a filter function for possible transformation.

			if (typeof filter === 'function')
			{
				function walk(k, v)
				{
					if (v && typeof v === 'object')
					{
						for (var i in v)
						{
							if (v.hasOwnProperty(i))
							{
								v[i] = walk(i, v[i]);
							}
						}
					}
					return filter(k, v);
				}

				j = walk('', j);
			}
			return j;
		}
	} catch (e) {
		// Fall through if the regexp test fails.
	}
	throw new Error("Failed to parse JSON string.");
};

// Translate the raw JSON string (rawDataDoc) into an object, find the
// data within the object we are interested in, and flatten it into
// a set of rows for our data set.

Spry.Data.JSONDataSet.prototype.loadDataIntoDataSet = function(rawDataDoc)
{
	if (this.preparseFunc)
		rawDataDoc = this.preparseFunc(this, rawDataDoc);

	var jsonObj;
	try	{ jsonObj = this.useParser ? this.parseJSON(rawDataDoc) : eval("(" + rawDataDoc + ")"); }
	catch(e)
	{
		Spry.Debug.reportError("Caught exception in JSONDataSet.loadDataIntoDataSet: " + e);
		jsonObj = {};
	}

	if (jsonObj == null)
		jsonObj = "null";

	var rs = Spry.Data.JSONDataSet.flattenDataIntoRecordSet(jsonObj, Spry.Data.Region.processDataRefString(null, this.path, this.dataSetsForDataRefStrings), this.pathIsObjectOfArrays);

	this.flattenSubPaths(rs, this.subPaths);

	this.doc = rawDataDoc;
	this.docObj = jsonObj;
	this.data = rs.data;
	this.dataHash = rs.dataHash;
	this.dataWasLoaded = (this.doc != null);
};
