/**
 * @aside guide list
 * @aside video list
 *
 * List is a custom styled DataView which allows Grouping, Indexing, Icons, and a Disclosure. See the
 * [Guide](#!/guide/list) and [Video](#!/video/list) for more.
 *
 *     @example miniphone preview
 *     Ext.create('Ext.List', {
 *         fullscreen: true,
 *         itemTpl: '{title}',
 *         data: [
 *             { title: 'Item 1' },
 *             { title: 'Item 2' },
 *             { title: 'Item 3' },
 *             { title: 'Item 4' }
 *         ]
 *     });
 *
 * A more advanced example showing a list of people grouped by last name:
 *
 *     @example miniphone preview
 *     Ext.define('Contact', {
 *         extend: 'Ext.data.Model',
 *         config: {
 *             fields: ['firstName', 'lastName']
 *         }
 *     });
 *
 *     var store = Ext.create('Ext.data.Store', {
 *        model: 'Contact',
 *        sorters: 'lastName',
 *
 *        grouper: {
 *            groupFn: function(record) {
 *                return record.get('lastName')[0];
 *            }
 *        },
 *
 *        data: [
 *            { firstName: 'Tommy',   lastName: 'Maintz'  },
 *            { firstName: 'Rob',     lastName: 'Dougan'  },
 *            { firstName: 'Ed',      lastName: 'Spencer' },
 *            { firstName: 'Jamie',   lastName: 'Avins'   },
 *            { firstName: 'Aaron',   lastName: 'Conran'  },
 *            { firstName: 'Dave',    lastName: 'Kaneda'  },
 *            { firstName: 'Jacky',   lastName: 'Nguyen'  },
 *            { firstName: 'Abraham', lastName: 'Elias'   },
 *            { firstName: 'Jay',     lastName: 'Robinson'},
 *            { firstName: 'Nigel',   lastName: 'White'   },
 *            { firstName: 'Don',     lastName: 'Griffin' },
 *            { firstName: 'Nico',    lastName: 'Ferrero' },
 *            { firstName: 'Jason',   lastName: 'Johnston'}
 *        ]
 *     });
 *
 *     Ext.create('Ext.List', {
 *        fullscreen: true,
 *        itemTpl: '<div class="contact">{firstName} <strong>{lastName}</strong></div>',
 *        store: store,
 *        grouped: true
 *     });
 *
 * If you want to dock items to the bottom or top of a List, you can use the scrollDock configuration on child items in this List. The following example adds a button to the bottom of the List.
 *
 *     @example phone preview
 *     Ext.define('Contact', {
 *         extend: 'Ext.data.Model',
 *         config: {
 *             fields: ['firstName', 'lastName']
 *         }
 *     });
 *
 *     var store = Ext.create('Ext.data.Store', {
 *        model: 'Contact',
 *        sorters: 'lastName',
 *
 *        grouper: {
 *            groupFn: function(record) {
 *                return record.get('lastName')[0];
 *            }
 *        },
 *
 *        data: [
 *            { firstName: 'Tommy',   lastName: 'Maintz'  },
 *            { firstName: 'Rob',     lastName: 'Dougan'  },
 *            { firstName: 'Ed',      lastName: 'Spencer' },
 *            { firstName: 'Jamie',   lastName: 'Avins'   },
 *            { firstName: 'Aaron',   lastName: 'Conran'  },
 *            { firstName: 'Dave',    lastName: 'Kaneda'  },
 *            { firstName: 'Jacky',   lastName: 'Nguyen'  },
 *            { firstName: 'Abraham', lastName: 'Elias'   },
 *            { firstName: 'Jay',     lastName: 'Robinson'},
 *            { firstName: 'Nigel',   lastName: 'White'   },
 *            { firstName: 'Don',     lastName: 'Griffin' },
 *            { firstName: 'Nico',    lastName: 'Ferrero' },
 *            { firstName: 'Jason',   lastName: 'Johnston'}
 *        ]
 *     });
 *
 *     Ext.create('Ext.List', {
 *         fullscreen: true,
 *         itemTpl: '<div class="contact">{firstName} <strong>{lastName}</strong></div>',
 *         store: store,
 *         items: [{
 *             xtype: 'button',
 *             scrollDock: 'bottom',
 *             docked: 'bottom',
 *             text: 'Load More...'
 *         }]
 *     });
 */
Ext.define('Ext.dataview.List', {
    alternateClassName: 'Ext.List',
    extend: 'Ext.dataview.DataView',
    xtype: 'list',

    mixins: ['Ext.mixin.Bindable'],

    requires: [
        'Ext.dataview.IndexBar',
        'Ext.dataview.ListItemHeader',
        'Ext.dataview.component.ListItem',
        'Ext.util.TranslatableList',
        'Ext.util.PositionMap'
    ],

    /**
     * @event disclose
     * @preventable doDisclose
     * Fires whenever a disclosure is handled
     * @param {Ext.dataview.List} this The List instance
     * @param {Ext.data.Model} record The record associated to the item
     * @param {HTMLElement} target The element disclosed
     * @param {Number} index The index of the item disclosed
     * @param {Ext.EventObject} e The event object
     */

    config: {
        /**
         * @cfg layout
         * Hide layout config in DataView. It only causes confusion.
         * @accessor
         * @private
         */
        layout: 'fit',

        /**
         * @cfg {Boolean/Object} indexBar
         * `true` to render an alphabet IndexBar docked on the right.
         * This can also be a config object that will be passed to {@link Ext.IndexBar}.
         * @accessor
         */
        indexBar: false,

        icon: null,

        /**
         * @cfg {Boolean} clearSelectionOnDeactivate
         * `true` to clear any selections on the list when the list is deactivated.
         * @removed 2.0.0
         */

        /**
         * @cfg {Boolean} preventSelectionOnDisclose `true` to prevent the item selection when the user
         * taps a disclose icon.
         * @accessor
         */
        preventSelectionOnDisclose: true,

        /**
         * @cfg baseCls
         * @inheritdoc
         */
        baseCls: Ext.baseCSSPrefix + 'list',

        /**
         * @cfg {Boolean} pinHeaders
         * Whether or not to pin headers on top of item groups while scrolling for an iPhone native list experience.
         * @accessor
         */
        pinHeaders: true,

        /**
         * @cfg {Boolean} grouped
         * Whether or not to group items in the provided Store with a header for each item.
         * @accessor
         */
        grouped: false,

        /**
         * @cfg {Boolean/Function/Object} onItemDisclosure
         * `true` to display a disclosure icon on each list item.
         * The list will still fire the disclose event, and the event can be stopped before itemtap.
         * By setting this config to a function, the function passed will be called when the disclosure
         * is tapped.
         * Finally you can specify an object with a 'scope' and 'handler'
         * property defined. This will also be bound to the tap event listener
         * and is useful when you want to change the scope of the handler.
         * @accessor
         */
        onItemDisclosure: null,

        /**
         * @cfg {String} disclosureProperty
         * A property to check on each record to display the disclosure on a per record basis.  This
         * property must be false to prevent the disclosure from being displayed on the item.
         * @accessor
         */
        disclosureProperty: 'disclosure',

        /**
         * @cfg {String} ui
         * The style of this list. Available options are `normal` and `round`.
         */
        ui: 'normal',

        /**
         * @cfg {Boolean} useComponents
         * Flag the use a component based DataView implementation.  This allows the full use of components in the
         * DataView at the cost of some performance.
         *
         * Checkout the [DataView Guide](#!/guide/dataview) for more information on using this configuration.
         * @accessor
         * @private
         */

        /**
         * @cfg {Object} itemConfig
         * A configuration object that is passed to every item created by a component based DataView. Because each
         * item that a DataView renders is a Component, we can pass configuration options to each component to
         * easily customize how each child component behaves.
         * Note this is only used when useComponents is true.
         * @accessor
         * @private
         */

        /**
         * @cfg {Number} maxItemCache
         * Maintains a cache of reusable components when using a component based DataView.  Improving performance at
         * the cost of memory.
         * Note this is currently only used when useComponents is true.
         * @accessor
         * @private
         */

        /**
         * @cfg {String} defaultType
         * The xtype used for the component based DataView. Defaults to dataitem.
         * Note this is only used when useComponents is true.
         * @accessor
         */
        defaultType: 'listitem',

        /**
         * @cfg {Object} itemMap
         * @private
         */
        itemMap: {
            minimumHeight: 47
        },

        /**
         * @cfg {Boolean} variableHeights
         * Whether or not this list contains items with variable heights. If you want to force the
         * items in the list to have a fixed height, set the {@link #itemHeight} configuration.
         * If you also variableHeights to false, the scrolling performance of the list will be
         * improved.
         */
        variableHeights: true,

        /**
         * @cfg {Number} itemHeight
         * This allows you to set the default item height and is used to roughly calculate the amount
         * of items needed to fill the list. By default items are around 50px high. If you set this
         * configuration in combination with setting the {@link #variableHeights} to false you
         * can improve the scrolling speed
         */
        itemHeight: 47,

        /**
         * @cfg {Boolean} refreshHeightOnUpdate
         * Set this to false if you make many updates to your list (like in an interval), but updates
         * won't affect the item's height. Doing this will increase the performance of these updates.
         * Note that if you have {@link #variableHeights} set to false, this configuration option has
         * no effect.
         */
        refreshHeightOnUpdate: true
    },

    constructor: function(config) {
        var me = this,
            layout;
        me.callParent(arguments);

        if (Ext.os.is.Android4 && !Ext.browser.is.ChromeMobile) {
            me.headerTranslateFn = Ext.Function.createThrottled(me.headerTranslateFn, 50, me);
        }

        //<debug>
        layout = this.getLayout();
        if (layout && !layout.isFit) {
            Ext.Logger.error('The base layout for a DataView must always be a Fit Layout');
        }
        //</debug>
    },

    topItemIndex: 0,
    topItemPosition: 0,

    updateItemHeight: function(itemHeight) {
        this.getItemMap().setMinimumHeight(itemHeight);
    },

    applyItemMap: function(itemMap) {
        return Ext.factory(itemMap, Ext.util.PositionMap, this.getItemMap());
    },

    // apply to the selection model to maintain visual UI cues
//    onItemTrigger: function(me, index, target, record, e) {
//        if (!(this.getPreventSelectionOnDisclose() && Ext.fly(e.target).hasCls(this.getBaseCls() + '-disclosure'))) {
//            this.callParent(arguments);
//        }
//    },

    beforeInitialize: function() {
        var me = this,
            container;

        me.listItems = [];
        me.scrollDockItems = {
            top: [],
            bottom: []
        };

        container = me.container = me.add(new Ext.Container({
            scrollable: {
                scroller: {
                    autoRefresh: false,
                    direction: 'vertical',
                    translatable: {
                        xclass: 'Ext.util.TranslatableList'
                    }
                }
            }
        }));

        container.getScrollable().getScroller().getTranslatable().setItems(me.listItems);

        // Tie List's scroller to its container's scroller
        me.setScrollable(container.getScrollable());
        me.scrollableBehavior = container.getScrollableBehavior();
    },

    initialize: function() {
        var me = this,
            container = me.container,
            i, ln;

        me.updatedItems = [];
        me.headerMap = [];

        me.on(me.getTriggerCtEvent(), me.onContainerTrigger, me);
        me.on(me.getTriggerEvent(), me.onItemTrigger, me);

        me.header = Ext.factory({
            xclass: 'Ext.dataview.ListItemHeader',
            html: '&nbsp;',
            translatable: true,
            role: 'globallistheader',
            cls: ['x-list-header', 'x-list-header-swap']
        });
        me.container.innerElement.insertFirst(me.header.element);

        me.headerTranslate = me.header.getTranslatable();
        me.headerTranslate.translate(0, -10000);

        if (!me.getGrouped()) {
            me.updatePinHeaders(null);
        }

        container.element.on({
            delegate: '.' + me.getBaseCls() + '-disclosure',
            tap: 'handleItemDisclosure',
            scope: me
        });

        container.element.on({
            resize: 'onResize',
            scope: me
        });

        // Android 2.x not a direct child
        container.innerElement.on({
            touchstart: 'onItemTouchStart',
            touchend: 'onItemTouchEnd',
            tap: 'onItemTap',
            taphold: 'onItemTapHold',
            singletap: 'onItemSingleTap',
            doubletap: 'onItemDoubleTap',
            swipe: 'onItemSwipe',
            delegate: '.' + Ext.baseCSSPrefix + 'list-item-body',
            scope: me
        });

        for (i = 0, ln = me.scrollDockItems.top.length; i < ln; i++) {
            container.add(me.scrollDockItems.top[i]);
        }

        for (i = 0, ln = me.scrollDockItems.bottom.length; i < ln; i++) {
            container.add(me.scrollDockItems.bottom[i]);
        }

        if (me.getStore()) {
            me.refresh();
        }
    },

    updateInline: function(newInline) {
        var me = this;
        me.callParent(arguments);
        if (newInline) {
            me.setOnItemDisclosure(false);
            me.setIndexBar(false);
            me.setGrouped(false);
        }
    },

    applyIndexBar: function(indexBar) {
        return Ext.factory(indexBar, Ext.dataview.IndexBar, this.getIndexBar());
    },

    updateIndexBar: function(indexBar) {
        var me = this;
        if (indexBar && me.getScrollable()) {
            me.indexBarElement = me.getScrollableBehavior().getScrollView().getElement().appendChild(indexBar.renderElement);

            indexBar.on({
                index: 'onIndex',
                scope: me
            });

            me.element.addCls(me.getBaseCls() + '-indexed');
        }
    },

    updateGrouped: function(grouped) {
        var me = this,
            baseCls = this.getBaseCls(),
            cls = baseCls + '-grouped',
            unCls = baseCls + '-ungrouped';

        if (grouped) {
            me.addCls(cls);
            me.removeCls(unCls);
            me.updatePinHeaders(me.getPinHeaders());
        }
        else {
            me.addCls(unCls);
            me.removeCls(cls);
            me.updatePinHeaders(null);
        }

        if (me.isPainted() && me.listItems.length) {
            me.setItemsCount(me.listItems.length);
        }
    },

    updatePinHeaders: function(pinnedHeaders) {
        if (this.headerTranslate) {
            this.headerTranslate.translate(0, -10000);
        }
    },

    updateItemTpl: function(newTpl, oldTpl) {
        var listItems = this.listItems,
            ln = listItems.length || 0,
            store = this.getStore(),
            i, listItem;

        for (i = 0; i < ln; i++) {
            listItem = listItems[i];
            listItem.setTpl(newTpl);
        }

        if (store && store.getCount()) {
            this.doRefresh();
        }
    },

    updateScrollerSize: function() {
        var me = this,
            totalHeight = me.getItemMap().getTotalHeight(),
            scroller = me.container.getScrollable().getScroller();

        if (totalHeight > 0) {
            scroller.givenSize = totalHeight;
            scroller.refresh();
        }
    },

    onResize: function() {
        var me = this,
            container = me.container,
            element = container.element,
            minimumHeight = me.getItemMap().getMinimumHeight(),
            containerSize;

        if (!me.listItems.length) {
            me.bind(container.getScrollable().getScroller().getTranslatable(), 'doTranslate', 'onTranslate');
        }

        me.containerSize = containerSize = element.getHeight();
        me.setItemsCount(Math.ceil(containerSize / minimumHeight) + 1);
    },

    scrollDockHeightRefresh: function() {
        var items = this.listItems,
            scrollDockItems = this.scrollDockItems,
            ln = items.length,
            i, item;

        for (i = 0; i < ln; i++) {
            item = items[i];
            if ((item.isFirst && scrollDockItems.top.length) || (item.isLast && scrollDockItems.bottom.length)) {
                this.updatedItems.push(item);
            }
        }
        this.refreshScroller();
    },

    headerTranslateFn: function(record, transY, headerTranslate) {
        var headerString = this.getStore().getGroupString(record);

        if (this.currentHeader !== headerString) {
            this.currentHeader = headerString;
            this.header.setHtml(headerString);
        }
        headerTranslate.translate(0, transY);
    },

    onTranslate: function(x, y, args) {
        var me = this,
            listItems = me.listItems,
            itemsCount = listItems.length,
            currentTopIndex = me.topItemIndex,
            itemMap = me.getItemMap(),
            store = me.getStore(),
            storeCount = store && store.getCount(),
            info = me.getListItemInfo(),
            grouped = me.getGrouped(),
            storeGroups = me.groups,
            headerMap = me.headerMap,
            headerTranslate = me.headerTranslate,
            pinHeaders = me.getPinHeaders(),
            maxIndex = storeCount - itemsCount + 1,
            topIndex, changedCount, i, index, item,
            closestHeader, record, pushedHeader, transY, element;

        if (me.updatedItems.length) {
            me.updateItemHeights();
        }

        me.topItemPosition = itemMap.findIndex(-y) || 0;
        me.indexOffset = me.topItemIndex = topIndex = Math.max(0, Math.min(me.topItemPosition, maxIndex));

        if (grouped && headerTranslate && storeGroups.length && pinHeaders) {
            closestHeader = itemMap.binarySearch(headerMap, -y);
            record = storeGroups[closestHeader].children[0];
            if (record) {
                pushedHeader = y + headerMap[closestHeader + 1] - me.headerHeight;
                // Top of the list or above (hide the floating header offscreen)
                if (y >= 0) {
                    transY = -10000;
                }
                // Scroll the floating header a bit
                else if (pushedHeader < 0) {
                    transY = pushedHeader;
                }
                // Stick to the top of the screen
                else {
                    transY = Math.max(0, y);
                }
                this.headerTranslateFn(record, transY, headerTranslate);

            }
        }

        args[1] = (itemMap.map[topIndex] || 0) + y;
        if (currentTopIndex !== topIndex && topIndex <= maxIndex) {
            // Scroll up
            if (currentTopIndex > topIndex) {
                changedCount = Math.min(itemsCount, currentTopIndex - topIndex);
                for (i = changedCount - 1; i >= 0; i--) {
                    item = listItems.pop();
                    listItems.unshift(item);
                    me.updateListItem(item, i + topIndex, info);
                }
            }
            else {
                // Scroll down
                changedCount = Math.min(itemsCount, topIndex - currentTopIndex);
                for (i = 0; i < changedCount; i++) {
                    item = listItems.shift();
                    listItems.push(item);
                    index = i + topIndex + itemsCount - changedCount;
                    me.updateListItem(item, index, info);
                }
            }
        }

        if (listItems.length && grouped && pinHeaders) {
            if (me.headerIndices[topIndex]) {
                element = listItems[0].getHeader().element;
                if (y < itemMap.map[topIndex]) {
                    element.setVisibility(false);
                }
                else {
                    element.setVisibility(true);
                }
            }
            for (i = 1; i <= changedCount; i++) {
                if (listItems[i]) {
                    listItems[i].getHeader().element.setVisibility(true);
                }
            }
        }
    },

    setItemsCount: function(itemsCount) {
        var me = this,
            listItems = me.listItems,
            minimumHeight = me.getItemMap().getMinimumHeight(),
            config = {
                xtype: me.getDefaultType(),
                itemConfig: me.getItemConfig(),
                tpl: me.getItemTpl(),
                minHeight: minimumHeight,
                cls: me.getItemCls()
            },
            info = me.getListItemInfo(),
            i, item;

        for (i = 0; i < itemsCount; i++) {
            // We begin by checking if we already have an item for this length
            item = listItems[i];

            // If we don't have an item yet at this index then create one
            if (!item) {
                item = Ext.factory(config);
                item.dataview = me;
                item.$height = minimumHeight;
                me.container.doAdd(item);
                listItems.push(item);
            }
            item.dataIndex = null;
            if (info.store) {
                me.updateListItem(item, i + me.topItemIndex, info);
            }
            else {
                item.setRecord(null);
                item.translate(0, -10000);
                item._list_hidden = true;
            }
        }

        me.updateScrollerSize();
    },

    getListItemInfo: function() {
        var me = this,
            baseCls = me.getBaseCls();

        return {
            store: me.getStore(),
            grouped: me.getGrouped(),
            baseCls: baseCls,
            selectedCls: me.getSelectedCls(),
            headerCls: baseCls + '-header-wrap',
            footerCls: baseCls + '-footer-wrap',
            firstCls: baseCls + '-item-first',
            lastCls: baseCls + '-item-last',
            itemMap: me.getItemMap(),
            variableHeights: me.getVariableHeights(),
            defaultItemHeight: me.getItemHeight()
        };
    },

    updateListItem: function(item, index, info) {
        var record = info.store.getAt(index);
        if (this.isSelected(record)) {
            item.addCls(info.selectedCls);
        }
        else {
            item.removeCls(info.selectedCls);
        }

        item.removeCls([info.headerCls, info.footerCls, info.firstCls, info.lastCls]);
        this.replaceItemContent(item, index, info)
    },

    taskRunner: function() {
        delete this.intervalId;
        if (this.scheduledTasks && this.scheduledTasks.length > 0) {
            var task = this.scheduledTasks.shift();
            this.doUpdateListItem(task.item, task.index, task.info);

            if (this.scheduledTasks.length === 0 && this.getVariableHeights() && !this.container.getScrollable().getScroller().getTranslatable().isAnimating) {
                this.refreshScroller();
            } else if (this.scheduledTasks.length > 0) {
                this.intervalId = requestAnimationFrame(Ext.Function.bind(this.taskRunner, this));
            }
        }
    },

    scheduledTasks: null,

    replaceItemContent: function(item, index, info) {
        var translatable = this.container.getScrollable().getScroller().getTranslatable();

        // This falls apart when scrolling up. Turning off for now.
        if (Ext.os.is.Android4
            && !Ext.browser.is.Chrome
            && !info.variableHeights
            && !info.grouped
            && translatable.isAnimating
            && translatable.activeEasingY
            && Math.abs(translatable.activeEasingY._startVelocity) > .75) {
            if (!this.scheduledTasks) {
                this.scheduledTasks = [];
            }
            for (var i = 0; i < this.scheduledTasks.length; i++) {
                if (this.scheduledTasks[i].item === item) {
                    Ext.Array.remove(this.scheduledTasks, this.scheduledTasks[i]);
                    break;
                }
            }
            this.scheduledTasks.push({
                item: item,
                index: index,
                info: info
            });

            if (!this.intervalId) {
                this.intervalId = requestAnimationFrame(Ext.Function.bind(this.taskRunner, this));
            }
        } else {
            this.doUpdateListItem(item, index, info);
        }
    },

    doUpdateListItem: function(item, index, info) {
        var record = info.store.getAt(index),
            headerIndices = this.headerIndices,
            footerIndices = this.footerIndices,
            headerItem = item.getHeader(),
            scrollDockItems = this.scrollDockItems,
            updatedItems = this.updatedItems,
            itemHeight = info.itemMap.getItemHeight(index),
            ln, i, scrollDockItem;

        if (!record) {
            item.setRecord(null);
            item.translate(0, -10000);
            item._list_hidden = true;
            return;
        }
        item._list_hidden = false;

        if (item.isFirst && scrollDockItems.top.length) {
            for (i = 0, ln = scrollDockItems.top.length; i < ln; i++) {
                scrollDockItem = scrollDockItems.top[i];
                scrollDockItem.addCls(Ext.baseCSSPrefix + 'list-scrolldock-hidden');
                item.remove(scrollDockItem, false);
            }
            item.isFirst = false;
        }

        if (item.isLast && scrollDockItems.bottom.length) {
            for (i = 0, ln = scrollDockItems.bottom.length; i < ln; i++) {
                scrollDockItem = scrollDockItems.bottom[i];
                scrollDockItem.addCls(Ext.baseCSSPrefix + 'list-scrolldock-hidden');
                item.remove(scrollDockItem, false);
            }
            item.isLast = false;
        }

        if (item.getRecord) {
            if (item.dataIndex !== index) {
                item.dataIndex = index;
                this.fireEvent('itemindexchange', this, record, index, item);
            }
            if (item.getRecord() === record) {
                item.updateRecord(record);
            } else {
                item.setRecord(record);
            }
        }

        if (this.isSelected(record)) {
            item.addCls(info.selectedCls);
        }
        else {
            item.removeCls(info.selectedCls);
        }

        item.removeCls([info.headerCls, info.footerCls, info.firstCls, info.lastCls]);

        if (info.grouped) {
            if (headerIndices[index]) {
                item.addCls(info.headerCls);
                headerItem.setHtml(info.store.getGroupString(record));
                headerItem.show();
                headerItem.element.setVisibility(true);

                // If this record is a group header, and the items height is still the default height
                // we need to read the actual size of the item (including the header)
                if (!info.variableHeights && itemHeight === info.defaultItemHeight) {
                    Ext.Array.include(updatedItems, item);
                }
            }
            else {
                headerItem.hide();

                // If this record is not a header (anymore) and its height is unequal to the default item height
                // it means the item must have gotten a different height because being a header before and now needs
                // to become the default height again
                if (!info.variableHeights && !footerIndices[index] && itemHeight !== info.defaultItemHeight) {
                    info.itemMap.setItemHeight(index, info.defaultItemHeight);
                    info.itemMap.update();
                }
            }

            if (footerIndices[index]) {
                item.addCls(info.footerCls);

                // If this record is a footer and its height is still the same as the default item height, we have
                // to make sure to read this items height to see if adding the foot cls effects its height
                if (!info.variableHeights && itemHeight === info.defaultItemHeight) {
                    Ext.Array.include(updatedItems, item);
                }
            }
        } else if (!info.variableHeights && itemHeight !== info.defaultItemHeight) {
            // If this list is not grouped, the only thing that can change the height of an item
            // can be scroll dock items. If an items height is not equal to the default item height
            // it means it must have had scroll dock items. In this case we set the items height
            // to become the default height again.
            info.itemMap.setItemHeight(index, info.defaultItemHeight);
            info.itemMap.update();
        }

        if (index === 0) {
            item.isFirst = true;
            item.addCls(info.firstCls);

            if (!info.grouped) {
                item.addCls(info.headerCls);
            }

            for (i = 0, ln = scrollDockItems.top.length; i < ln; i++) {
                scrollDockItem = scrollDockItems.top[i];
                item.insert(0, scrollDockItem);
                scrollDockItem.removeCls(Ext.baseCSSPrefix + 'list-scrolldock-hidden');
            }

            // If an item gets scrolldock items inside of it, we need to always read the height
            // in the next frame so we add it to the updatedItems array
            if (ln && !info.variableHeights) {
                Ext.Array.include(updatedItems, item);
            }
        }

        if (index === info.store.getCount() - 1) {
            item.isLast = true;
            item.addCls(info.lastCls);

            if (!info.grouped) {
                item.addCls(info.footerCls);
            }

            for (i = 0, ln = scrollDockItems.bottom.length; i < ln; i++) {
                scrollDockItem = scrollDockItems.bottom[i];
                item.insert(0, scrollDockItem);
                scrollDockItem.removeCls(Ext.baseCSSPrefix + 'list-scrolldock-hidden');
            }

            // If an item gets scrolldock items inside of it, we need to always read the height
            // in the next frame so we add it to the updatedItems array
            if (ln && !info.variableHeights) {
                Ext.Array.include(updatedItems, item);
            }
        }

        item.$height = info.itemMap.getItemHeight(index);

        if (info.variableHeights) {
            updatedItems.push(item);
        }
    },

    updateItemHeights: function() {
        if (!this.isPainted() && !this.pendingHeightUpdate) {
            this.pendingHeightUpdate = true;
            this.on('painted', this.updateItemHeights, this, {single: true});
            return;
        }

        var updatedItems = this.updatedItems,
            ln = updatedItems.length,
            itemMap = this.getItemMap(),
            scroller = this.container.getScrollable().getScroller(),
            minimumHeight = itemMap.getMinimumHeight(),
            headerIndices = this.headerIndices,
            headerMap = this.headerMap,
            translatable = scroller.getTranslatable(),
            itemIndex, i, item, height;

        this.pendingHeightUpdate = false;

        // First we do all the reads
        for (i = 0; i < ln; i++) {
            item = updatedItems[i];
            itemIndex = item.dataIndex;
            // itemIndex may not be set yet if the store is still being loaded
            if (itemIndex !== null) {
                height = item.element.getFirstChild().getHeight();
                height = Math.max(height, minimumHeight);

                if (headerIndices && !this.headerHeight && headerIndices[itemIndex]) {
                    this.headerHeight = parseInt(item.getHeader().element.getHeight(), 10);
                }

                itemMap.setItemHeight(itemIndex, height);
            }
        }

        itemMap.update();
        height = itemMap.getTotalHeight();

        headerMap.length = 0;
        for (i in headerIndices) {
            headerMap.push(itemMap.map[i]);
        }

        // Now do the dom writes
        for (i = 0; i < ln; i++) {
            item = updatedItems[i];
            itemIndex = item.dataIndex;
            item.$height = itemMap.getItemHeight(itemIndex);
        }

        if (height != scroller.givenSize) {
            scroller.setSize(height);
            scroller.refreshMaxPosition();

            if (translatable.isAnimating) {
                translatable.activeEasingY.setMinMomentumValue(-scroller.getMaxPosition().y);
            }
        }

        this.updatedItems.length = 0;
    },

    /**
     * Returns an item at the specified index.
     * @param {Number} index Index of the item.
     * @return {Ext.dom.Element/Ext.dataview.component.DataItem} item Item at the specified index.
     */
    getItemAt: function(index) {
        var listItems = this.listItems,
            ln = listItems.length,
            i, listItem;

        for (i = 0; i < ln; i++) {
            listItem = listItems[i];
            if (listItem.dataIndex === index) {
                return listItem;
            }
        }
    },

    /**
     * Returns an index for the specified item.
     * @param {Number} item The item to locate.
     * @return {Number} Index of the record bound to the specified item.
     */
    getItemIndex: function(item) {
        return item.dataIndex;
    },

    /**
     * Returns an array of the current items in the DataView.
     * @return {Ext.dom.Element[]/Ext.dataview.component.DataItem[]} Array of Items.
     */
    getViewItems: function() {
        return this.listItems;
    },

    doRefresh: function(list) {
        if (this.intervalId) {
            cancelAnimationFrame(this.intervalId);
            delete this.intervalId;
        }
        if (this.scheduledTasks) {
            this.scheduledTasks.length = 0;
        }
        var me = this,
            store = me.getStore(),
            scrollable = me.container.getScrollable(),
            scroller = scrollable && scrollable.getScroller(),
            painted = me.isPainted(),
            storeCount = store.getCount();

        me.getItemMap().populate(storeCount, this.topItemPosition);

        if (me.getGrouped()) {
            me.findGroupHeaderIndices();
        }

        // This will refresh the items on the screen with the new data
        if (me.listItems.length) {
            if (me.getScrollToTopOnRefresh()) {
                me.topItemIndex = 0;
                me.topItemPosition = 0;
                scroller.position.y = 0;
            }
            me.setItemsCount(me.listItems.length);
            if (painted) {
                me.refreshScroller(scroller);
            } else if (!me.pendingRefreshScroller) {
                me.pendingRefreshScroller = true;
                me.on('painted', function() {
                    me.pendingRefreshScroller = false;
                    me.refreshScroller(scroller);
                }, this, {single: true});
            }
        }

        // No items, hide all the items from the collection.
        if (storeCount < 1) {
            me.onStoreClear();
            return;
        } else {
            me.hideEmptyText();
        }
    },

    findGroupHeaderIndices: function() {
        var me = this,
            store = me.getStore(),
            storeLn = store.getCount(),
            groups = store.getGroups(),
            groupLn = groups.length,
            headerIndices = me.headerIndices = {},
            footerIndices = me.footerIndices = {},
            i, previousIndex, firstGroupedRecord, storeIndex;

        me.groups = groups;

        for (i = 0; i < groupLn; i++) {
            firstGroupedRecord = groups[i].children[0];
            storeIndex = store.indexOf(firstGroupedRecord);
            headerIndices[storeIndex] = true;

            previousIndex = storeIndex - 1;
            if (previousIndex) {
                footerIndices[previousIndex] = true;
            }
        }

        footerIndices[storeLn - 1] = true;

        return headerIndices;
    },

    // Handling adds and removes like this is fine for now. It should not perform much slower then a dedicated solution
    onStoreAdd: function() {
        this.doRefresh();
    },

    onStoreRemove: function() {
        this.doRefresh();
    },

    onStoreUpdate: function(store, record, newIndex, oldIndex) {
        var me = this,
            scroller = me.container.getScrollable().getScroller(),
            item;

        oldIndex = (typeof oldIndex === 'undefined') ? newIndex : oldIndex;

        if (oldIndex !== newIndex) {
            // Just refreshing the list here saves a lot of code and shouldnt be much slower
            me.doRefresh();
        }
        else {
            if (newIndex >= me.topItemIndex && newIndex < me.topItemIndex + me.listItems.length) {
                item = me.getItemAt(newIndex);
                if(item) {
                    me.doUpdateListItem(item, newIndex, me.getListItemInfo());

                    // Bypassing setter because sometimes we pass the same record (different data)
                    //me.updateListItem(me.getItemAt(newIndex), newIndex, me.getListItemInfo());
                    if (me.getVariableHeights() && me.getRefreshHeightOnUpdate()) {
                        me.updatedItems.push(item);
                        me.updateItemHeights();
                        me.refreshScroller(scroller);
                    }
                }
            }
        }
    },

    /*
     * @private
     * This is to fix the variable heights again since the item height might have changed after the update
     */

    refreshScroller: function(scroller) {
        if (!scroller) {
            scroller = this.container.getScrollable().getScroller()
        }
        scroller.scrollTo(0, scroller.position.y + 1);
        scroller.scrollTo(0, scroller.position.y - 1);
    },

    onStoreClear: function() {
        if (this.headerTranslate) {
            this.headerTranslate.translate(0, -10000);
        }
        this.showEmptyText();
    },

    onIndex: function(indexBar, index) {
        var me = this,
            key = index.toLowerCase(),
            store = me.getStore(),
            groups = store.getGroups(),
            ln = groups.length,
            scrollable = me.container.getScrollable(),
            scroller, group, i, closest, id;

        if (scrollable) {
            scroller = scrollable.getScroller();
        }
        else {
            return;
        }

        for (i = 0; i < ln; i++) {
            group = groups[i];
            id = group.name.toLowerCase();
            if (id == key || id > key) {
                closest = group;
                break;
            }
            else {
                closest = group;
            }
        }

        if (scrollable && closest) {
            index = store.indexOf(closest.children[0]);

            //stop the scroller from scrolling
            scroller.stopAnimation();

            //make sure the new offsetTop is not out of bounds for the scroller
            var containerSize = scroller.getContainerSize().y,
                size = scroller.getSize().y,
                maxOffset = size - containerSize,
                offsetTop = me.getItemMap().map[index],
                offset = (offsetTop > maxOffset) ? maxOffset : offsetTop;

            // This is kind of hacky, but since there might be variable heights we have to render the frame
            // twice. First to update all the content, then to read the heights and translate items accordingly
            scroller.scrollTo(0, offset);
            if (this.updatedItems.length > 0 && (!this.scheduledTasks || this.scheduledTasks.length === 0)) {
                this.refreshScroller();
            }
            //scroller.scrollTo(0, offset);
        }
    },

    applyOnItemDisclosure: function(config) {
        if (Ext.isFunction(config)) {
            return {
                scope: this,
                handler: config
            };
        }
        return config;
    },

    handleItemDisclosure: function(e) {
        var me = this,
            item = Ext.getCmp(Ext.get(e.getTarget()).up('.x-list-item').id),
            index = item.dataIndex,
            record = me.getStore().getAt(index);

        me.fireAction('disclose', [me, record, item, index, e], 'doDisclose');
    },

    doDisclose: function(me, record, item, index, e) {
        var onItemDisclosure = me.getOnItemDisclosure();

        if (onItemDisclosure && onItemDisclosure.handler) {
            onItemDisclosure.handler.call(onItemDisclosure.scope || me, record, item, index, e);
        }
    },

    updateItemCls: function(newCls, oldCls) {
        var items = this.listItems,
            ln = items.length,
            i, item;

        for (i = 0; i < ln; i++) {
            item = items[i];
            item.removeCls(oldCls);
            item.addCls(newCls);
        }
    },

    onItemTouchStart: function(e) {
        this.container.innerElement.on({
            touchmove: 'onItemTouchMove',
            delegate: '.' + Ext.baseCSSPrefix + 'list-item-body',
            single: true,
            scope: this
        });
        this.callParent(this.parseEvent(e));
    },

    onItemTouchMove: function(e) {
        this.callParent(this.parseEvent(e));
    },

    onItemTouchEnd: function(e) {
        this.container.innerElement.un({
            touchmove: 'onItemTouchMove',
            delegate: '.' + Ext.baseCSSPrefix + 'list-item-body',
            scope: this
        });
        this.callParent(this.parseEvent(e));
    },

    onItemTap: function(e) {
        this.callParent(this.parseEvent(e));
    },

    onItemTapHold: function(e) {
        this.callParent(this.parseEvent(e));
    },

    onItemSingleTap: function(e) {
        this.callParent(this.parseEvent(e));
    },

    onItemDoubleTap: function(e) {
        this.callParent(this.parseEvent(e));
    },

    onItemSwipe: function(e) {
        this.callParent(this.parseEvent(e));
    },

    parseEvent: function(e) {
        var me = this,
            target = Ext.fly(e.getTarget()).findParent('.' + Ext.baseCSSPrefix + 'list-item', 8),
            item = Ext.getCmp(target.id);

        return [me, item, item.dataIndex, e];
    },

    onItemAdd: function(item) {
        var me = this,
            config = item.config;

        if (config.scrollDock) {
            if (config.scrollDock == 'bottom') {
                me.scrollDockItems.bottom.push(item);
            } else {
                me.scrollDockItems.top.push(item);
            }
            item.addCls(Ext.baseCSSPrefix + 'list-scrolldock-hidden');
            if (me.container) {
                me.container.add(item);
            }
        } else {
            me.callParent(arguments);
        }
    },

    destroy: function() {
        Ext.destroy(this.getIndexBar(), this.indexBarElement, this.header, this.scrollDockItems.top, this.scrollDockItems.bottom);
        if (this.intervalId) {
            cancelAnimationFrame(this.intervalId);
            delete this.intervalId;
        }
        this.callParent();
    }
});