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

/**
 * Explorer tool.
 * Tool mainly composed of a tree used to allow the
 * user to browse the explorer and its resources.
 * @private
 */
Ext.define('Ametys.plugins.explorer.tool.ExplorerTool', {
	extend: 'Ametys.tool.Tool',
	
	/**
	 * @property {Ametys.explorer.tree.ExplorerTree} _tree The explorer tree
	 * @private
	 */
	
	/**
	 * @cfg {Boolean/String} [tree-ignore-files=false] True to ignore the files in the tree (ie. leaf nodes). Defaults to false.
	 */
	/**
	 * @property {Boolean} _treeIgnoreFiles See #cfg-tree-ignore-files
	 * @private
	 */
	
	/**
	 * @cfg {String} rootnodes-callmethod-role (required)
	 * The role of the component to be used for the call method in order to retrieve the root nodes of the tree.
	 */
	/**
	 * @cfg {String} rootnodes-callmethod-name (required)
	 * The name of the method to be used for the call method in order to retrieve the root nodes of the tree.
	 */
	/**
	 * @cfg {String} rootnodes-callmethod-parameters 
	 * The optional JSON parameters to be used during the call method for the root nodes retrieval. 
	 */
	
	constructor: function(config)
	{
		this.callParent(arguments);
		
		// Listening to some bus messages.
		Ametys.message.MessageBus.on(Ametys.message.Message.CREATED, this._onResourceCreated, this);
		Ametys.message.MessageBus.on(Ametys.message.Message.DELETED, this._onResourceDeleted, this);
		Ametys.message.MessageBus.on(Ametys.message.Message.MOVED, this._onResourceMoved, this);
		Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onResourceModified, this);
		Ametys.message.MessageBus.on(Ametys.message.Message.SELECTION_CHANGED, this._onSelectionChanged, this);
		
		this._treeIgnoreFiles = String(config['tree-ignore-files']) == 'true';
		
		// set root nodes params
		var jsonCallMethodParams = config['rootnodes-callmethod-parameters'],
			jsonCallMethodParams = jsonCallMethodParams ? [Ext.JSON.decode(jsonCallMethodParams)] : null;
		
		this._rootNodesCallMethodParams = {
			role: config['rootnodes-callmethod-role'],
			name: config['rootnodes-callmethod-name'],
			parameters: jsonCallMethodParams
		};
	},
	
	getMBSelectionInteraction: function() 
	{
		return Ametys.tool.Tool.MB_TYPE_ACTIVE;
	},
	
	createPanel: function()
	{
		this._tree = this._createTree();
		
		this._tree.on('selectionchange', this._onSelectNode, this);
		this._tree.on('itemmove', this._onItemMove, this);
		this._tree.on('rootnodeschanged', this._onRootNodesChanged, this);
		
		if (this._treeIgnoreFiles)
		{
			this._tree.on('rowdblclick', this._onRowDblClick, this);
		}
		this._tree.on('beforerender', this._onBeforeRender, this);
		
		return this._tree;
	},
	
	sendCurrentSelection: function()
	{
		Ext.create("Ametys.message.Message", {
			type: Ametys.message.Message.SELECTION_CHANGED,
			targets: this._tree.getMessageTargetConfiguration(this._currentNode)
		});
	},
	
	/**
	 * @private
	 * Set the root nodes for the explorer tree
	 */
	_onBeforeRender: function()
	{
        this.showRefreshing()
		Ametys.explorer.ExplorerNodeDAO.getRootNodesInfo(
			this._setRootNodesCb,
			this,
			this._rootNodesCallMethodParams.role,
			this._rootNodesCallMethodParams.name,
			this._rootNodesCallMethodParams.parameters
		);
	},
	
	/**
	 * @private
	 * Callback of #_setRootNodes
	 * @param {Object[]} response the server response
	 */
	_setRootNodesCb: function(response)
	{
        if (response)
        {
            this._tree.setRootNodes(response);
        }
        this.showRefreshed();
	},
	
	/**
	 * @protected
	 * Create the explorer tree
	 * @return {Ametys.explorer.tree.ExplorerTree} The created explorer tree
	 */
	_createTree: function()
	{
		return Ext.create('Ametys.explorer.tree.ExplorerTree', {
			inlineEditionEnable: true,
			border: false,
			
			ignoreFiles: this._treeIgnoreFiles
		});
	},
	
	/**
	 * Get the explorer tree
	 * @return {Ametys.explorer.tree.ExplorerTree} The tree
	 */
	getTree: function ()
	{
		return this._tree;
	},
	
	refresh: function(manual)
	{
		this._tree.refreshRootNodes(this.showUpToDate, this);
	},
		
	/**
	 * On row double click listener. Opens a tool displaying the contents of the clicked folder.
	 * @param {Ext.tree.View} view the tree view
	 * @param {Ametys.explorer.tree.ExplorerTree.NodeEntry} node the tree node
	 * @param {HTMLElement} tr The TR element for the cell.
	 * @param {Number} index The cell index
	 * @param {Ext.event.Event} evt The event object
	 * @private
	 */
	_onRowDblClick: function(view, node, tr, index, evt)
	{
		if (node)
		{
            Ametys.tool.ToolsManager.openTool("uitool-explorer-resource-collection", 
                    {
                        id: node.getId(),
                        applicationId: node.get('applicationId')
                    });
		}
	},
	
	/**
	 * Expand the tree and select a node by its id
	 * @param {String} id The node identifier
	 */
	selectNodeById: function (id)
	{
		var node = this._tree.getStore().getNodeById(id);
		this.selectNode(node);
	},
	
	/**
	 * Expand the tree and select a node
	 * @param {Ext.data.NodeInterface} node The node
	 */
	selectNode: function(node)
	{
		if (node)
		{
			this._tree.selectPath(node.getPath('name'), 'name');
		}
	},
		
	/**
	 * Expand the tree and select a node by its path
	 * @param {String} path The path of the node
	 */
	selectNodeByPath: function (path)
	{
		this.getTree().selectByPath(path);
	},
	
	/**
	 * @private
	 * Listener when root nodes are loaded in the tree
	 * @param {Ametys.explorer.tree.ExplorerTree} tree The explorer tree
	 */
	_onRootNodesChanged: function(tree)
	{
		// Expand first nodes
		this._tree.getRootNode().expandChildren(false, this._onRootNodesChangedAndExpanded, this);
	},
	
	/**
	 * @private
	 * When the newly loaded root nodes are expanded
	 */
	_onRootNodesChangedAndExpanded: function()
	{
		// Select first node
		this._tree.getSelectionModel().select(this._tree.getRootNode().firstChild);
	},
		
	/**
	 * This function is called when a node is selected.
	 * @param {Ext.selection.Model} sm The selection model
	 * @param {Ext.data.NodeInterface[]} selected The selected nodes
	 * @private
	 */
	_onSelectNode: function (sm, selected)
	{
		var node = selected[0];
		
		if (selected.length == 0 || !node)
		{
			if (this._currentNode != null)
			{
				this._currentNode = null;
				this.sendCurrentSelection();
			}
		}
		else
		{
			if (!this._currentNode || this._currentNode.getId() != node.getId())
			{
				this._currentNode = node;
				this.sendCurrentSelection();
			}
		}
	},
		
	/**
	 * Reload a node
	 * @param {String} id The id of the node to reload
	 * @param {Function} callback The call back function. Can be null.
	 * @param {Array} args The arguments to the callback function
	 * @private
	 */
	_refreshNode: function(id, callback, args)
	{
		this._tree.refreshNode(id, callback, args);
		this._tree.getStore().sort();
	},
		
	/**
	 * @private
	 * Call this function to rename a node
	 * @param {Ext.data.Model} parentNode The parent node
	 * @param {String} id The id of the node to rename
	 */
	_renameNode: function (parentNode, id)
	{
		if (parentNode != null && id != null)
		{
			var node = this.getTree().getStore().getNodeById(id)
			this.getTree().selectNode(node);
			
			Ext.defer (this._deferredEdit, 300, this, [node]);
		}
	},
		
	/**
	 * @private
	 * Deferred function to edit a node
	 * @param {Ext.data.Model} node The node to start edit
	 */
	_deferredEdit: function (node)
	{
		this._tree.editingPlugin.startEdit(node, 0);
	},
	
	/**
	 * Deletes a node from the resources tree and selects the parent node.
	 * @param {String} id The id of the node to delete.
	 * @private
	 */
	_deleteNode: function(id)
	{
		var node = this._tree.getStore().getNodeById(id);
		if (node != null)
		{
			var parentNode = node.parentNode;
			this._tree.getSelectionModel().select([parentNode]);
			node.remove();
		}
	},
	
	/**
	 * Update node UI
	 * @param {String} id the id of node to updated
	 * @private
	 */
	_updateNodeUI: function (id)
	{
		var node = this._tree.getStore().getNodeById(id);
		if (node)
		{
			Ametys.explorer.ExplorerNodeDAO.getExplorerNode(node.getId(), Ext.bind(this._updateNodeUICb, this, [node], true));
		}
	},
	
	/**
	 * Function called after #_updateNodeUI is processed
	 * @param {Ametys.explorer.ExplorerNode/Ametys.explorer.Resource} explorerNode The retrieved explorer node
	 * @param {Ext.data.Model} treeNode The node to update
	 * @private
	 */
	_updateNodeUICb: function (explorerNode, treeNode)
	{
		var name = explorerNode.getProperties().name;
		
		if (treeNode)
		{
			treeNode.beginEdit();
			treeNode.set('text', name);
			treeNode.set('name', name);
			
			// reset the iconCls to update it if needed
            treeNode.set('iconCls', explorerNode.cls);
            
			treeNode.endEdit();
			
			// Marks this Record as NOT dirty.
			treeNode.commit();
            
            this._tree.getStore().sort();
		}
	},

	/**
	 * Determines if the root ids belong to the tree
	 * @param {String} rootId The root id to check
	 * @return True if at least one id belongs to the tree
	 * @private
	 */
	_isOnTree : function (rootId)
	{
		return Ext.Array.contains(this._tree.getRootIds(), rootId);
	},
		
	// ----------------------- Listeners -------------------------------//
	/**
	 * This function is called when a node is moved to a new location in the tree. 
	 * Fires the {@link Ametys.message.Message#MOVED} message on bus.
	 * @param {Ext.data.Model} node The moved node
	 * @param {Ext.data.Model} oldParent The old parent of this node
	 * @param {Ext.data.Model} newParent The new parent of this node
	 * @param {Number} index The index the node was moved to
	 * @private
	 */
	_onItemMove: function (node, oldParent, newParent, index)
	{
		Ext.create("Ametys.message.Message", {
			type: Ametys.message.Message.MOVED,
			targets: this._tree.getMessageTargetConfiguration(node)
		});
	},
	
	/**
	 * Listener on {@link Ametys.message.Message#CREATED} message. 
	 * If the tool is concerned by the created object, the parent node will be refreshed.
	 * @param {Ametys.message.Message} message The created message.
	 * @private
	 */
	_onResourceCreated: function (message)
	{
		var explorerNodeTarget = message.getTarget(Ametys.message.MessageTarget.EXPLORER_COLLECTION);
		if (explorerNodeTarget)
		{
			// Ensure this explorer tool is concerned by the message.
			if (!this._isOnTree(explorerNodeTarget.getParameters().rootId))
			{
				return;
			}
			
			if (!this.hasFocus())
			{
				this.showOutOfDate();
			}
			else
			{
				// Refresh parent node and rename new created node
				this._refreshNode(explorerNodeTarget.getParameters().parentId, Ext.bind(this._renameNode, this, [explorerNodeTarget.getParameters().id], true));
				return;
			}
		}
			
		var resourceTarget = message.getTarget(Ametys.message.MessageTarget.RESOURCE);
		if (resourceTarget)
		{
			// Ensure this explorer tool is concerned by the message.
			if (!this._isOnTree (resourceTarget.getParameters().rootId))
			{
				return;
			}
			
			if (!this.hasFocus())
			{
				this.showOutOfDate();
			}
			else if (!this._treeIgnoreFiles)
			{
				// Refresh parent node
				this._refreshNode(resourceTarget.getParameters().parentId);
			}
			else
			{
				// open corresponding folder in a dedicated tool
				var parentNode = this._tree.getStore().getNodeById(resourceTarget.getParameters().parentId);
				
				Ametys.tool.ToolsManager.openTool("uitool-explorer-resource-collection", 
				{
		            id: node.getId(),
		            applicationId: node.get('applicationId')
		        });
			}
		}
	},
	
	/**
	 * Listener on {@link Ametys.message.Message#DELETED} message. 
	 * If the tool is concerned by the deleted object, the object's node will be deleted.
	 * @param {Ametys.message.Message} message The deleted message.
	 * @private
	 */
	_onResourceDeleted: function(message)
	{
		var targets = message.getTargets(function(target) {
			return Ametys.message.MessageTarget.EXPLORER_COLLECTION == (target.id || target.getId()) || Ametys.message.MessageTarget.RESOURCE == (target.id || target.getId());
		});
		
		if (targets.length > 0)
		{
			// Ensure this explorer tool is concerned by the message.
			if (!this._isOnTree (targets[0].getParameters().rootId))
			{
				return;
			}
			
			if (!this.hasFocus())
			{
				this.showOutOfDate(true);
			}
			else
			{
				for (var i=0; i < targets.length; i++)
				{
					this._deleteNode(targets[i].getParameters().id);
				}
			}
		}
	},
	
	/**
	 * Listener on {@link Ametys.message.Message#MOVED} message. 
	 * If the tool is concerned by the moved object, the new parent node will be refreshed.
	 * @param {Ametys.message.Message} message The moved message.
	 * @private
	 */
	_onResourceMoved: function(message)
	{
		var me = this;
		var targets = message.getTargets(function(target) {
			return Ametys.message.MessageTarget.EXPLORER_COLLECTION == (target.id || target.getId()) || Ametys.message.MessageTarget.RESOURCE == (target.id || target.getId());
		});
		
		if (targets.length > 0)
		{
			// Ensure this explorer tool is concerned by the message.
			if (!this._isOnTree (targets[0].getParameters().rootId))
			{
				return;
			}
			
			if (!this.hasFocus())
			{
				this.showOutOfDate(true);
			}
			else
			{
				for (var i=0; i < targets.length; i++)
				{
					var movedNode = this._tree.getStore().getNodeById(targets[i].getParameters().id);
					var targetNode = this._tree.getStore().getNodeById(targets[i].getParameters().parentId);
					
					if (movedNode.parentNode && movedNode.parentNode.getId() == targetNode.getId())
					{
						// Do nothing, the node was already moved in the tree
					}
					else
					{
						var movedNodeParentNodeInPath = false;
						var targetNodeNodeInPath = false;
						
						if (movedNode.parentNode && targetNode)
						{
							var movedNodeParentNodePath = movedNode.parentNode.getPath('name');
							var targetNodeNodePath = targetNode.getPath('name');
							
							movedNodeParentNodeInPath = !targetNodeNodePath || movedNodeParentNodePath && Ext.String.startsWith(movedNodeParentNodePath, targetNodeNodePath + '/');
							targetNodeNodeInPath = !movedNodeParentNodePath || targetNodeNodePath && Ext.String.startsWith(targetNodeNodePath, movedNodeParentNodePath + '/');
						}
						
						if (movedNodeParentNodeInPath == false && movedNode.parentNode)
						{
							this._refreshNode(movedNode.parentNode.getId());
						}
						
						if (targetNodeNodeInPath == false && targetNode)
						{
							this._refreshNode(targetNode.getId());
						}
					}
				}
			}
		}
	},
	
	/**
	 * Listener on {@link Ametys.message.Message#MODIFIED} message. 
	 * If the tool is concerned by the moved object, the node of modified object will be updated.
	 * @param {Ametys.message.Message} message The modified message.
	 * @private
	 */
	_onResourceModified: function(message)
	{
		var me = this;
		var targets = message.getTargets(function(target) {
			return Ametys.message.MessageTarget.EXPLORER_COLLECTION == (target.id || target.getId()) || Ametys.message.MessageTarget.RESOURCE == (target.id || target.getId());
		});
		
		if (targets.length > 0)
		{
			// Ensure this explorer tool is concerned by the message.
			if (!this._isOnTree (targets[0].getParameters().rootId))
			{
				return;
			}
			
			if (!this.hasFocus())
			{
				this.showOutOfDate();
			}
			else
			{
				for (var i=0; i < targets.length; i++)
				{					
					var id = targets[i].getParameters().id;
					var type = targets[i].id;
					if (message.getParameters().major && type != Ametys.message.MessageTarget.RESOURCE)
					{
						this._refreshNode(id);
					}
                    this._updateNodeUI(id);
				}
			}
		}
	},
	
	/**
	 * Listener on {@link Ametys.message.Message#SELECTION_CHANGED} message. 
	 * If the tool is concerned by the targets of the message, the selection on the tree will be updated accordingly
	 * @param {Ametys.message.Message} message The selection changed message
	 * @private
	 */
	_onSelectionChanged: function(message)
	{
		var explorerNodeTarget = message.getTarget(this._tree.applicationsTargetFilter);
		if (explorerNodeTarget)
		{
			// Ensure this explorer tool is concerned by the message.
			if (!this._isOnTree(explorerNodeTarget.getParameters().rootId))
			{
				return;
			}
			else
			{
				var id = explorerNodeTarget.getParameters().id;
				if (!this._currentNode || this._currentNode.getId() != id)
				{ 
					var node = this._tree.getStore().getNodeById(id);
					if (node)
					{
						this._currentNode = node; // avoid sending a new selection message.
						this.selectNode(node);
					}
				}
			}
		}
		
		var resourceTarget = message.getTarget(Ametys.message.MessageTarget.RESOURCE);
		if (resourceTarget)
		{
			// Ensure this explorer tool is concerned by the message.
			if (!this._isOnTree(resourceTarget.getParameters().rootId))
			{
				return;
			}
			else
			{
				
				var id = this._treeIgnoreFiles ? resourceTarget.getParameters().parentId : resourceTarget.getParameters().id;
				if (!this._currentNode || this._currentNode.getId() != id)
				{ 
					var node = this._tree.getStore().getNodeById(id);
					if (node)
					{
						this._currentNode = node; // avoid sending a new selection message.
						this.selectNode(node);
					}
				}
			}
		}
	}
});