/*
 *  Copyright 2017 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.
 */
/**
 * Tool for displaying the hierarchical simple contents.
 * @private
 */
Ext.define('Ametys.plugins.cms.content.tool.HierarchicalReferenceTablesTool', {
    extend: "Ametys.tool.Tool",
    
    /**
     * @property {Boolean} true if this tool supports candidates.
     */
    allowCandidates: false,
    
    /**
     * @private
     * @property {Ametys.plugins.cms.content.tree.HierarchicalReferenceTablesTree} _tree The tree of hierarchical reference table
     */
    
    /**
     * @private
     * @property {String[]} _supportedContentTypes The list of supported content types
     */
    
    /** 
     * @private
     * @property {Ext.data.NodeInterface[]} _lastOutOfDateNodes The nodes explaininng why the tree is out of date
     */
    
    constructor: function(config)
    {
        this._lastOutOfDateNodes = [];
        this._supportedContentTypes = [];
        
        this.callParent(arguments);
        
        Ametys.message.MessageBus.on(Ametys.message.Message.SELECTION_CHANGED, this._onSelectionChanged, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.CREATED, this._onMessageCreated, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onMessageModified, this);
        Ametys.message.MessageBus.on(Ametys.message.Message.DELETED, this._onMessageDeleted, this);
    },
    
    createPanel: function()
    {
        var plugins = [];
        if (Ametys.plugins.cms.content.actions.HierarchicalReferenceTablesActions)
        {
            // only add cell editing if action is available
            plugins = {
                ptype: 'cellediting',
                clicksToEdit: 1,
                editAfterSelect: true,
                listeners: {
                    'beforeedit': this._onBeforeEdit,
                    'edit': this._onEdit,
                    scope: this
                }
            };
        }
        
        this._tree = Ext.create('Ametys.plugins.cms.content.tree.HierarchicalReferenceTablesTree', {
            rootVisible: true,
            rootCfg: {
                expanded: false,
                text: "" // empty text before retrieving information about leaf content type
            },
            listeners: {
                'selectionchange': this.sendCurrentSelection,
                'itemdblclick': this._onItemDoubleClick,
                scope: this
            },
            
            plugins: plugins,
            enableColumnResize: false,
            hideHeaders: true,
            columns: [{
                xtype: 'treecolumn',
                dataIndex: 'text',
                flex: 1,
                editor: {
                    xtype: 'textfield',
                    allowBlank: false,
                    selectOnFocus: true
                }
            }],
            
            viewConfig: {
                plugins: {
                    ptype: 'ametystreeviewdragdrop',
                    containerScroll: true,
                    appendOnly: true,
                    sortOnDrop: true,
                    expandDelay: 500,
                    allowContainerDrops: false,
                    setAmetysDragInfos: Ext.bind(this._getDragInfo, this),
                    setAmetysDropZoneInfos: Ext.bind(this._getDropInfo, this)
                }
            }
        });
        
        return this._tree;
    },
    
    getMBSelectionInteraction: function() 
    {
        return Ametys.tool.Tool.MB_TYPE_ACTIVE;
    },
    
    /**
     * @inheritdoc
     * @param {Object} params The params to set
     * @param {String} params.contentType The content type of the leaves of the tree
     * @param {String} params.workflowEditActionId The id of the workflow action to edit a simple content
     * @param {String} params.contentDefaultTitle The default title of newly created contents in the ReferenceTableEditionTool
     * @param {String} params.contentLanguage The language of simple contents
     * @param {String} params.startSearchAtOpening "true" to start the search at the opening of the ReferenceTableEditionTool
     */
    setParams: function(params)
    {
        this.showRefreshing();
        
        this._tree.setReferenceTableId(params.contentType);
        this.serverCall("getReferenceTableProperties", [params.contentType], this._getReferenceTablePropertiesCb, {arguments: params});
    },
    
    /**
     * @private
     * Callback function invoked after getting properties of this hierarchical reference table.
     * @param {Object} result The reference table properties:
     * @param {String} result.rootContentType The id of the root content type
     * @param {String[]} result.supportedContentTypes The list of supported content types
     * @param {Boolean} result.allowCandidates True if this reference table supports candidates
     * @param {Object} params The tool parameters
     */
    _getReferenceTablePropertiesCb: function(result, params)
    {
        this._supportedContentTypes = result.supportedContentTypes;
        this.allowCandidates = result.allowCandidates;
        
        Ametys.plugins.cms.content.tool.HierarchicalReferenceTablesTool.superclass.setParams.call(this, params);
        
        var leafCtype = Ametys.cms.content.ContentTypeDAO.getContentType(this.getParams().contentType);
        var rootCtype = Ametys.cms.content.ContentTypeDAO.getContentType(result.rootContentType);
        
        this.setTitle(leafCtype.getLabel());
        this.setDescription(leafCtype.getDescription() || '');
        
        var iconGlyph = leafCtype.getIconGlyph();
        if (iconGlyph != null)
        {
            this.setGlyphIcon(iconGlyph);
            this.setIconDecorator(leafCtype.getIconDecorator());
        }
        else
        {
            this.setSmallIcon(leafCtype.getIconSmall());
            this.setMediumIcon(leafCtype.getIconMedium());
            this.setLargeIcon(leafCtype.getIconLarge());
        }
        
        this._tree.initializeTree(this.getParams().contentType, this.allowCandidates);
        
        this.showUpToDate();
        this.showRefreshed();
    },
    
    refresh: function()
    {
        if (this._destroyed)
        {
            return;
        }
        
        if (this._lastOutOfDateNodes.length > 0)
        {
            this.showRefreshing();
            
            this._reloadNodes(this._lastOutOfDateNodes);
        }
        
        this._lastOutOfDateNodes = [];
    },
    
    /**
     * @private
     * Reload nodes, one by one
     * @param {Ext.data.NodeInterface[]} nodes The nodes to be refreshed
     */
    _reloadNodes: function(nodes)
    {
        // only keep nodes still in store to avoid side-effects
        var store = this._tree.getStore(),
            data = store.getData(),
            me = this;
        nodes = nodes.filter(function(n) {return data.contains(n);});
        
        if (nodes.length > 0)
        {
            // We can only load one node at once
            store.load({
                node: nodes[0],
                callback: function(records) {
                    me._tree.expandNode (nodes[0], false, Ext.bind(me._reloadNodes, me, [Ext.Array.slice(nodes, 1)]));
                }
            });
        }
        else
        {
            this.showRefreshed();
            this.showUpToDate();
        }
    },
    
    sendCurrentSelection: function()
    {
        var selection = this._tree.getSelection();
        
        var targets = this._getMessageTargetCfg(selection);
        
        Ext.create('Ametys.message.Message', {
            type: Ametys.message.Message.SELECTION_CHANGED,
            targets: targets
        });
    },
    
    /**
     * @private
     * Gets the message targets configuration from an array of records
     * @param {Ext.data.Model[]} records The records
     * @return {Object[]} The message targets configuration
     */
    _getMessageTargetCfg: function(records)
    {
        var targets = [];
        
        var contentIds = [];
        var candidateIds = [];
        
        Ext.Array.each(records, function (record){
            var contentId = record.get('contentId');
            
            if (record.get('isCandidate'))
            {
                candidateIds.push(record.get('contentId'));
            }
            else
            {
                contentIds.push(record.get('contentId'));
            }
        });
        
        if (Ext.Array.contains(contentIds, "root"))
        {
            targets.push({
                id: Ametys.message.MessageTarget.REFERENCE_TABLE_CONTENT_ROOT,
                parameters: {
                    contentType: this.getParams().contentType
                }
            });
	        contentIds = Ext.Array.remove(contentIds, "root");
        }
        
        if (Ext.Array.contains(contentIds, "rootCandidate"))
        {
            targets.push({
                id: Ametys.message.MessageTarget.REFERENCE_TABLE_CONTENT_ROOT_CANDIDATE,
                parameters: {
                    contentType: this.getParams().contentType
                }
            });
            contentIds = Ext.Array.remove(contentIds, "rootCandidate");
        }
        
        if (contentIds.length > 0)
        {
            targets.push({
	            id: Ametys.message.MessageTarget.REFERENCE_TABLE_CONTENT,
	            parameters: {
	                ids: contentIds
	            }
	        });
        }
        
        if (candidateIds.length > 0)
        {
            targets.push({
                id: Ametys.message.MessageTarget.REFERENCE_TABLE_CANDIDATE,
                parameters: {
                    ids: candidateIds
                }
            });
        }
            
        return targets;
    },
    
    /**
     * @private
     * Listener called when an item in the tree is double clicked
     * @param {Ext.view.View} view The view
     * @param {Ext.data.Model} record The record that belongs to the item
     */
    _onItemDoubleClick: function(view, record)
    {
        if (record.isLeaf() || record.data.isCandidate || record.data.name == 'rootCandidate')
        {
            return;
        }
        
        if (!record.isRoot())
        {
        	var contentId = record.get('contentId');
        	this.serverCall("getParentAttributePath", [contentId], Ext.bind(this._openSearchTool, this, [contentId], 1));
        }
        else
        {
        	this.serverCall("getParentAttributePathForRoot", [record.data.contenttypesIds[0]], Ext.bind(this._openSearchTool, this, [""], 1));
        }
    },
    
    /**
     * @private
     * Open the search tool
     * @param {String} path The path of metadata holding the parent
     * @param {String} contentId The content of double-clicked content
     */
    _openSearchTool: function (path, contentId)
    {
        var toolParams = this.getParams(),
            parentMetadataCriteria = "reference-" + path + "-eq";
	        values = {},
            values[parentMetadataCriteria] = contentId;
            
	    var toolFactoryId = 'uitool-reference-table',
	        searchToolId = "reference-table-search-ui." + toolParams.contentType;
	    
	    Ametys.tool.ToolsManager.openTool(toolFactoryId, {
	        contentDefaultTitle: toolParams.contentDefaultTitle,
	        contentLanguage: toolParams.contentLanguage,
	        contentType: toolParams.contentType,
	        values: values,
	        id: searchToolId,
	        startSearchAtOpening: toolParams.startSearchAtOpening
	    }, 'cl');
    },
    
   /**
     * @private
     * Listener called before cell editing 
     * @param {Ext.grid.plugin.CellEditing} editor The cell editor
     * @param {Object} context An editing context event with the following properties:
     * @param {Ext.grid.Panel}         context.grid The owning grid Panel.
     * @param {Ext.data.Model}         context.record The record being edited.
     * @param {String}                 context.field The name of the field being edited.
     * @param {Mixed}                  context.value The field's current value.
     * @param {HTMLElement}            context.row The grid row element.
     * @param {Ext.grid.column.Column} context.column The {@link Ext.grid.column.Column} being edited.
     * @param {Number}                 context.rowIdx The index of the row being edited.
     * @param {Number}                 context.colIdx The index of the column being edited.
     * @param {Boolean}                context.cancel Set this to `true` to cancel the edit or return false from your handler.
     */
    _onBeforeEdit: function(editor, context)
    {
        var node = context.record;
        return node && !node.isRoot();
    },
    
    /**
     * @private
     * Listener called after cell editing
     * @param {Ext.grid.plugin.CellEditing} editor The cell editor
     * @param {Object} context An editing context with the following properties:
     * @param {Ext.grid.Panel}         context.grid The owning grid Panel.
     * @param {Ext.data.Model}         context.record The record being edited.
     * @param {String}                 context.field The name of the field being edited.
     * @param {Mixed}                  context.value The field's current value.
     * @param {HTMLElement}            context.row The grid row element.
     * @param {Ext.grid.column.Column} context.column The {@link Ext.grid.column.Column} being edited.
     * @param {Number}                 context.rowIdx The index of the row being edited.
     * @param {Number}                 context.colIdx The index of the column being edited.
     * @param {Mixed}                  context.originalValue The original value before being edited.
     */
    _onEdit: function(editor, context)
    {
        var record = context.record,
            contentId = record && record.get('contentId'),
            dirty = record && record.dirty,
            newTitle = context.value;
        
        if (contentId != null && context.field == "text" && newTitle && dirty)
        {
            var workflowActionId = this.getParams().workflowEditActionId;
            
            this.serverCall("getTitleForEdition", [contentId], function(titleInfo){
                
                var value = newTitle;
                if (titleInfo.type == 'multilingual-string')
                {
                    value = titleInfo.value || {};
                    value[Ametys.cms.language.LanguageDAO.getCurrentLanguage()] = newTitle;
                }
	            Ametys.plugins.cms.content.actions.HierarchicalReferenceTablesActions.doRename(contentId, workflowActionId, value, function (success) {
	                if (!success)
	                {
	                    // Cancel changes
	                    record.beginEdit();
	                    var oldTitle = context.originalValue;
	                    record.set('text', oldTitle);
	                }
	                record.endEdit();
	                record.commit();
	            }, this);
                
            });
        }
    },
    
    /**
     * Listener on creation message.
     * @param {Ametys.message.Message} message The creation message.
     * @private
     */
    _onMessageCreated: function(message)
    {
        var candidateTargets = message.getTargets(Ametys.message.MessageTarget.REFERENCE_TABLE_CANDIDATE);
        if (candidateTargets.length > 0)
        {
            this._lastOutOfDateNodes.push(this._tree.getRootCandidate());
            this.showOutOfDate(false);
        }
    },
    
    /**
     * Listener on edition message.
     * @param {Ametys.message.Message} message The edition message.
     * @private
     */
    _onMessageModified: function(message)
    {
        var me = this,
            targets = message.getTargets(Ametys.message.MessageTarget.REFERENCE_TABLE_CONTENT);
        
        if (targets.length > 0)
        {
            var nodes = this.getMatchingNodes(targets);
	        if (nodes.length > 0)
	        {
	        	me._updateMatchingNodes(targets);
	        	me._lastOutOfDateNodes = Ext.Array.merge(this._lastOutOfDateNodes, nodes);
	        	me.showOutOfDate(false);
	        }
        }
        
        targets = message.getTargets(Ametys.message.MessageTarget.REFERENCE_TABLE_CONTENT_ROOT);
            
        Ext.Array.each(targets, function(target) {
            var contentTypes = target.getParameters().contentType;
            var node = me._tree.getRootNode();
            
            if (Ext.Array.equals(node.get('contenttypesIds'), contentTypes) || Ext.Array.equals(this.getParameters().contentType, contentTypes))
            {
                me._lastOutOfDateNodes = Ext.Array.merge(me._lastOutOfDateNodes, node);
                me.showOutOfDate(false);
            }
        });
    },
    
    /**
     * Listener on deletion message.
     * @param {Ametys.message.Message} message The deletion message.
     * @private
     */
    _onMessageDeleted: function(message)
    {
        var me = this;
        var targets = message.getTargets(Ametys.message.MessageTarget.REFERENCE_TABLE_CONTENT);
        targets = targets.concat(message.getTargets(Ametys.message.MessageTarget.REFERENCE_TABLE_CANDIDATE));
        
        var currentSelection = this._tree.getSelectionModel().getSelection();
        
        var nodes = this.getMatchingNodes(targets);
        Ext.Array.each(nodes, function(node) {
            if (currentSelection.length > 0 && currentSelection[0] == node) 
            {
                // if deleted node is selected, select its parent
                me._tree.getSelectionModel().select([node.parentNode]);
            }
            node.remove();
        });
    },
    
    /**
     * @private
     * Filter nodes to keeps those which are in the tree
     * @param {Ametys.message.MessageTarget[]} targets The messages to filter
     * @return {Ext.data.NodeInterface[]} The matching nodes
     */
    getMatchingNodes: function(targets)
    {
        var tree = this._tree;
        var found = [];

        Ext.Array.each(targets, function(target) {
            var idModified = target.getParameters().id;
            if (idModified)
            {
                var index = tree.getStore().findExact('contentId', idModified);
                if (index != -1)
                {
                    found.push(tree.getStore().getAt(index));
                }
            }
        });
        
        return found;
    },
    
    /**
     * @private
     * Update the text of nodes based on message targets
     * @param {Ametys.message.MessageTarget[]} targets The message targets
     */
    _updateMatchingNodes: function(targets)
    {
        var tree = this._tree,
            me = this;
        Ext.Array.each(targets, function(target) {
            var idModified = target.getParameters().id;
            if (idModified)
            {
                var treeStore = tree.getStore();
                var index = treeStore.findExact('contentId', idModified);
                if (index != -1)
                {
                    var node = treeStore.getAt(index);
                    node.set('title', target.getParameters().title);
                    node.set('text', target.getParameters().title);
                    node.commit();
                    me._updateParentIfNeeded(treeStore, node, target);
                }
            }
        });
    },
    
    /**
     * @private
     * Update parent of a node if needed
     * @param {Ext.data.TreeStore} treeStore The tree store
     * @param {Ametys.plugins.cms.content.tree.HierarchicalReferenceTablesTree.HierarchicalReferenceTablesTreeEntry} node The node
     * @param {Ametys.message.MessageTarget} target The modified message target
     */
    _updateParentIfNeeded: function(treeStore, node, target)
    {
        var oldParentId = node.get('parentId'),
            oldParentIndex = treeStore.findExact('id', oldParentId),
            oldParentInTree = null,
            newParent = null;
        
        if (oldParentIndex == -1)
        {
            // is root node, do nothing
            return;
        }
        else
        {
            oldParentInTree = treeStore.getAt(oldParentIndex);
        }
        
        var parentIdInTarget = target.getParameters().parent;
        var parentIdInTree = oldParentInTree.get('contentId');
        if (parentIdInTarget == parentIdInTree || (parentIdInTree == 'root' && parentIdInTarget == null))
        {
            // same parent, the tree is up-to-date, no need to refresh some node (it probably comes from an internal D&D, or it is a regular edition)
            return;
        }
        
        // tree is out-of-date, we will try to move node (it probably comes from an external D&D)
        if (parentIdInTarget)
        {
            var index = treeStore.findExact('contentId', parentIdInTarget);
            if (index != -1)
            {
                newParent = treeStore.getAt(index);
            }
        }
        
        if (newParent)
        {
            newParent.insertBefore(node, null);
        }
        else if (oldParentInTree) // new parent is not in tree but old one is
        {
            node.remove();
        }
    },
    
    /**
     * @private
     * Listener when the selection has changes. Update the tree selection if needeed.
     * @param {Ametys.message.MessageBus} message The message bus
     */
    _onSelectionChanged: function(message)
    {
        message = message || Ametys.message.MessageBus.getCurrentSelectionMessage();
        
        var contentTargets = message.getTargets(Ametys.message.MessageTarget.REFERENCE_TABLE_CONTENT);
        if (contentTargets.length > 0)
        {
            var contentId = contentTargets[0].getParameters().id;
            
            var selection = this._tree.getSelectionModel().getSelection();
            if (selection.length > 0 && selection[0].get('contentId') == contentId)
            {
                // Nothing to do, same selection
            }
            else
            {
            	
                var index = this._tree.getStore().findExact('contentId', contentId);
                if (index != -1)
                {
                    // Find content's in tree, select it
                    var node = this._tree.getStore().getAt(index);
                    this._tree.setSelection(node);
                }
                else
                {
                    // is the contenttype one of the supported?
                    if (Ext.Array.intersect(contentTargets[0].getParameters().types, this._supportedContentTypes).length > 0)
                    {
                        // Try to get the path of current selection in tree
                        this._getPathInTree(contentTargets[0].getParameters().id, function(pathInTree, args) {
                            if (pathInTree)
                            {
                                // The selected content is part of tree, select it
                                this.selectByPath(pathInTree)
                            }
                        }, this);
                    }
                }
            }
        }
    },
    
    
    /**
     * @template
     * Get the path of current selected content into the current tree
     * @param {String} contentId The id of content 
     * @param {Function} callback The callback function to call after getting the path in tree. Parameters have to be:
     * @param {String} callback.path The path in tree or null if the content is not part of tree
     * @param {Object} callback.args The callback arguments
     * @param {Object} [scope] The scope for callback
     * @param {Object} [args] The callback arguments.
     */
    _getPathInTree: function(contentId, callback, scope, args)
    {
        Ametys.data.ServerComm.callMethod({
            role: "org.ametys.cms.content.referencetable.HierarchicalReferenceTablesHelper",
            methodName: "getPathInHierarchy",
            parameters: [contentId],
            callback: {
                handler: callback,
                scope: scope || this
            },
            waitMessage: false
        });
    },
    
    /**
     * Expand the tree to the given path
     * @param {String} path The path to expand
     * @param {Boolean} autofocus true to focus to last expanded node
     * @param {Boolean} expand true to expand to last expanded node
     */
    selectByPath: function (path, autofocus, expand)
    {
        if (path)
        {
            this._tree.selectPath(path, 'name', '/', function(success, lastNode) {
                if (success)
                {
                    if (autofocus)
                    {
                        this._tree.getView().focusNode(lastNode);
                    }
                    if (expand)
                    {
                        this._tree.expandNode(lastNode);
                    }
                }
            }, 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)
    {
        if (item.records.length > 0 && item.records[0].get('isCandidate'))
        {
            // A candidate cannot be moved
            return;
        }
        
        var messageTargets = this._getMessageTargetCfg(item.records);
        if (messageTargets.length > 0)
        {
            item.source = {
                relationTypes: [Ametys.relation.Relation.MOVE, Ametys.relation.Relation.REFERENCE, Ametys.relation.Relation.COPY],
                targets: messageTargets
            };
        }
    },
    
    /**
     * @private
     * This event is thrown before the beforeDrop event and create the target of the drop operation relation.
     * @param {Ext.data.Model[]} targetRecords The target records of the drop operation.
     * @param {Object} item The default drag data that will be transmitted. You have to add a 'target' item in it: 
     * @param {Object} item.target The target (in the relation way) of the drop operation. A Ametys.relation.RelationPoint config.
     */
    _getDropInfo: function(targetRecords, item)
    {
        var messageTargets = this._getMessageTargetCfg(targetRecords);
        if (messageTargets.length > 0)
        {
            item.target = {
                relationTypes: [Ametys.relation.Relation.MOVE, Ametys.relation.Relation.REFERENCE, Ametys.relation.Relation.COPY],
                targets: messageTargets
            };
        }
    }
});

Ext.define("Ametys.message.HierarchicalReferenceTableMessageTarget", {
    override: "Ametys.message.MessageTarget",
    
    statics: {
        /**
         * @member Ametys.message.MessageTarget
         * @readonly
         * @property {String} REFERENCE_TABLE_CONTENT_ROOT The target type is the root of a hierarchy of simple contents. The expected parameters are:
         * @property {String} REFERENCE_TABLE_CONTENT_ROOT.contentType The content type of the leaves
         */
        REFERENCE_TABLE_CONTENT_ROOT: "reference-table-content-root",
        
        /**
         * @member Ametys.message.MessageTarget
         * @readonly
         * @property {String} REFERENCE_TABLE_CONTENT_ROOT_CANDIDATE The target type is the root of the candidates. The expected parameters are:
         */
        REFERENCE_TABLE_CONTENT_ROOT_CANDIDATE: "reference-table-content-root-candidate",
        
        /**
         * @member Ametys.message.MessageTarget
         * @readonly
         * @property {String} REFERENCE_TABLE_CANDIDATE The target type is a 'candidate' for an entry of a reference table. See Ametys.plugins.cms.content.ContentMessageTargetFactory parameters to know more of the associated parameters. 
         */
        REFERENCE_TABLE_CANDIDATE: "reference-table-candidate"
    }
});