if(!commonspot)
	var commonspot = {};

/**
 * commonspot.spry: CommonSpot Spry package
 */
commonspot.spry = {};

commonspot.spry.MSG_EMPTY_DATASET = 'You are not allowed to update an empty dataset';
commonspot.spry.PK_COL = 'ds_RowID';

commonspot.spry.util = {};

/**
 * Based on the original Spry.Utils.getRecordSetFromXMLDoc() from Adobe
 * Use tmt.xml for XPath (a lightweight wrapper for browser's native XPath APIs), making xpath.js redundant
 * Compared to the original version this is quite faster but it doesn't support Safari 
 * It also may fails on more advanced XPath patterns, due to inconsistencies across browsers XPath implementations
 */
commonspot.spry.util.getRecordSetFromXMLDoc = function(xmlDoc, path, suppressColumns, dataPreprocessorFunc)
{
	if (!xmlDoc || !path)
		return null;

	var recordSet = new Object();
	recordSet.xmlDoc = xmlDoc;
	recordSet.xmlPath = path;
	recordSet.dataHash = new Object;
	recordSet.data = new Array;
	recordSet.getData = function() { return this.data; };

	// Use the XPath library to find the nodes that will
	// make up our data set. The result should be an array
	// of subtrees that we need to flatten.
	
	var nodeArray = tmt.xml.evaluateXPath(xmlDoc, path);

	var isDOMNodeArray = true;

	if (nodeArray && nodeArray.length > 0)
		isDOMNodeArray = nodeArray[0].nodeType != 2 /* Node.ATTRIBUTE_NODE */;

	var nextID = 0;

	// We now have the set of nodes that make up our data set
	// so process each one.

	for (var i = 0; i < nodeArray.length; i++)
	{
		var rowObj = null;

		if (suppressColumns)
			rowObj = new Object;
		else
		{
			if (isDOMNodeArray)
				rowObj = commonspot.util.xml.nodeToObject(nodeArray[i]); //Spry.Utils.CreateObjectForNode(nodeArray[i]);
			else // Must be a Node.ATTRIBUTE_NODE array.
			{
				rowObj = new Object;
				rowObj["@" + nodeArray[i].name] = nodeArray[i].value;
			}
		}

		if (rowObj)
		{
			// We want to make sure that every row has a unique ID and since we
			// we don't know which column, if any, in this recordSet is a unique
			// identifier, we generate a unique ID ourselves and store it under
			// the ds_RowID column in the row object.
			rowObj[commonspot.spry.PK_COL] = nextID++;
			
			// If we have a date preprocessor, apply it before loading the row
			if(dataPreprocessorFunc)
			{
				rowObj = dataPreprocessorFunc(rowObj);
			}

			if(rowObj)
			{
				rowObj['ds_XMLNode'] = nodeArray[i];
				recordSet.dataHash[rowObj[commonspot.spry.PK_COL]] = rowObj;
				recordSet.data.push(rowObj);
			}
		}
	}
	
	return recordSet;
};

/**
 * basic dataset notification logger, works with firebug console
 * add this function as an observer of a dataset, and all notifications from it get logged to the console
 * 
 * example setup:
 * 	commonspotLocalData.Admin_getInstalledPatches.addObserver(commonspot.spry.util.datasetLogger);
 * 
 * @param notificationType (string): name of notification being sent (onDataChanged, etc)
 * @param dataset (object): dataset that sent the notification
 * @param data (object, optional): arguments sent with the notification
 */
commonspot.spry.util.datasetLogger = function(notificationType, dataset, data)
{
	var datasetName = dataset.name ? ' (' + dataset.name + ')' : '';
	if(data)
		console.log('datasetLogger.' + notificationType + datasetName + '\ndataset data: ', dataset.data, '\nnotification data:', data);
	else
		console.log('datasetLogger.' + notificationType + datasetName + '\ndataset data', dataset.data);
}

/**
 * basic region notification logger, works with firebug console
 * add this function as an observer of a region, and all notifications from it get logged to the console:
 * region being observered MUST have an ID attribute, passed in when you add an observer to it
 * 
 * example setup:
 * 	Spry.Data.Region.addObserver([regionID], commonspot.spry.util.regionLogger);
 * 
 * @param notificationState (string): name of notification being sent (onLoadingData, etc)
 * @param notifier (object): object that sent the notification; NOT the region so far, not very useful
 * @param data (object): arguments sent with the notification. includes .regionID, id of notifying region
 */
commonspot.spry.util.regionLogger = function(notificationState, notifier, data)
{
	console.log('regionLogger.' + notificationState + ' (' + data.regionID + ': ' + data.state + ')\nnotification data: ', data);
}

/**
 * Extends Spry's XMLDataSet object by overwriting some methods and adding additional APIs
 * A critical improvement is the ability to pass a "dataRowPreprocessor" among the optional arguments. 
 * A dataRowPreprocessor works pretty much like a Spry's filter, it just get applied before any data is loaded and any dynamic region get populated
 * Optional keys inside dataSetOptions are:
 * 
 * xpath: an XPath expression {string}
 * dataRowPreprocessor: filter to be applied before data is loaded {function}
 * rows: data to be inserted in the dataset, either one single object or an array of objects
 */
commonspot.spry.Dataset = function(dataSetOptions)
{
	// Equalize the difference between our own signature (with optional value object holding arguments) 
	// and the original one from Spry.Data.XMLDataSet
	var xpathPattern = '';
	if(dataSetOptions && dataSetOptions.xpath)
	{
		xpathPattern = dataSetOptions.xpath;
	}
	this.dataRowPreprocessor = null;
	Spry.Data.XMLDataSet.call(this, null, xpathPattern, dataSetOptions);
	if(dataSetOptions && dataSetOptions.rows)
	{
		this.insert(dataSetOptions.rows);
	}
};

commonspot.spry.Dataset.prototype = new Spry.Data.XMLDataSet();
commonspot.spry.Dataset.prototype.constructor = commonspot.spry.Dataset;

/**
 * Removes all the records from the dataset and notify observers
 */
commonspot.spry.Dataset.prototype.clear = function()
{
	this.unfilteredData = null;
	this.filteredData = null;
	this.data = [];
	this.dataHash = {};
	this.pageOffset = 0;
	this.notifyObservers("onDataChanged");
};

/**
 * Adds rows to a dataset
 * Accept either one single object or an array of objects (multiple insert)
 */
commonspot.spry.Dataset.prototype.insert = function(rowObj)
{
	/*
	If we only get one record, we put it inside an array anyway, 
	so that from now on the code is the same, 
	either for a single record or an array of them
	*/
	var recordsHolder = [];
	
	if(commonspot.util.isArray(rowObj))
	{
		recordsHolder = rowObj;
	}
	else
	{
		recordsHolder.push(rowObj);
	}
	
	for(var i=0; i<recordsHolder.length; i++)
	{
		// Create new autonumber PK
		var recordID = this.data.length;
		recordsHolder[i][commonspot.spry.PK_COL] = recordID;
		this.dataHash[this.data.length] = recordsHolder[i];
		this.data.push(recordsHolder[i]);	
	}
	this.dataWasLoaded = true;
	this.notifyObservers("onDataChanged");
};

/**
 * Given a row, or an array of rows, 
 * remove the current content from the dataset and populate it from scratch
 */
commonspot.spry.Dataset.prototype.replaceAllData = function(rowObj)
{
	this.clear();
	this.insert(rowObj);
};

/**
 * Updates all the row based on the name/value pairs contained inside a value object
 * Throws an exception if the dataset is empty
 * Notifies observers only if some data gets modified, or if notifyAlways=true; defaults to false
 */
commonspot.spry.Dataset.prototype.update = function(valueObj, notifyAlways, rowNumber)
{
	if (this.getRowCount() == 0)
	{
		throw new commonspot.exception.FatalCommandError(commonspot.spry.MSG_EMPTY_DATASET);
		return false;
	}
	if (typeof notifyAlways == 'undefined')
		notifyAlways = false;
		
   if (typeof rowNumber == 'undefined')
      rowNumber = -1;
		   
   var doUpdate = true;   
	var dataWasModified = notifyAlways;
	var rowObj;
		
	for (var i = 0; i < this.data.length; i++)
	{
		if (rowNumber >= 0)
		{
			if (!this.data[rowNumber])
				this.data[rowNumber] = [];
						
			rowObj = this.data[rowNumber];		
		}
		else
		{
			rowObj = this.data[i];
		}
			
		for (var x in valueObj)
		{
			if (x.charAt(0) != '@' && x.substr(0, 3) != 'ds_')
			{
				if((!(x in rowObj)) || rowObj[x] !== valueObj[x])
				{	
					rowObj[x] = valueObj[x]; // Overwrite any field, even one that didn't exist before
					dataWasModified = true;
				}
			}			/* sleazy check for spry internal flds and faux-attributes we don't handle; do nothing */
		}
				
      if (rowNumber > 0) //only once...
         break;
	}
	
	if (dataWasModified)
	{
		this.dataWasLoaded = true;
		this.notifyObservers("onDataChanged");
	}
};

/**
 * Overwrites Spry's method to make it possible to run a filter just after the XML nodes are turned into data, 
 * before the data is loaded in the dataset and dynamic regions get populated  
 */
commonspot.spry.Dataset.prototype.loadDataIntoDataSet = function(rawDataDoc)
{
	var rs = null;
	var mainXPath = Spry.Data.Region.processDataRefString(null, this.xpath, this.dataSetsForDataRefStrings);
	var subPaths = this.subPaths;
	var suppressColumns = false;

	if (this.subPaths && this.subPaths.length > 0)
	{
		// Some subPaths were specified. Convert any data references in each subPath
		// to real data. While we're at it, convert any subPaths that are relative
		// to our main XPath to absolute paths.

		var processedSubPaths = [];
		var numSubPaths = subPaths.length;
		for (var i = 0; i < numSubPaths; i++)
		{
			var subPathStr = Spry.Data.Region.processDataRefString(null, subPaths[i], this.dataSetsForDataRefStrings);
			if (subPathStr.charAt(0) != '/')
				subPathStr = mainXPath + "/" + subPathStr;
			processedSubPaths.push(subPathStr);
		}

		// We need to add our main XPath to the set of subPaths and generate a path
		// tree so we can find the XPath to the common parent of all the paths, just
		// in case the user specified a path that was outside of our main XPath.

		processedSubPaths.unshift(mainXPath);
		var commonParent = this.convertXPathsToPathTree(processedSubPaths);

		// The root node of the resulting path tree should contain the XPath
		// to the common parent. Make this the XPath we generate our initial
		// set of rows from so we can group the results of flattening the other
		// subPaths in predictable/expected manner.

		mainXPath = commonParent.path;
		subPaths = commonParent.subPaths;

		// If the XPath to the common parent we calculated isn't our main XPath
		// or any of the subPaths specified by the user, it is used purely for
		// grouping and joining the data we will flatten. We don't want to include
		// any of the columns for the rows created for the common parent XPath since
		// the user did not ask for it.

		suppressColumns = commonParent.xpath ? false : true;
	}

	rs = commonspot.spry.util.getRecordSetFromXMLDoc(rawDataDoc, mainXPath, suppressColumns, this.dataRowPreprocessor);

	if (!rs)
	{
		Spry.Debug.reportError("Spry.Data.XMLDataSet.loadDataIntoDataSet() failed to create dataSet '" + this.name + "'for '" + this.xpath + "' - " + this.url + "\n");
		return;
	}

	this.doc = rs.xmlDoc;
	this.data = rs.data;
	this.dataHash = rs.dataHash;
	this.dataWasLoaded = (this.doc != null);
};

/*
 * overrides Spry's method, for several reasons:
 * 	- their "number" type uses new Number(), which evaluates as boolean 'true' even for 0
 * 	- we have additional datatypes
 * 	- we may have deserialized some data as non-string types, which don't need conversion
 */
commonspot.spry.Dataset.prototype.applyColumnTypes = function()
{
	var rows = this.getData(true);
	var numRows = rows.length;
	var colNames = [];

	if (numRows < 1)
		return;

	for (var cname in this.columnTypes)
	{
		if(cname === commonspot.spry.PK_COL)
			continue;
		var ctype = this.columnTypes[cname];
		if (ctype !== "string")
		{
			for (var i = 0; i < numRows; i++)
			{
				var row = rows[i];
				var val = row[cname];
				if (typeof val === "string")
				{
					if (ctype === "number" || ctype === "float")
					{
						val = parseFloat(val);
						row[cname] = isNaN(val) ? 0 : val;
					}
					else if (ctype === "html")
						row[cname] = commonspot.util.xml.decodeEntities(val);
					/*
					 dmerrill 9/3/09: if we wanted to support our more specific types...
					 else if (ctype === "bool")
						row[cname] = val.toBoolean();
					else if (ctype === "int")
					{
						val = parseInt(val);
						row[cname] = isNaN(val) ? 0 : val;
					}*/
				}
			}
		}
	}
};

/**
 * Spry's native getCurrentRow() rtns a *reference* to its internal data structure
 * if you change the value you get back, the dataset is updated...
 * 	...w/o notifying observers, or maintaining any related internal structures
 * this method returns a by-value copy of getCurrentRow(), safe to modify and pass back into update()
 */
commonspot.spry.Dataset.prototype.getCurrentRowClone = function()
{
	return new commonspot.util.cloneObject(this.getCurrentRow());
}

commonspot.spry.Dataset.prototype.getStatus = function()
{
	var status;
	var data = this.data[0];
	if(data && data.ds_XMLNode && data.ds_XMLNode.nextSibling && data.ds_XMLNode.nextSibling.childNodes)
		status = data.ds_XMLNode.nextSibling.childNodes;
	return status;
}

// returns values in one field of data array as a list
commonspot.spry.Dataset.prototype.getValueList = function(field, delim)
{
	delim = delim || ",";
	var data = this.getData();
	if(commonspot.util.isArray(data))
		return (data.length > 0) ? data.pluck(field).join(delim) : "";
	return data;
}
