// 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();
        }
};
