/*
 *  Copyright 2015 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.
 */

/**
 * This class provides a TreePanel for tree-structured resources (files and folders)
 * The tree includes a toolbar which allow to :
 *  - filter resources on name, 
 *  - refresh a node
 *  - expand a node 
 *  - collapse the whole tree.
 * The default tree store uses the {@link Ametys.explorer.tree.ExplorerTree.NodeEntry} model. Override the #createTreeStore method to use another Model.
 * 
 * Creates your own ExplorerTree class by inheriting this one and define at least the following methods: #getRootIds, #getDragInfo and #getDropInfo.
 * 
 * 		Ext.define("My.ExplorerTree", {
 * 			extend: "Ametys.explorer.tree.ExplorerTree",
 * 
 * 			getRootIds: function() {
 * 				return [...];
 * 			},
 * 
 * 			getDragInfo: function(item) {
 * 				...
 * 			}
 * 
 * 			getDropInfo: function(targetRecord, item) {
 * 				...
 * 			}
 * 		});
 */
Ext.define('Ametys.explorer.tree.ExplorerTree', {
	extend: 'Ext.tree.Panel',
	
	/**
	 * @property {String[]} _rootIds List of ids of the direct children of the root node.
	 * @private
	 */
	
	/**
	 * @cfg {Boolean} [inlineEditionEnable=false] True to enable inline edition of the tree node. Default to false.
	 */
	/**
	 * @property {Boolean} _inlineEditionEnable See #cfg-inlineEditionEnable
	 * @private
	 */
	
	/**
	 * @cfg {Boolean} [ignoreFiles=false] True to restrict the tree to folders (ignore files). Default to false.
	 */
	/**
	 * @property {Boolean} _ignoreFiles See #cfg-ignoreFiles
	 * @private
	 */
	
    /**
     * @cfg {String[]} [allowedExtensions] The allowed file extensions. If not null, only files with an allowed extension will be visibles in the tree.
     */
    /**
     * @property {String[]} _allowedExtensions See #cfg-allowedExtensions
     * @private
     */
    /**
     * @cfg {Boolean} [allowDragAndDrop=true] True to enable drag and drop in the tree
     */
    /**
     * @property {Boolean} _allowDragAndDrop See #cfg-allowDragAndDrop
     * @private
     */
	/**
	 * @private
	 * @property {Ext.Template} _resourceTooltipTpl The template used for resources' tooltip
	 */
	
	/**
	 * @private
	 * @property {Ext.data.TreeModel} _currentEditNode The current node being edited
	 */
	/**
	 * @private
	 * @property {String} _valueBeforeEdit The value of node text before editing it
	 */
	
	
	statics: {
		/**
		 * The default filter.
		 */
        DEFAULT_FILTER: [],

        /**
         * The filter to use to view only images
         */
        IMAGE_FILTER: ['jpg', 'jpeg', 'gif', 'png', 'svg'],
		/**
		 * @property {RegExp} _regexpImage The RegExp for image filter.
		 * @static
		 * @private
		 */
		_regexpImage: /\.(jpg|jpeg|gif|png|svg)$/i,

		/**
		 * The filter to use to view only flash
		 */
        FLASH_FILTER: ['swf'],
		/**
		 * @property {RegExp} _regexpFlash The RegExp for flash filter.
		 * @static
		 * @private
		 */
		_regexpFlash: /\.swf$/i,

        /**
         * The filter to use to view only video or audio files
         */
        MULTIMEDIA_FILTER: ['mp4', 'ogv', 'ogg', 'webm', 'swf', 'flv', 'mp3'],
		
		/**
		 * @property {RegExp} _regexpMultimedia The RegExp for multimedia filter.
		 * @static
		 * @private
		 */
		_regexpMultimedia: /\.(swf|flv|mp3)$/i,

        /**
         * The filter to use to view only video files
         */
        VIDEO_FILTER: ['mp4', 'ogv', 'ogg', 'webm'],
		
		/**
		 * @property {RegExp} _regexpVideo The RegExp for video filter.
		 * @static
		 * @private
		 */
		_regexpVideo: /\.(mp4|ogv|ogg|webm)$/i,

        /**
         * The filter to use to view audio files
         */
        SOUND_FILTER: ['mp3', 'oga', 'wav'],
		
		/**
		 * @property {RegExp} _regexpSound The RegExp for sound filter.
		 * @static
		 * @private
		 */
		_regexpSound: /\.mp3$/i,
        
        /**
         * The filter to use to view only mp3
         */
        PDF_FILTER: ['pdf'],
        /**
         * @property {RegExp} __regexpPdf The RegExp for pdf filter.
         * @static
         * @private
         */
        _regexpPdf: /\.pdf$/i,
		
		/**
		 * Get the icon in small format (16x16 pixels) of a file
		 * @param {String} name The file name
		 * @return {String} The small icon
		 */
		getFileSmallIcon: function (name)
		{
			var extension = Ametys.file.AbstractFileExplorerTree.getFileExtension(name);
			return Ametys.getPluginDirectPrefix('explorer') + "/icon/" + extension + ".png";
		},
		
		/**
		 * Get the icon in large format of a file. If the file is an image, a thumbnail will be calculated.
		 * @param {String} id The id of the resource
		 * @param {String} name The file name
		 * @return {String} The large icon
		 */
		getFileLargeIcon: function (id, name)
		{
			var extension = Ametys.file.AbstractFileExplorerTree.getFileExtension(name);
			if (extension == 'jpg' || extension == 'jpeg' || extension == 'gif' || extension == 'png')
			{
				return Ametys.getPluginDirectPrefix('explorer') + "/resource?id=" + id + "&maxWidth=100&maxHeight=100";
			}
			else
			{
				return Ametys.getPluginDirectPrefix('explorer') + "/thumbnail/" + extension + ".png";
			}
		},
		
		/**
		 * Get the URL to view a resource
		 * @param {String} id The resource id
		 */
		getViewHref: function(id)
		{
			return Ametys.getPluginDirectPrefix('explorer') + '/resource?id=' + encodeURIComponent(id);
		},
		
		/**
		 * Get the URL to download the resource
		 * @param {String} id The resource id
		 */
		getDownloadHref: function(id)
		{
			return Ametys.getPluginDirectPrefix('explorer') + '/resource?id=' + encodeURIComponent(id) + '&download=true';
		}
	},
	
	inheritableStatics: {
		/**
		 * @property {String} RESOURCES_TYPE the type for resources
		 * @readonly
		 */
		RESOURCE_TYPE: 'resource',
		
		/**
		 * @property {String} COLLECTION_TYPE the type for collections
		 * @readonly
		 */
		COLLECTION_TYPE: 'collection'
	},
	
	cls: 'explorer-tree',
	animate: true,
	scrollable: true,
	rootVisible: false,
	
	initComponent: function ()
	{
        var plugins = {};
        if (this._allowDragAndDrop)
        {
            plugins = {
                    ptype: 'ametystreeviewdragdrop',
                    containerScroll: true,
                    appendOnly: true,
                    sortOnDrop: true,
                    expandDelay: 500,
                    setAmetysDragInfos: Ext.bind(this.getDragInfo, this),
                    setAmetysDropZoneInfos: Ext.bind(this.getDropInfo, this)
                };
        }
		Ext.apply(this, {
			folderSort: true,
			root: {
				id: 'dummy',
                type: 'collection',
				editable: false,
				allowDrag: false,
				allowDrop: false,
				text : 'dummy',
				name: 'dummy',
				path: 'dummy',
				expanded: false
			},
			store: this.createTreeStore(),
			
			viewConfig: {
				plugins: plugins,
				loadMask:false
			}
		});
		
		this.callParent(arguments);
	},
	
	constructor: function (config) 
	{
		// Docked items
		config.dockedItems = this.getDockItems(config);
		
		// No result panel
		var noResultPanelCfg = this._getNoResultPanelCfg();
		if (noResultPanelCfg)
		{
			noResultPanelCfg.dock = noResultPanelCfg.dock || 'top';
			config.dockedItems.push(noResultPanelCfg);
		}
		
		this._inlineEditionEnable = config['inlineEditionEnable'];
		if (this._inlineEditionEnable === true)
		{
			config.plugins = {
				ptype: 'cellediting',
				clicksToEdit: 1,
				
				editAfterSelect: true,
				silentlyIgnoreInvalid: false,
				
				listeners: {
					'beforeedit' : Ext.bind(this._onBeforeEdit, this),
					'edit' : Ext.bind(this._onEdit, this)
				}
			};
			config.columns = [{
				xtype: 'treecolumn',
				dataIndex: 'text',
				flex: 1,
				editor: {
					xtype: 'textfield',
					allowBlank: false,
					selectOnFocus: true
				}
			}];
		}
        
		config.enableColumnResize = false;
		config.hideHeaders = true;
		
		// ignore files must be set before the call to the parent ctor
		this._ignoreFiles = (Ext.isBoolean(config.ignoreFiles) && config.ignoreFiles) || false;
		this._allowDragAndDrop = (Ext.isBoolean(config.allowDragAndDrop) && Ext.isDefined(config.allowDragAndDrop)) ? config.allowDragAndDrop : true;//default true
		this.callParent(arguments);
		
		/**
		 * @event filterupdated
		 * Fires when a filter was updated
		 * @param {Ametys.explorer.tree.ExplorerTree} tree The explorer tree
		 */
		/**
		 * @event rootnodeschanged
		 * Fires when the root nodes were loaded
		 * @param {Ametys.explorer.tree.ExplorerTree} tree The explorer tree
		 */
		
		this._resourceTooltipTpl = Ext.create ('Ext.Template', [
			'<u>{{i18n plugin.explorer:PLUGINS_EXPLORER_RESOURCE_TOOLTIP_AUTHOR}}</u> : {author}<br/>',
			'<span style="white-space: nowrap"><u>{{i18n plugin.explorer:PLUGINS_EXPLORER_RESOURCE_TOOLTIP_DATE}}</u> : </span> <span style="white-space: nowrap">{lastModified}</span><br/>',
			'<u>{{i18n plugin.explorer:PLUGINS_EXPLORER_RESOURCE_TOOLTIP_SIZE}}</u> : <span style="white-space: nowrap">{size}</span><br/>'
		]);
		
		this._allowedExtensions = config.allowedExtensions || config.filter || null;
        
		this._filterValue = null;
		
		this._rootIds = [];
		
		this.on('itemmouseenter', this._createQtip, this);
		this.on ('load', this._onLoad, this);
	},
	
	/**
	 * Get the ids of root nodes.
	 * @return {String[]} The ids of root nodes
	 */
	getRootIds: function ()
	{
		return this._rootIds;
	},
	
	/**
	 * Create the tree store
	 * @param {Object} config The tree panel configuration
	 * @return {Ext.data.TreeStore} The created tree store
	 */
	createTreeStore: function (config)
	{
		var url = this._ignoreFiles === true ? 'explorer-nodes' : 'child-nodes';
		
		return Ext.create('Ext.data.TreeStore', Ext.apply({
			model: 'Ametys.explorer.tree.ExplorerTree.NodeEntry',
			proxy: {
				type: 'ametys',
				plugin: 'explorer',
				url: url,
				reader: {
					type: 'xml',
					rootProperty: 'Nodes',
					record: '> Node'
					
				}
			},
            autoLoad : false,
            
			listeners: {
				'beforeload': {fn: this._handleBeforeLoad, scope: this}
			}
		}, this._getStoreSortInfo()));
	},
	
	/**
	 * @protected
	 * Must return an object containing sort configuration property for the internal {@link Ext.data.TreeStore}
	 */
	_getStoreSortInfo: function()
	{
		return {
			folderSort: true,
			sorters: [{
				sorterFn: function(n1, n2) {
					// sort of hack to be sure that root nodes are always
					// sorted the same way, whatever the language

					// folders first
					var isN1Collection = n1.data.type == 'collection';
					var isN2Collection = n2.data.type == 'collection';
					
					if (!((isN1Collection && isN2Collection) || (!isN1Collection && !isN2Collection)))
					{
						return isN1Collection && !isN2Collection ? -1 : 1;
					}
					
					var v1 = n1.getDepth() == 1 ? n1.get('name') : n1.get('text');
					var v2 = n2.getDepth() == 1 ? n2.get('name') : n2.get('text');
					
					// see {@link Ext.util.Sorter#defaultSorterFn}
					return Ext.String.enhancedCompare(v1, v2);
				}
			}]
		};
	},
	
	 /**
	 * Handle the before load event of the store in order to cancel the load request on the root node.
	 * Be careful, this is mandatory is our case only because we have an hidden root node, which automatically perform a load when the node is created.
	 * @param {Ext.data.Store} store This store
	 * @param {Ext.data.operation.Operation} operation The Ext.data.operation.Operation object that will be passed to the Proxy to
	 * load the Store
	 */
	_handleBeforeLoad: function(store, operation)
	{
		var node = operation.node,
			id = node != null ? node.getId() : null;
		
		// Cancel load
		if (!id || id === 'dummy')
		{
			return false;
		}
        
        operation.setParams( Ext.apply(operation.getParams() || {}, {
            allowedExtensions: this._allowedExtensions
        }));
	},
	
	/**
	 * Set the tree root nodes
	 * @param {Object[]/Ametys.explorer.tree.ExplorerTree.NodeEntry[]} rootNodes the root nodes array. Either explorer tree node entry objects or config objects.
	 * @param {Function} [callback] Function to execute once the root node expand completes
	 */
	setRootNodes: function(rootNodes, callback)
	{
		rootNodes = Ext.Array.from(rootNodes);
		
		this.getRootNode().removeAll();
		this._rootIds = [];
		
		for (var i=0; i < rootNodes.length; i++)
		{
			this.getRootNode().appendChild(rootNodes[i]);
			this._rootIds.push(rootNodes[i].id);
		}
		
		this.getRootNode().expand(false, callback);
		
		this.fireEvent("rootnodeschanged", this);
	},
	
	/**
	 * Refresh the root nodes
	 * @param {Function} callback The function called when all root nodes have been refreshed
	 * @param {Object} scope the callback scope
	 */
	refreshRootNodes: function(callback, scope)
	{
		var callback = Ext.isFunction(callback) ? callback : Ext.emptyFn,
			rl = this._rootIds.length,
            me = this;
		
        var i = 0;
        function refreshNextNode ()
        {
            if (i == rl)
            {
                callback.call(scope);
            }
            else
            {
                me.refreshNode(me._rootIds[i++], refreshNextNode);
            }
        }
        
        refreshNextNode();
	},
	
	/**
	 * Get the dock items
	 * @param {Object} config The initial tree panel configuration
	 * @return {Object[]} The dock items configuration
	 */
	getDockItems: function (config)
	{
		var dockedItems = config.dockedItems || [];
		
		var toolbarCfg = this._getToolbarCfg();
		if (toolbarCfg)
		{
			toolbarCfg.dock = toolbarCfg.dock || 'top';
			dockedItems.push(toolbarCfg);
		}
		
		return dockedItems;
	},
	
	/**
	 * Get the right id to check when renaming a resource
	 * @return the right id for rename action
	 */
	getRightIdOnRename: function(node)
	{
        return 'Plugin_Explorer_Folder_Edit';
	},
	
	/**
	 * Get the right id to check when dropping a resource
	 * @return the right id for dropping action
	 */
	getRightIdOnDrop: function(node)
	{
        return 'Plugin_Explorer_Folder_Add';
	},
	
	/**
	 * Get the right id to check when dragging a resource
	 * @return the right id for dragging action
	 */
	getRightIdOnDrag: function (node)
	{
        return 'Plugin_Explorer_Folder_Delete';
	},
	
	/**
	 * Determines if user has the given right on current selection or given node
	 * @param {String} rightId The right's id to check
	 * @param {Ext.data.Model} node The node to check right on. If null the right will be search on current message target. Otherwise, the right will be check in asynchronous mode by sending a server request. Use the callback function in this last case.
	 * @param {Function} callback Callback function to call after checking user right in asynchronous mode. Can be null if node is null.
	 * @return true if the user has right in synchronous mode.
	 */
	hasRight: function(rightId, node, callback)
	{
		if (!rightId)
		{
			return true;
		}
		
		var rights = [];
		
		var message = Ametys.message.MessageBus.getCurrentSelectionMessage(),
			target = message.getTarget(function(target) {
				return Ametys.message.MessageTarget.EXPLORER_COLLECTION == (target.id || target.getId()) || Ametys.message.MessageTarget.RESOURCE == (target.id || target.getId());
			});
	
		if (target)
		{
			rights = target.getParameters().rights || [];
		}
		
		if (Ext.Array.contains(rights, rightId))
		{
			return true;
		}
		
		return false;
	},
	
	/**
     * Destroy and create the node tooltip when the mouse enters the node
     * @param {Ext.view.View} view The tree view
     * @param {Ext.data.Model} node The tree node
     * @param {HTMLElement} el The node's element
     */
    _createQtip: function (view, node, el)
    {
		if (node.get('type') != this.self.RESOURCE_TYPE)
		{
			return;
		}
		
		Ext.QuickTips.unregister(el);
		Ext.QuickTips.register(Ext.apply({target: el, id: el.id + '-tooltip'}, this._getTooltip(node)));
	},
	
	/**
	 * Get the tooltip configuration
	 * @param {Ext.data.Model} node The tree node
	 * @returns {Object} The tooltip configuration. See Ametys.ui.fluent.Tooltip.
	 * @private
	 */
	_getTooltip: function(node)
	{
		var lastModified = Ext.util.Format.date(Ext.Date.parse(node.get('lastModified'), Ext.Date.patterns.ISO8601DateTime), "{{i18n plugin.core-ui:PLUGINS_CORE_UI_RESOURCE_TOOLTIP_DATE_FORMAT}}");
		
		var text = this._resourceTooltipTpl.applyTemplate ({
			author: node.get('author'), 
			lastModified: lastModified, 
			size: Ext.util.Format.fileSize(node.get('size'))
		});
		
		var extension = Ametys.file.AbstractFileExplorerTree.getFileExtension(node.get('name'));
		var isImg = extension == 'jpg' || extension == 'jpeg' || extension == 'gif' || extension == 'png';
		
		return {
			title: Ext.String.escapeHtml(node.get('name')),
			glyphIcon: isImg ? null : Ametys.file.AbstractFileExplorerTree.getFileIconGlyph(node.get('name')),
			image: isImg ? Ametys.explorer.tree.ExplorerTree.getFileLargeIcon(node.getId(), node.get('name')) : null,
			imageWidth: isImg ? 100 : 48,
			imageHeight: isImg ? 100 : 48,
			text: text,
			inribbon: false
		};
	},
	
	/**
	 * This function is called after loading node
	 * @param {Ext.data.TreeStore} store The tree store	 
	 * @param {Ext.data.TreeModel[]} records The records
	 * @param {Boolean} successful True if the operation was successful.
	 * @param {Ext.data.operation.Operation} operation The operation that triggered this load.
	 * @param {Ext.data.NodeInterface} node The node that was loaded.
	 */
	_onLoad: function (store, records, successful, operation, node)
	{
		// Ext.defer(this.clearFilter, 1, this, [], false);
	},
	
	/**
	 * @protected
	 * Retrieves the  top toolbar config object.
	 */
	_getToolbarCfg: function()
	{
		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_EXPLORER_UITOOL_EXPLORER_TREE_FILTER}}",
				enableKeyEvents: true,
				minLength: 3,
				minLengthText: "{{i18n PLUGINS_EXPLORER_UITOOL_EXPLORER_TREE_FILTER_INVALID}}",
				msgTarget: 'qtip',
				listeners: {change: Ext.Function.createBuffered(this._searchFilter, 500, this)}
			}, {
				// Clear filter
				tooltip: "{{i18n PLUGINS_EXPLORER_UITOOL_EXPLORER_TREE_CLEAR_FILTER}}",
				handler: Ext.bind (this._clearSearchFilter, this),
				iconCls: 'a-btn-glyph ametysicon-eraser11 size-16',
				cls: 'a-btn-light'
			}, 
			{
                xtype: 'tbspacer',
                flex: 0.0001
            }, {
				// Expand node
				ui: 'light',
				tooltip: "{{i18n PLUGINS_EXPLORER_UITOOL_EXPLORER_TREE_EXPAND_ALL}}",
				handler: Ext.bind (this._expandNode, this, [], false),
				icon: Ametys.getPluginResourcesPrefix('explorer') + '/img/tree/expand-all.gif',
				cls: 'x-btn-text-icon'
			}, {
				// Collapse all
				tooltip: "{{i18n PLUGINS_EXPLORER_UITOOL_EXPLORER_TREE_COLLAPSE_ALL}}",
				handler: Ext.bind (this.collapseAll, this, [], false),
				iconCls: 'a-btn-glyph ametysicon-minus-sign4 size-16',
				cls: 'a-btn-light'
			}, {
				// Refresh node
				ui: 'light',
                itemId: 'refresh-btn',
				tooltip: "{{i18n PLUGINS_EXPLORER_UITOOL_EXPLORER_TREE_REFRESH_PAGE}}",
				handler: Ext.bind (this.refreshNode, this, [], false),
				iconCls: 'a-btn-glyph ametysicon-arrow123 size-16',
				cls: 'a-btn-light'
			}]
		};
	},

	/**
	 * @protected
	 * Retrieves the 'no result' panel config
	 */
	_getNoResultPanelCfg: function()
	{
        return {
            dock: 'top',
            xtype: 'button',
            hidden: true,
            itemId: 'noresult',
            ui: 'tool-hintmessage',
            text: "{{i18n PLUGINS_EXPLORER_UITOOL_EXPLORER_TREE_FILTER_NO_MATCH}}" + "{{i18n PLUGINS_EXPLORER_UITOOL_EXPLORER_TREE_FILTER_NO_MATCH_ACTION}}",
            scope: this,
            handler: this._clearSearchFilter
        };
	},
	
	/**
	 * This listener is called on 'keyup' event on filter input field.
	 * Filters the tree by text input.
	 * @param {Ext.form.Field} field The field
	 * @private
	 */
	_searchFilter: function (field)
	{
		var value = Ext.String.trim(field.getValue());
		if (this._filterValue == value)
		{
			// Do nothing
			return;
		}
		
		this._filterValue = value;
		
		if (value.length > 2)
		{
            var rootNodes = [];
            for (var i=0; i < this._rootIds.length; i++)
	        {
	            rootNodes.push(this.getStore().getNodeById(this._rootIds[i]));
	        }
            this._getFilteredResources (value, rootNodes);
            
            // FIXME When filter is active, disable refresh button to avoid issues
            // Indeed, for obscure reasons, and probably due to a extjs issue, when the tree contains many files, sometimes the refresh of a node
            // make visible all files of the tree regardless of the active filter (from search input)
            this.down('#refresh-btn').disable();
		}
		else
		{
			this._hideNoResultPanel();
			this.clearFilter();
		}
	},
    
    /**
     * Get the resources the name matches the given value
     * @param {String} value The value to match
     * @param {Ext.data.Model[]} nodes The nodes where starting search
     * @param {Boolean} [childNodesOnly] set to 'true' to filter the child nodes only. 
     * @param {Ext.data.TreeModel} [rootNode] The node to start filtering 
     * @protected
     */
    _getFilteredResources: function (value, nodes, childNodesOnly, rootNode)
    {
        if (!this._filterRunning)
        {
            this._filterRunning = true;
	        this._filterCounter = nodes.length;
	        this._hasFilterResult = false;
	        this._filteredPaths =  {};
	        
	        for (var i=0; i < nodes.length; i++)
	        {
	            var node = nodes[i];
	            
	            Ametys.data.ServerComm.callMethod({
	                role: "org.ametys.plugins.explorer.resources.actions.ExplorerResourcesDAO", 
	                methodName: 'filterResourcesByRegExp', 
	                parameters: [node.getId(), value, this._allowedExtensions],
	                errorMessage: "{{i18n PLUGINS_EXPLORER_UITOOL_EXPLORER_TREE_SEARCH_ERROR}}",
	                callback: {
	                    handler: this._getFilteredResourcesCb,
	                    scope: this,
	                    arguments: {
	                        node: node,
	                        childNodesOnly: childNodesOnly,
	                        rootNode: rootNode || node
	                    }
	                }
	            });
	        }
        }
        else
        {
            Ext.defer(this._getFilteredResources, 100, this, [value, nodes, childNodesOnly, rootNode]);
        }
        
    },
    
    /**
     * @private
     * Callback function after searching resources
     * @param {Object} paths The paths of matching resources
     * @param {Object[]} args The callback arguments
     */
    _getFilteredResourcesCb: function (paths, args)
    {
        this._filterCounter--;
        
        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;
            var rootNode = args.rootNode || node;
            
            // store matching paths for this root node (they will be filtered after all root nodes are processed)
            this._filteredPaths[rootNode.getId()] = paths;
        }
        
        this._hasFilterResult = hasResult || this._hasFilterResult;
            
        if (this._filterCounter == 0)
        {
            if (!this._hasFilterResult)
	        {
	            this._showNoResultPanel();
	            if (this._filterField)
	            {
	                Ext.defer (this._filterField.markInvalid, 100, this._filterField, ["{{i18n PLUGINS_EXPLORER_UITOOL_EXPLORER_TREE_FILTER_NO_MATCH}}"]);
	            }
                this._filterRunning = false;
	        }
	        else
	        {
                // Expand all matching resources' paths, then filter after the last expand
                this._expandAndFilter(this._filteredPaths);
                
	            this._hideNoResultPanel(false)
	            if (this._filterField)
	            {
	                this._filterField.clearInvalid();
	            }
	        }
            
            this._filterCounter = 0;
            this._hasFilterResult = false;
            this._filteredPaths = {};
        }
    },
    
    /**
     * Expand the tree to the given paths. Then filter nodes matching the given paths by calling the #_filterPaths method
     * @param {Object} paths The filtered paths to expand for each root node
     * @private
     */
    _expandAndFilter: function(filteredPaths)
    {
        this._expandCounter = 0;
        
        for (var rootId in filteredPaths) 
        {
            this._expandCounter += filteredPaths[rootId].length;
        }
        
        for (var rootId in filteredPaths) 
        {
            var paths = filteredPaths[rootId];
            var rootNode = this.getStore().getNodeById(rootId);
            
            for (var i=0; i < paths.length; i++)
            {
                this.expandPath(rootNode.getPath('name') + paths[i], {
                    field:'name',
                    callback: Ext.bind (this._filterPaths, this, [filteredPaths])
                });
            }  
        }
    },
    
    /**
     * Filter nodes by path once the last expand has been processed
     * @param {Object} filteredPaths The paths to filter by for each root node
     * @private
     */
    _filterPaths: function (filteredPaths)
    {
        // only execute the filterBy after the last expandPath()
        if (--this._expandCounter == 0)
        {
            var filterFn = Ext.bind (this._filterByPath, this, [filteredPaths], true);
        
            // Ensure that expand is complete by deferring the filterBy function ...
            Ext.defer(this.filterBy, 50, this, [filterFn]);
            
            this._filterRunning = false;
        }
    },
    
    /**
     * Returns true if the node path is a part of given paths
     * @param {Ext.data.Model} node The node to test
     * @param {Object} filteredPaths The given paths by for each root node
     * @private
     */
    _filterByPath: function (node, filteredPaths)
    {
        var currentPath = node.getPath('name');
        
        for (var rootId in filteredPaths) 
        {
            var paths = filteredPaths[rootId];
            var rootNode = this.getStore().getNodeById(rootId);
            
            for (var i=0; i < paths.length; i++)
            {
                var path = rootNode.getPath('name') + paths[i] + '/';
                if (path.indexOf(currentPath + '/') == 0)
                {
                    return true;
                }
            }
        }
    },
	
	/**
	 * 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().filterBy(filterFn);
	},

	/**
	 * Clear all filters
	 */
	clearFilter: function ()
	{
		this._filterValue = null;
		this.down('#refresh-btn').enable();
        
        this.getStore().clearFilter();
	},
	
	/**
	 * Update the filter for files.
	 * @param {Function} filter The filter function. Use a filter constant. Can be null.
     * @param {String[]} allowedExtensions The allowed extensions. Can be null.
	 */
	updateFilter: function (filter, allowedExtensions)
	{
		this._allowedExtensions = allowedExtensions || filter || null;
        this.refreshRootNodes(this._updateFilterCb, this);
	},
	
	/**
	 * Callback function called after #updateFilter is processed
	 * @private
	 */
	_updateFilterCb: function ()
	{
		this.fireEvent ('filterupdated', this);
	},
	
	/**
	 * Clear the filter search
	 * @param {Ext.Button} btn The button
	 * @protected
	 */
	_clearSearchFilter: function(btn)
	{
		this.clearFilter();
		
		this.getDockedItems('container[dock="top"]')[0].down('#search-filter-input').reset();
		
		this._hideNoResultPanel();
	},
	
	/**
	 * Hide the panel showing there is no result.
	 * @private
	 */
	_hideNoResultPanel: function ()
	{
		var noResultPanel = this.getDockedItems('#noresult')[0];
		if (noResultPanel) 
		{
			noResultPanel.hide();
		}
	},
	
	/**
	 * Show the panel showing there is no result.
	 * @private
	 */
	_showNoResultPanel: function ()
	{
		var noResultPanel = this.getDockedItems('#noresult')[0];
		if (noResultPanel) 
		{
			noResultPanel.show();
		}
	},
	
	/**
	 * Expand the given node
	 * @param {String} node The node to expand.
	 * @param {Function} callback The callback function to call after reload. Can be null. Has the following parameters:
	 * @param {Ext.data.Model} callback.node The expanded node
	 * @private
	 */
	_expandNode: function (node, callback)
	{
		if (node == null)
		{
			var selection = this.getSelectionModel().getSelection();
			node = selection.length > 0 ? selection[0] : null;
		}
		
		if (node != null)
		{
			callback = callback ? Ext.bind(callback, null, [node]) : null;
			this.expandNode (node, false, callback);
		}
	},
	
	/**
	 * Select a node in the tree
	 * @param {Ext.data.Model/String} node The node itself or its id
	 */
	selectNode: function (node)
	{
		if (typeof node == 'string')
		{
			node = this.getStore().getNodeById(node);
		}
		
		this.getSelectionModel().select([node]);
	},
	
	/**
	 * Expand the tree and select a node by its complete path
	 * @param {String} path The complete path from the root node
     * @param {Boolean} autofocus Set to true to focus expanded node
	 */
	selectByPath: function (path, autofocus)
	{
		if (!Ext.String.startsWith(path, '/dummy'))
		{
			path = '/dummy' + path;
		}
        
		this.selectPath(path, 'name', '/', function(success, lastNode) {
            if (success && autofocus)
            {
                this.getView().focusNode(lastNode);
            }
        }, this);
	},
	
	/**
	 * Get the path of the resource.
	 * @param {Ext.data.Model} node The node of the resource
	 * @return {String} The path
	 */
	getResourcePath: function (node)
	{
		var path = node.getPath('name')
		if (path.indexOf('/dummy') == 0)
		{
			path = path.substr('/dummy'.length);
		}
		return path;
	},
	
	/**
	 * This function reload the given node
	 * @param {String} id The id of the node to reload
	 * @param {Function} callback The callback function to call after reload. Can be null. Has the following parameters:
	 * @param {Ext.data.Model} callback.node The refreshed node
	 */
	refreshNode: function (id, callback)
    {
		callback = Ext.isFunction(callback) ? callback : Ext.EmptyFn;
		
		var node;
		if (id == null)
		{
			var selection = this.getSelectionModel().getSelection();
			node = selection.length > 0 ? selection[0] : null;
		}
		else
		{
			node = this.getStore().getNodeById(id);
		}
		
		if (node != null && node.get('type') != Ametys.message.MessageTarget.RESOURCE)
		{
			// Set leaf to false, to allow children to be added during the load. Leaf will be set to true again if needed after the load.
			node.set('leaf', false);
			
            var me = this;
            this.getStore().load({
                node: node,
                callback: function () {
                    if (me._filterValue && me._filterValue.length > 2)
                    {
                        // Filter child nodes only
                        me._getFilteredResources (me._filterValue, [node], true, me._getRootNode(node));
                    }
                    else
                    {
                        Ext.defer(me._expandNode, 200, me, [node, callback]);
                    }
                },
                scope: this
            });
		}
	},
    
    /**
     * Get the root node of a given node
     * @private
     * @param {Ext.data.TreeModel} node The node to start search from
     * @return {Ext.data.TreeModel} The root node
     */
    _getRootNode: function(node)
    {
        for (var i=0; i < this._rootIds.length; i++)
        {
            var rootNode = this.getStore().getNodeById(this._rootIds[i]);
            if (Ext.String.startsWith(node.getPath('name'), rootNode.getPath('name')))
            {
                return rootNode;
            }
        }
        return null;
    },
	
	// ---------------------- Listeners ---------------------------------//
	/**
	 * @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 = [];
		
		for (var i = 0; i < item.records.length; i++)
		{
			var cfg = this.getMessageTargetConfiguration(item.records[i]);
			if (cfg != null)
			{
				targets.push(cfg);
			}
		}
	
		if (targets.length > 0)
		{
			item.source = {
				relationTypes: [Ametys.relation.Relation.MOVE, Ametys.relation.Relation.COPY, Ametys.relation.Relation.REFERENCE], 
				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 = [];
		
		for (var i = 0; i < targetRecords.length; i++)
		{
			var cfg = this.getMessageTargetConfiguration(targetRecords[i]);
			if (cfg != null)
			{
				targets.push(cfg);
			}
		}

		if (targets.length > 0)
		{
			item.target = {
				relationTypes: [Ametys.relation.Relation.MOVE], 
				targets: targets
			};
		}
	},
	
	/**
	 * @template
	 * Called by #getDragInfo, #getDragInfo or by tools to send the current selection
	 * @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)
		{
			// Empty selection
			return null;
		}
		else if (record.get('type') == Ametys.explorer.tree.ExplorerTree.COLLECTION_TYPE)
		{
			return {
				id: Ametys.message.MessageTarget.EXPLORER_COLLECTION,
				parameters: { ids: [record.getId()]}
			};
		}
		else if (record.get('type') == Ametys.explorer.tree.ExplorerTree.RESOURCE_TYPE)
		{
			return {
				id: Ametys.message.MessageTarget.RESOURCE,
				parameters: { ids: [record.getId()] }
			};
		}
		else
		{
			return null;
		}
	},
	
	/**
	 * This listener is called before cell editing is triggered.
	 * Check the user can edit the node. Returns false if the editing should be canceled.
	 * @param {Ext.grid.plugin.CellEditing} editor The cell editor
	 * @param {Object} context An editing context event with the following properties:
	 * @private
	 */
	_onBeforeEdit: function (editor, context)
	{
		var node = this.getSelectionModel().getSelection()[0];
		
		if (node.get('isModifiable') !== true)
		{
			return false;
		}
		
		// Check user rights
		if (!this.hasRight(this.getRightIdOnRename(node)))
		{
			return false;
		}
		
		var canRename = false;
		
		if (node.get('type') == Ametys.explorer.tree.ExplorerTree.RESOURCE_TYPE)
		{
		    canRename = Ametys.explorer.ExplorerNodeDAO.canRenameFile(node.get('path'));
		}
		else
		{
		    canRename = Ametys.explorer.ExplorerNodeDAO.canRenameNode(node);
		}
		
		if (canRename)
		{
			this._currentEditNode = node;
		}
		
		return canRename;
	},
	
	/**
	 * Fires after a cell is edited.
	 * @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} 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.
     * @private
	 */
	_onEdit: function (editor, context)
	{
		if (!this._currentEditNode || Ext.String.trim(context.value) == Ext.String.trim(context.originalValue))
		{
			return;
		}
		
		this._valueBeforeEdit = context.originalValue;
		
		if (this._currentEditNode.get('type') == Ametys.explorer.tree.ExplorerTree.RESOURCE_TYPE)
		{
			Ametys.explorer.resources.actions.File.rename(
					this._currentEditNode.getId(),
					context.originalValue,
					context.value,
					Ext.bind(this._renameCb, this)
			);
		}
		else
		{
			var renameFnHolder = Ametys.explorer.ExplorerNodeDAO.getRenameActionHolder();
			if (renameFnHolder && renameFnHolder.rename)
			{
				renameFnHolder.rename.apply(renameFnHolder, [
					this._currentEditNode.getId(),
					context.value,
					this._renameCb,
					this
				]);
			}
		}
	},
	
	/**
	 * @private
	 * Callback function called after renaming 
	 * @param {String} id The id of renamed file
	 * @param {String} name The name of the file
	 * @param {Boolean} success True if the rename action succeed
	 */
	_renameCb: function(id, name, success)
	{
		var node = this._currentEditNode;
		if (!success)
		{
			node.beginEdit();
			node.set('text', this._valueBeforeEdit);
			node.set('name', this._valueBeforeEdit);
			node.endEdit();
			node.commit();
		}
		else
		{
			node.beginEdit();
			node.set('text', name);
			node.set('name', name);
			node.endEdit();
			node.commit();
			
			// We should do this
			// this.getStore().sort();

			// This is a workaround but it has undesired side effects :
			// When you rename a file and then delete it, without having changed the selection, the deletion does not actually happen
			// see ISSUE CMS-6916
//			var sorters = this.getStore().getSorters().getRange();
//		    this.getStore().getSorters().clear();
//			this.getStore().sort(sorters);
			
			// second workaround
			node.parentNode.sort();
		}
		
		this._currentEditNode = null;
		this._valueBeforeEdit = null;
	}
});