/*
 *  Copyright 2019 Anyware Services
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

/**
 * Provides a Tree for displaying query containers and queries.
 * @private
 */
Ext.define('Ametys.plugins.queriesdirectory.tree.QueriesTree', {
    extend: 'Ext.tree.Panel',
    
    statics: {
        /**
         * Function to render query's type in result grid
         * @param {Object} value The type value
         * @param {Object} metaData A collection of metadata about the current cell
         * @param {Ext.data.Model} record The record
         * @private
         */
        renderType: function(value, metaData, record)
        {
            try
            {
                var query = Ametys.plugins.queriesdirectory.model.QueryFactory.create(value, record.getId(), {});
                return query.getTypeLabel();
            }
            catch (e)
            {
                return value;
            }
        },
        
        /**
         * Function to render query's title in result grid
         * @param {Object} title The data title
         * @param {Object} metaData A collection of metadata about the current cell
         * @param {Ext.data.Model} record The record
         * @private
         */
        renderTitle: function(title, metaData, record)
        {
            var queryType = record.get('type'),
                isQuery = record.get('isQuery'),
                iconCls = '';
            if (isQuery)
            {
                iconCls = 'ametysicon-data110';
                try
                {
                    metaData.tdAttr = Ametys.plugins.queriesdirectory.tree.QueriesTree.descriptionTooltip(record);
                    var query = Ametys.plugins.queriesdirectory.model.QueryFactory.create(queryType, record.getId(), {});
                    iconCls = query.getTypeIconCls();
                }
                catch (e)
                {
                    // Error while trying to get icon class for this query
                }
                return '<span class="a-grid-glyph query-entry ' + iconCls /*+ ' ' + record.get('decorator')*/ + '"></span>' + title;
            }
            else
            {
                return title;
            }
            
        },
    
        /**
         * Function to render query's documentation in result grid
         * @param {Object} title The data title
         * @param {Object} metaData A collection of metadata about the current cell
         * @param {Ext.data.Model} record The record
         * @private
         */
        renderDocumentation: function(documentation, metaData, record)
        {
            return '<a href="' + documentation + '" target="_blank">' + documentation + '</a>';
        },
    
        /**
         * Function to render query's title in result grid
         * @param {Object} title The data title
         * @param {Object} metaData A collection of metadata about the current cell
         * @param {Ext.data.Model} record The record
         * @private
         */
        renderDescription: function(description, metaData, record)
        {
            if (record.get('isQuery'))
            {
                metaData.tdAttr = Ametys.plugins.queriesdirectory.tree.QueriesTree.descriptionTooltip(record);
            }
            return description;
            
        },
        
        /**
         * Function to generate description tooltip
         * @param {Ext.data.Model} record The record
         * @private
         */
        descriptionTooltip: function(record)
        {
            return 'data-qtip="<strong>' + record.get("title").replaceAll('"', '&quot;') + '</strong>'
                + (record.get("description") && record.get("description") !== '' ? '<br/>' + record.get("description").replaceAll('"', '&quot;') : '')
                + (record.get("documentation") && record.get("documentation") !== '' ? '<br/>' + Ametys.plugins.queriesdirectory.tree.QueriesTree.renderDocumentation(record.get("documentation"), null, record).replaceAll('"', '&quot;') : '')
                + '"';
        }
    },
    
    /**
     * @cfg {Boolean} onlyContainers true to only have the containers
     */
    /**
     * @cfg {String} profile The profile ('read_access' or 'write_access') filter. By default, if null, the server will treat the request as 'read_access'
     */
    /**
     * @cfg {String} queryType The type of queries to return. Can be null to not filter by type.
     */
    
    /**
     * @private
     * @property {Object} _lastSearch The criteria used on the last search 
     */
    _lastSearch: {},

    /**
     * @private
     * @property {Boolean} _isComputingRootNode To prevent multiple requests to refresh the root node.
     */
    _isComputingRootNode: false,  
    
    constructor: function(config)
    {
        
        config.store = this._createStore(config);
        
        if (config.onlyContainers !== true)
        {
            config.dockedItems = [this._getToolbarCfg(config)];
            config.dockedItems.push(this._getNoResultPanel());
        }
        
        this._counter = {};
        
        var plugins = config.plugins;
        config.plugins = Ext.isArray(plugins) ? plugins : (plugins == null ? [] : [plugins]);
        config.plugins.push({
            ptype: 'cellediting',
            clicksToEdit: 1,
            editAfterSelect: true,
            listeners: {
                'beforeedit': this._onBeforeEdit,
                'edit': this._onEdit,
                scope: this
            }
        });
        
        config.viewConfig = config.viewConfig || {};
        Ext.apply(config.viewConfig, {
            plugins: {
                ptype: 'ametystreeviewdragdrop',
                containerScroll: true,
                appendOnly: true,
                sortOnDrop: true,
                expandDelay: 500,
                setAmetysDragInfos: Ext.bind(this.getDragInfo, this),
                setAmetysDropZoneInfos: Ext.bind(this.getDropInfo, this)
            }
        });
        
        this.callParent(arguments);
        
        Ametys.message.MessageBus.on(Ametys.message.Message.CREATED, this._onCreatedOrMovedMessage, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onUpdatedMessage, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.MOVED, this._onCreatedOrMovedMessage, this);
    },
    
    onDestroy: function()
    {
        Ametys.message.MessageBus.unAll(this);
        this.callParent(arguments);
    },
    
    /**
     * @private
     * Creates the queries directory store
     * @param {Object} config The configuration
     * @param {Boolean} [config.onlyContainers=false] See {@link #cfg-onlyContainers}
     * @param {String} [config.profile=null] See {@link #cfg-profile}
     * @param {String} [config.queryType=null] See {@link #cfg-queryType}
     * @return {Ext.data.Store} The query store
     */
    _createStore: function(config)
    {
        var onlyContainers = config.onlyContainers === true,
            profile = config.profile,
            queryType = config.queryType;
        
        var store = Ext.create('Ext.data.TreeStore', {
            model: 'Ametys.plugins.queriesdirectory.tree.QueriesTree.QueryEntry',
            sorters: [{property: 'text', direction:'ASC'}],
            
            proxy: {
                type: 'ametys',
                plugin: 'queries-directory',
                url: 'queries/list.json',
                reader: {
                    type: 'json',
                    rootProperty: 'queries'
                },
                extraParams: {
                    onlyContainers: onlyContainers,
                    profile: profile,
                    queryType: queryType
                }
            },
            
            folderSort: true,
            root: {
                id: 'root',
                name: 'root',
                editable: false,
                allowDrag: false,
                isQuery: false,
                expanded: true,
                text: "{{i18n PLUGINS_QUERIESDIRECTORY_UITOOL_QUERIES_ROOT_NAME}}",
                path: ''
            }
        });
        
        return store;
    },
    
    /**
     * @private
     * Gets the toolbar configuration
     * @param {Object} config The configuration
     * @return {Object} The toolbar configuration
     */
    _getToolbarCfg: function(config)
    {
        return {
            dock: 'top',
            xtype: 'toolbar',
            layout: {
                type: 'hbox',
                align: 'stretch'
            },
            defaultType: 'button',
            items: [{
                // Search input
                xtype: 'textfield',
                cls: 'ametys',
                flex: 1,
                maxWidth: 300,
                itemId: 'search-filter-input',
                emptyText: "{{i18n PLUGINS_QUERIESDIRECTORY_TREE_FILTER_EMPTY_TEXT}}",
                enableKeyEvents: true,
                minLength: 3,
                minLengthText: "{{i18n PLUGINS_QUERIESDIRECTORY_TREE_FILTER_MIN_LENGTH_INVALID}}",
                msgTarget: 'qtip',
                listeners: {change: Ext.Function.createBuffered(this._searchFilter, 500, this)}
            }, {
                // Clear search filter
                tooltip: "{{i18n PLUGINS_QUERIESDIRECTORY_TREE_FILTER_CLEAR}}",
                handler: this._clearSearchFilter,
                scope: this,
                iconCls: 'a-btn-glyph ametysicon-eraser11 size-16',
                cls: 'a-btn-light'
            }, {
                xtype: 'tbspacer',
                flex: 0.0001
            }, {
                // Filter by author
                tooltip: '{{i18n PLUGINS_QUERIESDIRECTORY_TREE_FILTER_BY_AUTHOR}}',
                itemId: 'search-filter-author',
                enableToggle: true,
                toggleHandler: this._searchFilter,
                scope: this,
                iconCls: 'a-btn-glyph ametysicon-body-people size-16',
                cls: 'a-btn-light'
            }, {
                xtype: 'tbseparator'
            }, {
                // Filter by type
                xtype: 'querytypefilter',
                itemId: 'search-filter-others',
                tree: this
            }]
        }
    },
    
    /**
     * @private
     * Get the 'no result' button configuration. This button is shown when filter matches no result.
     * @return {Object} the button configuration
     */
    _getNoResultPanel: function ()
    {
        return {
            dock: 'top',
            xtype: 'button',
            hidden: true,
            itemId: 'no-result',
            ui: 'tool-hintmessage',
            text: "{{i18n PLUGINS_QUERIESDIRECTORY_UITOOL_QUERIES_FILTER_NO_MATCH}}" + "{{i18n PLUGINS_QUERIESDIRECTORY_UITOOL_QUERIES_FILTER_NO_MATCH_ACTION}}",
            scope: this,
            handler: this._clearAllSearchFilters
        };
    },    
    
    /**
     * @private
     * Show or hide the 'no result' button.
     * @param {Boolean} show true to show the button, false to hide it.
     */
    _showHideNoResultPanel: function (show)
    {
        this.down("button[itemId='no-result']").setVisible(show);
    },    
    
    /**
     * @private
     * Filters queries
     */
    _searchFilter: function ()
    {
        var field = this.getDockedItems('container[dock="top"]')[0].down('#search-filter-input');
        var value = new String(field.getValue()).trim();
        
        var search = this.getDockedItems('container[dock="top"]')[0].down('#search-filter-others').getValues();
        search.search = value;
        search.author = this.getDockedItems('container[dock="top"]')[0].down('#search-filter-author').pressed;
        
        if (Ext.Object.equals(this._lastSearch, search))
        {
            // Do nothing
            return;
        }
        
        this._lastSearch = search;
        
        if (search.search.length > 2
            || search.author
            || search.request
            || search.solr
            || search.script
            || search.formatting)
        {   
            var rootNode = this.getRootNode();
            this._getFilteredQueries(rootNode);
        }
        else
        {
            this._showHideNoResultPanel(false);
            this.clearFilter();
        }
    },
    
    /**
     * Get the tags the name matches the given value
     * @param {String} value The value to match
     * @param {Ext.data.Model} node The node where starting search
     * @param {Boolean} [childNodesOnly] set to 'true' to filter the child nodes only. 
     * @private
     */
    _getFilteredQueries: function (node, childNodesOnly)
    {
        Ametys.data.ServerComm.callMethod({
            role: "org.ametys.plugins.queriesdirectory.QueryDAO", 
            methodName: 'filterQueries', 
            parameters: [node.getId(), 
                            this._lastSearch.search.length > 2 ? this._lastSearch.search : '',
                            this._lastSearch.author,
                            this._lastSearch.request,
                            this._lastSearch.solr,
                            this._lastSearch.script,
                            this._lastSearch.formatting ],
            errorMessage: "{{i18n PLUGINS_QUERIESDIRECTORY_UITOOL_QUERIES_FILTER_ERROR}}",
            cancelCode: 'QueriesTree',
            callback: {
                handler: this._filterQueriesCb,
                scope: this,
                arguments: {
                    node: node,
                    childNodesOnly: childNodesOnly
                }
            }
        });
    },    

    /**
     * @private
     * Callback function after searching pages
     * @param {Object} paths The paths of matching pages
     * @param {Object[]} args The callback arguments
     */
    _filterQueriesCb: function(paths, args) 
    {
        var hasResult = false;
        var node = args.node;
        
        if (!paths)
        {
            return;
        }
        
        if (paths.length == 0)
        {
            if (args.childNodesOnly)
            {
                // There is no child nodes matching but some other nodes are matching.
                hasResult = true;
                for (var i=0; i < node.childNodes.length; i++)
                {
                    this.filterBy (function () {return false}, node.childNodes[i]);
                }
            }
            else
            {
                this.filterBy (function () {return false}, node);
            }
        }
        else
        {
            hasResult = true;
            this._expandAndFilter (paths, this.getRootNode(), node);
        }
        
        if (!hasResult)
        {
            this._showHideNoResultPanel(true);
        }
        else
        {
            this._showHideNoResultPanel(false)
        }
    },    
    
    /**
     * Filters by a function. The specified function will be called for each Record in this Store. 
     * If the function returns true the Record is included, otherwise it is filtered out.
     * @param {Function} filterFn A function to be called.
     */
    filterBy: function (filterFn)
    {
        this.clearFilter();
        this.getStore().addFilter({id: 'filter', filterFn: filterFn});
    },

    /**
     * Expand the tree to the given paths.
     * @param {Object[]} paths The paths to expand
     * @param {Ext.data.Model} rootNode The concerned root node
     * @param {Ext.data.Model} node The node from which apply filter
     * @private
     */
    _expandAndFilter: function(paths, rootNode, node)
    {
        node = node || rootNode;
        
        this._counter[rootNode.getId()] = paths.length;
        for (var i=0; i < paths.length; i++)
        {
            var path = paths[i].substr(0, paths[i].lastIndexOf("#"));
            this.expandPath(path, 'id', '#', Ext.bind (this._filterPaths, this, [paths, rootNode, node], false));
        }   
    },
    
    /**
     * Filter nodes by path once the last expand has been processed
     * @param {String[]} paths The path to filter by
     * @param {Ext.data.Model} rootNode The concerned root node
     * @param {Ext.data.Model} node The node from which apply filter
     * @private
     */
    _filterPaths: function (paths, rootNode, node)
    {
        // only execute the filterBy after the last expandPath()
        if (--this._counter[rootNode.getId()] == 0)
        {
            var filterFn = Ext.bind (this._filterByPath, this, [paths, rootNode], true);
            
            // FIXME Ensure that expand is complete by deferring the filterBy function ...
            Ext.defer(this.filterBy, 50, this, [filterFn, node]);
        }
    },
    
    /**
     * Returns true if the node path is a part of given paths
     * @param {Ext.data.Model} node The node to test
     * @param {String[]} paths The paths
     * @param {Ext.data.Model} rootNode The root node to build the complete paths
     * @private
     */
    _filterByPath: function (node, paths, rootNode)
    {
        var currentPath = node.getPath('id', "#");
        for (var i=0; i < paths.length; i++)
        {
            var path = paths[i] + '#';
            if (path.indexOf(currentPath + '#') == 0)
            {
                return true;
            }
        }
        return false;
    },    
    
    _clearAllSearchFilters: function() 
    {
        this.getDockedItems('container[dock="top"]')[0].down('#search-filter-author').toggle(false, true);
        this.getDockedItems('container[dock="top"]')[0].down('#search-filter-others').untoggleAll(true);
        
        this._clearSearchFilter();
    },
    
    /**
     * @private
     * Clears the filter search
     */
    _clearSearchFilter: function()
    {
        this.getDockedItems('container[dock="top"]')[0].down('#search-filter-input').reset();
        this._searchFilter();
    },
    
    /**
     * Clear all filters
     */
    clearFilter: function ()
    {
        this.getStore().removeFilter('filter');
        
        var selection = this.getSelectionModel().getSelection()[0];
        if (selection)
        {
            this.ensureVisible(selection.getPath('id', "#"), {field: 'id'});
        }
    },
    
    /**
     * @private
     * Add filter on empty containers on the store
     */
    _addEmptyContainersFilter: function()
    {
        this.getStore().removeFilter('emptyContainers');
        this.getStore().addFilter({
            id: 'emptyContainers',
            filterFn: function(record) {
                var hasDescendant = record.get('hasNoDescendantQuery') === false,
                    isQuery = record.get('isQuery'),
                    show = isQuery || hasDescendant;
                return show;
            }
        });
    },
    
    /**
     * @private
     * Listener called before cell editing 
     * @param {Ext.grid.plugin.CellEditing} editor The cell editor
     * @param {Object} context An editing context event with the following properties:
     * @param {Ext.grid.Panel}         context.grid The owning grid Panel.
     * @param {Ext.data.Model}         context.record The record being edited.
     * @param {String}                 context.field The name of the field being edited.
     * @param {Mixed}                  context.value The field's current value.
     * @param {HTMLElement}            context.row The grid row element.
     * @param {Ext.grid.column.Column} context.column The {@link Ext.grid.column.Column} being edited.
     * @param {Number}                 context.rowIdx The index of the row being edited.
     * @param {Number}                 context.colIdx The index of the column being edited.
     * @param {Boolean}                context.cancel Set this to `true` to cancel the edit or return false from your handler.
     */
    _onBeforeEdit: function(editor, context)
    {
        var record = context.record;
        return record && !record.isRoot() && !record.get('isQuery') && record.get('canWrite');
    },
    
    /**
     * @private
     * Listener called after cell editing
     * @param {Ext.grid.plugin.CellEditing} editor The cell editor
     * @param {Object} context An editing context with the following properties:
     * @param {Ext.grid.Panel}         context.grid The owning grid Panel.
     * @param {Ext.data.Model}         context.record The record being edited.
     * @param {String}                 context.field The name of the field being edited.
     * @param {Mixed}                  context.value The field's current value.
     * @param {HTMLElement}            context.row The grid row element.
     * @param {Ext.grid.column.Column} context.column The {@link Ext.grid.column.Column} being edited.
     * @param {Number}                 context.rowIdx The index of the row being edited.
     * @param {Number}                 context.colIdx The index of the column being edited.
     * @param {Mixed}                  context.originalValue The original value before being edited.
     */
    _onEdit: function(editor, context)
    {
        var record = context.record,
            id = record.getId(),
            newName = context.value,
            oldName = context.originalValue;
        
        if (newName != oldName)
        {
            Ametys.plugins.queriesdirectory.QueriesDAO.renameQueryContainer([id, newName], renameCb, { ignoreCallbackOnError: false });
            function renameCb(response)
            {
                if (!response || !response.id)
                {
                    // edit failed
                    record.beginEdit();
                    record.set('text', oldName);
                    record.endEdit();
                    record.commit();
                }
            }
        }
    },
    
    /**
     * @private
     * This event is thrown by the getDragData to add the 'source' of the drag.
     * @param {Object} item The default drag data that will be transmitted. You have to add a 'source' item in it: 
     * @param {Ametys.relation.RelationPoint} item.source The source (in the relation way) of the drag operation. 
     */
    getDragInfo: function(item)
    {
        var targets = item.records
            .filter(hasRight)
            .map(this.getMessageTargetConfiguration)
            .filter(function(cfg) {return cfg != null;});
            
        function hasRight(record)
        {
            return record.get('canDelete');
        }
    
        if (targets.length > 0)
        {
            item.source = {
                relationTypes: [Ametys.relation.Relation.MOVE], 
                targets: targets
            };
        }
    },
    
    /**
     * @private
     * This event is thrown before the beforeDrop event and create the target of the drop operation relation.
     * @param {Ext.data.Model[]} targetRecords The target records of the drop operation.
     * @param {Object} item The default drag data that will be transmitted. You have to add a 'target' item in it: 
     * @param {Object} item.target The target (in the relation way) of the drop operation. A Ametys.relation.RelationPoint config.   
     */ 
    getDropInfo: function(targetRecords, item)
    {
        var targets = targetRecords
            .map(this.getMessageTargetConfiguration)
            .filter(function(cfg) {return cfg != null});

        if (targets.length > 0)
        {
            item.target = {
                relationTypes: [Ametys.relation.Relation.MOVE], 
                targets: targets
            };
        }
    },
    
    /**
     * @private
     * Gets the configuration for creating message target
     * @param {Ext.data.Model} record The tree record to convert to its Ametys.message.MessageTarget configuration
     * @return {Object} The configuration to create a Ametys.message.MessageTarget. Can be null, if the record is null or not relevant to be a messagetarget.
     */
    getMessageTargetConfiguration: function(record)
    {
        if (record == null)
        {
            return null;
        }
        else if (record.get('isQuery'))
        {
            return {
                id: Ametys.message.MessageTarget.QUERY,
                parameters: {
                    ids: [record.getId()]
                }
            };
        }
        else
        {
            return {
                id: Ametys.message.MessageTarget.QUERY_CONTAINER,
                parameters: {
                    id: record.getId(),
                    name: record.get('text'),
                    canWrite: record.get('canWrite'),
                    canEdit: record.get('canEdit'),
                    canRename: record.get('canRename'),
                    canAssignRights: record.get('canAssignRights'),
                    fullPath: record.getFullPath()
                }
            };
        }
    },
    
    /**
     * Listener on create messages
     * @param {Ametys.message.Message} message The creation message.
     * @private
     */
    _onCreatedOrMovedMessage: function(message)
    {
        var queryTarget = message.getTarget(Ametys.message.MessageTarget.QUERY);
        var queryContainerTarget = message.getTarget(Ametys.message.MessageTarget.QUERY_CONTAINER);
        if (queryTarget)
        {
            this._onQueryCreatedOrMoved(queryTarget, Ext.emptyFn);
        }
        else if (queryContainerTarget)
        {
            var editingPlugin = this.editingPlugin;
            this._onQueryCreatedOrMoved(queryContainerTarget, beginEdit);
            
            function beginEdit(created)
            {
                // TODO startEdit is deprecated, but I was not able to found the "new" way to do it
                editingPlugin.startEdit(created, 0);
            }
        }
    },
    
    /**
     * Called when a query or query container is created
     * @param {Ametys.message.MessageTarget} target The query message target
     * @param {Function} cb The callback
     * @private
     */
    _onQueryCreatedOrMoved: function(target, cb)
    {
        var tree = this,
            store = tree.getStore(),
            selModel = tree.getSelectionModel();
            selModel.deselectAll(),
            createdId = target.getParameters().id;
        Ametys.plugins.queriesdirectory.QueriesDAO.getIdsOfPath([createdId], expandAndSelectUntil);
        
        function expandAndSelectUntil(pathIds)
        {
            pathIds.splice(0, 0, 'root');
            loadNextPathEl([store.getById('root')]);
            
            function loadNextPathEl(amongRecords)
            {
                if (pathIds.length)
                {
                    var pathId = pathIds.shift();
                    var childNode = Ext.Array.findBy(amongRecords, function(childRecord) {
                        return childRecord.getId() == pathId;
                    });
                    if (childNode)
                    {
                        store.load({
                            node: childNode, 
                            callback: function(records) {
                                expand(childNode, records);
                            }
                        });
                    }
                }
                else
                {
                    var created = store.getById(createdId);
                    if (created)
                    {
                        selModel.select(created);
                        cb(created);
                    }
                }
            }
            
            function expand(loadedNode, records)
            {
                tree.expandNode(loadedNode, false, function() {
                    loadNextPathEl(records);
                });
            }
        }
    },
    
    /**
     * Listener on update messages
     * @param {Ametys.message.Message} message The edition message.
     * @private
     */
    _onUpdatedMessage: function(message)
    {
        var queryTarget = message.getTarget(Ametys.message.MessageTarget.QUERY),
            queryContainerTarget = message.getTarget(Ametys.message.MessageTarget.QUERY_CONTAINER);
        
        if (queryTarget)
        {
            this.reloadParent(queryTarget, true);
        }
        else if (queryContainerTarget)
        {
            var store = this.getStore(),
                node = store.getNodeById(queryContainerTarget.getParameters().id);
            
            if (node)
            {
                var newName = queryContainerTarget.getParameters().name;
                node.beginEdit();
                node.set('text', newName);
                node.endEdit();
                node.commit();
                store.sort();
            }
        }
    },
    
    /**
     * Listener on move messages
     * @param {Ametys.message.Message} message The moving message.
     * @private
     */
    _onMovedMessage: function(message)
    {
        var queryTarget = message.getTarget(Ametys.message.MessageTarget.QUERY),
            queryContainerTarget = message.getTarget(Ametys.message.MessageTarget.QUERY_CONTAINER),
            store = this.getStore();
        if ((queryTarget || queryContainerTarget) && store)
        {
            store.sort();
        }
    },
    
    /**
     * Reloads parent node
     * @param {Ametys.message.MessageTarget} target The message target
     * @param {Boolean} keepSelection true to keep old selection
     */
    reloadParent: function(target, keepSelection)
    {
        var queryId = target.getParameters().id,
            store = this.getStore(),
            node = store.getNodeById(queryId),
            parentId = node && node.get('parentId'),
            parentNode = store.getNodeById(parentId);
        if (parentNode)
        {
            var tree = this;
            var oldSelIds = tree.getSelection()
                    .map(function(record) {
                        return record.getId();
                    });
            
            store.load({node: parentNode, callback: keepSelection ? keepSelectionFn : Ext.emptyFn});
            
            function keepSelectionFn()
            {
                var selModel = tree.getSelectionModel();
                var oldSel = oldSelIds
                    .map(function(id) {
                        return store.getNodeById(id);
                    })
                    .filter(function(record) { return record != null });
                selModel.deselectAll();
                selModel.select(oldSel);
            }
        }
    },
    
    /**
     * Refresh the whole tree
     * @param {Function} [callback] function to call after refreshing
     */
    initRootNodeParameter: function (callback)
    {
        if (this._isComputingRootNode)
        {
            return;
        }
        
        this._isComputingRootNode = true;

        Ametys.data.ServerComm.callMethod({
            role: "org.ametys.plugins.queriesdirectory.QueryDAO",
            methodName: "getRootProperties",
            callback: {
                scope: this,
                handler: this._getRootNodeCb,
                arguments: {
                    callback: callback
                }
            },
            errorMessage: {
                category: this.self.getName(),
                msg: "{{i18n DAOS_QUERY_ROOT_ERROR}}"
            },
            waitMessage: true
        });
    },
    
    /**
     * @private
     * Callback function after retrieving root node
     * @param {Object} response The server response
     * @param {Object} args the callback arguments
     */
    _getRootNodeCb: function (response, args)
    {
        this._isComputingRootNode = false;
        
        this.setRootNode({
            path: '',
            name: 'root',
            canWrite: response.canWrite,
            canRename: false,
            canAssignRights: response.canAssignRights,
            canDelete: false,
            expanded: true,
            text: "{{i18n PLUGINS_QUERIESDIRECTORY_UITOOL_QUERIES_ROOT_NAME}}"
        });

        if (Ext.isFunction (args.callback))
        {
            this.store.getRoot().on('expand', function() { args.callback (response.id); }, this, { single: true });
        }
    }
});