/**
 * A lightweight component to display data in a simple tree structure.
 * @since 6.0.0
 */
Ext.define('Ext.list.Tree', {
    extend: 'Ext.Widget',
    xtype: 'treelist',
 
    requires: [
        'Ext.list.RootTreeItem'
    ],
 
    expanderFirstCls: Ext.baseCSSPrefix + 'treelist-expander-first',
    expanderOnlyCls: Ext.baseCSSPrefix + 'treelist-expander-only',
    highlightPathCls: Ext.baseCSSPrefix + 'treelist-highlight-path',
    microCls: Ext.baseCSSPrefix + 'treelist-micro',
 
    uiPrefix: Ext.baseCSSPrefix + 'treelist-',
 
    element: {
        reference: 'element',
        cls: Ext.baseCSSPrefix + 'treelist ' + Ext.baseCSSPrefix + 'unselectable',
        listeners: {
            click: 'onClick',
            mouseenter: 'onMouseEnter',
            mouseleave: 'onMouseLeave',
            mouseover: 'onMouseOver'
        },
        children: [{
            reference: 'toolsElement',
            cls: Ext.baseCSSPrefix + 'treelist-toolstrip',
            listeners: {
                click: 'onToolStripClick',
                mouseover: 'onToolStripMouseOver'
            }
        }]
    },
 
    cachedConfig: {
        animation: {
            duration: 500,
            easing: 'ease'
        },
 
        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: {
        /**
         * @cfg {Object} [defaults]
         * The default configuration for the widgets created for tree items.
         *
         * @cfg {String} [defaults.xtype="treelistitem"]
         * The type of item to create. By default, items are `{@link Ext.list.TreeItem treelistitem}`
         * instances. This can be customized but this `xtype` must reference a class that
         * ultimately derives from the `{@link Ext.list.AbstractTreeItem}` base class.
         */
        defaults: {
            xtype: 'treelistitem'
        },
 
        highlightPath: null,
 
        iconSize: null,
 
        indent: null,
 
        micro: null,
 
        overItem: null,
 
        /**
         * @cfg {Ext.data.TreeModel} selection
         * 
         * The current selected node.
         */
        selection: null, 
 
        /**
         * @cfg {Boolean} selectOnExpander 
         * `true` to select the node when clicking the expander.
         */
        selectOnExpander: false,
 
        /**
         * @cfg {Boolean} [singleExpand=false]
         * `true` if only 1 node per branch may be expanded.
         */
        singleExpand: null,
 
        /**
         * @cfg {String/Object/Ext.data.TreeStore}
         * The data source to which this component is bound.
         */
        store: null,
 
        ui: null
    },
 
    twoWayBindable: {
        selection: 1
    },
 
    publishes: {
        selection: 1
    },
 
    defaultBindProperty: 'store',
 
    constructor: function(config) {
        this.callParent([config]);
        // Important to publish the value here, so the vm can 
        // will know our intial state. 
        this.publishState('selection', this.getSelection());
    },
 
    beforeLayout: function () {
        // Only called in classic, ignored in modern 
        this.syncIconSize();
    },
 
    destroy: function () {
        var me = this;
 
        me.destroying = true;  // normally set in callParent 
 
        me.unfloatAll(); 
        me.activeFloater = null;
        me.setSelection(null);
        me.setStore(null);
        me.callParent();
    },
 
    updateOverItem: function (over, wasOver) {
        var map = {},
            state = 2,
            c, node;
 
        // Walk up the node hierarchy starting at the "over" item and set their "over" 
        // config appropriately (2 when over that row, 1 when over a descendant). 
        // 
        for (= over; c; c = this.getItem(node.parentNode)) {
            node = c.getNode();
            map[node.internalId] = true;
 
            c.setOver(state);
 
            state = 1;
        }
 
        if (wasOver) {
            // If we wasOver something else previously, walk up that node hierarchy and 
            // set their "over" to 0... until we encounter some node that we are still 
            // "over" (as determined in previous loop). 
            // 
            for (= wasOver; c; c = this.getItem(node.parentNode)) {
                node = c.getNode();
                if (map[node.internalId]) {
                    break;
                }
 
                c.setOver(0);
            }
        }
    },
 
    applySelection: function(selection, oldSelection) {
        var store = this.getStore();
        if (!store) {
            selection = null;
        }
        if (selection && selection.get('selectable') === false) {
            selection = oldSelection;
        }
        return selection;
    },
 
    updateSelection: function(selection, oldSelection) {
        var me = this,
            item;
 
        if (!me.destroying) {
            // getItem has guards around null, so we don't 
            // need to check for oldSelection/selection here 
            item = me.getItem(oldSelection);
            if (item) {
                item.setSelected(false);
            }
 
            item = me.getItem(selection);
            if (item) {
                item.setSelected(true);
            }
            me.fireEvent('selectionchange', me, selection);
        }
    },
 
    applyStore: function (store) {
        return store && Ext.StoreManager.lookup(store, 'tree');
    },
 
    updateStore: function (store, oldStore) {
        var me = this,
            root;
 
        if (oldStore) {
            if (oldStore.getAutoDestroy()) {
                oldStore.destroy();
            } else {
                me.storeListeners.destroy();
            }
            me.removeRoot();
            me.storeListeners = null;
        }
 
        if (store) {
            me.storeListeners = store.on({
                destroyable: true,
                scope: me,
                nodeappend: me.onNodeAppend,
                nodecollapse: me.onNodeCollapse,
                nodeexpand: me.onNodeExpand,
                nodeinsert: me.onNodeInsert,
                noderemove: me.onNodeRemove,
                rootchange: me.onRootChange,
                update: me.onNodeUpdate
            });
        
            root = store.getRoot();
            if (root) {
                me.createRootItem(root);
            }
        }
 
        if (!me.destroying) {
            me.updateLayout();
        }
    },
 
    updateExpanderFirst: function (expanderFirst) {
        this.element.toggleCls(this.expanderFirstCls, expanderFirst);
    },
 
    updateExpanderOnly: function (value) {
        this.element.toggleCls(this.expanderOnlyCls, !value);
    },
 
    updateHighlightPath: function (updatePath) {
        this.element.toggleCls(this.highlightPathCls, updatePath);
    },
 
    updateMicro: function (micro) {
        var me = this;
 
        if (!micro) {
            me.unfloatAll();
            me.activeFloater = null;
        }
 
        me.element.toggleCls(me.microCls, micro);
    },
 
    updateUi: function (ui, oldValue) {
        var el = this.element,
            uiPrefix = this.uiPrefix;
 
        if (oldValue) {
            el.removeCls(uiPrefix + oldValue);
        }
        if (ui) {
            el.addCls(uiPrefix + ui);
        }
 
        // Ensure that the cached iconSize is read from the style. 
        delete this.iconSize;
        this.syncIconSize();
    },
 
    /**
     * Get a child {@link Ext.list.AbstractTreeItem item} by node.
     * @param {Ext.data.TreeModel} node The node.
     * @return {Ext.list.AbstractTreeItem} The item. `null` if not found.
     */
    getItem: function (node) {
        var map = this.itemMap,
            ret;
 
        if (node && map) {
            ret = map[node.internalId];
        }
 
        return ret || null;
    },
 
    /**
     * This method is called to populate and return a config object for new nodes. This
     * can be overridden by derived classes to manipulate properties or `xtype` of the
     * returned object. Upon return, the object is passed to `{@link Ext#create}` and the
     * reference is stored as part of this tree.
     *
     * The base class implementation will apply any configured `{@link #defaults}` to the
     * object it returns.
     *
     * @param {Ext.data.TreeModel} node The node backing the item.
     * @param {Ext.list.AbstractTreeItem} parent The parent item. This is never `null` but
     * may be an instance of `{@link Ext.list.RootTreeItem}`.
     * @return {Object} The config object to pass to `{@link Ext#create}` for the item.
     * @template
     */
    getItemConfig: function (node, parent) {
        return Ext.apply({
            parentItem: parent.isRootListItem ? null : parent,
            owner: this,
            node: node,
            indent: this.getIndent()
        }, this.getDefaults());
    },
 
    privates: {
        checkForOutsideClick: function(e) {
            var floater = this.activeFloater;
            if (!floater.element.contains(e.target)) {
                this.unfloatAll();
            }
        },
 
        collapsingForExpand: false,
 
        /**
         * Create a new list item.
         * @param {Ext.data.TreeModel} node The node backing the item.
         * @param {Ext.list.AbstractTreeItem} parent The parent item.
         * @return {Ext.list.AbstractTreeItem} The list item.
         *
         * @private
         */
        createItem: function (node, parent) {
            var item = Ext.create(this.getItemConfig(node, parent)),
                toolEl;
 
            if (parent.isRootListItem) {
                toolEl = item.getToolElement();
                if (toolEl) {
                    this.toolsElement.appendChild(toolEl);
                    toolEl.dom.setAttribute('data-recordId', node.internalId);
                    toolEl.isTool = true;
                }
            }
 
            return (this.itemMap[node.internalId] = item); // <== assignment 
        },
 
        /**
         * Create a root item for this list.
         * @param {Ext.data.TreeModel} root The root node.
         *
         * @private
         */
        createRootItem: function (root) {
            var me = this,
                item;
 
            me.itemMap = {};
            me.rootItem = item = new Ext.list.RootTreeItem({
                indent: me.getIndent(),
                node: root,
                owner: me
            });
 
            me.element.appendChild(item.element);
 
            me.itemMap[root.internalId] = item;
        },
 
        floatItem: function(item, byHover) {
            var me = this,
                floater;
 
            if (item.getFloated()) {
                return;
            }
 
            me.unfloatAll();
 
            me.activeFloater = floater = item;
            me.floatedByHover = byHover;
 
            item.setFloated(true);
 
            if (byHover) {
                item.getToolElement().on('mouseleave', me.checkForMouseLeave, me);
                floater.element.on('mouseleave', me.checkForMouseLeave, me);
            } else {
                Ext.on('mousedown', me.checkForOutsideClick, me);
            }
        },
 
        /**
         * Handles when this element is clicked.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onClick: function (e) {
            var item = e.getTarget('[data-recordId]'),
                id;
 
            if (item) {
                id = item.getAttribute('data-recordId');
                item = this.itemMap[id];
                if (item) {
                    item.onClick(e);
                }
            }
        },
 
        onMouseEnter: function (e) {
            this.onMouseOver(e);
        },
 
        onMouseLeave: function () {
            this.setOverItem(null);
        },
 
        onMouseOver: function (e) {
            var comp = Ext.Component.fromElement(e.getTarget());
 
            this.setOverItem(comp && comp.isTreeListItem && comp);
        },
 
        checkForMouseLeave: function (e) {
            var floater = this.activeFloater,
                relatedTarget = e.getRelatedTarget();
 
            if (floater) {
                if (relatedTarget !== floater.getToolElement().dom && !floater.element.contains(relatedTarget)) {
                    this.unfloatAll();
                }
            }
        },
 
        /**
         * Handles a node being appended to a parent.
         * @param {Ext.data.TreeModel} parentNode The parent node.
         * @param {Ext.data.TreeModel} node The appended node.
         *
         * @private
         */
        onNodeAppend: function (parentNode, node) {
            // If it's a root we'll handle it on rootchange 
            if (parentNode) {
                var item = this.itemMap[parentNode.internalId];
 
                if (item) {
                    item.nodeInsert(node, null);
                }
            }
        },
 
        /**
         * Handles when a node collapses.
         * @param {Ext.data.TreeModel} node The node.
         *
         * @private
         */
        onNodeCollapse: function (node) {
            var item = this.itemMap[node.internalId];
 
            if (item) {
                item.nodeCollapse(node, this.collapsingForExpand);
            }
        },
 
        /**
         * Handles when a node expands.
         * @param {Ext.data.TreeModel} node The node.
         *
         * @private
         */
        onNodeExpand: function (node) {
            var me = this,
                item = me.itemMap[node.internalId],
                childNodes, len, i, parentNode, child;
 
            if (item) {
                if (!item.isRootItem && me.getSingleExpand()) {
                    me.collapsingForExpand = true;
                    parentNode = (item.getParentItem() || me.rootItem).getNode();
                    childNodes = parentNode.childNodes;
                    for (= 0, len = childNodes.length; i < len; ++i) {
                        child = childNodes[i];
                        if (child !== node) {
                            child.collapse();
                        }
                    }
                    me.collapsing = false;
                }
 
                item.nodeExpand(node);
            }
        },
 
        /**
         * Handles a node being inserted into a parent.
         * @param {Ext.data.TreeModel} parentNode The parent node.
         * @param {Ext.data.TreeModel} node The inserted node.
         * @param {Ext.data.TreeModel} refNode The node this was inserted before.
         *
         * @private
         */
        onNodeInsert: function (parentNode, node, refNode) {
            var item = this.itemMap[parentNode.internalId];
 
            if (item) {
                item.nodeInsert(node, refNode);
            }
        },
 
        /**
         * Handles a node being removed from a parent.
         * @param {Ext.data.TreeModel} parentNode The parent node.
         * @param {Ext.data.TreeModel} node The removed node.
         * @param {Boolean} isMove `true` if this node is moving inside the tree.
         *
         * @private
         */
        onNodeRemove: function (parentNode, node, isMove) {
            // If it's a move we don't need to do anything, we won't process it 
            // as a removal, the addition will handle it all. 
            // Also if the node being removed is the root we'll handle it in rootchange 
            if (parentNode && !isMove) {
                var item = this.itemMap[parentNode.internalId];
 
                if (item) {
                    item.nodeRemove(node);
                }
            }
        },  
 
        /**
         * Handles when a node updates.
         * @param {Ext.data.TreeStore} store The store.
         * @param {Ext.data.TreeModel} node The node.
         * @param {String} type The update type.
         * @param {String[]} modifiedFieldNames The modified field names, if known.
         *
         * @private
         */
        onNodeUpdate: function (store, node, type, modifiedFieldNames) {
            var item = this.itemMap[node.internalId];
 
            if (item) {
                item.nodeUpdate(node, modifiedFieldNames);
            }
        },
 
        /**
         * Handles when the root node in the tree changes.
         * @param {Ext.data.TreeModel} root The root.
         *
         * @private
         */
        onRootChange: function (root) {
            this.removeRoot();
 
            if (root) {
                this.createRootItem(root);
            }
 
            this.updateLayout();
        },
 
        /**
         * Removes a list item.
         * @param {Ext.data.TreeModel} node The node backing the item.
         *
         * @private
         */
        removeItem: function (node)  {
            var map = this.itemMap;
 
            if (map) {
                delete map[node.internalId];
            }
        },
 
        removeRoot: function () {
            var me = this,
                rootItem = me.rootItem;
 
            if (rootItem) {
                me.element.removeChild(rootItem.element);
                me.rootItem = me.itemMap = Ext.destroy(rootItem);
            }
        },
 
        /**
         * Handles when the toolstrip has a click.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onToolStripClick: function(e) {
            var item = e.getTarget('[data-recordId]'),
                id;
 
            if (item) {
                id = item.getAttribute('data-recordId');
                item = this.itemMap[id];
                if (item) {
                    if (item === this.activeFloater) {
                        this.unfloatAll();
                    } else {
                        this.floatItem(item, false);
                    }
                }
            }
        },
 
        /**
         * Handles when the toolstrip has a mouseover.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onToolStripMouseOver: function(e) {
            var item = e.getTarget('[data-recordId]'),
                id;
 
            if (item) {
                id = item.getAttribute('data-recordId');
                item = this.itemMap[id];
                if (item) {
                    this.floatItem(item, true);
                }
            }
        },
 
        syncIconSize: function() {
            var me = this,
                size = me.iconSize ||
                      (me.iconSize = parseInt(me.element.getStyle('background-position'), 10));
 
            me.setIconSize(size);
        },
 
        unfloatAll: function () {
            var me = this,
                floater = me.activeFloater;
 
            if (floater) {
                floater.setFloated(false);
                me.activeFloater = null;
 
                if (me.floatedByHover) {
                    floater.element.un('mouseleave', me.checkForMouseLeave, me);
                } else {
                    Ext.un('mousedown', me.checkForOutsideClick, me);
                }
            }
        },
 
        defaultIconSize: 22,
 
        updateIconSize: function (value) {
            this.setIndent(value || this.defaultIconSize);
        },
 
        updateIndent: function (value) {
            var rootItem = this.rootItem;
 
            if (rootItem) {
                rootItem.setIndent(value);
            }
        }
    }
});