// SpryNestedXMLDataSet.js - version 0.7 - Spry Pre-Release 1.6.1
//
// 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.NestedXMLDataSet = function(parentDataSet, xpath, options)
{
	this.parentDataSet = parentDataSet;
	this.xpath = xpath;
	this.nestedDataSets = [];
	this.nestedDataSetsHash = {};
	this.currentDS = null;
	this.currentDSAncestor = null;
	this.options = options;
	this.ignoreOnDataChanged = false;
	this.entityEncodeStrings = parentDataSet ? parentDataSet.entityEncodeStrings : true;

	Spry.Data.DataSet.call(this, options);

	parentDataSet.addObserver(this);
};

Spry.Data.NestedXMLDataSet.prototype = new Spry.Data.DataSet();
Spry.Data.NestedXMLDataSet.prototype.constructor = Spry.Data.NestedXMLDataSet.prototype;

Spry.Data.NestedXMLDataSet.prototype.getParentDataSet = function()
{
	return this.parentDataSet;
};

Spry.Data.NestedXMLDataSet.prototype.getNestedDataSetForParentRow = function(parentRow)
{
	// Return the internal nested data set associated with the parent's
	// specified row object.
	
	var xmlNode = parentRow.ds_XMLNode;
	if (xmlNode && this.nestedDataSets)
	{
		// Before we go through all of the trouble of looking up the data set
		// we want, check to see if it is already our current data set!
		
		if (this.currentDSAncestor && this.currentDSAncestor == xmlNode)
			return this.currentDS;

		// The caller is asking for a data set that isn't our current one.
		// Manually walk through all of the data sets we have, and return the
		// one that is associated with xmlNode.

		var nDSArr = this.nestedDataSets;
		var nDSArrLen = nDSArr.length;
		for (var i = 0; i < nDSArrLen; i++)
		{
			var dsObj = nDSArr[i];
			if (dsObj && xmlNode == dsObj.ancestor)
				return dsObj.dataSet;
		}
	}
	return null;
};

Spry.Data.NestedXMLDataSet.prototype.getNestedXMLDataSetsArray = function()
{
	// Return an array of all of our internal nested data sets.

	var resultsArray = [];
	if (this.nestedDataSets)
	{
		var arrDS = this.nestedDataSets;
		var numDS = this.nestedDataSets.length;
		for (var i = 0; i < numDS; i++)
			resultsArray.push(arrDS[i].dataSet);
	}
	return resultsArray;
};

Spry.Data.NestedXMLDataSet.prototype.onDataChanged = function(notifier, data)
{
	// This function gets called any time the *parent* data set gets changed.

	if (!this.ignoreOnDataChanged)
		this.loadData();
};

Spry.Data.NestedXMLDataSet.prototype.onCurrentRowChanged = function(notifier, data)
{
	// The current row for our parent data set changed. We need to sync
	// our internal state so that our current data set is the nested data
	// for the parent's current row.
	//
	// From the outside, this appears as if the *entire* data inside this
	// data set changes. We don't want any of our nested child data sets
	// to recalculate their internal nested data structures, we simply want
	// them to select the correct one from the set they already have. To do
	// this, we dispatch a pre and post context change message that allows
	// them to figure out what is going on, and that they can safely ignore
	// any onDataChanged message they get from their parent.

	this.notifyObservers("onPreParentContextChange");
	this.currentDS = null;
	this.currentDSAncestor = null;
	var pCurRow = this.parentDataSet.getCurrentRow();
	if (pCurRow)
	{
		var nestedDS = this.getNestedDataSetForParentRow(pCurRow);
		if (nestedDS)
		{
			this.currentDS = nestedDS;
			this.currentDSAncestor = pCurRow.ds_XMLNode;
		}
	}
	this.notifyObservers("onDataChanged");
	this.notifyObservers("onPostParentContextChange");
	this.ignoreOnDataChanged = false;
};

// If we get an onPostParentContextChange, we want to treat it as if we got an
// onCurrentRowChanged message from our parent, that way, we don't have to recalculate
// any of our internal data, we just have to select the correct data set
// that matches our parent's current row.

Spry.Data.NestedXMLDataSet.prototype.onPostParentContextChange = Spry.Data.NestedXMLDataSet.prototype.onCurrentRowChanged;
Spry.Data.NestedXMLDataSet.prototype.onPreParentContextChange = function(notifier, data)
{
	this.ignoreOnDataChanged = true;
};

Spry.Data.NestedXMLDataSet.prototype.filterAndSortData = function()
{
	// This method is almost identical to the one from the
	// DataSet base class, except that it does not set the
	// current row id.

	// If there is a data filter installed, run it.

	if (this.filterDataFunc)
		this.filterData(this.filterDataFunc, true);

	// If the distinct flag was set, run through all the records in the recordset
	// and toss out any that are duplicates.

	if (this.distinctOnLoad)
		this.distinct(this.distinctFieldsOnLoad);

	// If sortOnLoad was set, sort the data based on the columns
	// specified in sortOnLoad.

	if (this.keepSorted && this.getSortColumn())
		this.sort(this.lastSortColumns, this.lastSortOrder);
	else if (this.sortOnLoad)
		this.sort(this.sortOnLoad, this.sortOrderOnLoad);

	// If there is a view filter installed, run it.

	if (this.filterFunc)
		this.filter(this.filterFunc, true);
};

Spry.Data.NestedXMLDataSet.prototype.loadData = function()
{
	var parentDS = this.parentDataSet;

	if (!parentDS || parentDS.getLoadDataRequestIsPending() || !this.xpath)
		return;

	if (!parentDS.getDataWasLoaded())
	{
		// Someone is asking us to load our data, but our parent
		// hasn't even initiated a load yet. Tell our parent to
		// load its data, so we can get our data!

		parentDS.loadData();
		return;
	}

	this.notifyObservers("onPreLoad");

	this.nestedDataSets = [];
	this.currentDS = null;
	this.currentDSAncestor = null;

	this.data = [];
	this.dataHash = {};

	var self = this;

	var ancestorDS = [ parentDS ];
	if (parentDS.getNestedXMLDataSetsArray)
		ancestorDS = parentDS.getNestedXMLDataSetsArray();

	var currentAncestor = null;
	var currentAncestorRow = parentDS.getCurrentRow();
	if (currentAncestorRow)
		currentAncestor = currentAncestorRow.ds_XMLNode;

	var numAncestors = ancestorDS.length;
	for (var i = 0; i < numAncestors; i++)
	{
		// Run through each row of *every* ancestor data set and create
		// a nested data set.

		var aDS = ancestorDS[i];
		var aData = aDS.getData(true);
		if (aData)
		{
			var aDataLen = aData.length;
			for (var j = 0; j < aDataLen; j++)
			{
				var row = aData[j];
				if (row && row.ds_XMLNode)
				{
					// Create an internal nested data set for this row.

					var ds = new Spry.Data.DataSet(this.options);

					// Make sure the internal nested data set has the same set
					// of columnTypes as the nested data set itself.

					for (var cname in this.columnTypes)
						ds.setColumnType(cname, this.columnTypes[cname]);

					// Flatten any data that matches our XPath and stuff it into
					// our new nested data set.

					var dataArr = Spry.Data.XMLDataSet.getRecordSetFromXMLDoc(row.ds_XMLNode, this.xpath, false, this.entityEncodeStrings);
					ds.setDataFromArray(dataArr.data, true);

					// Create an object that stores the relationship between our
					// internal nested data set, and the ancestor node that was used
					// extract the data for the data set.

					var dsObj = new Object;
					dsObj.ancestor = row.ds_XMLNode;
					dsObj.dataSet = ds;

					this.nestedDataSets.push(dsObj);

					// If this ancestor is the one for our parent's current row,
					// make the current data set our current data set.

					if (row.ds_XMLNode == currentAncestor)
					{
						this.currentDS = ds;
						this.currentDSAncestor = this.ds_XMLNode;
					}
		
					// Add an observer on the nested data set so that whenever it dispatches
					// a notifications, we forward it on as if we generated the notification.
		
					ds.addObserver(function(notificationType, notifier, data){ if (notifier == self.currentDS) setTimeout(function() { self.notifyObservers(notificationType, data); }, 0); });
				}
			}
		}
	}

	this.pendingRequest = new Object;
	this.dataWasLoaded = false;

	this.pendingRequest.timer = setTimeout(function() {
		self.pendingRequest = null;
		self.dataWasLoaded = true;

		self.disableNotifications();
		self.filterAndSortData();
		self.enableNotifications();

		self.notifyObservers("onPostLoad");
		self.notifyObservers("onDataChanged");
	}, 0);
};

Spry.Data.NestedXMLDataSet.prototype.getData = function(unfiltered)
{
	if (this.currentDS)
		return this.currentDS.getData(unfiltered);
	return [];
};

Spry.Data.NestedXMLDataSet.prototype.getRowCount = function(unfiltered)
{
	if (this.currentDS)
		return this.currentDS.getRowCount(unfiltered);
	return 0;
};

Spry.Data.NestedXMLDataSet.prototype.getRowByID = function(rowID)
{
	if (this.currentDS)
		return this.currentDS.getRowByID(rowID);
	return undefined;
};

Spry.Data.NestedXMLDataSet.prototype.getRowByRowNumber = function(rowNumber, unfiltered)
{
	if (this.currentDS)
		return this.currentDS.getRowByRowNumber(rowNumber, unfiltered);
	return null;
};

Spry.Data.NestedXMLDataSet.prototype.getCurrentRow = function()
{
	if (this.currentDS)
		return this.currentDS.getCurrentRow();
	return null;
};

Spry.Data.NestedXMLDataSet.prototype.setCurrentRow = function(rowID)
{
	if (this.currentDS)
		return this.currentDS.setCurrentRow(rowID);
};

Spry.Data.NestedXMLDataSet.prototype.getRowNumber = function(row)
{
	if (this.currentDS)
		return this.currentDS.getRowNumber(row);
	return 0;
};

Spry.Data.NestedXMLDataSet.prototype.getCurrentRowNumber = function()
{
	if (this.currentDS)
		return this.currentDS.getCurrentRowNumber();
	return 0;
};

Spry.Data.NestedXMLDataSet.prototype.getCurrentRowID = function()
{
	if (this.currentDS)
		return this.currentDS.getCurrentRowID();
	return 0;
};

Spry.Data.NestedXMLDataSet.prototype.setCurrentRowNumber = function(rowNumber)
{
	if (this.currentDS)
		return this.currentDS.setCurrentRowNumber(rowNumber);
};

Spry.Data.NestedXMLDataSet.prototype.findRowsWithColumnValues = function(valueObj, firstMatchOnly, unfiltered)
{
	if (this.currentDS)
		return this.currentDS.findRowsWithColumnValues(valueObj, firstMatchOnly, unfiltered);
	return firstMatchOnly ? null : [];
};

Spry.Data.NestedXMLDataSet.prototype.setColumnType = function(columnNames, columnType)
{
	if (columnNames)
	{
		// Make sure the nested xml data set tracks the column types
		// that the user sets so that if our data changes, we can re-apply
		// the column types.

		Spry.Data.DataSet.prototype.setColumnType.call(this, columnNames, columnType);

		// Now apply the column types to any internal nested data sets
		// that exist.

		var dsArr = this.nestedDataSets;
		var dsArrLen = dsArr.length;
		for (var i = 0; i < dsArrLen; i++)
			dsArr[i].dataSet.setColumnType(columnNames, columnType);
	}
};

Spry.Data.NestedXMLDataSet.prototype.getColumnType = function(columnName)
{
	if (this.currentDS)
		return this.currentDS.getColumnType(columnName);
	return "string";
};

Spry.Data.NestedXMLDataSet.prototype.distinct = function(columnNames)
{
	if (columnNames)
	{
		var dsArr = this.nestedDataSets;
		var dsArrLen = dsArr.length;
		for (var i = 0; i < dsArrLen; i++)
			dsArr[i].dataSet.distinct(columnNames);
	}
};

Spry.Data.NestedXMLDataSet.prototype.sort = function(columnNames, sortOrder)
{
	if (columnNames)
	{
		// Forward the sort request to all internal data sets.

		var dsArr = this.nestedDataSets;
		var dsArrLen = dsArr.length;
		for (var i = 0; i < dsArrLen; i++)
			dsArr[i].dataSet.sort(columnNames, sortOrder);

		// Make sure we store a local copy of the last sort order
		// column so we can restore it if new data is loaded.

		if (dsArrLen > 0)
		{
			var ds = dsArr[0].dataSet;
			this.lastSortColumns = ds.lastSortColumns.slice(0); // Copy the array.
			this.lastSortOrder = ds.lastSortOrder;
		}
	}
};

Spry.Data.NestedXMLDataSet.prototype.filterData = function(filterFunc, filterOnly)
{
	// Store a copy of the filterFunc so we can apply it
	// if the data set loads new data.

	this.filterDataFunc = filterFunc;

	// Now set the filterFunc on all of the internal
	// data sets.

	var dsArr = this.nestedDataSets;
	var dsArrLen = dsArr.length;
	for (var i = 0; i < dsArrLen; i++)
		dsArr[i].dataSet.filterData(filterFunc, filterOnly);
};

Spry.Data.NestedXMLDataSet.prototype.filter = function(filterFunc, filterOnly)
{
	// Store a copy of the filterFunc so we can apply it
	// if the data set loads new data.

	this.filterFunc = filterFunc;

	// Now set the filterFunc on all of the internal
	// data sets.

	var dsArr = this.nestedDataSets;
	var dsArrLen = dsArr.length;
	for (var i = 0; i < dsArrLen; i++)
		dsArr[i].dataSet.filter(filterFunc, filterOnly);
};

Spry.Data.NestedXMLDataSet.prototype.setXPath = function(path)
{
	if (this.xpath != path)
	{
		this.xpath = path;
		this.loadData();
	}
};