////////////////////////////////////////////////////////////////////////////////
//
//  ADOBE SYSTEMS INCORPORATED
//  Copyright 2009 Adobe Systems Incorporated
//  All Rights Reserved.
//
//  NOTICE: Adobe permits you to use, modify, and distribute this file
//  in accordance with the terms of the license agreement accompanying it.
//
////////////////////////////////////////////////////////////////////////////////

package mx.collections
{

import flash.events.EventDispatcher;
import flash.utils.Dictionary;
import flash.xml.XMLNode;

import mx.collections.errors.ItemPendingError;
import mx.core.EventPriority;
import mx.core.mx_internal;
import mx.events.CollectionEvent;
import mx.events.CollectionEventKind;
import mx.events.PropertyChangeEvent;
import mx.utils.IXMLNotifiable;
import mx.utils.UIDUtil;
import mx.utils.XMLNotifier;

use namespace mx_internal;

/**
 *  The HierarchicalCollectionView class provides a hierarchical view of a standard collection.
 *
 *  @mxml
 *
 *  The <code>&lt;mx.HierarchicalCollectionView&gt;</code> inherits all the tag attributes of its superclass, 
 *  and defines the following tag attributes:</p>
 *
 *  <pre>
 *  &lt;mx:HierarchicalCollectionView
 *  <b>Properties </b>
 *    showRoot="true|false"
 *    source="<i>No default</i>"
 *  /&gt;
 *  </pre> 
 *  
 *  @langversion 3.0
 *  @playerversion Flash 9
 *  @playerversion AIR 1.1
 *  @productversion Flex 3
 */
public class HierarchicalCollectionView extends EventDispatcher
                                        implements IHierarchicalCollectionView, IXMLNotifiable
{
    include "../core/Version.as";
    
    //--------------------------------------------------------------------------
    //
    //  Constructor
    //
    //--------------------------------------------------------------------------

    /**
     *  Constructor.
     *
     *  @param hierarchicalData The data structure containing the hierarchical data.
     *
     *  @param argOpenNodes The Object that defines a node to appear as open.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function HierarchicalCollectionView(
                            hierarchicalData:IHierarchicalData = null,
                            argOpenNodes:Object = null)
    {
        super();
        
        if (hierarchicalData)
            initializeCollection(hierarchicalData.getRoot(), hierarchicalData, argOpenNodes);
    }
    
    //--------------------------------------------------------------------------
    //
    // Variables
    //
    //--------------------------------------------------------------------------
    
    /**
     *  @private
     */
    private var hierarchicalData:IHierarchicalData;

    /**
     *  @private
     */
    private var cursor:HierarchicalCollectionViewCursor;

    /**
     *  @private
     *  The total number of nodes we know about.
     */
    private var currentLength:int;
    
    /**
     *  @private
     *  Top level XML node if there is one
     */
    private var parentNode:XML;

    /**
     *  @private
     *  Mapping of nodes to children.  Used by getChildren.
     */
    private var childrenMap:Dictionary;
    
    /**
     *  @private
     */
    private var childrenMapCache:Dictionary = new Dictionary(true);
    
    /**
     *  @private
     */
    mx_internal var treeData:ICollectionView;
    
    /**
     *  @private
     *  Mapping of UID to parents.  Must be maintained as things get removed/added
     *  This map is created as objects are visited
     */
    mx_internal var parentMap:Object;

    //--------------------------------------------------------------------------
    //
    //  Properties
    //
    //--------------------------------------------------------------------------

    //----------------------------------
    //  hasRoot
    //----------------------------------
    
    private var _hasRoot:Boolean;
    
    /** 
     *  @inheritDoc
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function get hasRoot():Boolean
    {
        return _hasRoot;
    }
    
    //----------------------------------
    //  openNodes
    //----------------------------------
    
    /**
     *  @private
     *  Storage for the openNodes property.
     */
    private var _openNodes:Object;
    
    /**
     *  @inheritDoc
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function get openNodes():Object
    {
        return _openNodes;  
    }
    
    /**
     *  @private
     */
    public function set openNodes(value:Object):void
    {
        // openNodes cant be null
        if (value)
        {
            _openNodes = {};
            for each (var item:* in value)
            {
                _openNodes[UIDUtil.getUID(item)] = item;
            }
        }
        else
            _openNodes = {};
        
        if (hierarchicalData)
        {
            //calc initial length
            currentLength = calculateLength();
            
            // need to refresh the collection after setting openNodes
            var event:CollectionEvent =
                new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
            event.kind = CollectionEventKind.REFRESH;
            dispatchEvent(event);
        }
    }
    
    //----------------------------------
    //  showRoot
    //----------------------------------
    
    private var _showRoot:Boolean = true;
    
    [Bindable]
    [Inspectable(category="Data", enumeration="true,false", defaultValue="true")]
    
    /**
     *  @inheritDoc
     *
     *  @default true
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function get showRoot():Boolean
    {
        return _showRoot;
    }

    /**
     *  @private
     */
    public function set showRoot(value:Boolean):void
    {
        if (_showRoot != value)
        {
            _showRoot = value;
            if (hierarchicalData)
            {
                initializeCollection(hierarchicalData.getRoot(), hierarchicalData, openNodes);
                
                //setting showRoot resets the collection
                var event:CollectionEvent =
                    new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
                event.kind = CollectionEventKind.RESET;
                dispatchEvent(event);
            }
        }
    }
    
    //----------------------------------
    //  source
    //----------------------------------
    
    /**
     *  @inheritDoc
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function get source():IHierarchicalData
    {
        return hierarchicalData;
    }

    /**
     *  @private
     */
    public function set source(value:IHierarchicalData):void
    {
        initializeCollection(value.getRoot(), value, openNodes);
    }
    
    //----------------------------------
    //  filter
    //----------------------------------

    /**
     *  @private
     *  Storage for the filterFunction property.
     */
    private var _filterFunction:Function;

    [Bindable("filterFunctionChanged")]
    [Inspectable(category="General")]
    
    /**
     *  @private
     */
    public function get filterFunction():Function
    {
        return _filterFunction;
    }

    /**
     *  @private
     */
    public function set filterFunction(value:Function):void
    {
        _filterFunction = value;
    }

    //----------------------------------
    //  length
    //----------------------------------

    /**
     *  The length of the currently parsed collection.  
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function get length():int
    {
        return currentLength;
    }

    //----------------------------------
    //  sort
    //----------------------------------
    
    /**
     *  @private
     *  Storage for the sort property.
     */
    private var _sort:ISort;

    [Bindable("sortChanged")]
    [Inspectable(category="General")]
    
    /**
     *  @private
     */
    public function get sort():ISort
    {
        return _sort;
    }

    /**
     *  @private
     */
    public function set sort(value:ISort):void
    {
        _sort = value;
    }
    
    //--------------------------------------------------------------------------
    //
    // ICollectionView Methods
    //
    //--------------------------------------------------------------------------

    /**
     *  Returns a new instance of a view iterator over the items in this view.
     *
     *  @return IViewCursor instance.
     *
     *  @see mx.utils.IViewCursor
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function createCursor():IViewCursor
    {
        return new HierarchicalCollectionViewCursor(
            this, treeData, hierarchicalData);
    }

    /**
     *  Checks the collection for the data item using standard equality test.
     *
     *  @param item The Object that defines the node to look for.
     *
     *  @param <code>true</code> if the collection contains the item,
     *
     *  @return <code>true</code> if the data item is in the collection, 
     *  and <code>false</code> if not.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function contains(item:Object):Boolean
    {
        var cursor:IViewCursor = createCursor();
        while (!cursor.afterLast)
        {
            if (cursor.current == item)
                return true;
                
            try
            {
                cursor.moveNext();
            }
            catch (e:ItemPendingError)
            {
                // item is pending.
                // we are not sure if the item is present or not, 
                // so return false
                return false;
            }
        }
        return false;
    }

    /**
     *  @private
     */
    public function disableAutoUpdate():void
    {
        // propogate to all the child collections
        treeData.disableAutoUpdate();
        for (var p:Object in childrenMap)
            ICollectionView(childrenMap[p]).disableAutoUpdate();
    }

    /**
     *  @private
     */
    public function enableAutoUpdate():void
    {
        // propogate to all the child collections
        treeData.enableAutoUpdate();
        for (var p:Object in childrenMap)
            ICollectionView(childrenMap[p]).enableAutoUpdate();
    }

    /**
     *  @private
     */
    public function itemUpdated(item:Object, property:Object = null,
                                oldValue:Object = null,
                                newValue:Object = null):void
    {
        var event:CollectionEvent =
            new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
        event.kind = CollectionEventKind.UPDATE;

        var objEvent:PropertyChangeEvent =
            new PropertyChangeEvent(PropertyChangeEvent.PROPERTY_CHANGE);
        objEvent.source = item;
        objEvent.property = property;
        objEvent.oldValue = oldValue;
        objEvent.newValue = newValue;
        event.items.push(objEvent);

        dispatchEvent(event);
    }
    
    /**
     *  @inheritDoc 
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function refresh():Boolean
    {   
        return internalRefresh(true);
    }
    
    //--------------------------------------------------------------------------
    //
    // IHierarchicalCollectionView Methods
    //
    //--------------------------------------------------------------------------
    
    /**
     * @inheritDoc
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */ 
    public function getNodeDepth(node:Object):int
    {
        var depth:int = 1;
        var parent:Object = getParentItem(node);
        
        while(parent != null)
        {
            parent = getParentItem(parent);
            depth++;
        }
        depth = (hasRoot && !showRoot) ? (depth - 1) : depth;
        
        // depth cant be less then 1
        return (depth < 1) ? 1 : depth;
    }
    
    /**
     *  Returns the parent of a node.  
     *  The parent of a top-level node is <code>null</code>.
     *
     *  @param node The Object that defines the node.
     *
     *  @return The parent node containing the node, 
     *  <code>null</code> for a top-level node,  
     *  and <code>undefined</code> if the parent cannot be determined.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function getParentItem(node:Object):*
    {
        var uid:String = UIDUtil.getUID(node);
        if (parentMap.hasOwnProperty(uid))
            return parentMap[uid];

        return undefined;
    }
    
    /**
     *  @inheritDoc 
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function getChildren(node:Object):ICollectionView
    {
        // Using uid because XML cant be referenced correctly using Dictionary
        var uid:String = UIDUtil.getUID(node);
        var children:* = hierarchicalData.getChildren(node);
        
        var childrenCollection:ICollectionView = childrenMapCache[uid];
        
        if (children is XMLList && childrenCollection)
        {       
            //We don't want to send a RESET type of collectionChange event in this case. 
            XMLListCollection(childrenCollection).mx_internal::dispatchResetEvent = false; 
            XMLListCollection(childrenCollection).source = children;
            
            // refresh the collection to apply the sort/filter
            childrenCollection.refresh();
        }
        
        // check the cache and return from it.
        // useful in sorting/filtering.
        if(childrenCollection)
        {
            // node might have changed, so update the childrenMap
            childrenMap[node] = childrenCollection;
            return childrenCollection;
        }
        
        // if there is no children, return null
        if (!children)
            return null;
        
        //then wrap children in ICollectionView if necessary
        if (children is ICollectionView)
        {
            childrenCollection = ICollectionView(children);
        }
        else if (children is Array)
        {
            childrenCollection = new ArrayCollection(children);
        }
        else if (children is XMLList)
        {
            childrenCollection =  new XMLListCollection(children);
        }
        else
        {
            var childArray:Array = new Array(children);
            if (childArray != null)
            {
                childrenCollection =  new ArrayCollection(childArray);
            }
        }
        
        childrenMapCache[uid] = childrenCollection;
        
        
        var oldChildren:ICollectionView = childrenMap[node];
        if (oldChildren != childrenCollection)
        {
            if (oldChildren != null)
            {
                oldChildren.removeEventListener(CollectionEvent.COLLECTION_CHANGE,
                                      nestedCollectionChangeHandler);
            }
            childrenCollection.addEventListener(CollectionEvent.COLLECTION_CHANGE,
                                          nestedCollectionChangeHandler, false, 0, true);
            childrenMap[node] = childrenCollection;
        }
        
        return childrenCollection;
    }
    
    /**
     *  @inheritDoc
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function openNode(node:Object):void
    {
        var uid:String = UIDUtil.getUID(node);
        
        // check if the node is already opened
        if (_openNodes[uid] != null)
            return;
        
        // add the node to the openNodes object and update the length
        _openNodes[uid] = node;
        
        // apply the sort/filter to the child collection of the opened node.
        var childrenCollection:ICollectionView = getChildren(node);
        
        // return if there are no children
        if (!childrenCollection)
            return;
        
        if (sortCanBeApplied(childrenCollection) && !(childrenCollection.sort == null && sort == null))
        {
            childrenCollection.sort = this.sort;
        }
        if (!(childrenCollection.filterFunction == null && filterFunction == null))
        {
            childrenCollection.filterFunction = this.filterFunction;
        }
        childrenCollection.refresh();

        updateParentMapAndLength(childrenCollection, node);
    }
    
    /**
     *  @inheritDoc
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function closeNode(node:Object):void
    {
        var childrenCollection:ICollectionView = childrenMap[node];

        // removes the node from the openNodes object and update the length
        delete _openNodes[UIDUtil.getUID(node)];

        if (childrenCollection)
        {
            var cursor:IViewCursor = childrenCollection.createCursor();
            while (!cursor.afterLast)
            {
                var uid:String = UIDUtil.getUID(cursor.current);
                delete parentMap[uid];

                try
                {
                    cursor.moveNext();
                }
                catch (e:ItemPendingError)
                {
                    break;
                }
        
            }
        }
        
        // update the length
        updateLength();
    }
    
    /**
     *  @inheritDoc
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function addChild(parent:Object, newChild:Object):Boolean
    {
        if (parent == null)
            return addChildAt(parent, newChild, treeData.length);
        else
            return addChildAt(parent, newChild, getChildren(parent).length);
    }

    /**
     *  @inheritDoc
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function removeChild(parent:Object, child:Object):Boolean
    {
        var cursor:IViewCursor;
        var index:int = 0;
        if (parent == null)
        {
            cursor = treeData.createCursor();
        }
        else
        {
             var children:ICollectionView = getChildren(parent);
             cursor = children.createCursor();
        }
        while (!cursor.afterLast)
        {
            if (cursor.current == child)
            {
                cursor.remove();
                return true;
            }
                
            try
            {
                cursor.moveNext();
            }
            catch (e:ItemPendingError)
            {
                // Items are pending - so return false
                return false;
            }
        }
        
        return false;
    }
    
    /**
     *  Add a child node to a node at the specified index. 
     *  This implementation does the following:
     * 
     *  <ul>
     *      <li>If the <code>parent</code> is null or undefined,
     *          inserts the <code>child</code> at the 
     *          specified <code>index</code> in the collection specified 
     *          by <code>source</code>.
     *      </li>
     *      <li>If the <code>parent</code> has a <code>children</code>
     *          field or property, the method adds the <code>child</code> 
     *          to it at the <code>index</code> location.
     *          In this case, the <code>source</code> is not required.
     *     </li>
     *     <li>If the <code>parent</code> does not have a <code>children</code>
     *          field or property, the method adds the <code>children</code> 
     *          to the <code>parent</code>. The method then adds the 
     *          <code>child</code> to the parent at the 
     *          <code>index</code> location. 
     *          In this case, the <code>source</code> is not required.
     *     </li>
     *     <li>If the <code>index</code> value is greater than the collection 
     *         length or number of children in the parent, adds the object as
     *         the last child.
     *     </li>
     * </ul>
     *
     *  @param node The Object that defines the parent node.
     * 
     *  @param newChild The Object that defines the child node.
     * 
     *  @param index The 0-based index of where to insert the child node.
     * 
     *  @param source The entire collection that this node is a part of.
     * 
     *  @return <code>true</code> if the child is added successfully.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function addChildAt(parent:Object, newChild:Object, index:int):Boolean
    {   
        var cursor:IViewCursor;
        if (!parent)
        {
            cursor = treeData.createCursor();
        }
        else
        {
            if (!hierarchicalData.canHaveChildren(parent))
                return false;
            
            var children:ICollectionView = getChildren(parent);
            
            cursor = children.createCursor();
        }
        
        try
        {
            cursor.seek(CursorBookmark.FIRST, index);
        }
        catch (e:ItemPendingError)
        {
            // Item Pending
            return false;
        }
        
        cursor.insert(newChild);
        
        return true;
    }

    /**
     *  Removes the child node from a node at the specified index.
     *
     *  @param parent The Object that defines the parent node.
     * 
     *  @param index The 0-based index of  the child node to remove relative to the parent.
     * 
     *  @return <code>true</code> if the child is removed successfully.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function removeChildAt(parent:Object, index:int):Boolean
    {
        var cursor:IViewCursor;
        if (!parent)
        {
            cursor = treeData.createCursor();
        }
        else
        {
            var children:ICollectionView = getChildren(parent);
            cursor = children.createCursor();
        }
        
        try
        {
            cursor.seek(CursorBookmark.FIRST, index);
        }
        catch (e:ItemPendingError)
        {
            // Item Pending
            return false;
        }
        
        if (cursor.beforeFirst || cursor.afterLast)
            return false;
        
        cursor.remove();
        return true;
    }
    
    //--------------------------------------------------------------------------
    //
    // Methods
    //
    //--------------------------------------------------------------------------
    
    /**
     * @private
     * 
     * returns the collection view of the given object
     */ 
    private function getCollection(value:Object):ICollectionView
    {
        // handle strings and xml
        if (typeof(value)=="string")
            value = new XML(value);
        else if (value is XMLNode)
            value = new XML(XMLNode(value).toString());
        else if (value is XMLList)
            value = new XMLListCollection(value as XMLList);
        
        if (value is XML)
        {
            var xl:XMLList = new XMLList();
            xl += value;
            return new XMLListCollection(xl);
        }
        //if already a collection dont make new one
        else if (value is ICollectionView)
        {
            return ICollectionView(value);
        }
        else if (value is Array)
        {
            return new ArrayCollection(value as Array);
        }
        //all other types get wrapped in an ArrayCollection
        else if (value is Object)
        {         
            // convert to an array containing this one item
            var tmp:Array = [];
            tmp.push(value);
            return new ArrayCollection(tmp);
        }
        else
        {
            return new ArrayCollection();
        }
    }
    
    /**
     *  @private
     *  
     *  Initialize the collection. set its various properties and
     *  update its length.
     */
    private function initializeCollection(model:Object,
                            hierarchicalData:IHierarchicalData,
                            argOpenNodes:Object = null):void
    {
        parentMap = {};

        childrenMap = new Dictionary(true);
        childrenMapCache = new Dictionary(true);
        
        if (treeData)
            treeData.removeEventListener(CollectionEvent.COLLECTION_CHANGE,
                                         collectionChangeHandler,
                                         false);
        
        if (this.hierarchicalData)
            this.hierarchicalData.removeEventListener(CollectionEvent.COLLECTION_CHANGE,
                                         collectionChangeHandler,
                                         false);
        
        treeData = getCollection(model);
        
        if (treeData)
            _hasRoot = treeData.length == 1;
        
        var tmpCollection:Object = model;
        // are we swallowing the root?
        if (hierarchicalData && !showRoot && hasRoot)
        {
            var obj:Object = treeData.createCursor().current;
            if (hierarchicalData.hasChildren(obj))
            {
                // then get rootItem children
                tmpCollection = hierarchicalData.getChildren(obj);
                treeData = getCollection(tmpCollection);
            }
        }

        // listen for add/remove events from developer as weak reference
        treeData.addEventListener(CollectionEvent.COLLECTION_CHANGE,
                                  collectionChangeHandler,
                                  false,
                                  EventPriority.DEFAULT_HANDLER,
                                  true);
                                  
        this.hierarchicalData = hierarchicalData;
        
        // listen for reset/refresh events
        this.hierarchicalData.addEventListener(
                                    CollectionEvent.COLLECTION_CHANGE,
                                    collectionChangeHandler,
                                    false,
                                    EventPriority.DEFAULT_HANDLER,
                                    true);
        
        // openNodes cant be null
        if (argOpenNodes)
            _openNodes = argOpenNodes;
        else
            _openNodes = {};
        
        //calc initial length
        currentLength = calculateLength();
    }
    
    /**
     *  @private
     *  
     *  Update the parent map and adjust the length.
     */ 
    private function updateParentMapAndLength(collection:ICollectionView, node:Object):void
    {
        var cursor:IViewCursor = collection.createCursor();
        currentLength += collection.length;
        
        while (!cursor.afterLast)
        {
            var item:Object = cursor.current;
            var uid:String = UIDUtil.getUID(item);
            parentMap[uid] = node;
            
            // check that the node is opened or not.
            // If it is open, then update the length with the node's children.
            if (_openNodes[uid] != null)
            {
                var childrenCollection:ICollectionView = getChildren(item);
                if (childrenCollection)
                    updateParentMapAndLength(childrenCollection, item);
            }
            
            try
            {
                cursor.moveNext();
            }
            catch (e:ItemPendingError)
            {
                break;
            }
        }
    }
    
    /**
     *  @private
     *  Calculate the total length of the collection, but only count nodes
     *  that we can reach.
     */
    public function calculateLength(node:Object = null, parent:Object = null):int
    {
        var length:int = 0;
        var childNodes:ICollectionView;
        var modelOffset:int = 0;
        var firstNode:Boolean = true;

        if (node == null)
        {
            // special case counting the whole thing
            // watch for page faults
            var modelCursor:IViewCursor = treeData.createCursor();
            if (modelCursor.beforeFirst)
            {
                // indicates that an IPE occured on the first item
                return treeData.length;
            }
            while (!modelCursor.afterLast)
            {
                node = modelCursor.current;
                if (node is XML)
                {
                    if (firstNode)
                    {   
                        firstNode = false;
                        var parNode:* = node.parent();
                        if (parNode != null)
                        {
                            startTrackUpdates(parNode);
                            childrenMap[parNode] = treeData;
                            parentNode = parNode;
                        }
                    }
                    startTrackUpdates(node);
                }
                if (node === null)
                    length += 1;
                else
                    length += calculateLength(node, null) + 1;
                modelOffset++;
                try
                {
                    modelCursor.moveNext();
                }
                catch (e:ItemPendingError)
                {
                    // just stop where we are, no sense paging
                    // the whole thing just to get length. make a rough
                    // guess assuming that all un-paged nodes are closed
                    length += treeData.length - modelOffset;
                    return length;
                }
            }
        }
        else
        {
            var uid:String = UIDUtil.getUID(node);
            parentMap[uid] = parent;
            if (node != null &&
                openNodes[uid] &&
                hierarchicalData.canHaveChildren(node) &&
                hierarchicalData.hasChildren(node))
            {
                childNodes = getChildren(node);
                if (childNodes != null)
                {
                    var childCursor:IViewCursor = childNodes.createCursor();
                    try
                    {
                        childCursor.seek(CursorBookmark.FIRST);
                        while (!childCursor.afterLast)
                        {
                            if (node is XML)
                                startTrackUpdates(childCursor.current);
                            length += calculateLength(childCursor.current, node) + 1;
                            modelOffset++;
                            
                            try
                            {
                                childCursor.moveNext();
                            }
                            catch (e:ItemPendingError)
                            {
                                // just stop where we are, no sense paging
                                // the whole thing just to get length. make a rough
                                // guess assuming that all un-paged nodes are closed
                                length += childNodes.length - modelOffset;
                                return length;
                            }
                        }
                    }
                    catch (e:ItemPendingError)
                    {
                        // assume that the child collection has one item
                        length += 1;
                    }
                }
            }
        }
        return length;
    }
    
    /**
     *  @private
     */
    private function internalRefresh(dispatch:Boolean):Boolean
    {
        var obj:Object;
        var coll:ICollectionView
        var needUpdate:Boolean = false;
        
        // apply filter function to all the collections including the child collections
        if (!(treeData.filterFunction == null && filterFunction == null))
        {
            treeData.filterFunction = filterFunction;
            treeData.refresh();
            needUpdate = true;
        }
        
        for each(obj in openNodes)
        {
            coll = getChildren(obj);
            if (coll && !(coll.filterFunction == null && filterFunction == null))
            {
                coll.filterFunction = filterFunction;
                coll.refresh();
                needUpdate = true
            }
        }
        
        // if filter is applied to any collection, only then update the length
        if (needUpdate)
            updateLength();  // length will change after filtering, so update it.
        
        
        // apply sort to all the collections including the child collections
        if (sortCanBeApplied(treeData) && !(treeData.sort == null && sort == null))
        {
            treeData.sort = sort;
            treeData.refresh();
            dispatch = true;
        }
        
        // recursive sort for every field
        for each(obj in openNodes)
        {
            
            coll = getChildren(obj);
            
            if (coll && sortCanBeApplied(coll) && !(coll.sort == null && sort == null))
            {
                coll.sort = sort;
                coll.refresh();
                dispatch = true;
            }
        }
        
        // No concept of a sort level, so commenting the code
        /* for(var i:int = 0;i<sort.fields.length;i++)
        {
            if(sort.fields[i].hasOwnProperty("sortLevel"))
                sortLevel = sort.fields[i].sortLevel;
            //else
                //sortLevel = 1;
            
            
            if(sortLevel == 1)
            {
                var srt:ISort = new Sort();
                        
                if(sort.usingCustomCompareFunction)
                {
                    srt.compareFunction = sort.compareFunction;
                }
                
                srt.unique = sort.unique;
                        
                srt.fields = [sort.fields[i]];
                treeData.sort = srt;
                treeData.refresh();
            }
            else
            {
                for each(var obj:Object in openNodes)
                {
                    
                    var depth:int = 2;
                    var o:Object = obj;
                    while(getParentItem(o) != null)
                    {
                        depth++;
                        o = getParentItem(o);
                    }
                    
                    if(depth == sortLevel)
                    {
                        var coll:ICollectionView = getChildren(obj);
                        
                        var srt:ISort = new Sort();
                        
                        if(sort.usingCustomCompareFunction)
                            srt.compareFunction = sort.compareFunction;
                        
                        srt.unique = sort.unique;
                        
                        srt.fields = [sort.fields[i]];
                        
                        coll.sort = srt;
                        coll.refresh();
                    }
                }
            }
        } */
            
        if (dispatch)
        {
            var refreshEvent:CollectionEvent =
                new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
            refreshEvent.kind = CollectionEventKind.REFRESH;
            dispatchEvent(refreshEvent);
        }
        return true;
    }
    
    /**
     * @private
     * Check if a collection has the properties on which the sort is applied.   
     */
    private function sortCanBeApplied(coll:ICollectionView):Boolean
    {
        if (sort == null)
            return true;
        
        // get the current item
        var obj:Object = coll.createCursor().current;
        
        if (!obj || !sort.fields)
            return false;
        
        // check for the properties (sort fields) in the current object
        for (var i:int = 0; i < sort.fields.length; i++)
        {
            var sf:SortField = sort.fields[i];
            if (!obj.hasOwnProperty(sf.name))
                return false;
        }
        return true;
    }
    
    /**
     * @private
     * Force a recalulation of length   
     */
     private function updateLength(node:Object=null, parent:Object = null):void
     {
        currentLength = calculateLength();
     }

    /**
     * @private
     *  Fill the node array with the node and all of its visible children
     *  update the parentMap as you go.
     */
    private function getVisibleNodes(node:Object, parent:Object, nodeArray:Array):void
    {
        var childNodes:ICollectionView;
        nodeArray.push(node);

        var uid:String = UIDUtil.getUID(node);
        parentMap[uid] = parent;
        if (openNodes[uid] != null &&
            hierarchicalData.canHaveChildren(node) &&
            hierarchicalData.hasChildren(node))
        {
            childNodes = getChildren(node);
            if (childNodes != null)
            {
                var cursor:IViewCursor = childNodes.createCursor();
                while (!cursor.afterLast)
                {
                    getVisibleNodes(cursor.current, node, nodeArray);
                    try
                    {
                        cursor.moveNext();
                    }
                    catch (e:ItemPendingError)
                    {
                        // Items are pending - so return
                        return;
                    }
                }
            }
        }
    }

    /**
     *  @private
     *  Factor in the available open children before this location in the model
     */
    private function getVisibleLocation(oldLocation:int):int
    {
        var newLocation:int = 0;
        var modelCursor:IViewCursor = treeData.createCursor();
        for (var i:int = 0; i < oldLocation && !modelCursor.afterLast; i++)
        {
            newLocation += calculateLength(modelCursor.current, null) + 1;
            try
            {
                modelCursor.moveNext();
            }
            catch (e:ItemPendingError)
            {
                // just return the location as the items are pending
                return newLocation;
            }
        }
        return newLocation;
    }

    /**
     * @private
     * factor in the open children before this location in a sub collection
     */
    private function getVisibleLocationInSubCollection(parent:Object, oldLocation:int):int
    {
        var newLocation:int = oldLocation;
        var target:Object = parent;
        parent = getParentItem(parent);
        var children:ICollectionView;
        var cursor:IViewCursor;
        while (parent != null)
        {
            children = childrenMap[parent];
            cursor = children.createCursor();
            while (!cursor.afterLast)
            {
                if (cursor.current == target)
                {
                    newLocation++;
                    break;
                }
                newLocation += calculateLength(cursor.current, parent) + 1;
                try
                {
                    cursor.moveNext();
                }
                catch (e:ItemPendingError)
                {
                    break;
                }
            }
            target = parent;
            parent = getParentItem(parent);
        }
        cursor = treeData.createCursor();
        while (!cursor.afterLast)
        {
            if (cursor.current == target)
            {
                newLocation++;
                break;
            }
            newLocation += calculateLength(cursor.current, parent) + 1;
            try
            {
                cursor.moveNext();
            }
            catch (e:ItemPendingError)
            {
                break;
            }
        }
        return newLocation;
    }

    //--------------------------------------------------------------------------
    //
    //  Event handlers
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    public function collectionChangeHandler(event:CollectionEvent):void
    {
        var i:int;
        var n:int;
        var location:int;
        var uid:String;
        var parent:Object;
        var node:Object;
        var items:Array;

        var convertedEvent:CollectionEvent;
        
        if (event is CollectionEvent)
        {
            var ce:CollectionEvent = CollectionEvent(event);
            if (ce.kind == CollectionEventKind.REFRESH)
            {
                // collection refreshed, update length
                updateLength();
            }
            else if (ce.kind == CollectionEventKind.RESET)
            {
                // initialize the collection again - its source is modified
                if (hierarchicalData)
                    initializeCollection(hierarchicalData.getRoot(), hierarchicalData, openNodes);
                updateLength();
                internalRefresh(false);
                dispatchEvent(event);
            }
            else if (ce.kind == CollectionEventKind.ADD)
            {
                n = ce.items.length;
                convertedEvent = new CollectionEvent(
                                        CollectionEvent.COLLECTION_CHANGE,
                                        false, 
                                        true,
                                        ce.kind);
                convertedEvent.location = getVisibleLocation(ce.location);
                for (i = 0; i < n; i++)
                {
                    node = ce.items[i];
                    if (node is XML)
                        startTrackUpdates(node);
                    getVisibleNodes(node, null, convertedEvent.items);
                }
                currentLength += convertedEvent.items.length;
                dispatchEvent(convertedEvent);
            }
            else if (ce.kind == CollectionEventKind.REMOVE)
            {
                n = ce.items.length;
                convertedEvent = new CollectionEvent(
                                        CollectionEvent.COLLECTION_CHANGE,
                                        false, 
                                        true,
                                        ce.kind);
                convertedEvent.location = getVisibleLocation(ce.location);
                for (i = 0; i < n; i++)
                {
                    node = ce.items[i];
                    if (node is XML)
                        stopTrackUpdates(node);
                    getVisibleNodes(node, null, convertedEvent.items);
                }
                currentLength -= convertedEvent.items.length;
                dispatchEvent(convertedEvent);
                
            }
            else if (ce.kind == CollectionEventKind.UPDATE)
            {
                // so far, nobody cares about the details so just
                // send it
                dispatchEvent(event);
            }
            else if (ce.kind == CollectionEventKind.REPLACE)
            {
                // someday handle case where node is marked as open
                // before it becomes the replacement.
                // for now, just pass on the data and remove
                // old visible rows
                n = ce.items.length;
                convertedEvent = new CollectionEvent(
                                        CollectionEvent.COLLECTION_CHANGE,
                                        false, 
                                        true,
                                        CollectionEventKind.REMOVE);

                for (i = 0; i < n; i++)
                {
                    node = ce.items[i].oldValue;
                    if (node is XML)
                        stopTrackUpdates(node);
                    getVisibleNodes(node, null, convertedEvent.items);
                }

                // prune the replacements from this list
                var j:int = 0;
                for (i = 0; i < n; i++)
                {
                    node = ce.items[i].oldValue;
                    while (convertedEvent.items[j] != node)
                        j++;
                    convertedEvent.items.splice(j, 1);
                }
                if (convertedEvent.items.length)
                {
                    currentLength -= convertedEvent.items.length;
                    // nobody cares about location yet.
                    dispatchEvent(convertedEvent);
                }
                dispatchEvent(event);
            }
        }
    }

    /**
     *  @private
     */
    public function nestedCollectionChangeHandler(event:CollectionEvent):void
    {
        var i:int;
        var n:int;
        var location:int;
        var uid:String;
        var parent:Object;
        var node:Object;
        var items:Array;
        var convertedEvent:CollectionEvent;

        if (event is CollectionEvent)
        {
            var ce:CollectionEvent = CollectionEvent(event);
            if (ce.kind == CollectionEventKind.mx_internal::EXPAND)
            {
                event.stopImmediatePropagation();
            }
            else if (ce.kind == CollectionEventKind.ADD)
            {
                // optimize someday.  We do a full tree walk so we can
                // not only count how many but find the parents of the
                // new nodes.  A better scheme would be to just
                // increment by the number of visible nodes, but we
                // don't have a good way to get the parents.
                updateLength();
                n = ce.items.length;
                convertedEvent = new CollectionEvent(
                                        CollectionEvent.COLLECTION_CHANGE,
                                        false, 
                                        true,
                                        ce.kind);
                for (i = 0; i < n; i++)
                {
                    node = ce.items[i];
                    if (node is XML)
                        startTrackUpdates(node);
                    parent = getParentItem(node);
                    if (parent != null)
                        getVisibleNodes(node, parent, convertedEvent.items);
                }
                convertedEvent.location = getVisibleLocationInSubCollection(parent, ce.location);
                dispatchEvent(convertedEvent);
            }
            else if (ce.kind == CollectionEventKind.REMOVE)
            {
                n = ce.items.length;
                convertedEvent = new CollectionEvent(
                                        CollectionEvent.COLLECTION_CHANGE,
                                        false, 
                                        true,
                                        ce.kind);
                for (i = 0; i < n; i++)
                {
                    node = ce.items[i];
                    if (node is XML)
                        stopTrackUpdates(node);
                    parent = getParentItem(node);
                    if (parent != null)
                        getVisibleNodes(node, parent, convertedEvent.items);
                }
                convertedEvent.location = getVisibleLocationInSubCollection(parent, ce.location);
                currentLength -= convertedEvent.items.length;
                dispatchEvent(convertedEvent);
            }
            else if (ce.kind == CollectionEventKind.UPDATE)
            {
                // so far, nobody cares about the details so just
                // send it
                dispatchEvent(event);
            }
            else if (ce.kind == CollectionEventKind.REPLACE)
            {
                // someday handle case where node is marked as open
                // before it becomes the replacement.
                // for now, just pass on the data and remove
                // old visible rows
                n = ce.items.length;
                convertedEvent = new CollectionEvent(
                                        CollectionEvent.COLLECTION_CHANGE,
                                        false, 
                                        true,
                                        CollectionEventKind.REMOVE);

                for (i = 0; i < n; i++)
                {
                    node = ce.items[i].oldValue;
                    parent = getParentItem(node);
                    
                    if (parent != null)
                    {
                        getVisibleNodes(node, parent, convertedEvent.items);
                        // update the parent of the new item only 
                        // if the parent node is opened
                        if (_openNodes[UIDUtil.getUID(parent)] != null)
                        {
                            var newNode:Object = ce.items[i].newValue;
                            uid = UIDUtil.getUID(newNode);
                            parentMap[uid] = parent;
                            
                            // delete the parent map for the old node
                            delete parentMap[UIDUtil.getUID(node)];
                        }
                    }
                }

                // prune the replacements from this list
                var j:int = 0;
                for (i = 0; i < n; i++)
                {
                    node = ce.items[i].oldValue;
                    if (node is XML)
                        stopTrackUpdates(node);
                    while (convertedEvent.items[j] != node)
                        j++;
                    convertedEvent.items.splice(j, 1);
                }
                if (convertedEvent.items.length)
                {
                    currentLength -= convertedEvent.items.length;
                    // nobody cares about location yet.
                    dispatchEvent(convertedEvent);
                }
                dispatchEvent(event);
            }
            else if (ce.kind == CollectionEventKind.RESET)
            {
                // removeAll() sends a RESET.
                // when we get a reset we don't know what went away
                // and we don't know how many things went away, so
                // we just fake a refresh as if there was a filter
                // applied that filtered out whatever went away
                updateLength();
                convertedEvent = new CollectionEvent(
                                        CollectionEvent.COLLECTION_CHANGE,
                                        false, 
                                        true,
                                        CollectionEventKind.REFRESH);
                dispatchEvent(convertedEvent);
            }
        }
    }
    
    /**
     *  @private
     *  Called whenever an XML object contained in a list is updated
     *  in some way.  The initial implementation stab is very lenient,
     *  any changeType will cause an update no matter how much further down
     *  in a hierarchy.  
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function xmlNotification(currentTarget:Object, 
                                        type:String, 
                                        target:Object, 
                                        value:Object, 
                                        detail:Object):void
    {
        var prop:String;
        var oldValue:Object;
        var newValue:Object;
        var children:XMLListCollection;
        var location:int;
        var event:CollectionEvent;
        var list:XMLListAdapter;
        
        // trace("currentTarget", currentTarget.toXMLString());
        // trace("target", target.toXMLString());
        // trace("value", value.toXMLString());
        // trace("type", type);

        if (currentTarget === target)
        {
            switch(type)
            {
                case "nodeAdded":
                {
                    for (var q:* in childrenMap)
                    {
                        if (q === currentTarget)
                        {
                            list = childrenMap[q].list as XMLListAdapter;
                            if (list && !list.busy())
                            {
                                if (childrenMap[q] === treeData)
                                {
                                    children = treeData as XMLListCollection;
                                    if (parentNode != null)
                                    {
                                        children.mx_internal::dispatchResetEvent = false;
                                        children.source = parentNode.*;
                                        children.refresh();
                                    }
                                }
                                else
                                {
                                    // this should refresh the collection
                                    children = getChildren(q) as XMLListCollection; 
                                }
                                if (children)
                                {
                                    // now we fake an event on behalf of the
                                    // child collection
                                    location = value.childIndex();
                                    event = new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
                                    event.kind = CollectionEventKind.ADD;
                                    event.location = location;
                                    event.items = [ value ];
                                    children.dispatchEvent(event);
                                }
                            }
                            break;
                        }
                    }
                    break;
                }

                /* needed?
                case "nodeChanged":
                {
                    prop = value.localName();
                    oldValue = detail;
                    newValue = value;
                    break;
                }
                */

                case "nodeRemoved":
                {
                    // lookup doesn't work, must scan instead
                    for (var p:* in childrenMap)
                    {
                        if (p === currentTarget)
                        {
                            children = childrenMap[p];
                            list = children.list as XMLListAdapter;
                            if (list && !list.busy())
                            {
                                var xmllist:XMLList = children.source as XMLList;

                                if (childrenMap[p] === treeData)
                                {
                                    children = treeData as XMLListCollection;
                                    if (parentNode)
                                    {
                                        children.mx_internal::dispatchResetEvent = false;
                                        children.source = parentNode.*;
                                        children.refresh();
                                    }
                                }
                                else
                                {
                                    var oldChildren:XMLListCollection = children;
                                    // this should refresh the collection
                                    children = getChildren(p) as XMLListCollection;
                                    if (!children)
                                    {
                                        // last item got removed so there's no child collection
                                        oldChildren.addEventListener(CollectionEvent.COLLECTION_CHANGE,
                                                                      nestedCollectionChangeHandler, false, 0, true);

                                        event = new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
                                        event.kind = CollectionEventKind.REMOVE;
                                        event.location = 0;
                                        event.items = [ value ];
                                        oldChildren.dispatchEvent(event);
                                        oldChildren.removeEventListener(CollectionEvent.COLLECTION_CHANGE,
                                                                      nestedCollectionChangeHandler);

                                    }
                                }
                                if (children)
                                {
                                    var n:int = xmllist.length();
                                    for (var i:int = 0; i < n; i++)
                                    {
                                        if (xmllist[i] === value)
                                        {
                                            event = new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
                                            event.kind = CollectionEventKind.REMOVE;
                                            event.location = location;
                                            event.items = [ value ];
                                            children.dispatchEvent(event);
                                            break;
                                        }
                                    }
                                }
                            }
                            break;
                        }
                    }
                    break;
                }

                default:
                {
                    break;
                }
            }
        }
    }

    /** 
     *  This is called by addItemAt and when the source is initially
     *  assigned.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    private function startTrackUpdates(item:Object):void
    {
        XMLNotifier.getInstance().watchXML(item, this);
    }
    
    /** 
     *  This is called by removeItemAt, removeAll, and before a new
     *  source is assigned.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    private function stopTrackUpdates(item:Object):void
    {
        XMLNotifier.getInstance().unwatchXML(item, this);
    }
}

}
