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

/**
 * @private
 * This tool displays the JCR repository nodes in a tree.
 * 
 * FIXME Bug when the tool has never been focused (its tab is inactive) and tries to select a node (to reflect another selection). 
 * See https://www.sencha.com/forum/showthread.php?302708-Select-a-node-of-a-not-rendered-tree
 */
Ext.define('Ametys.repository.tool.JcrViewTool',
{
    extend: 'Ametys.repository.tool.RepositoryTool',
    
    config:
    {
        /**
         * @cfg {String} [defaultWorkspaceName='default'] the default repository workspace name.
         */
        defaultWorkspaceName: 'default'
    },
    
    /**
	 * @property {String} pendingChangesCls The CSS class used when a node has pending changes
	 */
    pendingChangesCls: "pending-changes",
    
    /**
     * @property {String} [messageTargetType=Ametys.message.MessageTarget.REPOSITORY_NODE] The type of message target for nodes holding by the main tree panel
     */
    messageTargetType: null,
    
    /**
     * @private
     * @property {Ext.tree.Panel} _tree The main tree panel 
     */
    _tree: null,
    
    /**
     * @private
     * @property {Ametys.repository.tree.SearchTreePanel} _searchTree The tree panel for search results.
     */
    _searchTree: null,
    
    
    /**
     * @property {String[]} _nodesWithPendingChanges An array of the path of the nodes with pending changes. 
     */
    _nodesWithPendingChanges: [],
    
    
    constructor: function(config)
    {
        this.callParent(arguments);
        
        // Bus messages listeners
        Ametys.message.MessageBus.on(Ametys.message.Message.SELECTION_CHANGED, this._onMessageSelectionChanged, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.CREATED, this._onMessageCreated, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.MOVED, this._onMessageMoved, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onMessageModified, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.MODIFYING, this._onMessageModifying, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.DELETED, this._onMessageDeleted, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.SORTED, this._onMessageSorted, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.REVERTED, this._onMessageReverted, this);
        
        this.messageTargetType = Ametys.message.MessageTarget.REPOSITORY_NODE;
    },
    
    createPanel: function()
    {
        // Create the main tree
        this._tree = this._createTree();
        this._tree.getSelectionModel().on('selectionchange', this.sendCurrentSelection, this);
        this._tree.on('itemdblclick', this.openNode, this);
        
        // Create the tree for search result
        this._searchTree = Ext.create('Ametys.repository.tree.SearchTreePanel', {
            maxHeight: 200,
            split: true
        });
        
        this._searchTree.getSelectionModel().on('select', this.onSelectSearchNode, this);
        this._searchTree.on('itemdblclick', this.openNode, this);
        
        return {
        	xtype: 'panel',
        	layout: {
                type: 'vbox',
                align: 'stretch'
            },
            cls: 'ametys-repository-jcr-tool',
            
            scrollable: false,
            border: false,
            
            items: [
                this._tree,
                this._searchTree
            ]
        };
    },
    
    /**
     * @protected
     * Creates the tree panel
     * @return {Ext.tree.Panel} the tree
     */
    _createTree: function ()
    {
        return Ext.create('Ametys.repository.tree.JcrTreePanel', {
            flex: 1,
            split: true,
            defaultWorkspaceName: this.getConfig('defaultWorkspaceName'),
            
            viewConfig: {
                plugins: [{
                    ptype: 'ametystreeviewdragdrop',
                    containerScroll: true,
                    appendOnly: false,
                    sortOnDrop: false,
                    expandDelay: 500,
                    allowContainerDrops: false,
                    setAmetysDragInfos: Ext.bind(this.getDragInfo, this),
                    setAmetysDropZoneInfos: Ext.bind(this.getDropInfo, this)
                }]
            },
            
            listeners: {
                'load': {fn: this._onLoad, scope: this}
            }
        });
    },
    
        /**
     * @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.   
     * @param {"append"/"before"/"after"} dropPosition The drop mode
     */ 
    getDropInfo: function(targetRecords, item, dropPosition)
    {
        var targets = [];

        var positionInTargets = -1

        for (var i = 0; i < targetRecords.length; i++)
        {
            var node = targetRecords[i];

            if (node.isRoot() || dropPosition == 'append')
            {
                var cfg = this.getMessageTargetConfiguration(node);
                if (cfg != null)
                {
                    targets.push(cfg);
                }
            }
            else if (node.parentNode != null) // dropPosition == 'before' or 'after'
            {
                // Get node position 
                for (var i=0; i < node.parentNode.childNodes.length; i++)
                {
                    if (node.parentNode.childNodes[i].getId() == node.getId())
                    {
                        positionInTargets = i + (dropPosition == 'after' ? + 1 : 0);
                        break;
                    }
                }
                
                var cfg = this.getMessageTargetConfiguration(node.parentNode);
                if (cfg != null)
                {
                    targets.push(cfg);
                }
            }
        }

        if (targets.length > 0)
        {
            item.target = {
                relationTypes: [Ametys.relation.Relation.MOVE], 
                targets: targets,
                positionInTargets: positionInTargets
            };
        }
    },
    
    /**
     * @protected
     * Creates the tree panel for search results
     * @return {Ext.tree.Panel} the search tree
     */
    _createSearchTree: function ()
    {
        return Ext.create('Ametys.repository.tree.SearchTreePanel', {
            maxHeight: 200,
            split: true
        });
    },
    
    getMBSelectionInteraction: function() 
    {
        return Ametys.tool.Tool.MB_TYPE_ACTIVE;
    },
    
    getType: function()
    {
        return Ametys.tool.Tool.TYPE_REPOSITORY;  
    },
    
    sendCurrentSelection: function()
    {
        var selection = this._tree.getSelectionModel().getSelection();
        
        var targets = [];
        if (selection.length > 0)
        {
            var node = selection[0];
            targets.push(this.getMessageTargetConfiguration(node));
        }
        
        Ext.create('Ametys.message.Message', {
            type: Ametys.message.Message.SELECTION_CHANGED,
            targets: targets
        });
    },
    
    /**
     * @private
     * Get the target configuration object for given record
     * @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
        {
            return {
                id: this.messageTargetType,
                parameters: {
                    paths: [record.get('path')],
                    workspaceName: this._tree.getCurrentWorkspaceName()
                }
            };
        }
    },
    
    setParams: function (params)
    {
        this.callParent(arguments);
        
        var workspaceName = params.workspaceName || 'default';
        var query = params.query;
        var queryLanguage = params.queryLanguage || 'xpath';
        var path = params.path;
        
        if (!Ext.isEmpty(query))
        {
            this._executeSearchQuery(query, queryLanguage, workspaceName);
        }
        
        if (!Ext.isEmpty(path))
        {
            this._tree.selectNodeByPath(path);
        }
    },
    
    /**
     * Get the JCR node tree.
     * @return {Ametys.repository.tree.JcrTreePanel} The JCR node tree.
     */
    getTree: function()
    {
        return this._tree;
    },
    
    /**
     * @private
     * Function invoked after loading a node.
     * Stores the path of nodes with pending changes.
     * @param {Ext.data.TreeStore} store The tree store
     * @param {Ext.data.Model[]} records The loaded records
     * @param {Boolean} successful true if success
     * @param {Ext.data.operation.Read} operation The data operation
     */
    _onLoad: function (store, records, successful, operation)
    {
        if (operation.node.isRoot())
        {
            // Reset pending changes
            this._nodesWithPendingChanges = [];
        }
        
        var me = this;
        Ext.Array.each(records, function (record) {
            if (record.get('isNew') || record.get('isModified'))
            {
                me._nodesWithPendingChanges.push(record.get('path'));
            }
        });
    },
    
    /**
     * @private
     * This listener is invoked when the selection has changed
     * @param {Ametys.message.Message} message the selection message
     */
    _onMessageSelectionChanged: function(message)
    {
        var target = message.getTarget(this.messageTargetType);
        if (target != null)
        {
            var selection = this._tree.getSelectionModel().getSelection();
            var node = this._tree.getStore().getNodeById(target.getParameters().id);
            if (node != null && selection.length > 0 && node == selection[0])
            {
                // same selection
                return;
            }
            
            if (target.getParameters().workspaceName == this._tree.getCurrentWorkspaceName())
            {
                this._tree.selectNodeByPath(target.getParameters().pathWithGroups || target.getParameters().path);
            }
        }
    },
    
    /**
     * @private
     * This listener is invoked when a node has been created
     * @param {Ametys.message.Message} message The bus message.
     */
    _onMessageCreated: function(message)
    {
        var target = message.getTarget(this.messageTargetType);
        if (target != null && target.getParameters().workspaceName == this._tree.getCurrentWorkspaceName())
        {
            var path = target.getParameters().path;
            var parentPath = path.substring(0, path.lastIndexOf('/'));
            
            var parentNode = this._tree.getNodeByPath(parentPath);
            if (parentNode != null)
            {
                this._tree.getStore().load({node: parentNode});
                this.setInDirtyState(parentNode);
            }
        }
    },
    
        /**
     * @private
     * This listener is invoked when a node has been created
     * @param {Ametys.message.Message} message The bus message.
     */
    _onMessageMoved: function(message)
    {
        var me= this;
        var target = message.getTarget(this.messageTargetType);
        if (target != null && target.getParameters().workspaceName == this._tree.getCurrentWorkspaceName())
        {
            var path = target.getParameters().path;
            var parentPath = path.substring(0, path.lastIndexOf('/'));
            
            var parentNode = me._tree.getNodeByPath(parentPath);
            if (parentNode != null)
            {
                me._tree.getStore().load({node: parentNode, callback: function() {me._tree.getStore().load({node: parentNode})}}); // For some reason, the first load does nothing in some cases (sub node)
                me.setInDirtyState(parentNode);
            }
        }
    },
    
     /**
     * @private
     * This listener is invoked when a node has on-going changes
     * @param {Ametys.message.Message} message The bus message.
     */
    _onMessageModifying: function(message)
    {
        var target = message.getTarget(this.messageTargetType);
        if (target != null && target.getParameters().workspaceName == this._tree.getCurrentWorkspaceName())
        {
            var path = target.getParameters().path;
            var node = this._tree.getNodeByPath(path);
            
            if (node != null)
            {
                this.setInDirtyState(node);
            }
        }
    },
    
    /**
     * @private
     * This listener is invoked when a node has been deleted
     * @param {Ametys.message.Message} message The bus message.
     */
    _onMessageDeleted: function(message)
    {
        var target = message.getTarget(this.messageTargetType);
        if (target != null && target.getParameters().workspaceName == this._tree.getCurrentWorkspaceName())
        {
            var path = target.getParameters().path;
            var node = this._tree.getNodeByPath(path);
            
            if (node != null)
            {
                var parentNode = node.parentNode;
                this._tree.getSelectionModel().select([parentNode]);
				node.remove();
                
                this.setInDirtyState(parentNode);
            }
        }
    },
    
    /**
     * @protected
     * Set the node in a dirty state
     * @param {Ext.data.NodeInterface} node The node
     */
    setInDirtyState: function (node)
    {
        this._nodesWithPendingChanges.push(node.get('path'));
        node.set('iconCls', 'a-tree-glyph ametysicon-document112 decorator-ametysicon-clock56', {convert: false});
        this._tree.getView().addRowCls(node, this.pendingChangesCls);
        node.commit();
    },
    
    /**
     * @protected
     * Remove the node dirty state
     * @param {Ext.data.NodeInterface} node The node
     */
    removeDirtyState: function (node)
    {
        node.set('iconCls', 'a-tree-glyph ametysicon-document112', {convert: false});
        this._tree.getView().removeRowCls(node, this.pendingChangesCls);
        node.commit();
    },
    
    /**
     * @private
     * This listener is invoked when the repository session has been saved.
     * @param {Ametys.message.Message} message The bus message.
     */
    _onMessageModified: function(message)
    {
        var target = message.getTarget(Ametys.message.MessageTarget.REPOSITORY_SESSION);
        var currentWorkspace = Ametys.repository.RepositoryApp.getCurrentWorkspace();
        if (target != null && target.getParameters().workspaceName == currentWorkspace)
        {
            this._refreshTreeChanges();
        }
    },
    
    /**
     * @private
     * This listener is invoked when the repository session has been rolled back.
     * @param {Ametys.message.Message} message The bus message.
     */
    _onMessageReverted: function(message)
    {
        var target = message.getTarget(Ametys.message.MessageTarget.REPOSITORY_SESSION);
        var currentWorkspace = Ametys.repository.RepositoryApp.getCurrentWorkspace();
        if (target != null && target.getParameters().workspaceName == currentWorkspace)
        {
            this._refreshTreeChanges();
        }
    },
    
    /**
     * Triggered when a node is sorted.
     * @param {Ametys.message.Message} message The bus message.
     */
    _onMessageSorted: function(message)
    {
        var target = message.getTarget(this.messageTargetType);
        if (target != null && target.getParameters().workspaceName == this._tree.getCurrentWorkspaceName())
        {
            var path = target.getParameters().path;
            var workspaceName = target.getParameters().workspaceName;
            var newOrder = message.getParameters().order;
            
            var node = this._tree.getNodeByPath(path);
            if (node != null)
            {
                // Get the first parent which is a node (not a group).
                var jcrNode = node;
                while (!jcrNode.isNode())
                {
                    jcrNode = jcrNode.parentNode;
                }
                
                if (jcrNode.get('order') == null || jcrNode.get('order') == '')
                {
                    jcrNode.set('order', Ametys.repository.RepositoryApp.getDefaultSort());
                }
                
                // If the order was changed, refresh the node and change the tooltip.
                if (jcrNode.get('order') != newOrder)
                {
                    jcrNode.set('order', newOrder);
                    
                    this._tree.reloadAndSelect(jcrNode);
                }
                
                jcrNode.commit();
            }
        }
    },
    
    /**
     * @private
     * Execute a search query
     * @param {String} query The string query
     * @param {String} language The query language (XPath, SQL, JCR-SQL2)
     * @param {String} workspaceName the workspace name. The query is not executed if the workspace is not the same as current tree workspace
     */
    _executeSearchQuery: function(query, language, workspaceName)
    {
        if (workspaceName == this._tree.getCurrentWorkspaceName())
        {
            this._searchTree.setQuery(query, language, workspaceName);
            this._searchTree.getStore().reload();
        }
    },
    
    /**
     * Refresh the tree changes to reflect the current 
     * @protected
     */
    _refreshTreeChanges: function()
    {
        var me = this;
        var store = me._tree.getStore();
        Ext.Array.each(me._nodesWithPendingChanges, function (path) {
        
            var nodes = store.query('path', path, false, false, true);
            nodes.each (function (node) {
                // Refresh the node and update UI
                store.load({node: node});
                me.removeDirtyState(node);
            });
        });
        
        me._nodesWithPendingChanges = [];
    },
    
    /**
     * Determines if the tree contains nodes with pending changes
     * @return true if there is unsaved nodes.
     */
    hasPendingChanges: function()
    {
        return this._nodesWithPendingChanges != null && this._nodesWithPendingChanges.length > 0;
    },
    
    /**
     * Listens before a node selection to prevent selecting the root node.
     * @param {Ext.selection.TreeModel} sm The tree selection model.
     * @param {Ext.data.NodeInterface} record The selected node.
     * @param {Number} index The row index of the record
     * @param {Object} eOpts Options added to the addListener
     * @private
     */
    beforeSelectSearchNode: function(sm, record, index, eOpts)
    {
        // Prevent selecting the root node.
        if (record == null || record.isRoot())
        {
            return false;
        }
    },
    
    /**
     * Listens for search node selection to select the corresponding node in the JCR tree.
     * @param {Ext.selection.TreeModel} sm The tree selection model.
     * @param {Ext.data.NodeInterface} record The selected node.
     * @param {Number} index The row index of the record
     * @param {Object} eOpts Options added to the addListener
     * @private
     */
    onSelectSearchNode: function(sm, record, index, eOpts)
    {
        if (record != null)
        {
            var path = record.get('pathWithGroups') || record.get('path') || record.get('text');
            this._tree.selectNodeByPath(path);
        }
    },
    
    /**
     * Called when a node is double-clicked in the tree.
     * @param {Ext.tree.View} view the tree view.
     * @param {Ext.data.Model} node the double-clicked node
     * @protected
     */
    openNode: function(view, node)
    {
        var workspaceName = Ametys.repository.RepositoryApp.getCurrentWorkspace();
        Ametys.repository.tool.NodePropertiesTool.open(node.get('path'), workspaceName);
    }
    
});