////////////////////////////////////////////////////////////////////////////////
//
//  ADOBE SYSTEMS INCORPORATED
//  Copyright 2005-2007 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.Event;
import flash.utils.getQualifiedClassName;

import mx.collections.errors.ItemPendingError;
import mx.core.mx_internal;
import mx.events.CollectionEvent;
import mx.events.CollectionEventKind;
import mx.events.PropertyChangeEvent;
import mx.events.PropertyChangeEventKind;
import mx.utils.OnDemandEventDispatcher;

use namespace mx_internal;  // for mx_internal functions pendingItemSucceeded,Failed()

/**
 *  Dispatched when the list's length has changed or when a list
 *  element is replaced.
 *
 *  @eventType mx.events.CollectionEvent.COLLECTION_CHANGE
 *  
 *  @langversion 3.0
 *  @playerversion Flash 9
 *  @playerversion AIR 1.1
 *  @productversion Flex 3
 */
[Event(name="collectionChange", type="mx.events.CollectionEvent")]

/**
 *  The AsyncListView class is an implementation of the IList interface 
 *  that handles ItemPendingErrors errors 
 *  thrown by the <code>getItemAt()</code>, <code>removeItemAt()</code>, 
 *  and <code>toArray()</code> methods.
 * 
 *  <p>The <code>getItemAt()</code> method handles ItemPendingErrors by returning a provisional 
 *  "pending" item until the underlying request succeeds or fails.  The provisional
 *  item is produced by calling the function specified by the <code>createPendingItemFunction</code>
 *  property. .  If the request
 *  succeeds, the actual item replaces the provisional one.
 *  If it fails,  the provisional item is replaced with the item returned by calling
 *  the function specified by the <code>createFailedItemFunction</code> property.</p>
 * 
 *  <p>This class delegates the IList methods and properties to its <code>list</code>.
 *  If a list isn't specified, methods that mutate the collection are no-ops, 
 *  and methods that query the collection return an empty value, such as null or zero
 *  as appropriate.</p>
 * 
 *  <p>This class is intended to be used with Spark components based on DataGroup,
 *  such as List and ComboBox. The Spark classes do not provide intrinsic support for 
 *  ItemPendingError handling.</p>
 * 
 *  <p>AsyncListView does not support re-insertion of pending or failed items.  Once 
 *  a failed or pending item is removed, its connection to a pending request for data 
 *  is lost.  Using drag and drop to move a pending item in an ASyncListView, or sorting 
 *  an ASyncListView that contains pending or failed items, is not supported because 
 *  these operations remove and then re-insert list items.</p>
 * 
 *  @mxml
 *
 *  <p>The <code>&lt;mx:AsyncListView&gt;</code> tag inherits all the attributes of its
 *  superclass, and adds the following attributes:</p>
 *
 *  <pre>
 *  &lt;mx:AsyncListView
 *  <b>Properties</b>
 *    createFailedItemFunction="null"
 *    createPendingItemFunction="null"
 *    list="null"
 *  /&gt;
 *  </pre>
 * 
 *  @langversion 3.0
 *  @playerversion Flash 10
 *  @playerversion AIR 1.5
 *  @productversion Flex 4
 */
public class AsyncListView extends OnDemandEventDispatcher implements IList
{
    /**
     *  Constructor.
     *
     *  @param list Initial value of the list property, the IList we're delegating to.
     * 
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 1.5
     *  @productversion Flex 4
     */
    public function AsyncListView(list:IList = null)
    {
        super();
        this.list = list;
    }
    
    //--------------------------------------------------------------------------
    //
    //  Properties
    //
    //--------------------------------------------------------------------------

    //----------------------------------
    //  list
    //----------------------------------
    
    private var _list:IList;
    
    [Inspectable(category="General")]
    [Bindable("listChanged")]
    
    /**
     *  The IList object that this collection wraps. That means the object to which all of 
     *  the IList methods are delegated.
     * 
     *  <p>If this property is null, the IList mutation methods, such as <code>setItemAt()</code>,
     *  are no-ops. The IList query methods, such <code>getItemAt()</code>, return null
     *  or zero (-1 for <code>getItemIndex()</code>), as appropriate.</p>
     * 
     *  @default null  
     * 
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 1.5
     *  @productversion Flex 4
     */
    public function get list():IList
    {
        return _list;
    }
    
    /**
     *  @private
     */
    public function set list(value:IList):void
    {
        if (_list == value)
            return;

        deleteAllPendingResponders();
        oldLength = -1;
        if (_list)
            _list.removeEventListener(CollectionEvent.COLLECTION_CHANGE, handleCollectionChangeEvent);
        _list = value;
        if (_list)
        {
            _list.addEventListener(CollectionEvent.COLLECTION_CHANGE, handleCollectionChangeEvent, false, 0, true);
            oldLength = _list.length;
        }

        dispatchEvent(new Event("listChanged"));
        dispatchEvent(new CollectionEvent(CollectionEvent.COLLECTION_CHANGE, false, false, CollectionEventKind.RESET));
    }

    /**
     *  @private
     */
    private function deleteAllPendingResponders():void
    {
        for each (var responder:ListItemResponder in pendingResponders)
        {
            if (responder)
                responder.index = -1;
        }
        pendingResponders.length = 0;
        failedItems.length = 0;
    }
    
    /**
     *  The previous known length of the list before handling a CollectionEvent.
     *  oldLength is updated by the list setter and isValidCollectionEvent().
     */
    private var oldLength:int = -1;
    
    /**
     *  This method checks the validity of incoming CollectionEvents. 
     *  In some cases, a CollectionEvent from the underlying list may have already
     *  been received once, or have been erroneously dispatched (See SDK-30594).
     *  Thus, we check the incoming event's location against the last known length'
     *  of the list (oldLength).
     * 
     *  <p>Returns false if the index is less than 0 or greater than the previous
     *  length of the list.
     *  This only applies to ADD, REMOVE, REPLACE, and MOVE CollectionEvents.
     *  It also updates oldLength to be the current length of the list, so it
     *  should not be called twice.</p>
     */
    private function isValidCollectionEvent(ce:CollectionEvent):Boolean
    {
        if (oldLength < 0)
            return true;
        
        const location:int = ce.location;
        
        switch (ce.kind)
        {
            case CollectionEventKind.ADD:
            {
                if (location < 0 || location > oldLength)
                    return false;
                break;
            }
                
            case CollectionEventKind.REMOVE:
            case CollectionEventKind.REPLACE:
            case CollectionEventKind.MOVE:
            {
                if (location < 0 || location >= oldLength)
                    return false;
                break;
            }
        }
        
        oldLength = length;
        return true;
    }

    /**
     *  @private
     *  Fixup the pendingResponders and failedItems arrays after a change to the list.  
     *  Generally speaking, if a list[index] item changes, the pending responder for 
     *  that index is no longer needed.
     *  
     *  All "collectionChange" events are redispatched to the AsyncListView listeners.
     */
    private function handleCollectionChangeEvent(ce:CollectionEvent):void
    {
        if (!isValidCollectionEvent(ce))
            return;
        
        switch (ce.kind)
        {
            case CollectionEventKind.REPLACE:
            case CollectionEventKind.UPDATE:
                deletePendingResponders(ce);
                break;
                
            case CollectionEventKind.MOVE:
                movePendingResponders(ce);
                break;
                
            case CollectionEventKind.ADD:
                shiftPendingRespondersRight(ce);
                break;

            case CollectionEventKind.REMOVE:
                shiftPendingRespondersLeft(ce);
                break;
                
            case CollectionEventKind.RESET:
            case CollectionEventKind.REFRESH:
                deleteAllPendingResponders();
                break;
        }

        dispatchEvent(ce);  // redispatch to CollectionEvent listeners on this
    }
    
    /**
     *  @private
     *  Delete the ListItemResponder at the specified index, if any.
     *  If a pending responder exists, return its item.
     *   
     *  This method assumes that the responder hasn't run yet, it sets
     *  the ListItemResponder index to -1 to prevent it from updating
     *  this AsyncListView later.
     */
    private function deletePendingResponder(index:int):Object
    {
        if ((index < 0) || (index >= pendingResponders.length))
            return null;

        const pendingResponder:ListItemResponder = pendingResponders[index];
        if (pendingResponder)
        {
            delete pendingResponders[index];
            ListItemResponder(pendingResponder).index = -1; 
            return pendingResponder.item;
        }
        
        return null;
    }

    /**
     *  @private
     *  Handler for a CollectionEventKind.UPDATE or REPLACE event. In either
     *  case a contiguous block of items (ce.items) beginning with index=ce.location
     *  has been changed.  If there are any pending requests for these indices, we
     *  assume they're no longer valid, i.e. we assume that getItemAt() should no longer
     *  return the pending item.  Likewise for failed items.
     */
    private function deletePendingResponders(ce:CollectionEvent):void
    {
        var index:int = ce.location;
        for each (var item:Object in ce.items)
        {
            deletePendingResponder(index);
            delete failedItems[index];
            index += 1;
        }
    }

    /**
     *  @private
     *  Handler for a CollectionEventKind.MOVE event.  The event indicates that a 
     *  contiguous block of items (ce.items), beginning with index=ce.oldLocation,
     *  has been moved to ce.location.  If pendingRequests already exist at ce.location,
     *  they're deleted first.
     */
    private function movePendingResponders(ce:CollectionEvent):void
    {
        var fromIndex:int = ce.oldLocation;
        var toIndex:int = ce.location;
        for each (var item:Object in ce.items)
        {
            var pendingResponder:ListItemResponder = pendingResponders[fromIndex];
            if (pendingResponder)
            {
                delete pendingResponders[fromIndex];
                ListItemResponder(pendingResponder).index = toIndex; 
                deletePendingResponder(toIndex); // in case we're copying over a pending request
                pendingResponders[toIndex] = pendingResponder;
            }
            
            var failedItem:* = failedItems[fromIndex];
            if (failedItem !== undefined)
            {
                delete failedItems[fromIndex];
                failedItems[toIndex] = failedItem;
            }
            
            fromIndex += 1;
            toIndex += 1;
        }
    }

    /**
     *  @private
     *  Handler for a CollectionEventKind.ADD.  The event indicates 
     *  that a block of ce.items.length items starting at ce.location was inserted,
     *  which implies that all of the pendingResponders whose index is greater than or
     *  equal to ce.location, must be shifted right by ce.items.length.  The failedItems
     *  array is handled similarly.
     */
    private function shiftPendingRespondersRight(ce:CollectionEvent):void
    {
        const delta:int = ce.items.length;
        const startIndex:int = ce.location;

        const pendingRespondersCopy:Array = sparseCopy(pendingResponders);
        pendingResponders.length = 0;
        for each (var responder:ListItemResponder in pendingRespondersCopy)
        {
            if (responder.index >= startIndex)
                responder.index += delta;
            pendingResponders[responder.index] = responder;
        }
        
        for (var index:int = failedItems.length - 1; index >= startIndex; index--)
        {
            var failedItem:* = failedItems[index];
            if (failedItem !== undefined)
            {
                delete failedItems[index];
                failedItems[index + delta] = failedItem;
            }
        }
    }

    /**
     *  @private
     *  Handler for a CollectionEventKind.REMOVE.  The event indicates 
     *  that a block of ce.items.length items starting at ce.location was removed,
     *  which implies that all of the pendingResponders whose index is greater than or
     *  equal to ce.location, must be shifted left by ce.items.length.  The failedItems
     *  array is handled similarly.
     */
    private function shiftPendingRespondersLeft(ce:CollectionEvent):void
    {
        const delta:int = ce.items.length;
        const startIndex:int = ce.location + delta;
        
        const pendingRespondersCopy:Array = sparseCopy(pendingResponders);
        pendingResponders.length = 0;
        for each (var responder:ListItemResponder in pendingRespondersCopy)
        {
            if (responder.index >= startIndex)
                responder.index -= delta;
            pendingResponders[responder.index] = responder;
        }
        
        const failedItemsLength:int = failedItems.length;
        for (var index:int = startIndex; index < failedItemsLength; index++)
        {
            var failedItem:* = failedItems[index];
            if (failedItem !== undefined)
            {
                delete failedItems[index];
                failedItems[index - delta] = failedItem;
            }
        }        
    }
    
    /**
     *  Applying concat() to a sparse array produces a new array that's
     *  not sparse, nulls replace items that were undefined.  Although the 
     *  result of this method is not sparse, it only includes items that 
     *  were in the original array.
     */
    private function sparseCopy(a:Array):Array
    {
        const r:Array = new Array();
        var index:int = 0;
        for each (var item:* in a)
        {
            if (item !== undefined)
                r[index++] = item;
        }
        return r;
    }
    
    //----------------------------------
    //  createPendingItemFunction
    //----------------------------------
    
    private var _createPendingItemFunction:Function = defaultCreatePendingItemFunction;
    
    /**
     *  @private
     */
    private function defaultCreatePendingItemFunction(index:int, ipe:ItemPendingError):Object
    {
        return null;        
    }
    
    /**
     *  A callback function used to create a provisional item when
     *  the initial request causes an <code>ItemPendingError</code> to be thrown.
     *  If the request eventually succeeds, the provisional item is automatically
     *  replaced by the actual item.  If the request fails, then the item is replaced
     *  with one created with the callback function specified by the 
     *  <code>createFailedItemFunction</code> property.
     *  
     *  <p>The value of this property must be a function with two parameters: the index
     *  of the requested data provider item, and the ItemPendingError itself. In most
     *  cases, the second parameter can be ignored.
     *  The following example shows an implementation of the callback function:
     *
     *   <pre>
     * function createPendingItem(index:int, ipe:ItemPendingError):Object
     * {
     *     return "[" + index + "request is pending...]";        
     * }
     *   </pre>
     *   </p>
     * 
     *  <p>Setting this property does not affect provisional pending items that were already
     *  created.  Setting this property to null prevents provisional pending items 
     *  from being created.</p>
     * 
     *  @default A function that unconditionally returns null. 
     *  @see #getItemAt()
     *  @see #createFailedItemFunction
     *  @see mx.collections.errors.ItemPendingError
     * 
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 1.5
     *  @productversion Flex 4
     */
    public function get createPendingItemFunction():Function
    {
        return _createPendingItemFunction;
    }
    
    /**
     *  @private
     */
    public function set createPendingItemFunction(value:Function):void
    {
        _createPendingItemFunction = value;
    }
    
    
    //----------------------------------
    //  createFailedItemFunction
    //----------------------------------
    
    private var _createFailedItemFunction:Function = defaultCreateFailedItemFunction;
    
    /**
     *  @private
     */
    private function defaultCreateFailedItemFunction(index:int, info:Object):Object
    {
        return null;        
    }
    
    /**
     *  A callback function used to create a substitute item when
     *  a request which had caused an <code>ItemPendingError</code> to be thrown, 
     *  subsequently fails.  The existing item, typically a pending item created
     *  by the callback function specified by the <code>createPendingItemFunction()</code> property, 
     *  is replaced with the failed item.
     *  
     *  <p>The value of this property must be a function with two parameters: the index
     *  of the requested item, and the failure "info" object, which is
     *  passed along from the IResponder <code>fault()</code> method.  
     *  In most cases you can ignore the second parameter.
     *  Shown below is an example implementation of the callback function:</p> 
     * 
     *  <pre>
     * function createFailedItem(index:int, info:Object):Object
     * {
     *     return "[" + index + "request failed]";        
     * }
     *   </pre>
     *  
     * 
     *  <p>Setting this property does not affect failed items that were already
     *  created.  Setting this property to null prevents failed items from being created.
     *  </p>
     * 
     *  @default A function that unconditionally returns null. 
     *  @see #getItemAt()
     *  @see #createPendingItemFunction
     *  @see mx.rpc.IResponder#fault
     * 
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 1.5
     *  @productversion Flex 4
     */
    public function get createFailedItemFunction():Function
    {
        return _createFailedItemFunction;
    }
    
    /**
     *  @private
     */
    public function set createFailedItemFunction(value:Function):void
    {
        _createFailedItemFunction = value;
    }

    //--------------------------------------------------------------------------
    //
    //  Methods
    //
    //--------------------------------------------------------------------------

    private const pendingResponders:Array = new Array();
    private const failedItems:Array = new Array();
    
    /**
     *  @private
     *  Called by the ListItemProvider/result() method when a pending request
     *  completes successfully.
     * 
     *  @param index The item's index.
     *  @param info The informational object passed to IResponder/result().
     *  @see mx.rpc.IResponder#result
     */
    mx_internal function pendingRequestSucceeded(index:int, info:Object):void
    {
        delete pendingResponders[index];
    }

    /**
     *  @private
     *  Called by the ListItemProvider/fault() method when a pending request
     *  fails.
     * 
     *  @param index The item's index.
     *  @param info The informational object passed to IResponder/fault().
     *  @see mx.rpc.IResponder#fault
     */
    mx_internal function pendingRequestFailed(index:int, info:Object):void
    {
        delete pendingResponders[index];

        if (createFailedItemFunction === null)
            return;
        
        const item:Object = createFailedItemFunction(index, info);
        failedItems[index] = item;
            
        // dispatch collection and property change events
        
        const hasCollectionListener:Boolean = hasEventListener(CollectionEvent.COLLECTION_CHANGE);
        const hasPropertyListener:Boolean = hasEventListener(PropertyChangeEvent.PROPERTY_CHANGE);
        var pce:PropertyChangeEvent; 
                
        if (hasCollectionListener || hasPropertyListener)
        {
            pce = new PropertyChangeEvent(PropertyChangeEvent.PROPERTY_CHANGE);
            pce.kind = PropertyChangeEventKind.UPDATE;
            pce.oldValue = null;
            pce.newValue = item;
            pce.property = index;
        }
                
        if (hasCollectionListener)
        {
            var ce:CollectionEvent = new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
            ce.kind = CollectionEventKind.REPLACE;
            ce.location = index;
            ce.items.push(pce);
            dispatchEvent(ce);
        }
                
        if (hasPropertyListener)
            dispatchEvent(pce);
    }
            
    
    //--------------------------------------------------------------------------
    //
    //  IList Implementation
    //
    //--------------------------------------------------------------------------

    [Bindable("collectionChange")]
    
    /**
     *  @inheritDoc
     *  
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 1.5
     *  @productversion Flex 4
     */
    public function get length():int
    {
        try
        {
            return (list) ? list.length : 0;
        }
        catch (ignore:ItemPendingError)
        {
            // The mx.data DataList class can throw an IPE here. We ignore it because
            // when the length is determined, a CollectionChanged event will be
            // be dispatched.  See handleCollectionChangeEvent().
        }
        
        return 0;
    }
    
    /**
     *  @inheritDoc
     *  
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 1.5
     *  @productversion Flex 4
     */
    public function addItem(item:Object):void
    {
        if (list)
        {
            try
            {
                list.addItem(item);
            }
            catch (ignore:ItemPendingError)
            {
                // The mx.data DataList class can throw an IPE here. We ignore it because
                // when the item is actually added, a CollectionChanged event will be
                // be dispatched.  See handleCollectionChangeEvent().
            }            
        }
    }
    
    /**
     *  @inheritDoc
     *  
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 1.5
     *  @productversion Flex 4
     */
    public function addItemAt(item:Object, index:int):void
    {
        if (list)
        {
            try
            {
                list.addItemAt(item, index);
            }
            catch (ignore:ItemPendingError)
            {
                // The mx.data DataList class can throw an IPE here. We ignore it because
                // when the item is actually added, a CollectionChanged event will be
                // be dispatched.  See handleCollectionChangeEvent().
            }            
        }
    }
    
    /**
     *  Returns the value of <code>list.getItemAt(index)</code>.
     * 
     *  <p>This method catches ItemPendingErrors (IPEs) generated as a consequence of 
     *  calling <code>getItemAt()</code>.  If an IPE is thrown, an <code>IResponder</code> is added to
     *  the IPE and a provisional "pending" item, created with the 
     *  <code>createPendingItemFunction</code> is returned.   If the underlying request
     *  eventually succeeds, the pending item is replaced with the real item.  If it fails,
     *  the pending item is replaced with a value produced by calling
     *  <code>createFailedItemFunction</code>.</p>
     * 
     *  @param index The list index from which to retrieve the item.
     *
     *  @param prefetch An <code>int</code> indicating both the direction
     *    and number of items to fetch during the request if the item is not local.
     * 
     *  @throws RangeError if <code>index &lt; 0</code> or <code>index >= length</code>.
     * 
     *  @return The list item at the specified index.
     * 
     *  @see #createPendingItemFunction
     *  @see #createFailedItemFunction
     *  @see mx.collections.errors.ItemPendingError
     *  @see mx.rpc.IResponder 
     *  
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 1.5
     *  @productversion Flex 4
     */
    public function getItemAt(index:int, prefetch:int=0):Object
    {
        if (!list)
            return null;
        
        const failedItem:* = failedItems[index];
        if (failedItem !== undefined)
            return failedItem;

        const pendingResponder:ListItemResponder = pendingResponders[index];
        if (pendingResponder)
            return pendingResponder.item;

        var item:Object = null;
        try
        {
            return list.getItemAt(index, prefetch);
        }
        catch (ipe:ItemPendingError)
        {
            const createPendingItem:Function = createPendingItemFunction;
            if (createPendingItem !== null)
                item = createPendingItem(index, ipe);
            var responder:ListItemResponder = new ListItemResponder(this, index, item);
            pendingResponders[index] = responder;
            ipe.addResponder(responder);
        }
        return item;
    }  
    
    /**
     *  @inheritDoc
     *  
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 1.5
     *  @productversion Flex 4
     */
    public function getItemIndex(item:Object):int
    {
        const failedItemIndex:int = failedItems.indexOf(item);
        if (failedItemIndex != -1)
            return failedItemIndex;
        
        for each (var responder:ListItemResponder in pendingResponders)
            if (responder && responder.item === item)
                return responder.index;
        return (list) ? list.getItemIndex(item) : -1;
    }
    
    /**
     *  @inheritDoc
     *  
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 1.5
     *  @productversion Flex 4
     */
    public function itemUpdated(item:Object, property:Object=null, oldValue:Object=null, newValue:Object=null):void
    {
        if (list)
            list.itemUpdated(item, property, oldValue, newValue);
    }
    
    /**
     *  @inheritDoc
     *  
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 1.5
     *  @productversion Flex 4
     */
    public function removeAll():void
    {
        if (list)
            list.removeAll();
    }
    
    /**
     *  Removes the actual or pending item at the specified index and returns it.
     *  All items whose index is greater than the specified index  
     *  have their index reduced by 1.
     * 
     *  <p>If there is no actual or pending item at the specified index, for
     *  example because a call to <code>getItemAt(index)</code> hasn't caused the data to be 
     *  paged in, then the underlying <code>list</code> may throw an ItemPendingError.  
     *  The  implementation ignores the ItemPendingError and returns null.</p>
     *
     *  @param index The list index from which to retrieve the item.
     *
     *  @throws RangeError if <code>index &lt; 0</code> or <code>index >= length</code>.
     *
     *  @return The item that was removed or null.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 1.5
     *  @productversion Flex 4
     */
    public function removeItemAt(index:int):Object
    {
        if (!list)
            return null;
        
        const failedItem:* = failedItems[index];
        delete failedItems[index];
        const pendingItem:Object = deletePendingResponder(index);
        try
        {
            const actualItem:Object = list.removeItemAt(index);
            if (failedItem !== undefined)
                return failedItem;
            return (pendingItem) ? pendingItem : actualItem;
            
        }
        catch (ipe:ItemPendingError)
        {
            // If list[index] doesn't exist yet, an IPE will be thrown.  There's nothing 
            // we can do about that, so ignore it.
        }
        return (failedItem !== undefined) ? failedItem : pendingItem; 
    }
    
    /**
     *  @inheritDoc
     *  
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 1.5
     *  @productversion Flex 4
     */
    public function setItemAt(item:Object, index:int):Object
    {
        if (!list)
            return null;
        
        const failedItem:* = failedItems[index];
        const pendingResponder:ListItemResponder = pendingResponders[index];

        var setItemValue:Object = null;  // return null if IPE
        try
        {
            setItemValue = list.setItemAt(item, index);
        }
        catch (ignore:ItemPendingError)
        {
            // The mx.data DataList class can throw an IPE here. We ignore it because
            // when the item is actually changed, a CollectionChanged event will be
            // be dispatched.  See handleCollectionChangeEvent().
        }            
        
        if (failedItem !== undefined)
            return failedItem;
        else
            return (pendingResponder) ? pendingResponder.item : setItemValue;
    }
    
    /**
     *  Returns an array with the same elements as this AsyncListView.  The array is initialized
     *  by retrieving each item with <code>getItemAt()</code>, so pending items are substituted where actual
     *  values aren't available yet.   The array will not be updated when the AsyncListView replaces
     *  the pending items with actual (or failed) values.
     *  
     *  @return an array with the same elements as this AsyncListView.
     *
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 1.5
     *  @productversion Flex 4
     */
    public function toArray():Array
    {
        if (!list)
            return [];
        
        const a:Array = new Array(list.length);
        for(var i:int = 0; i < a.length; i++)
            a[i] = getItemAt(i);
        return a;
    }
    
 
    /**
     *  Returns a string that contains the list's length and the number of pending item requests.  
     *  It does not trigger pending requests.
     * 
     *  @return A brief description of the list.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 10
     *  @playerversion AIR 1.5
     *  @productversion Flex 4
     */
    public function toString():String
    {
        var s:String = getQualifiedClassName(this);

        if (list)
        {
            var nRequests:int = 0;
            for each (var responder:ListItemResponder in pendingResponders)
            {
                if (responder)
                    nRequests += 1;
            }
            s += " length=" + length + ", " + nRequests + " pending requests";
        }
        else
            s += " no list";

        return s;
    }

}
}

import mx.rpc.IResponder;
import mx.collections.AsyncListView;
import mx.core.mx_internal;

use namespace mx_internal;  // for mx_internal functions pendingItemSucceeded,Failed()

class ListItemResponder implements IResponder
{
    private var asyncListView:AsyncListView;
    public var index:int = -1;
    public var item:Object = null;
    
    public function ListItemResponder(asyncListView:AsyncListView, index:int, item:Object)
    {
        super();
        this.asyncListView = asyncListView;
        this.index = index;
        this.item = item;
    }
    
    public function result(info:Object):void
    {
        if (index != -1)
            asyncListView.pendingRequestSucceeded(index, info);
    }
    
    public function fault(info:Object):void
    {
        if (index != -1)
            asyncListView.pendingRequestFailed(index, info);
    }
}