/**
 * DataView makes it easy to create lots of components dynamically, usually based off a {@link Ext.data.Store Store}.
 * It's great for rendering lots of data from your server backend or any other data source and is what powers
 * components like {@link Ext.List}.
 *
 * Use DataView whenever you want to show sets of the same component many times, for examples in apps like these:
 *
 * - List of messages in an email app
 * - Showing latest news/tweets
 * - Tiled set of albums in an HTML5 music player
 *
 * # Creating a Simple DataView
 *
 * At its simplest, a DataView is just a Store full of data and a simple template that we use to render each item:
 *
 *     @example miniphone preview
 *     var touchTeam = Ext.create('Ext.DataView', {
 *         fullscreen: true,
 *         store: {
 *             fields: ['name', 'age'],
 *             data: [
 *                 {name: 'Jamie',  age: 100},
 *                 {name: 'Rob',   age: 21},
 *                 {name: 'Tommy', age: 24},
 *                 {name: 'Jacky', age: 24},
 *                 {name: 'Ed',   age: 26}
 *             ]
 *         },
 *
 *         itemTpl: '<div>{name} is {age} years old</div>'
 *     });
 *
 * Here we just defined everything inline so it's all local with nothing being loaded from a server. For each of the 5
 * data items defined in our Store, DataView will render a {@link Ext.Component Component} and pass in the name and age
 * data. The component will use the tpl we provided above, rendering the data in the curly bracket placeholders we
 * provided.
 *
 * Because DataView is integrated with Store, any changes to the Store are immediately reflected on the screen. For
 * example, if we add a new record to the Store it will be rendered into our DataView:
 *
 *     touchTeam.getStore().add({
 *         name: 'Abe Elias',
 *         age: 33
 *     });
 *
 * We didn't have to manually update the DataView, it's just automatically updated. The same happens if we modify one
 * of the existing records in the Store:
 *
 *     touchTeam.getStore().getAt(0).set('age', 42);
 *
 * This will get the first record in the Store (Jamie), change the age to 42 and automatically update what's on the
 * screen.
 *
 *     @example miniphone
 *     var touchTeam = Ext.create('Ext.DataView', {
 *         fullscreen: true,
 *         store: {
 *             fields: ['name', 'age'],
 *             data: [
 *                 {name: 'Jamie',  age: 100},
 *                 {name: 'Rob',   age: 21},
 *                 {name: 'Tommy', age: 24},
 *                 {name: 'Jacky', age: 24},
 *                 {name: 'Ed',   age: 26}
 *             ]
 *         },
 *
 *         itemTpl: '<div>{name} is {age} years old</div>'
 *     });
 *
 *     touchTeam.getStore().add({
 *         name: 'Abe Elias',
 *         age: 33
 *     });
 *
 *     touchTeam.getStore.getAt(0).set('age', 42);
 *
 * # Loading data from a server
 *
 * We often want to load data from our server or some other web service so that we don't have to hard code it all
 * locally. Let's say we want to load all of the latest tweets about Sencha Touch into a DataView, and for each one
 * render the user's profile picture, user name and tweet message. To do this all we have to do is modify the
 * {@link #store} and {@link #itemTpl} a little:
 *
 *     @example portrait
 *     Ext.create('Ext.DataView', {
 *         fullscreen: true,
 *         cls: 'twitterView',
 *         store: {
 *             autoLoad: true,
 *             fields: ['from_user', 'text', 'profile_image_url'],
 *
 *             proxy: {
 *                 type: 'jsonp',
 *                 url: 'http://search.twitter.com/search.json?q=Sencha Touch',
 *
 *                 reader: {
 *                     type: 'json',
 *                     root: 'results'
 *                 }
 *             }
 *         },
 *
 *         itemTpl: '<img src="{profile_image_url}" /><h2>{from_user}</h2><p>{text}</p><div style="clear: both"></div>'
 *     });
 *
 * The Store no longer has hard coded data, instead we've provided a {@link Ext.data.proxy.Proxy Proxy}, which fetches
 * the data for us. In this case we used a JSON-P proxy so that we can load from Twitter's JSON-P search API. We also
 * specified the fields present for each tweet, and used Store's {@link Ext.data.Store#autoLoad autoLoad} configuration
 * to load automatically. Finally, we configured a Reader to decode the response from Twitter, telling it to expect
 * JSON and that the tweets can be found in the 'results' part of the JSON response.
 *
 * The last thing we did is update our template to render the image, twitter username and message. All we need to do
 * now is add a little CSS to style the list the way we want it and we end up with a very basic twitter viewer. Click
 * the preview button on the example above to see it in action.
 */
Ext.define('Ext.dataview.DataView', {
    extend: 'Ext.Container',

    alternateClassName: 'Ext.DataView',

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

    xtype: 'dataview',

    requires: [
        'Ext.data.StoreManager'
    ],

    config: {
        /**
         * @cfg {Ext.data.Store/Object} store
         * Can be either a Store instance or a configuration object that will be turned into a Store. The Store is used
         * to populate the set of items that will be rendered in the DataView. See the DataView intro documentation for
         * more information about the relationship between Store and DataView.
         * @accessor
         */
        store: null,

        // @inherit
        baseCls: Ext.baseCSSPrefix + 'dataview',

        /**
         * @cfg {String} emptyText
         * The text to display in the view when there is no data to display
         */
        emptyText: null,

        /**
         * @cfg {Boolean} deferEmptyText True to defer emptyText being applied until the store's first load
         */
        deferEmptyText: true,

        /**
         * @cfg {String/String[]/Ext.XTemplate} itemTpl
         * The tpl to use for each of the items displayed in this DataView.
         */
        itemTpl: '<div>{text}</div>',

        /**
         * @cfg {String} pressedCls
         * The CSS class to apply to an item on the view while it is being pressed.
         * @accessor
         */
        pressedCls : 'x-item-pressed',

        /**
         * @cfg {String} selectedCls
         * The CSS class to apply to an item on the view while it is selected.
         * @accessor
         */
        selectedCls: 'x-item-selected',

        /**
         * @cfg {String} triggerEvent
         * Determines what type of touch event causes an item to be selected.
         * Valid options are 'tap' and 'singletap'.
         * @accessor
         */
        triggerEvent: 'tap',

        /**
         * @cfg {String} triggerCtEvent
         * Determines what type of touch event is recognized as a touch on the container.
         * Valid options are 'tap' and 'singletap'.
         * @accessor
         */
        triggerCtEvent: 'tap',

        /**
         * @cfg {Boolean} deselectOnContainerClick
         * When set to true, tapping on the DataView's background (i.e. not on
         * an item in the DataView) will deselect any currently selected items.
         * @accessor
         */
        deselectOnContainerClick: true,

        // @inherit
        scrollable: true,

        /**
         * @cfg {Number} pressedDelay
         * The amount of delay between the tapstart and the moment we add the pressedCls.
         * Settings this to true defaults to 100ms.
         * @accessor
         */
        pressedDelay: 100,

        /**
         * @cfg {String} loadingText
         * A string to display during data load operations (defaults to 'Loading...').  If specified, this text will be
         * displayed in a loading div and the view's contents will be cleared while loading, otherwise the view's
         * contents will continue to display normally until the new data is loaded and the contents are replaced.
         */
        loadingText: 'Loading...'
    },

    inheritableStatics: {
        /**
         * @private
         * This complex-looking method takes a given Model instance and returns an object containing all data from
         * all of that Model's *loaded* associations. It does this recursively - for example if we have a User which
         * hasMany Orders, and each Order hasMany OrderItems, it will return an object like this:
         *
         *     {
         *         orders: [
         *             {
         *                 id: 123,
         *                 status: 'shipped',
         *                 orderItems: [
         *                     ...
         *                 ]
         *             }
         *         ]
         *     }
         *
         * This makes it easy to iterate over loaded associations in a DataView.
         *
         * @param {Ext.data.Model} record The Model instance
         * @param {Array} ids PRIVATE. The set of Model instance internalIds that have already been loaded
         * @return {Object} The nested data set for the Model's loaded associations
         * @static
         * @inheritable
         */
        prepareAssociatedData: function(record, ids) {
            //we keep track of all of the internalIds of the models that we have loaded so far in here
            ids = ids || [];

            var associations     = record.associations.items,
                associationCount = associations.length,
                associationData  = {},
                i = 0,
                j = 0,
                associatedStore, associatedRecords, associatedRecord,
                associatedRecordCount, association, internalId;

            for (; i < associationCount; i++) {
                association = associations[i];

                //this is the hasMany store filled with the associated data
                associatedStore = record[association.storeName];

                //we will use this to contain each associated record's data
                associationData[association.name] = [];

                //if it's loaded, put it into the association data
                if (associatedStore && associatedStore.data.length > 0) {
                    associatedRecords = associatedStore.data.items;
                    associatedRecordCount = associatedRecords.length;

                    //now we're finally iterating over the records in the association. We do this recursively
                    for (; j < associatedRecordCount; j++) {
                        associatedRecord = associatedRecords[j];
                        internalId = associatedRecord.internalId;

                        //when we load the associations for a specific model instance we add it to the set of loaded ids so that
                        //we don't load it twice. If we don't do this, we can fall into endless recursive loading failures.
                        if (ids.indexOf(internalId) == -1) {
                            ids.push(internalId);

                            associationData[association.name][j] = associatedRecord.data;
                            Ext.apply(associationData[association.name][j], this.prepareAssociatedData(associatedRecord, ids));
                        }
                    }
                }
            }

            return associationData;
        }
    },

    constructor: function() {
        this.mixins.selectable.constructor.apply(this, arguments);
        this.callParent(arguments);
    },

    storeEventHooks: {
        beforeload: 'onBeforeLoad',
        load      : 'refresh',
        sort      : 'refresh',
        filter    : 'refresh',
        add       : 'onStoreAdd',
        remove    : 'onStoreRemove',
        update    : 'onStoreUpdate',
        clear     : 'onStoreClear'
    },

    doInitialize: function() {
        var me = this,
            triggerObj = {
                delegate: '> div',
                scope: me
            },
            clearObj = {
                scope: me
            },
            elementContainerElement;


        me.getViewItems();
        elementContainerElement = me.elementContainer.element;

        clearObj[me.getTriggerCtEvent()] = 'onContainerTrigger';
        me.element.on(clearObj);

        triggerObj[me.getTriggerEvent()] = 'onItemTrigger';
        elementContainerElement.on(triggerObj);

        elementContainerElement.on({
            delegate: '> div',
            scope   : me,

            touchstart: 'onItemTouchStart',
            touchend  : 'onItemTouchEnd',
            tap       : 'onItemTap',
            touchmove : 'onItemTouchMove',
            doubletap : 'onItemDoubleTap',
            swipe     : 'onItemSwipe'
        });
    },

    //@private
    initialize: function() {
        this.doInitialize();
        this.callParent(arguments);
    },

    // apply to the selection model to maintain visual UI cues
    onItemTrigger: function(e) {
        this.selectWithEvent(this.getStore().getAt(e.getTarget().getAttribute('itemIndex')), e);
    },

    // apply to the selection model to maintain visual UI cues
    onContainerTrigger: function(e) {
        var me = this;
        if (e.target != me.element.dom) {
            return;
        }
        if (me.getDeselectOnContainerClick() && me.getStore()) {
            me.deselectAll();
        }
    },

    doAddPressedCls: function(record) {
        var me = this,
        item = me.getViewItems()[me.getStore().indexOf(record)];
        Ext.get(item).addCls(me.getPressedCls());
    },

    onItemTouchStart: function(e) {
        var me = this,
            target = e.getTarget(),
            index = target.getAttribute('itemIndex'),
            store = me.getStore(),
            record = store && store.getAt(index),
            pressedDelay = me.getPressedDelay(),
            item = Ext.get(target);

        if (record) {
            if (pressedDelay > 0) {
                me.pressedTimeout = Ext.defer(me.doAddPressedCls, pressedDelay, me, [record]);
            }
            else {
                me.doAddPressedCls(record);
            }
        }

        item.on({
            touchmove: 'onItemTouchMove',
            scope   : me,
            single: true
        });

        me.fireAction('itemtouchstart', [me, index, target, e], 'doItemTouchStart');
    },

    doItemTouchStart: Ext.emptyFn,

    onItemTouchEnd: function(e) {
        var me = this,
            target = e.getTarget(),
            index = target.getAttribute('itemIndex'),
            store = me.getStore(),
            record = store && store.getAt(index),
            item = Ext.get(target);

        if (this.hasOwnProperty('pressedTimeout')) {
            clearTimeout(this.pressedTimeout);
            delete this.pressedTimeout;
        }

        if (record) {
            Ext.get(target).removeCls(me.getPressedCls());
        }

        item.un({
            touchmove: 'onItemTouchMove',
            scope   : me
        });

        me.fireAction('itemtouchend', [me, index, target, e], 'doItemTouchEnd');
    },

    doItemTouchEnd: Ext.emptyFn,

    onItemTouchMove: function(e) {
        var me = this,
            target = e.getTarget(),
            index = target.getAttribute('itemIndex'),
            store = me.getStore(),
            record = store && store.getAt(index),
            item = Ext.get(target);

        if (me.hasOwnProperty('pressedTimeout')) {
            clearTimeout(me.pressedTimeout);
            delete me.pressedTimeout;
        }

        if (record) {
            item.removeCls(me.getPressedCls());
        }
    },

    onItemTap: function(e) {
        var me = this,
            target = e.getTarget(),
            index = target.getAttribute('itemIndex'),
            item = Ext.get(target);

        me.fireAction('itemtap', [me, index, item, e], 'doItemTap');
    },

    doItemTap: Ext.emptyFn,

    onItemDoubleTap: function(e) {
        var me = this,
            target = e.getTarget(),
            index = target.getAttribute('itemIndex'),
            item = Ext.get(target);

        me.fireAction('itemdoubletap', [me, index, item, e], 'doItemDoubleTap');
    },

    doItemDoubleTap: Ext.emptyFn,

    onItemSwipe: function(e) {
        var me = this,
            target = e.getTarget(),
            index = target.getAttribute('itemIndex'),
            item = Ext.get(target);

        me.fireAction('itemswipe', [me, index, item, e], 'doItemSwipe');
    },

    doItemSwipe: Ext.emptyFn,

    // invoked by the selection model to maintain visual UI cues
    onItemSelect: function(record, suppressEvent) {
        var me = this;
        if (suppressEvent) {
            me.doItemSelect(me, record);
        } else {
            me.fireAction('select', [me, record], 'doItemSelect');
        }
    },

    // invoked by the selection model to maintain visual UI cues
    doItemSelect: function(me, record) {
        var item = Ext.get(me.getViewItems()[me.getStore().indexOf(record)]);
        item.removeCls(me.getPressedCls());
        item.addCls(me.getSelectedCls());
    },

    // invoked by the selection model to maintain visual UI cues
    onItemDeselect: function(record, suppressEvent) {
        var me = this;
        if (suppressEvent) {
            me.doItemDeSelect(me, record);
        }
        else {
            me.fireAction('deselect', [me, record, suppressEvent], 'doItemDeSelect');
        }
    },

    doItemDeSelect: function(me, record) {
        var item = Ext.get(me.getViewItems()[me.getStore().indexOf(record)]);
        if (item) {
            item.removeCls([me.getPressedCls(), me.getSelectedCls()]);
        }
    },

    updateData: function(data) {
        var store = this.getStore();
        if (!store) {
            this.setStore(Ext.create('Ext.data.ArrayStore', {
                fields: data
            }));
        } else {
            store.add(data);
        }
    },

    applyStore: function(store) {
        var me = this,
            loadMask = me.loadMask,
            bindEvents = Ext.apply({}, me.storeEventHooks, { scope: me });

        if (store) {
            store = Ext.data.StoreManager.lookup(store);
            if (store && Ext.isObject(store) && store.isStore) {
                store.on(bindEvents);
                if (loadMask) {
                    loadMask.bindStore(store);
                }
            }
        }

        return store;
    },

    updateStore: function(newStore, oldStore) {
        var me = this,
            loadMask = me.loadMask,
            bindEvents = Ext.apply({}, me.storeEventHooks, { scope: me });

        if (oldStore && Ext.isObject(oldStore) && oldStore.isStore) {
            if (oldStore.autoDestroy) {
                oldStore.destroy();
            }
            else {
                oldStore.un(bindEvents);
            }
        }

        if (newStore) {
            me.refresh();
        }
        else if (loadMask) {
            loadMask.bindStore(null);
        }
    },

    onBeforeLoad: function() {
        var loadingText = this.getLoadingText();
        if (loadingText) {
            this.mask(loadingText, null, true);
        }
    },

    /**
     * Refreshes the view by reloading the data from the store and re-rendering the template.
     */
    refresh: function() {
        var me = this;

        //remove any masks on the store
        this.unmask();

        if (!me.getStore()) {
            if (!this.getDeferEmptyText()) {
                this.doEmptyText();
            }
            return;
        }
        me.fireAction('refresh', [me], 'doRefresh');
    },

    applyItemTpl: function(config) {
        return (Ext.isObject(config) && config.isTemplate) ? config : new Ext.XTemplate(config);
    },

    onAfterRender: function() {
        var me = this;
        me.callParent(arguments);
        me.updateStore(me.getStore());
    },

    updateListItem: function(record, item) {
        var data = record.getData();
        if (record) {
            // TODO: Move this into nestedStore...
            Ext.apply(data, this.self.prepareAssociatedData(record));
        }
        var index = this.getStore().indexOf(record),
            html;

        item.setAttribute('itemIndex', index);
        html = this.getItemTpl().apply(data);
        item.innerHTML = html;
    },

    addListItem: function(index, record) {
        var data = record.getData();

        if (record) {
            // TODO: Move this into nestedStore...
            Ext.apply(data, this.self.prepareAssociatedData(record));
        }
        var element = this.elementContainer.element,
            childNodes = element.dom.childNodes,
            ln = childNodes.length,
            wrapElement;

        wrapElement = Ext.Element.create(this.getItemElementConfig(index, data));

        if (!ln || index == ln) {
            wrapElement.appendTo(element);
        } else {
            wrapElement.insertBefore(childNodes[index]);
        }
    },

    getItemElementConfig: function(index, data) {
        return {
            cls: this.getBaseCls() + '-item',
            itemIndex: index,
            html: this.getItemTpl().apply(data)
        };
    },

    // Remove
    moveItemsToCache: function(from, to) {
        var me = this,
            items = me.getViewItems(),
            i = to - from,
            item;

        for (; i >= 0; i--) {
            item = items[from + i];
            item.parentNode.removeChild(item);
        }
        if (me.getViewItems().length == 0) {
            this.doEmptyText();
        }
    },

    doEmptyText: function() {
        var emptyText = this.getEmptyText();
        if (emptyText) {
            this.elementContainer.setHtml('');
            this.elementContainer.setHtml(emptyText);
        }
    },

    // Add
    moveItemsFromCache: function(records, index) {
        var me = this,
            ln = records.length,
            i = 0,
            record;

        for (; i < ln; i++) {
            record = records[i];
            me.addListItem(index + i, record);
        }
        // Remove any emptyText that may be displayed
        me.elementContainer.setHtml('');
    },

    getViewItems: function() {
        if (!this.elementContainer) {
            this.elementContainer = this.add(new Ext.Component());
        }
        return this.elementContainer.element.dom.childNodes;
    },

    doRefresh: function(me) {
        var store = me.getStore(),
            records = store.getRange(),
            items = me.getViewItems(),
            recordsLn = records.length,
            itemsLn = items.length,
            deltaLn = recordsLn - itemsLn,
            i, item;

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

        // Too many items, hide the unused ones
        if (deltaLn < 0) {
            this.moveItemsToCache(itemsLn + deltaLn, itemsLn - 1);
            // Items can changed, we need to refresh our references
            items = me.getViewItems();
            itemsLn = items.length;
        }
        // Not enough items, create new ones
        else if (deltaLn > 0) {
            this.doCreateItems(store.getRange(itemsLn), itemsLn);
        }

        // Update Data and insert the new html for existing items
        for (i = 0; i < itemsLn; i++) {
            item = items[i];
            me.updateListItem(records[i], item);
        }
    },

    doCreateItems: function(records, ln) {
        this.moveItemsFromCache(records, ln);
    },

    onStoreClear: function() {
        var me = this,
            items = me.getViewItems();

        this.moveItemsToCache(0, items.length - 1);
        this.doEmptyText();
    },

    // private
    onStoreAdd: function(store, records, index) {
        if (records) {
            this.doCreateItems(records, index);
        }
    },

    // private
    onStoreRemove: function(store, record, index) {
        this.moveItemsToCache(index, index);
    },

    // private
    onStoreUpdate: function(store, record) {
        // Bypassing setter because sometimes we pass the same record (different data)
        this.updateListItem(record, this.getViewItems()[store.indexOf(record)]);
    }
}, function() {
    //<deprecated product=touch since=2.0>

    /**
     * Binds a new {@link Ext.data.Store Store} to this DataView.
     * @deprecated 2.0 please use {@link #setStore} instead
     * @method bindStore
     */
    Ext.deprecateClassMethod(this, 'bindStore', this.prototype.setStore, "'bindStore()' is deprecated, please use 'setStore' instead");
    //</deprecated>
});