////////////////////////////////////////////////////////////////////////////////
//
//  Licensed to the Apache Software Foundation (ASF) under one or more
//  contributor license agreements.  See the NOTICE file distributed with
//  this work for additional information regarding copyright ownership.
//  The ASF licenses this file to You under the Apache License, Version 2.0
//  (the "License"); you may not use this file except in compliance with
//  the License.  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////

package mx.controls.treeClasses
{

	import mx.collections.CursorBookmark;
	import mx.collections.ICollectionView;
	import mx.collections.IViewCursor;
	import mx.events.CollectionEvent;
	import mx.events.CollectionEventKind;
	
	import org.apache.royale.events.EventDispatcher;

	[ExcludeClass]

/**
 *  @private
 *  This class provides a heirarchical view (a tree-like) view of a standard collection. 
 *  The collection that this Cursor walks across need not be heirarchical but may be flat. 
 *  This class delegates to the ITreeDataDescriptor for information regarding the tree 
 *  structure of the data it walks across. 
 *  
 *  @see HierarchicalCollectionView
 */
public class HierarchicalViewCursor extends EventDispatcher
									implements IViewCursor
{
//    include "../../core/Version.as";

    //--------------------------------------------------------------------------
    //
    //  Constructor
    //
    //--------------------------------------------------------------------------

    /**
	 *  Constructor.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function HierarchicalViewCursor(
							collection:HierarchicalCollectionView,
							model:ICollectionView,
							dataDescriptor:ITreeDataDescriptor,
							itemToUID:Function,
							openNodes:Object)
    {
		super();
		
		//fields
        this.collection = collection;
		this.model = model;
        this.dataDescriptor = dataDescriptor;
		this.itemToUID = itemToUID;
		this.openNodes = openNodes;

      
        //events
        //collection.addEventListener(CollectionEvent.COLLECTION_CHANGE, collectionChangeHandler, false, 0, true);
        collection.addEventListener(CollectionEvent.COLLECTION_CHANGE, collectionChangeHandler);

		//init
		modelCursor = model.createCursor();
		
		//check to see if the model has more than one top level items
		more = model.length > 1;
    }

    /**
     *  Finalizes the cursor, to clean up resources.
     *  Required because weak references are not available in JS.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Royale 0.9.8
     */
    public function finalizeThis():void
    {
        if (modelCursor) modelCursor.finalizeThis();
        if (collection) collection.removeEventListener(CollectionEvent.COLLECTION_CHANGE, collectionChangeHandler);
    }

    //--------------------------------------------------------------------------
    //
    //  Variables
    //
    //--------------------------------------------------------------------------
 
    /**
     *  @private
     */
    private var dataDescriptor:ITreeDataDescriptor;

    /**
     *  @private
	 *  Its effective offset into the "array".
     */
    private var currentIndex:int = 0;
    
    /**
     *  @private
	 *  The current index into the childNodes array
     */
    private var currentChildIndex:int = 0;
    
    /**
     *  @private
     *  The depth of the current node.
     */
    private var _currentDepth:int = 1; 
    
    /**
     *  @private
	 *  The current set of childNodes we are walking.
     */
	private var childNodes:Object = [];
	
	/**
	 *  @private
	 *  The current set of parentNodes that we have walked from
	 */
	private var parentNodes:Array = [];
    
    /**
     *  @private
	 *  A stack of the currentChildIndex in all parents of the currentNode.
     */
	private var childIndexStack:Array = [];

    /**
     *  @private
     *  The collection that stores the user data
     */
    private var model:ICollectionView;
    
    /**
     *  @private
     *  The collection wrapper of the model
     */
	private var collection:HierarchicalCollectionView;
    
    /**
     *  @private
     */
	private var openNodes:Object;
	
	/**
	 *  @private
	 *  Flag indicating model has more data
	 */ 
	private var more:Boolean;
	
	/**
	 *  @private
	 *  Cursor from the model
	 */ 
	private var modelCursor:IViewCursor;
	
	/**
	 *  @private
	 */
	private var itemToUID:Function;

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

	//----------------------------------
	// index
 	//----------------------------------
	/**
	 * @private
	 */
	public function get index():int
	{
		return currentIndex;
	}
	
    //----------------------------------
	//  bookmark
    //----------------------------------
    /**
     *  @private
     */
    public function get bookmark():CursorBookmark
    {
        return new CursorBookmark(currentIndex.toString());
    }

    //---------------------------------- 
	//  current
    //----------------------------------
    
    /**
     *  @private
     */
    public function get current():Object
    {
        try 
        {
        	if (childIndexStack.length == 0)
        	{
        		return modelCursor.current;
        	}
        	else
        	{
        		return childNodes[currentChildIndex];
        	}
 		}
        catch(e:RangeError)
		{
		}
		return null;
    }


	//---------------------------------
	// currentDepth
	//---------------------------------
	/**
	 *  @private
	 */
	public function get currentDepth():int
	{
		return _currentDepth;
	}


	//----------------------------------
	//  beforeFirst
	//----------------------------------
    public function get beforeFirst():Boolean
    {
    	return (currentIndex <= collection.length && current == null);
    }
    
	//----------------------------------
	//  afterLast
	//----------------------------------
    public function get afterLast():Boolean
    {
        return (currentIndex >= collection.length && current == null); 
    } 
    
	//----------------------------------
	//  view
	//----------------------------------
    public function get view():ICollectionView
    {
        return model;
    }

    //--------------------------------------------------------------------------
    //
    //  Methods
    //
    //--------------------------------------------------------------------------
	
    /**
     *  @private
     *  Determines if a node is visible on the screen
     */
    private function isItemVisible(node:Object):Boolean
    { 
    	var parentNode:Object = collection.getParentItem(node);
        while (parentNode != null)
        {
        	if (openNodes[itemToUID(parentNode)] == null)
                return false;
            
        	parentNode = collection.getParentItem(parentNode);
        }
	    return true;
    }

    /**
     *  @private
     *  Creates a stack of parent nodes by walking upwards
     */
    private function getParentStack(node:Object):Array
    {
        var nodeParents:Array = [];
        
		// Make a list of parents of the node.
        var obj:Object = collection.getParentItem(node);
        while (obj != null)
        {
            nodeParents.unshift(obj);
            obj = collection.getParentItem(obj);
        }
		return nodeParents;
    }

    /**
     *  @private
	 *  When something happens to the tree, find out if it happened
	 *  to children that occur before the current child in the tree walk.
     */
    private function isNodeBefore(node:Object, currentNode:Object):Boolean
    {
		if (currentNode == null)
			return false;

		var n:int;
		var i:int;
		var childNodes:ICollectionView;
        var cursor:IViewCursor;
        var sameParent:Object;

        var nodeParents:Array = getParentStack(node);
        var curParents:Array = getParentStack(currentNode);

        // Starting from the root, compare parents
		// (assumes the root must be the same).
        while (nodeParents.length && curParents.length)
        {
            var nodeParent:Object = nodeParents.shift();
            var curParent:Object = curParents.shift();
            
			// If not the same parentm then which ever one is first
			// in the child list is before the other.
            if (nodeParent != curParent)
            {
                // The last parent must be common.
                sameParent = collection.getParentItem(nodeParent);
                
				// Get the child list.
				if (sameParent != null && 
					dataDescriptor.isBranch(sameParent, model) &&
					dataDescriptor.hasChildren(sameParent, model))
            	{
					childNodes = dataDescriptor.getChildren(sameParent, model);
            	}
    			else
    			{
    				childNodes = model; 
    			}
				// Walk it until you hit one or the other.
                cursor = childNodes.createCursor();
                do
                {
                    var child:Object = cursor.current;
                    
					if (child == curParent)
                        return false;

                    if (child == nodeParent)
                        return true;
                } while (cursor.moveNext());
            }
        }

		if (nodeParents.length)
			node = nodeParents.shift();
		if (curParents.length)
			currentNode = curParents.shift();

        // If we get here, they have the same parentage or one or both
		// had a root parent. Who's first?
		childNodes = model; 
        cursor = childNodes.createCursor();
        do 
        {
            child = cursor.current;

            if (child == currentNode)
                return false;

            if (child == node)
                return true;
        } while (cursor.moveNext());
        
        return false;
    }

    /**
     *  @private
     */
    public function findAny(values:Object):Boolean
    {
        seek(CursorBookmark.FIRST);
        
		var done:Boolean = false;
        while (!done)
        {
            var o:Object = dataDescriptor.getData(current);
            
			var matches:Boolean = true;
            for (var p:String in values)
            {
                if (o[p] != values[p])
                {
                    matches = false;
                    break;
                }
            }

            if (matches)
                return true;

            done = moveNext();
        }

        return false;
    }

    /**
     *  @private
     */
    public function findFirst(values:Object):Boolean
    {
        return findAny(values);
    }

    /**
     *  @private
     */
    public function findLast(values:Object):Boolean
    {
        seek(CursorBookmark.LAST);
        
		var done:Boolean = false;
        while (!done)
        {
            var o:Object = current; 
            
			var matches:Boolean = true;
            for (var p:String in values)
            {
                if (o[p] != values[p])
                {
                    matches = false;
                    break;
                }
            }
            
			if (matches)
                return true;

            done = movePrevious();
        }

        return false;
    }


    /**
     *  @private
     *  Move one node forward from current.  
     *  This may include moving up or down one or more levels.
     */
    public function moveNext():Boolean 
    {
    	var currentNode:Object = current;
        //if there is no currentNode then we cant move forward and must be off the ends
    	if (currentNode == null) 
    		return false; 
    	
		var uid:String = itemToUID(currentNode);
		if (!collection.parentMap.hasOwnProperty(uid))
			collection.parentMap[uid] = parentNodes[parentNodes.length - 1];

		// If current node is a branch and is open, the first child is our next item so return it
		if (openNodes[uid] &&
			dataDescriptor.isBranch(currentNode, model) && 
			dataDescriptor.hasChildren(currentNode, model))
	    {
	        	var previousChildNodes:Object = childNodes;
	            childNodes = arrayLike(dataDescriptor.getChildren(currentNode, model));
				if (childNodes.length > 0)
				{
					childIndexStack.push(currentChildIndex);
					parentNodes.push(currentNode);
					currentChildIndex = 0;
					currentNode = childNodes[0];
					currentIndex++;
					_currentDepth++;
					return true;
				}
				else
					childNodes = previousChildNodes;
	    }

        // Otherwise until we find the next child (could be on any level)
        while (true)
        {
            // If we hit the end of this list, pop up a level.
            if (childNodes != null && 
            	childNodes.length > 0 && 
            	currentChildIndex >= Math.max(childNodes.length - 1, 0))
            {
            	//check for the end of the tree here.
                if (childIndexStack.length < 1 && !more)  
                {
                	currentNode = null;
                    currentIndex++;
                    _currentDepth = 1;
                    return false;
                }
                else 
                {  
                	//pop up to parent
                	currentNode = parentNodes.pop(); 
                	//get parents siblings 
                	var grandParent:Object = parentNodes[parentNodes.length-1];
                	//we could probably assume that a non-null grandparent has descendants 
                	//but the analogy only goes so far... 
                	if (grandParent != null && 
                		dataDescriptor.isBranch(grandParent, model) &&
                		dataDescriptor.hasChildren(grandParent, model))
                	{
	                	childNodes = arrayLike(dataDescriptor.getChildren(grandParent, model));
               		}
               		else
               		{
               			childNodes = [];
               		}
                	//get new current nodes index
                	currentChildIndex = childIndexStack.pop();
                	//pop the level up one
                	_currentDepth--;
                }
            }
            else
            {
            	//if no childnodes then we're probably at the top level
            	if (childIndexStack.length == 0)
				{
					//check for more top level siblings
					//and if we're here the depth should be 1
					_currentDepth = 1;
					more = modelCursor.moveNext();
					if (more) 
					{
						currentNode = modelCursor.current;
						break;
					} 
					else 
					{
						//at the end of the tree
						_currentDepth = 1;
	                    currentIndex++;  //this should push us to afterLast
	                    currentNode = null;
	                    return false;
					}
				}
				else 
				{
					//get the next child node
					try
					{
						currentNode = childNodes[++currentChildIndex];
						break;
					}
					catch(e:RangeError)
					{
						//lets try to recover
						return false;
					}
    			}
            } 
        }
        currentIndex++;
        return true;
    }
    
    /**
     *  @private
	 *  Performs a backward tree walk.
     */
    public function movePrevious():Boolean
    {
    	var currentNode:Object = current;
    	// If there are no items, there's no current node, so return false.
        if (currentNode == null)
			return false;
    	
    	//if not at top level
		if (parentNodes && parentNodes.length > 0)
		{
			if (currentChildIndex == 0)
        	{
        		//at the first node in this branch so move to parent
        		currentNode = parentNodes.pop();
        		currentChildIndex = childIndexStack.pop();
        		var grandParent:Object = parentNodes[parentNodes.length-1];
                //we could probably assume that a non-null grandparent has descendants 
                //but the analogy only goes so far... 
                if (grandParent != null && 
                	dataDescriptor.isBranch(grandParent, model) &&
                	dataDescriptor.hasChildren(grandParent, model))
                {
        			childNodes = arrayLike(dataDescriptor.getChildren(grandParent, model));
                }
               	else
                {
                	//null is valid, but error prone so we'll make it empty
                	childNodes = [];  
                }
        		_currentDepth--;
        		currentIndex--;
        		return true;
        	}
        	else 
        	{
        		// get previous child sibling
        		try 
        		{
        			currentNode = childNodes[--currentChildIndex];
        		}
        		catch(e:RangeError)
        		{
        			//lets try to recover
					return false;
        		}
        	}  	
		}
		//handle top level siblings
		else 
		{
			more = modelCursor.movePrevious();
			if (more)
			{
				//move back one node and then loop through children
				currentNode = modelCursor.current;
			}
			//if past the begining of the tree return false
			else 
			{
				//currentIndex--;  //should be before first
				currentIndex = -1;
				currentNode = null;
				return false;
			}
		}
		while (true)
        {
            // If there are children, walk backwards on the children
            // and consider youself after your children.
			if (openNodes[itemToUID(currentNode)] &&
				dataDescriptor.isBranch(currentNode, model) &&
			    dataDescriptor.hasChildren(currentNode, model))
            {
				var previousChildNodes:Object = childNodes;
            	childNodes = arrayLike(dataDescriptor.getChildren(currentNode, model));
				if (childNodes.length > 0)
				{
            		childIndexStack.push(currentChildIndex);
            		parentNodes.push(currentNode);
            		currentChildIndex = childNodes.length - 1;
            		currentNode = childNodes[currentChildIndex];
           			_currentDepth++;
				}
				else
				{
					childNodes = previousChildNodes;
					break;
				}
            }	
			else
            {
            	//No more open branches so we'll bail
                break;
            }
        }
        currentIndex--; 
        return true;
    }

    /**
     *  @private
     */
    public function seek(bookmark:CursorBookmark, offset:int = 0,
						 prefetch:int = 0):void
    {
    	var value:int;
    	
		if (bookmark == CursorBookmark.FIRST)
        {
            value = 0;
        }
        else if (bookmark == CursorBookmark.CURRENT)
        {
            value = currentIndex;
        }
        else if (bookmark == CursorBookmark.LAST)
        {
            value = collection.length - 1;
        }
        else
        {
            value = int(bookmark.value);
        }
		
        value = Math.max(Math.min(value + offset, collection.length), 0);
        var dc:int = Math.abs(currentIndex - value);
        var de:int = Math.abs(collection.length - value);
		var movedown:Boolean = true;
		// if we're closer to the current than the beginning
        if (dc < value)
        {
            // if we're closer to the end than the current position
            if (de < dc)
            {
                moveToLast();

                if (de == 0)
                {		
                	// if de = 0; we need to be "off the end"
                    moveNext();
                    value = 0;
                }
                else
				{
                    value = de - 1;
				}
                movedown = false;
            }
            else
            {
                movedown = currentIndex < value;
                value = dc;
                // if current is off the end, reset
                if (currentIndex == collection.length)
                {
                    value--;
                    moveToLast();
                }
            }
        }
        else // we're closer to the beginning than the current
        {
            // if we're closer to the end than the beginning
            if (de < value)
            {
                moveToLast();
                if (de == 0)
                {		
                	// if de = 0; we need to be "off the end"
                    moveNext();
                    value = 0;
                }
                else
				{
                    value = de - 1;
				}
                movedown = false;
            }
            else
            {
	            moveToFirst();
            }
        }

        if (movedown)
        {
            while (value && value > 0) 
            {
                moveNext();
                value--;
            }
        }
        else
        {
            while (value && value > 0)  
            {
                movePrevious();
                value--;
            }
        }    
    }
    
    /**
     *  @private
     */
    private function moveToFirst():void
    {
		childNodes = [];
		modelCursor.seek(CursorBookmark.FIRST, 0);
		more = model.length > 1;
        currentChildIndex = 0;
        parentNodes = [];
        childIndexStack = [];
        currentIndex = 0;
        _currentDepth = 1;
    }
    
    /**
     *  @private
     */
    public function moveToLast():void
    {
		childNodes = [];
		childIndexStack = [];
		_currentDepth = 1;
		parentNodes = [];

		//first move to the end of the top level collection
		modelCursor.seek(CursorBookmark.LAST, 0);
		//if its a branch and open then get children for the last item
		var currentNode:Object = modelCursor.current;
		//if current node is open get its children
		while (openNodes[itemToUID(currentNode)] &&
			   dataDescriptor.isBranch(currentNode, model) &&
			   dataDescriptor.hasChildren(currentNode, model))
        {
        	var previousChildNodes:Object = childNodes;
        	childNodes = arrayLike(dataDescriptor.getChildren(currentNode, model));
        	if (childNodes != null && childNodes.length > 0)
        	{
        		parentNodes.push(currentNode);
        		childIndexStack.push(currentChildIndex);
        		currentNode = childNodes[childNodes.length - 1];
        		currentChildIndex = childNodes.length - 1;
           		_currentDepth++;
         	}
         	else 
         	{
				childNodes = previousChildNodes;
				break;
         	}
        }
        currentIndex = collection.length - 1;
    }
    
    /**
     *  @private
     */
    public function insert(item:Object):void
    {
        //No impl
    }
    
    /**
     *  @private
     */
    public function remove():Object
    {
        return null;
    }
    
    //--------------------------------------------------------------------------
    //
    //  Event handlers
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    public function collectionChangeHandler(event:CollectionEvent):void
    {
		var i:int;
		var n:int;
        var node:Object;
		var nodeParent:Object;
        var parent:Object;
        var parentStack:Array;
        //var parentTable:Dictionary;
        var parentTable:Array;
        var parentTableValues:Array;
		var isBefore:Boolean = false;
		

		// get the parent of the current item
        parentStack = getParentStack(current);
		// hash it by parent to map to depth
		parentTable = [];
        parentTableValues = [];            
		n = parentStack.length;
		// don't insert the immediate parent
		for (i = 0; i < n - 1; i++)
		{
			// 0 is null parent (the model)
			//parentTable[parentStack[i]] = i + 1;
            parentTable.push(parentStack[i]);
            parentTableValues.push(i + 1);
		}
		// remember the current parent
		parent = parentStack[parentStack.length - 1];

		if (event.kind == CollectionEventKind.ADD)
        {
			n = event.items.length;
            if (event.location <= currentIndex)
            {
				currentIndex += n;
				isBefore = true;
            }

			for (i = 0; i < n; i++)
			{
        		node = event.items[i];
				if (isBefore)
				{
					// if the added node is before the current
					// and they share parent's then we have to
					// adjust the currentChildIndex or
					// the stack of child indexes.
					nodeParent = collection.getParentItem(node);
					if (nodeParent == parent)
						currentChildIndex++;
                    /*
					else if (parentTable[nodeParent] != null)
					{
						childIndexStack[parentTable[nodeParent]]++;
					}
                    */
                    else
                    {
                        var pi:int = parentTable.indexOf(nodeParent);
                        if (pi != -1)
                            childIndexStack[parentTableValues[pi]]++;
                    }
				}
			}
			
        }
        else if (event.kind == CollectionEventKind.REMOVE)
        {
			n = event.items.length;
            if (event.location <= currentIndex)
            {
				if (event.location + n >= currentIndex)
				{
					// the current node was removed
					// the list classes expect that we
					// leave the cursor on whatever falls
					// into that slot
					var newIndex:int = event.location;
					moveToFirst();
					seek(CursorBookmark.FIRST, newIndex);
					for (i = 0; i < n; i++)
					{
        				node = event.items[i];
						delete collection.parentMap[itemToUID(node)];
					}
					return;
				}

                currentIndex -= n;
				isBefore = true;
            }

			for (i = 0; i < n; i++)
			{
        		node = event.items[i];
				if (isBefore)
				{
					// if the removed node is before the current
					// and they share parent's then we have to
					// adjust the currentChildIndex or
					// the stack of child indexes.
					nodeParent = collection.getParentItem(node);
					if (nodeParent == parent)
						currentChildIndex--;
                    /*
					else if (parentTable[nodeParent] != null)
					{
						childIndexStack[parentTable[nodeParent]]--;
					}
                    */
                    else
                    {
                        pi = parentTable.indexOf(nodeParent);
                        if (pi != -1)
                            childIndexStack[parentTableValues[pi]]--;
                    }
				}
				delete collection.parentMap[itemToUID(node)];
			}
			
        }
    }
    
    private function arrayLike(collection:ICollectionView):Object
    {
        // for SWF, we assume all collections implement Proxy and
        // [] lookups will work.
        COMPILE::SWF
        {
            return collection;
        }
        // for JS, the runtime can't know how to interpret [] lookups
        // so we generate an array.
        COMPILE::JS
        {
            var output:Array = [];
	    if (collection.length == 0)
	    {
		    return output;
	    }
            var cursor:IViewCursor = collection.createCursor();
            do {
                output.push(cursor.current);
            } while (cursor.moveNext());
            return output;
        }
    }
}

}
