/**
 * The TreeGrid provides a tree-structured UI representation of tree-structured data.
 * TreeGrids must be bound to a {@link Ext.data.TreeStore}.
 *
 * TreeGrid supports multiple columns through the {@link #columns} configuration.
 *
 * By default a TreeGrid contains a single column that uses the `text` field of
 * the store's nodes.
 *
 * Here is a simple TreeGrid using inline data:
 *
 *     @example
 *     var ts = Ext.create('Ext.data.TreeStore', {
 *         root: {
 *             text: 'Genre',
 *             expanded: true,
 *             children: [
 *                 {
 *                     text: 'Comedy',
 *                     children: [
 *                         { leaf: true, text: '30 Rock' },
 *                         { leaf: true, text: 'Arrested Development' },
 *                         { leaf: true, text: 'Bob\'s Burgers' },
 *                         { leaf: true, text: 'Curb your Enthusiasm' },
 *                         { leaf: true, text: 'Futurama' }
 *                     ]
 *                 },
 *                 {
 *                     text: 'Drama',
 *                     children: [
 *                         { leaf: true, text: 'Breaking Bad', },
 *                         { leaf: true, text: 'Game of Thrones' },
 *                         { leaf: true, text: 'Lost' },
 *                         { leaf: true, text: 'Preacher' },
 *                         { leaf: true, text: 'The Wire' }
 *                     ]
 *                 },
 *                 {
 *                     text: 'Science Fiction',
 *                     children: [
 *                         { leaf: true, text: 'Black Mirror' },
 *                         { leaf: true, text: 'Doctor Who' },
 *                         { leaf: true, text: 'Eureka' },
 *                         { leaf: true, text: 'Futurama' },
 *                         { leaf: true, text: 'The Twilight Zone' },
 *                         { leaf: true, text: 'X-Files' }
 *                     ]
 *                 }
 *             ]
 *         }
 *     });
 *
 *     Ext.create({
 *         fullscreen: true,
 *         xtype: 'panel',
 *
 *         items: [{
 *             xtype: 'tree',
 *             height: 600,
 *             width: 400,
 *             store: ts,
 *             title: 'Favorite Shows by Genre'
 *         }]
 *     });
 */
Ext.define('Ext.grid.Tree', {
    extend: 'Ext.grid.Grid',
    xtype: 'tree',
    alternateClassName: 'Ext.tree.Tree',
 
    classCls: Ext.baseCSSPrefix + 'tree',
    expanderLastCls: Ext.baseCSSPrefix + 'expander-last',
    expanderFirstCls: Ext.baseCSSPrefix + 'expander-first',
    expanderOnlyCls: Ext.baseCSSPrefix + 'expander-only',
    cellExpanderCls: Ext.baseCSSPrefix + 'cell-expander',
 
    /**
     * @event beforenodeexpand
     * Fires before an row is visually expanded. May be vetoed by returning false from a handler.
     * @param {Ext.grid.Row} row                    The row to be expanded
     * @param {Ext.data.NodeInterface} record       The record to be expanded
     */
 
    /**
     * @event nodeexpand
     * Fires after an row has been visually expanded and its child nodes are visible in the tree.
     * @param {Ext.grid.Row} row                    The row that was expanded
     * @param {Ext.data.NodeInterface} record       The record that was expanded
     */
 
    /**
     * @event beforenodecollapse
     * Fires before an row is visually collapsed. May be vetoed by returning false from a handler.
     * @param {Ext.grid.Row} node                   The row to be collapsed
     * @param {Ext.data.NodeInterface} record       The record to be collapsed
     */
 
    /**
     * @event nodecollapse
     * Fires after an row has been visually collapsed and its child nodes are no longer
     * visible in the tree.
     * @param {Ext.grid.Row} node                   The row that was collapsed
     * @param {Ext.data.NodeInterface} record       The record that was collapsed
     */
 
    /**
     * @event checkchange 
     * Fires when a node with a checkbox's checked property changes.
     * @param {Ext.grid.cell.Tree} cell               The cell who's checked property was changed.
     * @param {Boolean} checked                       The cell's new checked state.
     * @param {Ext.data.Model} record                 The record that was checked
     * @param {Ext.event.Event} e                     The tap event.
     * @since 7.0
     */
 
    /**
     * @event beforecheckchange  cell, checked, current, record
     * Fires before a node with a checkbox's checked property changes.
     * @param {Ext.grid.cell.Tree} this               The cell who's checked property was changed.
     * @param {Boolean} checked                       The cell's new checked state.
     * @param {Boolean} current                       The cell's old checked state.
     * @param {Ext.data.Model} record                 The record that was checked
     * @param {Ext.event.Event} e                     The tap event.
     * @since 7.0
     */
 
    cachedConfig: {
 
        /**
         * @cfg {Boolean} expanderFirst
         * `true` to display the expander to the left of the item text.
         * `false` to display the expander to the right of the item text.
         */
        expanderFirst: true,
 
        /**
         * @cfg {Boolean} expanderOnly
         * `true` to expand only on the click of the expander element. Setting this to
         * `false` will allow expansion on click of any part of the element.
         */
        expanderOnly: true
    },
 
    config: {
        root: {},
 
        /**
         * @cfg {Boolean} selectOnExpander
         * `true` to select the node when clicking the expander.
         */
        selectOnExpander: false,
 
        /**
         * @cfg {Boolean} [singleExpand]
         * `true` if only 1 node per branch may be expanded.
         */
        singleExpand: false,
 
        rootVisible: true,
 
        displayField: 'text',
 
        columns: false, // Non-null to force running the applier.
 
        rowLines: false,
 
        /**
         * @cfg {Boolean} [folderSort=false]
         * True to automatically prepend a leaf sorter to the store.
         */
        folderSort: false
    },
 
    eventsSelector: '.' + Ext.baseCSSPrefix + 'grid-cell',
 
    applyColumns: function(columns) {
        if (!columns) {
            this.setHideHeaders(true);
            columns = [{
                xtype: 'treecolumn',
                text: 'Name',
                dataIndex: this.getDisplayField(),
                minWidth: 100,
                flex: 1
            }
            ];
        }
 
        return columns;
    },
 
    onRootChange: function(newRoot, oldRoot) {
        var me = this,
            fireEventArgs;
 
        if (oldRoot) {
            delete oldRoot.fireEventArgs;
        }
 
        // We take over from event firing so we can relay.
        // Cannot use Function.createSequence. That does not return the return values
        if (newRoot) {
            fireEventArgs = newRoot.fireEventArgs;
 
            newRoot.fireEventArgs = function(eventName) {
                // Fire on the original firer
                var ret = fireEventArgs.apply(newRoot, arguments);
 
                // If not stopped, fire through this Tree
                if (ret !== false) {
                    arguments[0] = me.rootEventsMap[eventName] || ('item' + eventName);
                    ret = me.fireEventArgs.apply(me, arguments);
                }
 
                return ret;
            };
        }
    },
 
    updateExpanderFirst: function(expanderFirst) {
        var el = this.element;
 
        el.toggleCls(this.expanderFirstCls, expanderFirst);
        el.toggleCls(this.expanderLastCls, !expanderFirst);
    },
 
    updateExpanderOnly: function(expanderOnly) {
        var el = this.element;
 
        el.toggleCls(this.expanderOnlyCls, expanderOnly);
        el.toggleCls(this.cellExpanderCls, !expanderOnly);
    },
 
    /**
     * Sets root node of this tree. All trees *always* have a root node. It may be
     * {@link #rootVisible hidden}.
     *
     * If the passed node has not already been loaded with child nodes, and has its expanded field
     * set, this triggers the {@link #cfg-store} to load the child nodes of the root.
     * @param {Ext.data.TreeModel/Object} root
     * @return {Ext.data.TreeModel} The new root
     */
    setRootNode: function(root) {
        var store = this.getStore();
 
        root = store.setRoot(root);
 
        return root;
    },
 
    /**
     * Returns the root node for this tree.
     * @return {Ext.data.TreeModel} 
     */
    getRootNode: function() {
        var store = this.getStore();
 
        return store ? store.getRoot() : null;
    },
 
    /**
     * Expands a record that is loaded in the tree.
     * @param {Ext.data.Model} record The record to expand
     * @param {Boolean} [deep] True to expand nodes all the way down the tree hierarchy.
     * @param {Function} [callback] The function to run after the expand is completed
     * @param {Object} [scope] The scope of the callback function.
     */
    expandNode: function(record, deep, callback, scope) {
        return record.expand(deep, callback, scope || this);
    },
 
    /**
     * Collapses a record that is loaded in the tree.
     * @param {Ext.data.Model} record The record to collapse
     * @param {Boolean} [deep] True to collapse nodes all the way up the tree hierarchy.
     * @param {Function} [callback] The function to run after the collapse is completed
     * @param {Object} [scope] The scope of the callback function.
     */
    collapseNode: function(record, deep, callback, scope) {
        return record.collapse(deep, callback, scope || this);
    },
 
    /**
     * Expand all nodes
     * @param {Function} [callback] A function to execute when the expand finishes.
     * @param {Object} [scope] The scope of the callback function
     */
    expandAll: function(callback, scope) {
        var me = this,
            root = me.getRootNode();
 
        if (root) {
            Ext.suspendLayouts();
            root.expand(true, callback, scope || me);
            Ext.resumeLayouts(true);
        }
    },
 
    /**
     * Collapse all nodes
     * @param {Function} [callback] A function to execute when the collapse finishes.
     * @param {Object} [scope] The scope of the callback function
     */
    collapseAll: function(callback, scope) {
        var me = this,
            root = me.getRootNode();
 
        if (root) {
            Ext.suspendLayouts();
            scope = scope || me;
 
            if (me.getStore().rootVisible) {
                root.collapse(true, callback, scope);
            }
            else {
                root.collapseChildren(true, callback, scope);
            }
 
            Ext.resumeLayouts(true);
        }
    },
 
    /**
     * @method getChecked
     * Retrieve an array of checked records.
     * @return {Ext.data.NodeInterface[]} An array containing the checked records
     * @since 7.0
     */
    getChecked: function() {
        var checked = [],
            rootNode = this.getRootNode(),
            isChecked = rootNode.get('checked'),
            childNodes = rootNode.childNodes;
 
        if (isChecked === true) {
            checked.push(rootNode);
        }
 
        this.getCheckedChildItems(childNodes, checked);
 
        return checked;
    },
 
    privates: {
        rootEventsMap: {
            beforeappend: 'beforeitemappend',
            beforeremove: 'beforeritememove',
            beforemove: 'beforeitemmove',
            beforeinsert: 'beforeiteminsert',
            beforeexpand: 'beforeitemexpand',
            beforecollapse: 'beforeitemcollapse'
        },
 
        doChildTouchStart: function(location) {
            var cell = location.cell;
 
            if (cell && (!cell.isTreeCell ||
                this.getSelectOnExpander() ||
                location.event.target !== cell.expanderElement.dom)) {
                this.callParent([location]);
            }
        },
 
        updateStore: function(newStore, oldStore) {
            var me = this,
                newRoot;
 
            // We take over from event firing so we can relay
            if (oldStore) {
                Ext.destroy(me.storeListeners, me.storeRelayers);
            }
 
            if (newStore) {
                me.store = newStore;
                newRoot = newStore.getRoot();
 
                // If there is no root node defined, then create one.
                // Ensure a first onRootChange is called so we can hook into the event firing
                if (newRoot) {
                    me.onRootChange(newRoot);
                }
                else {
                    newStore.setRoot(me.getRoot());
                    newRoot = newStore.getRoot();
                }
 
                // Store must have the same idea about root visibility as us before callParent
                // binds it.
                if (!('rootVisible' in newStore.initialConfig)) {
                    newStore.setRootVisible(me.getRootVisible());
                }
 
                // TreeStore must have an upward link to the TreePanel so that nodes can find their
                // owning tree in NodeInterface.getOwnerTree
                // TODO: NodeInterface.getOwnerTree is deprecated. Data class must not be coupled
                // to UI. Remove this link when that method is removed.
                newStore.ownerTree = me;
 
                me.callParent([newStore, oldStore]);
 
                newStore.folderSort = me.getFolderSort();
 
                // Monitor the TreeStore for the root node being changed. Return a Destroyable
                // object
                me.storeListeners = me.mon(newStore, {
                    destroyable: true,
                    rootchange: me.onRootChange,
                    scope: me
                });
 
                // Relay store events. relayEvents always returns a Destroyable object.
                me.storeRelayers = me.relayEvents(newStore, [
                    /**
                     * @event beforeload
                     * @inheritdoc Ext.data.TreeStore#beforeload
                     */
                    'beforeload',
 
                    /**
                     * @event load
                     * @inheritdoc Ext.data.TreeStore#load
                     */
                    'load'
                ]);
 
                // If rootVisible is false, we *might* need to expand the node.
                // If store is autoLoad, that will already have been kicked off.
                // If its already expanded, or in the process of loading, the TreeStore
                // has started that at the end of updateRoot
                if (!newStore.rootVisible &&
                    !newStore.autoLoad &&
                    !(newRoot.isExpanded() ||
                        newRoot.isLoading())) {
                    // A hidden root must be expanded, unless it's overridden with autoLoad: false.
                    // If it's loaded, set its expanded field (silently), and skip ahead to the
                    // onNodeExpand callback.
                    if (newRoot.isLoaded()) {
                        newRoot.data.expanded = true;
                        newStore.onNodeExpand(newRoot, newRoot.childNodes);
                    }
                    // Root is not loaded; go through the expand mechanism to force a load
                    // unless we were told explicitly not to load the store by setting
                    // autoLoad: false. This is useful with Direct proxy in cases when
                    // Direct API is loaded dynamically and may not be available at the time
                    // when TreePanel is created.
                    else if (newStore.autoLoad !== false && !newStore.hasPendingLoad()) {
                        newRoot.data.expanded = false;
                        newRoot.expand();
                    }
                }
            }
        },
 
        /**
         * get checked nodes
         * @param {Array} [childNodes] childNodes of a parent node
         * @param {Array} [checked] array of checked nodes
         */
        getCheckedChildItems: function(childNodes, checked) {
            var i, childNode;
 
            for (= 0; i < childNodes.length; i++) {
                childNode = childNodes[i];
 
                if (childNode.get('checked') === true) {
                    checked.push(childNode);
                }
 
                if (childNode.childNodes.length) {
                    this.getCheckedChildItems(childNode.childNodes, checked);
                }
            }
        }
    }
});