/*
 *  Copyright 2016 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 tags
 * @private
 */
Ext.define('Ametys.plugins.cms.tag.TagsTreePanel', {
    extend: 'Ext.tree.Panel', 
    
	scrollable: true,
    animate: true,
    rootVisible: false,
    cls: 'tags-tree',
    
    /**
     * @protected
	 * @property {Ext.Template} _tagTooltipTpl The template used for tags' tooltip
	 */
    _tagTooltipTpl : Ext.create('Ext.XTemplate', [ 
                         '<tpl if="description && description != \'\'">',
                            '{description}<br/>',
                         '</tpl>',
                         '<u>{{i18n PLUGINS_CMS_UITOOL_TAG_TOOLTIP_VISIBILITY}}</u> : <span style="white-space: nowrap">{visibility}</span><br/>',
                         '<tpl if="target && target != \'\'">',
                            '<u>{{i18n PLUGINS_CMS_UITOOL_TAG_TOOLTIP_TARGET}}</u> : <span style="white-space: nowrap">{target}</span><br/>',
                         '</tpl>',
                         '<tpl if="isColorable">',
                           '<tpl if="color && color != \'\'">',
                              '<u>{{i18n PLUGINS_CMS_UITOOL_TAG_TOOLTIP_COLOR}}</u> : <span class="{class}-tooltip-{color}" /><br/>',
                           '<tpl else>',
                              '<u>{{i18n PLUGINS_CMS_UITOOL_TAG_TOOLTIP_COLOR}}</u> : <span class="{class}-tooltip-default" /><br/>',
                           '</tpl>',
                         '</tpl>'
                        ]),
        
    /**
     * @protected
     * @property {String} messageNoTagMatches Message when no tag matches
     */
    messageNoTagMatches : "{{i18n PLUGINS_CMS_TAGS_TREE_FILTER_NO_MATCH}}",
    
    /**
     * @protected
     * @property {String} messageNoTagMatchesAction Message of the action to do when no tag matches
     */
    messageNoTagMatchesAction : "{{i18n PLUGINS_CMS_TAGS_TREE_FILTER_NO_MATCH_ACTION}}",
                        
    /**
	 * @cfg {String[]} [taggableObjects=null] The current object's ids to be tagged, used to check user rights on private tags. Can be null.
	 */
    taggableObjects: null,
    
    /**
     * @cfg {Boolean} checkMode To select tags by checkboxes. Defaults to 'false'.
     */
    checkMode : false,
    /**
     * @cfg {String[]} values List of checkboxes name to check, if checkMode=true.
     */
    values : [],
	/**
	 * @cfg {Boolean} [allowCreation=false] 'true' to allow tag creation. Defaults to 'false'.
	 */
	allowCreation: false,
	/**
	 * @cfg {Boolean} [allowProviders=false] 'true' to allow to select the tags providers. Defaults to 'false'.
	 */
	allowProviders: false,
	/**
	 * @cfg {Boolean} [onlyTagsWithChildren=false] 'true' to allow to select only tags with children. Defaults to 'false'
	 */
	onlyTagsWithChildren: false,
	/**
	 * @cfg {Boolean} [onlyCustomTags=false] 'true' to get only tags from the JCR provider
	 */
	onlyCustomTags: false,
    /**
     * @cfg {String} [targetType] The type of tag to display. Set to target type to display only matching tags. Set to null to display all tags
     */
    targetType: null,    
    /**
     * @cfg {String/String[]} filterTarget Filters on target type. 
     *  Set to 'TARGET_TYPE' to disable tags with the target type. 
     *  Set to 'TARGET_TYPE_PRIVATE' to disable private tags with the target type.
     *  Set to null or empty array to enable all tags.
     */
    filterTarget: null,
    /**
     * @cfg {Object} [contextualParameters] Optional contextual parameters to be passed to the server requests. Will be merged with the current app parameters.
     */
    contextualParameters: null,
    /**
     * @cfg {String} [tagModel] The tag model id
     */
    tagModel: "Ametys.plugins.cms.tag.TagsTreePanel.TagNode",
    /**
     * @cfg {String} [tagsDAO] The tags DAO
     */
    tagsDAO: "org.ametys.cms.tag.TagsDAO",
    /**
     * @cfg {String} [plugin="cms"] The plugin for the store.
     */
    plugin: "cms",
    /**
     * @cfg {String} [url="tags.json"] The url for the store.
     */
    url: 'tags.json',
    /**
     * @cfg {String} [toolbarFilterText="{{i18n PLUGINS_CMS_TAGS_TREE_FILTER}}"] The text inside the filter.
     */
    toolbarFilterText: "{{i18n PLUGINS_CMS_TAGS_TREE_FILTER}}",
    /**
     * @cfg {String} [toolbarFilterInvalidText="{{i18n PLUGINS_CMS_TAGS_TREE_FILTER_INVALID}}"] The message when the filter is invalid.
     */
    toolbarFilterInvalidText: "{{i18n PLUGINS_CMS_TAGS_TREE_FILTER_INVALID}}",
    /**
     * @cfg {String} [toolbarFilterClearText="{{i18n PLUGINS_CMS_TAGS_TREE_CLEAR_FILTER}}"] The tooltip to clear the filter.
     */
    toolbarFilterClearText: "{{i18n PLUGINS_CMS_TAGS_TREE_CLEAR_FILTER}}",
    /**
     * @cfg {String} [toolbarFilterByCheckText="{{i18n PLUGINS_CMS_TAGS_TREE_FILTER_BY_CHECK}}"] The tooltip to display only selected tags.
     */
    toolbarFilterByCheckText: "{{i18n PLUGINS_CMS_TAGS_TREE_FILTER_BY_CHECK}}",
    /**
     * @cfg {String} [toolbarCollapseAllText="{{i18n PLUGINS_CMS_TAGS_TREE_COLLAPSE_ALL}}"] The tooltip to collapse all tags.
     */
    toolbarCollapseAllText: "{{i18n PLUGINS_CMS_TAGS_TREE_COLLAPSE_ALL}}",

    initComponent: function ()
    {
        this.filterTarget = Ext.Array.from(this.filterTarget);
        
        Ext.apply(this, {
            folderSort: false,
            root: {
                hasChild: true,
                editable: false,
                allowDrag: false,
                allowDrop: false,
                expanded: true,
                name: 'tag-root',
                id: 'tag-root',
                type: 'tag-root',
                iconCls : 'ametysicon-tag25'
            },
            store: this.createTreeStore(),
            rootVisible: false
        });
    
        this.on('load', this._onLoad, this);
        this.on('checkchange', this._onCheckChange, this);
        this.on('itemexpand', this._onItemExpand, this);
        this.on('viewready', this._onViewReady, this);
        this.on('itemmouseenter', this._createQtip, this);
        
        this.contextualParameters = Ext.applyIf(this.contextualParameters || {}, Ametys.getAppParameters());
        
        this.callParent();
    },

    constructor: function(config) 
    {
        config.rootVisible = config.rootVisible || false;
        config.toolbarFilterText = config.toolbarFilterText || this.toolbarFilterText;
        config.toolbarFilterInvalidText = config.toolbarFilterInvalidText || this.toolbarFilterInvalidText;
        config.toolbarFilterClearText = config.toolbarFilterClearText || this.toolbarFilterClearText;
        config.toolbarFilterByCheckText = config.toolbarFilterByCheckText || this.toolbarFilterByCheckText;
        config.toolbarCollapseAllText = config.toolbarCollapseAllText || this.toolbarCollapseAllText;
        this._counter = {};
        config.dockedItems = this._getDockedItems(config);
                
        this.callParent(arguments);
    },
    
    /**
     * @protected
     * Get the docked items
     * @param {Object} config The initial tree panel configuration
     * @return {Object[]} The docked items configuration
     */
    _getDockedItems: function (config)
    {
        var dockedItems = config.dockedItems || [];
        
        // No result panel
        var topToolbarCfg = this._getTopToolbarCfg(config);
        if (topToolbarCfg)
        {
            topToolbarCfg.dock = topToolbarCfg.dock || 'top';
            dockedItems.push(topToolbarCfg);
        }
        
        var noResultPanelCfg = this._getNoResultPanelCfg();
        if (noResultPanelCfg)
        {
            noResultPanelCfg.dock = noResultPanelCfg.dock || 'top';
            dockedItems.push(noResultPanelCfg);
        }

        return dockedItems;
    },
    
    /**
     * Set values
     * @param [values] the tag names
     */
    setValues: function (values)
    {
    	this.values = values;
    	
    	this._checkValuesExist();
    	
        var values = (Ext.isArray(this.values) ? this.values : [ this.values ]);
        Ametys.data.ServerComm.callMethod({
            role: this.tagsDAO, 
            methodName: 'getTagPaths', 
            parameters: [ values, Ametys.getAppParameters()],
            errorMessage: "{{i18n PLUGINS_CMS_HANDLE_TAGS_TAG_ERROR}}",
            callback: {
                handler: this._getTagPathsCb,
                ignoreOnError: false,
                scope: this
            },
            waitMessage: {
                target: this,
                msg: "{{i18n plugin.core-ui:PLUGINS_CORE_UI_LOADMASK_DEFAULT_MESSAGE}}"
            }
        });
    },
    
    /**
     * @private
     * Callback function invoked after getting path of selected path.
     * Selects or checks nodes.
     * @param {String[]} paths The paths of selected tags.
     */
    _getTagPathsCb: function(paths)
    {
        var me = this;
        
        Ext.Array.each (paths, function (path) {
            // don't expand last node
            path = path.substr(0, path.lastIndexOf("/"));

            me.expandNodeByPath(path, function (sucessfull, node) {
                if (sucessfull)
                {
                    if (me.multiple) 
                    {
                        node.set('checked', true);
                    }
                    else
                    {
                        me.getSelectionModel().select(node);
                    }
                }
            });
        });
    },

    /**
     * @protected
     * @param {Object} config The initial tree panel configuration
     * Retrieves the 'no result' panel config
     */
    _getNoResultPanelCfg: function(config)
    {
        return {
            dock: 'top',
            xtype: 'button',
            hidden: true,
            itemId: 'noresult',
            ui: 'tool-hintmessage',
            text: this.messageNoTagMatches + this.messageNoTagMatchesAction,
            scope: this,
            handler: this._clearSearchFilter
        };
    },
    
    /**
     * @protected
     * Create the tree store
     * @param {Object} config The tree panel configuration
     * @return {Ext.data.TreeStore} The created tree store
     */
    createTreeStore: function (config)
    {
        var store =  Ext.create('Ext.data.TreeStore', {
            model : this.tagModel,
            proxy: {
                type: 'ametys',
                plugin: this.plugin,
                url: this.url,
                reader: {
                    type: 'json'
                }
            },
            sorters: [{property: 'priority', direction:'DESC'}, {property: 'text', direction:'ASC'}]
        });
        
       store.addListener ('beforeload', this._onStoreBeforeLoad, this);
       return store;
        
    },
    
    /**
     * @private
     * Listener for the view, set an interceptor on check for disabled node
     */
    _onViewReady: function()
    {
        var view = this.getView();
        
        // onCheckChange interceptor. When this function return false, the onCheckChange method of the view is not called, hence the value of the checkbox does not change.
        var onCheckChangeInterceptor = function(event)
        {
            if (event.record.get('disabled') || event.record.get('authorized') === false)
            {
                return false;
            }
        };
        
        Ext.apply(view, {
            onCheckChange: Ext.Function.createInterceptor(view.onCheckChange, onCheckChangeInterceptor)
        });
    },
    
    /**
     * @private
     * Set the request parameters before loading the store.
     * @param {Ext.data.Store} store The store.
     * @param {Ext.data.operation.Operation} operation The Ext.data.operation.Operation object that will be passed to the Proxy to load the Store.
     */
    _onStoreBeforeLoad: function (store, operation)
    {
        var params = operation.getParams() || {};
        
        Ext.apply(params, {
            nodeName: (operation.node !== undefined ? operation.node.get('name') : ""),
            checkMode: this.checkMode,
            onlyCustomTags: this.onlyCustomTags,
            objectTargetIds: this.taggableObjects,
            targetType: this.targetType,
            contextualParameters: this.contextualParameters
        });
        
        operation.setParams(params);
    },
    
    /**
     * @private
     * Function after tree loading
     * @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 The operation that triggered this load.
     * @param {Ext.data.NodeInterface} node The loaded node
     */
    _onLoad: function (store, records, successful, operation, node)
    {
    	this._setInfos(node);
    	
        if (this.checkMode === true)
        {
            this._setCheckboxes(node);
        }
        
        this._checkValuesExist();
    }, 

    /**
     * @private
     * On node expand, filter nodes based on the "checked only" filter
     * @param {Ext.data.NodeInterface} node the node expanded
     */
    _onItemExpand: function(node)
    {
        if (this._checkFilter === true)
        {
            var childs = node.childNodes;
            for (var i in childs)
            {
                if (Ext.Array.contains(this.values, childs[i].get('name')) === false)
                {
                    var view = this.getView(); 
                    var uiNode = view ? view.getNodeByRecord(childs[i]) : null;
                    if (uiNode)
                    {
                        Ext.get(uiNode).setDisplayed('none');
                    }
                }
            }
        }
    },
    
    /**
     * @protected
     * Retrieves the top toolbar config object.
     * @param {Object} config The initial tree panel configuration
     * @return {Boolean} config.checkMode if true, display the filter button "checked only"
     */
    _getTopToolbarCfg: function(config)
    {
        return {
        	dock: 'top',
			xtype: 'toolbar',
            layout: { 
                type: 'hbox',
                align: 'stretch'
            },
			border: false,
			defaultType: 'button',
            items: [{
                        // Search input
		            	xtype: 'textfield',
						cls: 'ametys',
						flex: 1,
						minWidth: 170,
		                maxWidth: 300,
                        itemId: 'search-filter-input',
                        emptyText: config.toolbarFilterText,
                        minLength: 3,
                        minLengthText: config.toolbarFilterInvalidText,
                        msgTarget: 'qtip',
                        listeners: {change: Ext.Function.createBuffered(this._searchFilter, 500, this)},
						style: {
                            marginRight: '0px'
                        }
                    }, 
                    {
                        // Clear filter
                        tooltip: config.toolbarFilterClearText,
                        handler: Ext.bind (this._clearSearchFilter, this),
                        iconCls: 'a-btn-glyph ametysicon-eraser11 size-16',
						cls: 'a-btn-light'
                    }, 
                    '->',
                    {
                        tooltip: config.toolbarFilterByCheckText,
                        enableToggle: true,
                        toggleHandler: Ext.bind(this._filterByCheckedNodes, this),
                        hidden: !config.checkMode,
                        iconCls: 'a-btn-glyph ametysicon-check51 size-16',
						cls: 'a-btn-light'
                    },
                    {
                        // Collapse all
                        tooltip: config.toolbarCollapseAllText,
                        handler: Ext.bind (this._collapseAll, this),
                        iconCls: 'a-btn-glyph ametysicon-minus-sign4 size-16',
						cls: 'a-btn-light'
                    }
            ]
        };
    },
    
    /**
     * 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 = new String(field.getValue()).trim();
        this._filterField = field;
        
        if (this._filterValue == value)
        {
            // Do nothing
            return;
        }
        
        this._filterValue = value;
        
        if (this._checkFilter)
        {
            this._filterByCheckedNodes(null, true);
        }
        else if (value.length > 2)
        {   
            this._getFilteredTags (value);
        }
        else
        {
            this.clearFilter(true);
        }
    },
    
    /**
     * Get the tags the name matches the given value
     * @param {String} value The value to match
     * @private
     */
    _getFilteredTags: function (value)
    {
        Ametys.data.ServerComm.callMethod({
            role: this.tagsDAO, 
            methodName: 'filterTagsByRegExp', 
            parameters: [value, Ametys.getAppParameters()],
            errorMessage: "{{i18n PLUGINS_CMS_TAGS_TREE_SEARCH_ERROR}}",
            callback: {
                handler: this._filterTagsCb,
                scope: this
            }
        });
    },
    
    /**
     * @private
     * Callback function after searching terms
     * @param {String[]} paths The result paths
     * @param {Object[]} args The callback arguments
     */
    _filterTagsCb: function(paths, args) 
    {
        var hasResult = false;
        var rootNode = this.getRootNode();
        
        if (!paths)
        {
            return;
        }
        
        if (paths.length == 0)
        {
            this.filterBy (function () {return false});
        }
        else
        {
            hasResult = true;
            var childs = rootNode.childNodes;
            this._expandAndFilter (paths, rootNode);
        }
        
        if (!hasResult)
        {
            this._showNoResultPanel();
            if (this._filterField)
            {
                Ext.defer (this._filterField.markInvalid, 100, this._filterField, [this.messageNoTagMatches]);
            }
        }
        else
        {
            this._hideNoResultPanel();
            if (this._filterField)
            {
                this._filterField.clearInvalid();
            }
        }
    },
    
    
    /**
     * 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();
        }
    },
    
    /**
     * Clear the filter search
     * @param {Ext.Button} btn The button
     * @private
     */
    _clearSearchFilter: function(btn)
    {
        this.clearFilter();
        
        var topItems = this.getDockedItems('toolbar[dock="top"]');
        topItems[topItems.length - 1].down('#search-filter-input').reset();
        
        var selection = this.getSelectionModel().getSelection()[0];
		if (selection)
		{
			this.ensureVisible(selection.getPath('name'), {field: 'name'});
		}
    },

    /**
     * Show the tree nodes without considering the filter value
     * @param {Boolean} keepCheckFilter keep the "filter by checked node" toogled or not
     */
    clearFilter: function(keepCheckFilter)
    {
        this.getStore().clearFilter();
        
        this._hideNoResultPanel();
        
        if (!keepCheckFilter)
        {
            this._checkFilter = false;
            var topItems = this.getDockedItems('toolbar[dock="top"]');
            var checkboxFilter = topItems.length > 0 ? topItems[topItems.length - 1].child("component[cls~=checkFilter]") : null;
            if (checkboxFilter)
            {
                checkboxFilter.toggle(false);
            }
        }
    },
    
    /**
     * Expand the tree to the given paths. Then filter nodes matching the given paths by calling the #_filterPaths method
     * @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 (rootNode.getPath('name') + '/' + path, 'name', null, 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('name');
        for (var i=0; i < paths.length; i++)
        {
            var path = rootNode.getPath('name') + '/' + paths[i] + '/';
            if (path.indexOf(currentPath + '/') == 0)
            {
                return true;
            }
        }
        return 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().filterBy(filterFn);
    },

    /**
     * Triggered by the button "filter by checked only". Call the server to get the tags which matches the filter
     * @param {Ext.button.Button} button The button which triggered this method. Parameter not used.
     * @param {Boolean} state If true, filter by "checked only" 
     * @private
     */
    _filterByCheckedNodes: function (button, state)
    {
        this._checkFilter = state;
        var filterValue = "";
        if (this._filterField !== undefined)
        {
            filterValue = new String(this._filterField.getValue()).trim();
        }
        if (filterValue && filterValue.length < 3)
        {   
            filterValue = "";
        }
        
        if (filterValue === "" && state === false)
        {
            this.clearFilter();
        }
        else if (state === false)
        {    
            Ametys.data.ServerComm.callMethod({
                role: this.tagsDAO, 
                methodName: 'filterTagsByRegExp', 
                parameters: [filterValue, Ametys.getAppParameters()],
                errorMessage: "{{i18n PLUGINS_CMS_TAGS_TREE_SEARCH_ERROR}}",
                callback: {
                    handler: this._filterTagsCb,
                    scope: this
                },
                waitMessage: {
                    target: this,
                    msg: "{{i18n plugin.core-ui:PLUGINS_CORE_UI_LOADMASK_DEFAULT_MESSAGE}}"
                }
            });
        }
        else
        {
            Ametys.data.ServerComm.callMethod({
                role: this.tagsDAO, 
                methodName: 'filterTagsFromListByRegExp', 
                parameters: [filterValue, this.values, Ametys.getAppParameters()],
                errorMessage: "{{i18n PLUGINS_CMS_TAGS_TREE_SEARCH_ERROR}}",
                callback: {
                    handler: this._filterTagsCb,
                    scope: this
                },
                waitMessage: {
                    target: this,
                    msg: "{{i18n plugin.core-ui:PLUGINS_CORE_UI_LOADMASK_DEFAULT_MESSAGE}}"
                }
            });
            
        }
    },
    
    /**
	 * Collapse the whole tree
	 * @private
	 */
	_collapseAll: function ()
	{
		this.getRootNode().collapseChildren(true);
	},
    
    /**
     * Load the root node
     */
    onRender: function(ct, position)
    {
        this.callParent(arguments);
    	this.getRootNode().expand();
    },
    
    /**
     * Iterate through the node childs, and call #_setInfo
     * @param {Ext.data.Model} node The node to iterate
     * @private
     */
    _setInfos: function (node)
    {
        var childNodes = node.childNodes;
        for (var i=0; i < childNodes.length; i++)
        {
            this._setInfo (childNodes[i]);
            this._setInfos (childNodes[i]);
        }
    },

    /**
     * Initialize the node, by setting the tooltip and autoexpand the providers
     * @param {Ext.data.Model} node The node to initialize
     * @private
     */
    _setInfo: function (node)
    {
        // auto expand providers
        if (node.get('type') == 'provider')
        {
            this.expandNode(node);
        }

        if (this.filterTarget != null && this._isFilteredTarget(node))
        {
            node.set("disabled", true);
        }

        if (node.get("disabled") == true)
        {
            var cls = node.get('cls');
            cls += (cls ? ' ' : '') + 'disabled';
            node.set('cls', cls);
        }
    },
    
    /**
     * @private
     * 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)
    {
		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.
	 */
	_getTooltip: function(node)
	{
		var text = this._tagTooltipTpl.applyTemplate ({
			description: (Ext.String.escapeHtml(node.get('description')) || "").replace(/\n/g, "<br/>"), 
			visibility : (node.get('visibility') == 'PRIVATE' ? "{{i18n PLUGINS_CMS_UITOOL_TAG_TOOLTIP_VISIBILITY_PRIVATE}}" : "{{i18n PLUGINS_CMS_UITOOL_TAG_TOOLTIP_VISIBILITY_PUBLIC}}"),
            target: node.get('target') && Ext.isObject(node.get('target')) ? node.get('target').label || node.get('target').name : null,
		    color: node.get('color'),
		    class: node.get('class'),
		    isColorable: node.get('isColorable')
        });
		
		return {
			title: Ext.String.escapeHtml(node.get('title')) + " (" + node.get('name') + ")",
			glyphIcon: node.get('tooltipIconCls'),
			imageWidth: 48,
			imageHeight: 48,
			text: text,
			inribbon: false
		};
	},

    /**
     * @private
     * Check if a node match the specified filters for the target type
     * @param {Ext.data.Model} node The node to initialize
     * @return {Boolean} returns true if the filter is matched
     */
    _isFilteredTarget: function(node)
    {
        for (var i = 0; i < this.filterTarget.length; i++) 
        {
            var target = Ext.isObject(node.get('target')) ? node.get("target").name : node.get("target");
            var filter = this.filterTarget[i];

            if (target == filter || (node.get('visibility') == "PRIVATE" && target + "_PRIVATE" == filter))
            {
                return true;
            }
        }

        return false;
    },
    
    /**
     * Iterate through the children nodes, and call #_setCheckbox
     * @param {Ext.data.Model} node The node to iterate
     * @private
     */
    _setCheckboxes: function (node)
    {
        var childNodes = node.childNodes;
        for (var i=0; i < childNodes.length; i++)
        {
            this._setCheckbox (childNodes[i]);
            this._setCheckboxes (childNodes[i]);
        }
    },

    /**
     * Put a checkbox on nodes that can be checked, with the corresponding state. Does not verify any rights. 
     * @param {Ext.data.Model} node The node 
     * @private
     */
    _setCheckbox: function (node)
    {
        if ((this.allowProviders === true || node.get('type') !== 'provider') &&
                (this.onlyTagsWithChildren === false || node.get('leaf') === false))
        {
            node.set('checked', Ext.Array.contains(this.values, node.get('name')));
        }
        else
        {
            node.set('checked', null);
        }
    },

    /**
     * Verify if a node can be checked. If it can, his value is updated
     * @param {Ext.data.Model} node The node which state was updated
     * @param {Boolean} checked The new node state
     * @private
     */
    _onCheckChange: function(node, checked) 
    {
        if (node.get('disabled') === true)
        {
            node.set('checked', !checked);
            return ;
        }
        if (checked === true && Ext.Array.contains(this.values, node.get('name')) === false)
        {
            this.values.push(node.get('name'));
        }
        else if (checked === false && Ext.Array.contains(this.values, node.get('name')) === true)
        {
            Ext.Array.remove(this.values, node.get('name'));
            if (this._checkFilter === true)
            {
                this._filterByCheckedNodes(null, true);
            }
        }
    },
    
    /**
     * Expand the tree to the given path. 
     * @param {String[]} path The path to expand
     * @param {Function} callback The callback function to call after expand. Can be null.
     */
    expandNodeByPath: function(path, callback) 
    {
        var rootNode = this.getRootNode();
        
        this.expandPath (rootNode.getPath('name') + '/' + path, 'name', null, callback);
    },
    
    /**
     * @private
     * Ensure that the values exists
     */
    _checkValuesExist: function()
    {
        Ametys.data.ServerComm.callMethod({
            role: this.tagsDAO, 
            methodName: 'checkTags', 
            parameters: [this.values, this.onlyCustomTags, {target: this.targetType}, this.contextualParameters],
            callback: {
                handler: this._checkValuesExistCb,
                scope: this
            }
        });
    },
    
    /**
     * @private
     * Callback of #_checkValuesExist
     * Updates the list of values
     */
    _checkValuesExistCb: function (result)
    {
        if (result)
        {
            this.values = result;
        }
    }
});