/**
 * NestedList provides a miller column interface to navigate between nested sets
 * and provide a clean interface with limited screen real-estate.
 *
 *     @example miniphone preview
 *      var data = {
 *          text: 'Groceries',
 *          items: [{
 *              text: 'Drinks',
 *              items: [{
 *                  text: 'Water',
 *                  items: [{
 *                      text: 'Sparkling',
 *                      leaf: true
 *                  }, {
 *                      text: 'Still',
 *                      leaf: true
 *                  }]
 *              }, {
 *                  text: 'Coffee',
 *                  leaf: true
 *              }, {
 *                  text: 'Espresso',
 *                  leaf: true
 *              }, {
 *                  text: 'Redbull',
 *                  leaf: true
 *              }, {
 *                  text: 'Coke',
 *                  leaf: true
 *              }, {
 *                  text: 'Diet Coke',
 *                  leaf: true
 *              }]
 *          }, {
 *              text: 'Fruit',
 *              items: [{
 *                  text: 'Bananas',
 *                  leaf: true
 *              }, {
 *                  text: 'Lemon',
 *                  leaf: true
 *              }]
 *          }, {
 *              text: 'Snacks',
 *              items: [{
 *                  text: 'Nuts',
 *                  leaf: true
 *              }, {
 *                  text: 'Pretzels',
 *                  leaf: true
 *              }, {
 *                  text: 'Wasabi Peas',
 *                  leaf: true
 *              }]
 *          }]
 *      };
 *
 *      Ext.define('ListItem', {
 *          extend: 'Ext.data.Model',
 *          config: {
 *              fields: [{
 *                  name: 'text',
 *                  type: 'string'
 *              }]
 *          }
 *      });
 *
 *      var store = Ext.create('Ext.data.TreeStore', {
 *          model: 'ListItem',
 *          defaultRootProperty: 'items',
 *          root: data
 *      });
 *
 *      var nestedList = Ext.create('Ext.NestedList', {
 *          fullscreen: true,
 *          title: 'Groceries',
 *          displayField: 'text',
 *          store: store
 *      });
 */
Ext.define('Ext.dataview.NestedList', {
    alternateClassName: 'Ext.NestedList',
    extend: 'Ext.Container',
    xtype: 'nestedlist',
    requires: [
        'Ext.dataview.List',
        'Ext.TitleBar',
        'Ext.Button',
        'Ext.XTemplate',
        'Ext.data.StoreManager',
        'Ext.data.TreeStore',
        'Ext.data.NodeStore'
    ],

    config: {
        /**
         * @cfg
         * @inheritdoc
         */
        baseCls: Ext.baseCSSPrefix + 'nested-list',

        /**
         * @cfg {String/Object/Boolean} cardSwitchAnimation
         * Animation to be used during transitions of cards.
         * @removed 2.0.0 please use {@link Ext.layout.Card#animation}
         */

        /**
         * @cfg {String} backText
         * The label to display for the back button.
         * @accessor
         */
        backText: 'Back',

        /**
         * @cfg {Boolean} useTitleAsBackText
         * `true` to use title as a label for back button.
         * @accessor
         */
        useTitleAsBackText: true,

        /**
         * @cfg {Boolean} updateTitleText
         * Update the title with the currently selected category.
         * @accessor
         */
        updateTitleText: true,

        /**
         * @cfg {String} displayField
         * Display field to use when setting item text and title.
         * This configuration is ignored when overriding {@link #getItemTextTpl} or
         * {@link #getTitleTextTpl} for the item text or title.
         * @accessor
         */
        displayField: 'text',

        /**
         * @cfg {String} loadingText
         * Loading text to display when a subtree is loading.
         * @accessor
         */
        loadingText: 'Loading...',

        /**
         * @cfg {String} emptyText
         * Empty text to display when a subtree is empty.
         * @accessor
         */
        emptyText: 'No items available.',

        /**
         * @cfg {Boolean/Function} onItemDisclosure
         * Maps to the {@link Ext.List#onItemDisclosure} configuration for individual lists.
         * @accessor
         */
        onItemDisclosure: false,

        /**
         * @cfg {Boolean} allowDeselect
         * Set to `true` to allow the user to deselect leaf items via interaction.
         * @accessor
         */
        allowDeselect: false,

        /**
         * @deprecated 2.0.0 Please set the {@link #toolbar} configuration to `false` instead
         * @cfg {Boolean} useToolbar `true` to show the header toolbar.
         * @accessor
         */
        useToolbar: null,

        /**
         * @cfg {Ext.Toolbar/Object/Boolean} toolbar
         * The configuration to be used for the toolbar displayed in this nested list.
         * @accessor
         */
        toolbar: {
            docked: 'top',
            xtype: 'titlebar',
            ui: 'light',
            inline: true
        },

        /**
         * @cfg {String} title The title of the toolbar
         * @accessor
         */
        title: '',

        /**
         * @cfg {String} layout
         * @hide
         * @accessor
         */
        layout: {
            type: 'card',
            animation: {
                type: 'slide',
                duration: 250,
                direction: 'left'
            }
        },

        /**
         * @cfg {Ext.data.TreeStore/String} store The tree store to be used for this nested list.
         */
        store: null,

        /**
         * @cfg {Ext.Container} detailContainer The container of the `detailCard`.
         * A detailContainer is a reference to the container where a detail card
         * displays.
         *
         * See http://docs.sencha.com/touch/2-2/#!/guide/nested_list-section-4
         * and http://en.wikipedia.org/wiki/Miller_columns
         *
         * The two possible values for a detailContainer are undefined (default),
         * which indicates that a detailCard appear in the same container, or you
         * can specify a new container location. The default condition uses the
         * current List container.
         *
         * The following example shows creating a location for a detailContainer:
         *
         * var detailContainer = Ext.create('Ext.Container', {
         *     layout: 'card'
         * });
         *
         * var nestedList = Ext.create('Ext.NestedList', {
         *     store: treeStore,
         *     detailCard: true,
         *     detailContainer: detailContainer
         * });
         *
         * The default value is typically used for phone devices in portrait mode
         * where the small screen size dictates that the detailCard replace the
         * current container.
         * @accessor
         */
        detailContainer: undefined,

        /**
         * @cfg {Ext.Component} detailCard provides the information for a leaf
         * in a Miller column list. In a Miller column, users follow a
         * hierarchial tree structure to a leaf, which provides information
         * about the item in the list. The detailCard lists the information at
         * the leaf.
         *
         * See http://docs.sencha.com/touch/2-2/#!/guide/nested_list-section-3
         * and http://en.wikipedia.org/wiki/Miller_columns
         *
         * @accessor
         */
        detailCard: null,

        /**
         * @cfg {Object} backButton The configuration for the back button used in the nested list.
         */
        backButton: {
            ui: 'back',
            hidden: true
        },

        /**
         * @cfg {Object} listConfig An optional config object which is merged with the default
         * configuration used to create each nested list.
         */
        listConfig: null,

        /**
         * @cfg {Boolean} useSimpleItems
         * Set this to false if you want the lists in this NestedList to create complex container list items.
         */
        useSimpleItems: 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: null,

        /**
         * @cfg {Boolean} variableHeights
         * This configuration allows you optimize the picker by not having it read the DOM heights of list items.
         * Instead it will assume (and set) the height to be the {@link #itemHeight}.
         */
        variableHeights: false,

        /**
         * @private
         */
        lastNode: null,

        /**
         * @private
         */
        lastActiveList: null,

        ui: null,

        clearSelectionOnListChange: true
    },

    /**
     * @event itemtap
     * Fires when a node is tapped on.
     * @param {Ext.dataview.NestedList} this
     * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
     * @param {Number} index The index of the item tapped.
     * @param {Ext.dom.Element} target The element tapped.
     * @param {Ext.data.Record} record The record tapped.
     * @param {Ext.event.Event} e The event object.
     */

    /**
     * @event itemdoubletap
     * Fires when a node is double tapped on.
     * @param {Ext.dataview.NestedList} this
     * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
     * @param {Number} index The index of the item that was tapped.
     * @param {Ext.dom.Element} target The element tapped.
     * @param {Ext.data.Record} record The record tapped.
     * @param {Ext.event.Event} e The event object.
     */

    /**
     * @event containertap
     * Fires when a tap occurs and it is not on a template node.
     * @param {Ext.dataview.NestedList} this
     * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
     * @param {Ext.event.Event} e The raw event object.
     */

    /**
     * @event selectionchange
     * Fires when the selected nodes change.
     * @param {Ext.dataview.NestedList} this
     * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
     * @param {Array} selections Array of the selected nodes.
     */

    /**
     * @event beforeselectionchange
     * Fires before a selection is made.
     * @param {Ext.dataview.NestedList} this
     * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
     * @param {HTMLElement} node The node to be selected.
     * @param {Array} selections Array of currently selected nodes.
     * @deprecated 2.0.0 Please listen to the {@link #selectionchange} event with an order of `before` instead.
     */

    /**
     * @event listchange
     * Fires when the user taps a list item.
     * @param {Ext.dataview.NestedList} this
     * @param {Object} listitem The new active list.
     */

    /**
     * @event leafitemtap
     * Fires when the user taps a leaf list item.
     * @param {Ext.dataview.NestedList} this
     * @param {Ext.List} list The subList the item is on.
     * @param {Number} index The index of the item tapped.
     * @param {Ext.dom.Element} target The element tapped.
     * @param {Ext.data.Record} record The record tapped.
     * @param {Ext.event.Event} e The event.
     */

    /**
     * @event back
     * @preventable doBack
     * Fires when the user taps Back.
     * @param {Ext.dataview.NestedList} this
     * @param {HTMLElement} node The node to be selected.
     * @param {Ext.dataview.List} lastActiveList The Ext.dataview.List that was last active.
     * @param {Boolean} detailCardActive Flag set if the detail card is currently active.
     */

    /**
     * @event beforeload
     * Fires before a request is made for a new data object.
     * @param {Ext.dataview.NestedList} this
     * @param {Ext.data.Store} store The store instance.
     * @param {Ext.data.Operation} operation The Ext.data.Operation object that will be passed to the Proxy to
     * load the Store.
     */

    /**
     * @event load
     * Fires whenever records have been loaded into the store.
     * @param {Ext.dataview.NestedList} this
     * @param {Ext.data.Store} store The store instance.
     * @param {Ext.util.Grouper[]} records An array of records.
     * @param {Boolean} successful `true` if the operation was successful.
     * @param {Ext.data.Operation} operation The associated operation.
     */
    constructor: function (config) {
        if (Ext.isObject(config)) {
            if (config.getTitleTextTpl) {
                this.getTitleTextTpl = config.getTitleTextTpl;
            }
            if (config.getItemTextTpl) {
                this.getItemTextTpl = config.getItemTextTpl;
            }
        }
        this.callParent([config]);
    },

    onItemInteraction: function () {
        if (this.isGoingTo) {
            return false;
        }
    },

    applyDetailContainer: function (config) {
        if (!config) {
            config = this;
        }

        return config;
    },

    updateDetailContainer: function (newContainer, oldContainer) {
        if (newContainer) {
            newContainer.on('beforeactiveitemchange', 'onBeforeDetailContainerChange', this);
            newContainer.on('activeitemchange', 'onDetailContainerChange', this);
        }
    },

    onBeforeDetailContainerChange: function () {
        this.isGoingTo = true;
    },

    onDetailContainerChange: function () {
        this.isGoingTo = false;
    },

    /**
     * Called when an list item has been tapped.
     * @param {Ext.List} list The subList the item is on.
     * @param {Number} index The id of the item tapped.
     * @param {Ext.Element} target The list item tapped.
     * @param {Ext.data.Record} record The record which as tapped.
     * @param {Ext.event.Event} e The event.
     */
    onItemTap: function (list, index, target, record, e) {
        var me = this,
            store = list.getStore(),
            node = store.getAt(index);

        me.fireEvent('itemtap', this, list, index, target, record, e);
        if (node.isLeaf()) {
            me.fireEvent('leafitemtap', this, list, index, target, record, e);
            me.goToLeaf(node);
        }
        else {
            this.goToNode(node);
        }
    },

    onBeforeSelect: function () {
        this.fireEvent.apply(this, [].concat('beforeselect', this, Array.prototype.slice.call(arguments)));
    },

    onContainerTap: function () {
        this.fireEvent.apply(this, [].concat('containertap', this, Array.prototype.slice.call(arguments)));
    },

    onSelectionChange: function () {
        this.fireEvent.apply(this, [].concat('selectionchange', this, Array.prototype.slice.call(arguments)));
    },

    onItemDoubleTap: function () {
        this.fireEvent.apply(this, [].concat('itemdoubletap', this, Array.prototype.slice.call(arguments)));
    },

    onStoreBeforeLoad: function () {
        var loadingText = this.getLoadingText(),
            scroller = this.getScrollable();

        if (loadingText) {
            this.setMasked({
                xtype: 'loadmask',
                message: loadingText
            });

            //disable scrolling while it is masked
            if (scroller) {
                scroller.setDisabled(true);
            }
        }

        this.fireEvent.apply(this, [].concat('beforeload', this, Array.prototype.slice.call(arguments)));
    },

    onStoreLoad: function (store, records, successful, operation, parentNode) {
        this.setMasked(false);
        this.fireEvent.apply(this, [].concat('load', this, Array.prototype.slice.call(arguments)));

        if (store.indexOf(this.getLastNode()) === -1) {
            this.goToNode(store.getRoot());
        }
    },

    /**
     * Called when the backButton has been tapped.
     */
    onBackTap: function () {
        var me = this,
            node = me.getLastNode(),
            detailCard = me.getDetailCard(),
            detailCardActive = detailCard && me.getActiveItem() == detailCard,
            lastActiveList = me.getLastActiveList();

        this.fireAction('back', [this, node, lastActiveList, detailCardActive], 'doBack');
    },

    doBack: function (me, node, lastActiveList, detailCardActive) {
        var layout = me.getLayout(),
            animation = layout ? layout.getAnimation() : null;

        if (detailCardActive && lastActiveList) {
            if (animation) {
                animation.setReverse(true);
            }
            me.setActiveItem(lastActiveList);
            me.setLastNode(node.parentNode);
            me.syncToolbar();
        } else {
            me.goToNode(node.parentNode);
        }
    },

    updateData: function (data) {
        if (!this.getStore()) {
            this.setStore(new Ext.data.TreeStore({
                root: data
            }));
        }
    },

    applyStore: function (store) {
        if (store) {
            if (Ext.isString(store)) {
                // store id
                store = Ext.data.StoreManager.get(store);
            } else {
                // store instance or store config
                if (!(store instanceof Ext.data.TreeStore)) {
                    store = Ext.factory(store, Ext.data.TreeStore, null);
                }
            }

            // <debug>
            if (!store) {
                Ext.Logger.warn("The specified Store cannot be found", this);
            }
            //</debug>
        }

        return store;
    },

    storeListeners: {
        rootchange: 'onStoreRootChange',
        load: 'onStoreLoad',
        beforeload: 'onStoreBeforeLoad'
    },

    updateStore: function (newStore, oldStore) {
        var me = this,
            listeners = this.storeListeners;

        listeners.scope = me;

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

        if (newStore) {
            newStore.on(listeners);
            me.goToNode(newStore.getRoot());
        }
    },

    onStoreRootChange: function (store, node) {
        this.goToNode(node);
    },

    applyDetailCard: function (detailCard, oldDetailCard) {
        return Ext.factory(detailCard, Ext.Component, detailCard === null ? oldDetailCard : undefined);
    },

    applyBackButton: function (config) {
        return Ext.factory(config, Ext.Button, this.getBackButton());
    },

    updateBackButton: function (newButton, oldButton) {
        if (newButton) {
            var me = this, 
                toolbar;

            newButton.on('tap', me.onBackTap, me);
            newButton.setText(me.getBackText());

            toolbar = me.getToolbar();
            if (me.$backButtonContainer) {
                me.$backButtonContainer.insert(0, newButton);
            } else {
                toolbar.insert(0, newButton);
            }
        } else if (oldButton) {
            oldButton.destroy();
        }
    },

    applyToolbar: function (config) {
        if (config && config.splitNavigation) {
            Ext.apply(config, {
                docked: 'top',
                xtype: 'titlebar',
                ui: 'light'
            });

            var containerConfig = (config.splitNavigation === true) ? {} : config.splitNavigation;

            this.$backButtonContainer = this.add(Ext.apply({
                xtype: 'toolbar',
                docked: 'bottom',
                hidden: true,
                ui: 'dark'
            }, containerConfig));
        }

        return Ext.factory(config, Ext.TitleBar, this.getToolbar());
    },

    updateToolbar: function (newToolbar, oldToolbar) {
        var me = this;
        if (newToolbar) {
            newToolbar.setTitle(me.getTitle());
            if (!newToolbar.getParent()) {
                me.add(newToolbar);
            }
        } else if (oldToolbar) {
            oldToolbar.destroy();
        }
    },

    updateUseToolbar: function (newUseToolbar, oldUseToolbar) {
        if (!newUseToolbar) {
            this.setToolbar(false);
        }
    },

    updateTitle: function (newTitle) {
        var me = this,
            toolbar = me.getToolbar();

        if (toolbar && me.getUpdateTitleText()) {
            toolbar.setTitle(newTitle);
        }
    },

    /**
     * Override this method to provide custom template rendering of individual
     * nodes. The template will receive all data within the Record and will also
     * receive whether or not it is a leaf node.
     * @param {Ext.data.Record} node
     * @return {String}
     */
    getItemTextTpl: function (node) {
        return '{' + this.getDisplayField() + '}';
    },

    /**
     * Override this method to provide custom template rendering of titles/back
     * buttons when {@link #useTitleAsBackText} is enabled.
     * @param {Ext.data.Record} node
     * @return {String}
     */
    getTitleTextTpl: function (node) {
        return '{' + this.getDisplayField() + '}';
    },

    /**
     * @private
     */
    renderTitleText: function (node, forBackButton) {
        if (!node.titleTpl) {
            node.titleTpl = Ext.create('Ext.XTemplate', this.getTitleTextTpl(node));
        }

        if (node.isRoot()) {
            var initialTitle = this.getInitialConfig('title');
            return (forBackButton && initialTitle === '') ? this.getInitialConfig('backText') : initialTitle;
        }

        return  node.titleTpl.applyTemplate(node.data);
    },

    /**
     * Method to handle going to a specific node within this nested list. Node must be part of the
     * internal {@link #store}.
     * @param {Ext.data.NodeInterface} node The specified node to navigate to.
     */
    goToNode: function (node) {
        if (!node) {
            return;
        }

        var me = this,
            activeItem = me.getActiveItem(),
            detailCard = me.getDetailCard(),
            detailCardActive = detailCard && me.getActiveItem() == detailCard,
            reverse = me.goToNodeReverseAnimation(node),
            firstList = me.firstList,
            secondList = me.secondList,
            layout = me.getLayout(),
            animation = layout ? layout.getAnimation() : null,
            list;

        //if the node is a leaf, throw an error
        if (node.isLeaf()) {
            throw new Error('goToNode: passed a node which is a leaf.');
        }

        //if we are currently at the passed node, do nothing.
        if (node === me.getLastNode() && !detailCardActive) {
            return;
        }

        if (detailCardActive) {
            if (animation) {
                animation.setReverse(true);
            }
            list = me.getLastActiveList();
            list.getStore().setNode(node);
            node.expand();
            me.setActiveItem(list);
        } else {
            if (animation) {
                animation.setReverse(reverse);
            }

            if (firstList && secondList) {
                //firstList and secondList have both been created
                activeItem = me.getActiveItem();

                me.setLastActiveList(activeItem);
                list = (activeItem == firstList) ? secondList : firstList;

                list.getStore().setNode(node);
                node.expand();

                me.setActiveItem(list);
                if (me.getClearSelectionOnListChange()) {
                    list.deselectAll();
                }
            }
            else if (firstList) {
                //only firstList has been created
                me.setLastActiveList(me.getActiveItem());
                me.setActiveItem(me.getList(node));
                me.secondList = me.getActiveItem();
            }
            else {
                //no lists have been created
                me.setActiveItem(me.getList(node));
                me.firstList = me.getActiveItem();
            }
        }

        me.fireEvent('listchange', me, me.getActiveItem());

        me.setLastNode(node);

        me.syncToolbar();
    },


    /**
     * The leaf you want to navigate to. You should pass a node instance.
     * @param {Ext.data.NodeInterface} node The specified node to navigate to.
     */
    goToLeaf: function (node) {
        if (!node.isLeaf()) {
            throw new Error('goToLeaf: passed a node which is not a leaf.');
        }

        var me = this,
            card = me.getDetailCard(),
            container = me.getDetailContainer(),
            sharedContainer = container === me,
            layout = me.getLayout(),
            animation = layout ? layout.getAnimation() : false,
            activeItem;

        if (card) {
            if (container.getItems().indexOf(card) === -1) {
                container.add(card);
            }
            if (sharedContainer) {
                activeItem = me.getActiveItem();
                if (activeItem instanceof Ext.dataview.List) {
                    me.setLastActiveList(activeItem);
                }
                me.setLastNode(node);
            }
            if (animation) {
                animation.setReverse(false);
            }
            container.setActiveItem(card);
            me.syncToolbar();
        }
    },

    /**
     * @private
     * Method which updates the {@link #backButton} and {@link #toolbar} with the latest information from
     * the current node.
     */
    syncToolbar: function (forceDetail) {
        var me = this,
            detailCard = me.getDetailCard(),
            node = me.getLastNode(),
            detailActive = forceDetail || (detailCard && (me.getActiveItem() == detailCard)),
            parentNode = (detailActive) ? node : node.parentNode,
            backButton = me.getBackButton();

        //show/hide the backButton, and update the backButton text, if one exists
        if (backButton) {
            var toolbar = me.getToolbar(),
                splitNavigation = toolbar.getInitialConfig("splitNavigation");

            if (splitNavigation) {
                me.$backButtonContainer[parentNode ? 'show' : 'hide']();
            }

            backButton[parentNode ? 'show' : 'hide']();
            if (parentNode && me.getUseTitleAsBackText()) {
                backButton.setText(me.renderTitleText(node.parentNode, true));
            }
        }

        if (node) {
            me.setTitle(me.renderTitleText(node));
        }
    },

    updateBackText: function (newText) {
        this.getBackButton().setText(newText);
    },

    /**
     * @private
     * Returns `true` if the passed node should have a reverse animation from the previous current node.
     * @param {Ext.data.NodeInterface} node
     */
    goToNodeReverseAnimation: function (node) {
        var lastNode = this.getLastNode();

        if (!lastNode) {
            return false;
        }

        return (!lastNode.contains(node) && lastNode.isAncestor(node)) ? true : false;
    },

    /**
     * @private
     * Returns the list config for a specified node.
     * @param {HTMLElement} node The node for the list config.
     */
    getList: function (node) {
        var me = this,
            treeStore = new Ext.data.NodeStore({
                recursive: false,
                node: node,
                rootVisible: false,
                model: me.getStore().getModel(),
                proxy: 'memory'
            });

        node.expand();

        return Ext.Object.merge({
            xtype: 'list',
            useSimpleItems: me.getUseSimpleItems(),
            pressedDelay: 250,
            autoDestroy: true,
            store: treeStore,
            onItemDisclosure: me.getOnItemDisclosure(),
            allowDeselect: me.getAllowDeselect(),
            itemHeight: me.getItemHeight(),
            variableHeights: me.getVariableHeights(),
            emptyText: me.getEmptyText(),
            listeners: [{
                itemdoubletap: 'onItemDoubleTap',
                itemtap: 'onItemTap',
                beforeselectionchange: 'onBeforeSelect',
                containertap: 'onContainerTap',
                scope: me
            }, {
                selectionchange: 'onSelectionChange',
                itemtouchstart: 'onItemInteraction',
                itemtap: 'onItemInteraction',
                order: 'before',
                scope: me
            }],
            itemTpl: '<span<tpl if="leaf == true"> class="x-list-item-leaf"</tpl>>' + me.getItemTextTpl(node) + '</span>'
        }, me.getListConfig());
    }
});

