/*
 *  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 contents.
 */
Ext.define('Ametys.plugins.contentstree.ContentsTreePanel', {
	extend: 'Ext.tree.Panel',
	
	/**
	 * @cfg {String} treeId (required) The id of tree configuration
	 */
	
    /**
     * @cfg {String} [serverRole="org.ametys.plugins.contentstree.ContentsTreeHelper"] The role of server component which handle tree
     */

    /**
     * @cfg {String} [methodArguments=['contentId', 'tree']] The arguments name when calling the server
     */

    /**
     * @cfg {String} [filterMethodName="filterChildrenContentByRegExp"] The server method name of filter tree
     */
    
	/**
	 * @cfg {String} [model=Ametys.plugins.contentstree.ContentsTreePanel.ContentsTreeModel] The class name of the model for the store. Default to {@link Ametys.plugins.contentstree.ContentsTreePanel.ContentsTreeModel}
	 */
	
    /**
     * @cfg {Boolean} [displayToolbar=true] Set to true to display the toolbar for some help tools.
     */

    /**
     * @cfg {Number} [toolbarPosition=0] Set to toolbar position into docked items. Default to 0.
     */

	/**
     * @cfg {Boolean} [checkMode=false] Set to true to select contents by check boxes.
     */

	/**
     * @cfg {Boolean} [checkRoot=true] If {@link #cfg-checkMode} is true, set this to false if you do not want a checkbox for the root node.
     */
	
	constructor: function(config)
	{
		config.checkMode = config.checkMode || false;
		if (!config.model && config.checkMode)
		{
			config.model = config.checkRoot == false ? 'Ametys.plugins.contentstree.ContentsTreePanel.CheckableExceptRootContentsTreeModel' : 'Ametys.plugins.contentstree.ContentsTreePanel.CheckableContentsTreeModel';
		}
        else if (!config.model)
        {
			config.model = 'Ametys.plugins.contentstree.ContentsTreePanel.ContentsTreeModel';
        }
        
        config.serverRole = config.serverRole || 'org.ametys.plugins.contentstree.ContentsTreeHelper';
        config.methodArguments = config.methodArguments || ['contentId', 'path', 'tree'];
        config.filterMethodName = config.filterMethodName || 'filterChildrenContentByRegExp';
		
		config.store = Ext.create('Ext.data.TreeStore', {
            model : config.model,
            autoLoad : false,
            
            proxy : {
                type : 'ametys',
				
                role: config.serverRole,
                methodName: 'getChildrenContent',
                methodArguments: config.methodArguments,
                reader : {
                    type : 'json'
                },
                extraParams: {
                	tree: config.treeId
                }
            },
            
            listeners: {
            	'beforeload': function(store, operation)
            	{
            		if (operation.node.get('contentId'))
            		{
            			operation.setParams(Ext.apply(operation.getParams() || {}, {
            				contentId: operation.node.get('contentId'),
                            path: operation.node.getPath('contentId', ';').substring(1).split(';')
            			}));
            		}
            		else 
            		{
            			// FIXME Sometimes the contentId is not set and it result to an exception			
            			try
            			{
            				throw new Error("This should not happen");
            			}
            			catch(e)
            			{
            				console.error(e);
            			}
            			return false;
            		}
            	}
            }
        });
        
        config.displayField = config.displayField || 'title';
        
        this._counter = {};
        
        config.dockedItems = config.dockedItems || [];
        
        config.displayToolbar = config.displayToolbar !== false;
        if (config.displayToolbar)
        {
            // Get toolbar bar configuration
	        var toolbarCfg = this._getToolBarConfig(config);
	        if (!Ext.isEmpty(toolbarCfg))
	        {
	            Ext.Array.insert(config.dockedItems, config.toolbarPosition || 0, [toolbarCfg]);
	            config.dockedItems.push(this._getNoResultPanel());
	        }
        }
        
        this.callParent(arguments);
	},
    
    /**
     * @protected
     * Get configuration for toolbar
     * @param {Object} the tree configuration object
     * @return {Object} the toolbar configuration object
     */
    _getToolBarConfig: function (config)
    {
        return {
            dock: 'top',
            xtype: 'toolbar',
            layout: { 
                type: 'hbox',
                align: 'stretch'
            },
            defaultType: 'button',
            items: [{
                        // Filter input
                        xtype: 'textfield',
                        cls: 'ametys',
                        flex: 1,
                        maxWidth: 300,
                        itemId: 'search-filter-input',
                        emptyText: "{{i18n PLUGINS_CONTENTSTREE_TREE_FILTER}}",
                        minLength: 3,
                        minLengthText: "{{i18n PLUGINS_CONTENTSTREE_TREE_FILTER_INVALID}}",
                        msgTarget: 'qtip',
                        listeners: {change: Ext.Function.createBuffered(this._searchFilter, 500, this)},
                        style: {
                            marginRight: '0px'
                        }
                    }, 
                    {
                        // Clear filter
                        tooltip: "{{i18n PLUGINS_CONTENTSTREE_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
                    },
                    {
                        // Collapse all
                        tooltip: "{{i18n PLUGINS_CONTENTSTREE_TREE_COLLAPSE_ALL}}",
                        handler: Ext.bind (this.collapseNode, this, [], false),
                        iconCls: 'a-btn-glyph ametysicon-minus-sign4 size-16',
                        cls: 'a-btn-light'
                    }, 
                    {
                        // Refresh node
                        tooltip: "{{i18n PLUGINS_CONTENTSTREE_TREE_REFRESH_NODE}}",
                        handler: Ext.bind (this.refreshNode, this, [], false),
                        iconCls: 'a-btn-glyph ametysicon-arrow123 size-16',
                        cls: 'a-btn-light'
                    }
            ]
        };
    },
    
    /**
     * @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_CONTENTSTREE_TREE_FILTER_NO_MATCH}}" + "{{i18n PLUGINS_CONTENTSTREE_TREE_FILTER_NO_MATCH_ACTION}}",
            scope: this,
            handler: this.clearSearchFilter
        };
    },
    
    /**
     * @private
     * Show or hide the 'no result' button.
     * @param {Boolean} show true to show the button, false to hide it.
     */
    _showHideNoResultPanel: function (show)
    {
        if (this.down("button[itemId='no-result']"))
        {
            this.down("button[itemId='no-result']").setVisible(show);
        }
    },
    
    /**
     * Updates the node UI (icon, text, ...)
     * @param {Ext.data.NodeInterface} node the node
     */
    updateNodeUI: function(node)
    {
        // Nothing to do
    },
    
    /**
     * 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 (value.length > 2)
        {   
            var rootNode = this.getRootNode();
            this._getFilteredContents (value, 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
     */
    _getFilteredContents: function (value, node, childNodesOnly)
    {
        Ametys.data.ServerComm.callMethod({
            role: this.getInitialConfig('serverRole'), 
            methodName: this.getInitialConfig('filterMethodName'), 
            parameters: [node.get('contentId'), this.getInitialConfig('treeId'), value],
            errorMessage: "{{i18n PLUGINS_CONTENTSTREE_TREE_SEARCH_ERROR}}",
            callback: {
                handler: this._filterContentsCb,
                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
     */
    _filterContentsCb: function(paths, args) 
    {
        var hasResult = false;
        var node = args.node || this.getRootNode();
        
        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 childs = node.childNodes;
            this._expandAndFilter (paths, this.getRootNode(), node);
        }
        
        if (!hasResult)
        {
            this._showHideNoResultPanel(true);
            if (this._filterField)
            {
                Ext.defer (this._filterField.markInvalid, 100, this._filterField, ["{{i18n PLUGINS_CONTENTSTREE_TREE_FILTER_NO_MATCH}}"]);
            }
        }
        else
        {
            this._showHideNoResultPanel(false);
            if (this._filterField)
            {
                this._filterField.clearInvalid();
            }
        }
    },
    
    /**
     * 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.get('contentId')] = paths.length;
        for (var i=0; i < paths.length; i++)
        {
            var path = paths[i].substr(0, paths[i].lastIndexOf(";"));
            this.expandPath (';' + path, 'contentId', ';', 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.get('contentId')] == 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('contentId', ';');
        for (var i=0; i < paths.length; i++)
        {
            var path = ';' + 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);
    },
    
    /**
     * Clear the filter search if exists
     */
    clearSearchFilter: function()
    {
        this.clearFilter();
        
        if (this._filterField)
        {
            this._filterField.reset();
        }
        
        this._filterValue = null;
        this._showHideNoResultPanel(false);
        
        var selection = this.getSelectionModel().getSelection()[0];
        if (selection)
        {
            this.ensureVisible(selection.getPath('name'), {field: 'name'});
        }
    },
    
    /**
     * Clear all filters
     */
    clearFilter: function (node)
    {
        this.getStore().clearFilter();
        
        var selection = this.getSelectionModel().getSelection()[0];
        if (selection)
        {
            this.ensureVisible(selection.getPath());
        }
    },
    
    /**
     * Collapse recursively the children of the node, then select the collapsed node.
     * @param {Ext.data.NodeInterface} [node] The node the collapse. Can be null to collapse the whole tree.
     */
    collapseNode: function (node)
    {
        node = node || this.getRootNode();
        node.collapseChildren(true);
        this.getSelectionModel().select(node);
    },
    
    /**
     * This function reload the given node
     */
    refreshNode: function ()
    {
        var selection = this.getSelectionModel().getSelection();
        node = selection.length > 0 ? selection[0] : null;
        
        // Workaround - Refresh selection in case node is not existing anymore (deleted by another user for example).
        this.getSelectionModel().deselect(node, true);
        this.getSelectionModel().select(node);
            
        if (node != null)
        {
            let me = this;
            this._reloadNodes([node], function() {
                if (me._filterValue && me._filterValue.length > 2)
                {
                    // Filter child nodes only
                    me._getFilteredContents (me._filterValue, node, true);
                }
                else
                {
                    Ext.defer(me.expandNode, 200, me, [node]);
                }
            });
        }
    },
	
    /**
     * Compoute the path of the node in the tree
     * @param {Ext.data.NodeInterface} node The node to compute
     */
	getNodePath: function(node)
	{
		return node.getPath('contentId', ';').substring(1).split(';')
	},
    
    /**
     * @private
     * Reload nodes, one by one
     * @param {Ext.data.NodeInterface[]} nodes The nodes to be refreshed
     * @param {Function} callback At the end
     */
    _reloadNodes: function(nodes, callback)
    {
        if (nodes.length > 0)
        {
            var me = this,
                node = nodes[0];
            
            // We can only load one node at once
            Ametys.data.ServerComm.callMethod({
                role: this.getInitialConfig('serverRole'), 
                methodName: 'getNodeInformations', 
                parameters: this._getNodeInformationsParameters(node),
                errorMessage: false, // ignore error
                callback: {
                    handler: function(data) {
                        // Update node data
                        let newData = {};
                        for (var i in data)
                        {
                            if (i == "data")
                            {
                                for (var j in data[i])
                                {
                                    newData[j] = data[i][j];
                                }
                            }
                            else
                            {
                                newData[i] = data[i];
                            }
                        }
                        newData.leaf = false;
                        if (newData.id.startsWith('random-id-'))
                        {
                            delete newData.id // Changing non-worthy id will lead to an internal modification that would sometimes display the saveBar during a few instants  
                        }
                        
                        node.set(newData, {commit: true});
                        
                        me.getStore().load({
                            node: node,
                            callback: function() {
                                me.updateNodeUI(node);
                                me._reloadNodes(Ext.Array.slice(nodes, 1), callback);
                            }
                        });
                    }
                }
            });
        }
        else if (callback)
        {
            callback();
        }
    },
	
	/**
	 * Retrieves from the server information about the content root and set it as root of the tree.
	 * @param {String} contentId The id of the new content root node
	 * @param {Object} otherConfig An additional config object to set to the root.
	 * @param {Function} callback The callback function to call after the root node is set
	 */
	setContentRootNode: function(contentId, otherConfig, callback)
	{
        let parameters = this.getContentRootNodeParameters(contentId, otherConfig);
		Ametys.data.ServerComm.callMethod({
			role: this.getInitialConfig('serverRole'),
			methodName: "getRootNodeInformations",
			parameters: parameters,
			callback: {
				handler: this._setContentRootNodeCb,
				scope: this,
				arguments: {
					configToSet: otherConfig || {},
					callback: callback
				}
			},
            errorMessage: true,
			waitMessage: false
		});
	},
	
	/**
     * @protected
     * Works in setContentRootNode server call
     * @param {String} contentId The id of the new content root node
     * @param {Object} otherConfig An additional config object to set to the root.
     */
	getContentRootNodeParameters: function(contentId, otherConfig)
	{
        return [
            contentId,
            this.treeId
        ];
    },
    
    /**
     * @protected
     * Get the parameters to the server call to get node informations
     * @param {Model} node The node
     * @return {Object[]} The parameters to this._serverRole#getNodeInformations
     */
    _getNodeInformationsParameters(node)
    {
        return [node.get('contentId'), this.getNodePath(node)];
    },    
	
	/**
	 * @private
	 * The callback function of #setContentRootNode
	 * @param {Object} response The root node information retrieved by the server
	 * @param {Object} arguments The callback arguments
	 * @param {Object} arguments.configToSet A additional config object to set to the root
	 * @param {Function} arguments.callback The callback function to call after the root node is set
	 */
	_setContentRootNodeCb: function(response, arguments)
	{
		var newRootCfg = Ext.apply(arguments.configToSet, response);
        if (!this.getStore())
        {
            return;
        }
        
        // Rootnode data should be extracted as any other node
        var reader = this.getStore().getProxy().getReader();
        var model = this.getStore().getModel();
        var fieldExtractorInfo = reader.getFieldExtractorInfo(model);
        var modelData = reader.extractModelData(newRootCfg, fieldExtractorInfo);

		this.setRootNode(modelData);
		this.getStore().removedNodes = []; // Bug fix for this that is not reinit correctly when changing root node and leads to bug when rejectingChanges
		
		this._onRootNodeLoad(this.getStore().root);
		
		if (Ext.isFunction(arguments.callback))
		{
			arguments.callback.call();
		}
	},
	
	/**
     * @protected
     * Called when loading the root node
     * @param {Ext.data.Model} record The root node
     */
	_onRootNodeLoad: function(record)
	{
        // Nothing
    }
	
});