/*
 *  Copyright 2013 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 to display a set of attributes.
 * Each attribute can be checked or unchecked.
 * By default, the tree includes a toolbar which allow to select/deselect every attribute in the tree.
 * @private
 */
Ext.define('Ametys.plugins.cms.content.tree.ViewTree', {
	extend: 'Ext.tree.Panel',
	
	statics: {
		/**
		 * @private
		 * @readonly
		 * @property {RegExp} _duplicateSlashes Utility regexp used to remove duplicate slashes in a path. 
		 */
		_duplicateSlashes: /\/+/g
	},
	
	cls: 'view-tree',
	rootVisible: false,
	animate: true,
	minHeight: 200,
	maxHeight: 600,
	
	/**
	 * @property {Object} rootNodeConfig Default configuration for the root node.
	 * @protected
	 */
	rootNodeConfig: {
		text: 'root',
		id: 'root',
		expanded: false
	},

	/**
	 * @cfg {Boolean} adaptTreePanelSize Adapt the height of the tree between min and max height on node collapse/expand.
	 */
	adaptTreePanelSize: false,
	

	/**
	 * @cfg {String} contentId Identifier of a content which content type will be used to the view.
	 * This could be used as a convenience instead of providing a content type. But either #contentType or #contentId is required.
	 * If both are provided, only #contentId will be taken into account.
	 */
	/**
	 * @property {String} _contentId See #cfg-contentId
	 * @private
	 */
	
	/**
	 * @cfg {String} contentType Content type to use to generate the view.
	 * Either #contentType or #contentId is required.
	 * If both are provided, only #contentId will be taken into account.
	 */
	/**
	 * @property {String} _contentType See #cfg-contentType
	 * @private
	 */
	
	/**
	 * @cfg {Boolean} checkMandatory Indicates if mandatory attribute should be checked and disabled. This only applie to main content (the first level of the tree).
	 * As attribute of type content are either checked by reference (in which case their attributes are not checkable), 
	 * or checked in mode 'create new' and their mandatory attribute will always by checked and disabled.
	 */
	/**
	 * @property {Boolean} _checkMandatory See #cfg-checkMandatory
	 * @private
	 */
	
	/**
	 * @cfg {String} [viewName="default-edition"] The name of the view to display.
	 */
	/**
	 * @property {String} _viewName See #cfg-viewName
	 * @private
	 */
     
     /**
     * @cfg {String} [fallbackViewName="main"] The ame of the fallback view to display.
     */
    /**
     * @property {String} _fallbackViewName See #cfg-fallbackViewName
     * @private
     */
	
	/**
	 * @cfg {String} [viewMode="edition"] The mode of the view. 'edition' or 'view'
	 */
	/**
	 * @property {String} _viewMode See #viewMode
	 * @private
	 */
	
	/**
	 * @property {Boolean} _inErrorState Indicates if the tree has encountered an error, this typically means that the desired view could not be loaded.
	 * @private
	 */
	
	/**
	 * @property {Ext.LoadMask} _loadMask Mask bound to the tree, displayed when the tree is loading.
	 * @private
	 */
	
    /**
     * @property {Boolean} _checkByDefault Indicates if the checkboxes should be checked or not.
     * @private
     */

    /**
     * @private
     * @property {Ext.Template} _attributeTooltipTpl The template used for attribute's tooltip
     */
    _attributeTooltipTpl : Ext.create('Ext.XTemplate', [
                            '<div class="ametys-tooltip-tree-attribute">',
                            '<div class="ametys-tooltip-tree-attribute-text">{description}</div>',
                            '<tpl if="warns && warns.length">',
                                '<ul class="warns"><tpl for="warns"><li>{.}</li></tpl></ul>',
                            '</tpl>',
                            '<tpl if="infos && infos.length">',
                                '<ul class="infos"><tpl for="infos"><li>{.}</li></tpl></ul>',
                            '</tpl>',
                            '<div class="x-clear">',
                            '</div>',
                            '</div>'
                        ]),
	
	constructor: function (config) 
	{
		// Store config
		config.store = this._getTreeStoreCfg();
		
		// Docked items
		config.dockedItems = this._getDockItemsCfg(config);
		
		this.callParent(arguments);
		
		this.on('viewready', this._onViewReady, this);
		this.on('itemmouseenter', this._createQtip, this);
		
		if (this.adaptTreePanelSize)
		{
			this.on('afteritemcollapse', this._onItemCollapse, this);
			this.on('afteritemexpand', this._onItemExpand, this);
		}
	},
	
	/**
	 * @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);
		
		var tooltipCfg = {
			target: el,
			id: el.id + '-tooltip',
			inribbon: false
		}; 
		
		if (node.get('type') == 'error')
		{
			var errorMsg = "{{i18n plugin.cms:CONTENT_COPY_VIEW_TREE_NODE_ERRORTYPE_TEXT}}";
			errorMsg += node.get('viewName') + " / " + node.get('viewMode') + " / " + node.get('contentType');
			
			tooltipCfg.text = errorMsg;
		}
		else
		{
			var infos = [];
			var warns = [];
			
			if (node.get('mandatory') && node.get('disabled') && node.get('checked'))
			{
				infos.push("{{i18n plugin.cms:CONTENT_COPY_VIEW_TREE_NODE_TOOLTIP_DESC_MANDATORY}}");
			}
			
			if (node.get('mode') == 'create')
			{
				infos.push("{{i18n plugin.cms:CONTENT_COPY_VIEW_TREE_NODE_TOOLTIP_DESC_MODE_CREATE}}");
			}
			else if (node.get('mode') == 'reference')
			{
				infos.push("{{i18n plugin.cms:CONTENT_COPY_VIEW_TREE_NODE_TOOLTIP_DESC_MODE_REFERENCE}}");
			}
			
			if (node.get('type') == 'repeater')
			{
				warns.push("{{i18n plugin.cms:CONTENT_COPY_VIEW_TREE_NODE_TOOLTIP_DESC_REPEATER}}");
			}
			
			var text = this._attributeTooltipTpl.applyTemplate({
	        	description: node.get('description'),
	        	infos: infos,
	        	warns: warns
	        });
			
			tooltipCfg.title = node.get('label');
			tooltipCfg.text= text;
		}
		
		Ext.QuickTips.register(tooltipCfg);
	},

	/**
	 * This method should be called to initialize the tree.
	 * Either after its creation, or to change the current configuration.
	 * The view will be reloaded.
	 */
	initialize: function(config)
	{
		this._isLoading = true;
		
		// content identifier or content type is mandatory.
		this._contentId = config.contentId || null;
		this._contentType = config.contentType || null;
		
		this._viewMode = config.viewMode || 'edition';
		this._viewName = config.viewName || 'default-edition';
        this._fallbackViewName = config.fallbackViewName || 'main';
		
		this._checkMandatory = config.checkMandatory || false;
		this._creationNotAllowed = config.creationNotAllowed || false;
		
		this._checkByDefault = config.checkByDefault != null ? config.checkByDefault : true;
		
		this._inErrorState = true; // wait for first load to remove the error state.
		
		// Load store.
		var rootNode = this.getStore().getRoot();
		if (rootNode.isLoaded())
		{
			rootNode.removeAll();
			this.setRootNode(this.rootNodeConfig);
		}
		
		this.getStore().load({
			callback: function() {
				this._isLoading = false;
				this.unmask();
				
				this.selectAndFocusFirstNode();
			},
			scope: this
		});
	},
	
	/**
	 * Select and focus the first node of the tree if it exists
	 */
	selectAndFocusFirstNode: function()
	{
		var childNodes = this.getRootNode().childNodes;
		if (childNodes[0])
		{
			var firstNode = childNodes[0];
			this.getSelectionModel().select(firstNode);
			this.getView().focusNode(firstNode);
		}
	},
	
	/**
	 * Returns true if the tree in in error state
	 */
	isInErrorState : function ()
	{
		return this._inErrorState;
	},
	
	/**
	 * Retrieves the data of the tree under the form of an object that contains interesting information for each node.
	 * It go through every node of the tree and call the Ametys.plugins.cms.content.tree.ViewTree.ViewNodeModel#getValues method on the node.
	 * See {@link Ametys.plugins.cms.content.tree.ViewTree.ViewNodeModel}
	 * The returned object respect the structure of the tree but only the checked node are present (non checkable, unchecked or error nodes are filtered out).
	 * This is a short example of the returned object :
	 * 		{
	 * 			attributeA: null, // values returned by the getValues method of the attributeA node
	 * 			attributeB: { // values returned by the getValues method of the attributeB node
	 * 				$mode: 'reference'
	 * 			},
	 * 			attributeC: { // values returned by the getValues method of the attributeB node, plus the child attribute nodes because attributeC has children.
	 * 				$mode: 'create',
	 * 				$loaded: true,
	 * 				attributeC1: null, // etc
	 * 				attributeC2: { ... }
	 * 			}
	 * 		}
	 * @return {Object} The described object in the above description or null if the tree is in error state.
	 */
	getValues: function()
	{
		if (this._inErrorState)
		{
			return null;
		}
		
		var me = this;
		var path, sanitizedPath;
		var walker;
		
		// Minimal values
		var values = {
			'$loaded': true
		};
		
		var rootNode = this.getRootNode();
		rootNode.cascadeBy(function(child) {
			
			if (rootNode == child)
			{
				// ignore root node.
				return;
			}
			
			if (child.get('checked') !== true || child.get('type') == 'error')
			{
				// non-checked or error node encountered
				// stop cascading in that branch.
				return false;
			}
			
			path = child.getPath('name') || '';
			sanitizedPath = path.replace(this.self._duplicateSlashes, '/');
			
			walker = values;
			Ext.Array.forEach(sanitizedPath.split('/'), function(attribute, index, array) {
				if (attribute != '')
				{
					// walking through the attribute road.
					if (index < array.length - 1)
					{
						walker = walker[attribute] = walker[attribute] || {};
					}
					else
					{
						// end of the attribute road, put valuable info.
						walker[attribute] = child.getValues()
					}
				}
			});
		});
		
		return values;
	},
	
	// --------------- Tree store and its listeners --------------------//
	
	/**
	 * @protected
	 * Create the tree store
	 * @param {Object} config The tree panel configuration
	 * @return {Ext.data.TreeStore} The created tree store
	 */
	_getTreeStoreCfg: function(config)
	{
		return {
			model: 'Ametys.plugins.cms.content.tree.ViewTree.ViewNodeModel',
			autoLoad: false,
			proxy: {
				type: 'ametys',
				plugin: 'cms',
				url: 'view/definition.json',
				reader: {
					type: 'json',
                    rootProperty: function(data) {
                        if (data.elements)
                        {
                            return Object.values(data.elements);
                        }

                        if (data.type == 'content')
                        {
                            return null;
                        }
                        
                        return [];
                    }
				}
			},
			root: this.rootNodeConfig,
			listeners: {
				beforeload: {fn: this._onBeforeLoad, scope: this},
				load: {fn: this._onLoad, scope: this},
				nodebeforeappend: {fn: this._onBeforeNodeAppend, scope: this},
				nodeappend: {fn: this._onNodeAppend, scope: this}
			}
		};
	},
	
	/**
	 * @private
	 * Listen to the *beforeload* of the tree store
	 * Inject extra parameters to the load request
	 * See {@link Ext.data.TreeStore#event-beforeload}
	 */
	_onBeforeLoad: function (store, operation, eOpts)
	{
		var params = Ext.apply(operation.getParams() || {}, {
			viewMode: this._viewMode,
			viewName: this._viewName,
            fallbackViewName: this._fallbackViewName
		});	
		
		var node = operation.node;
		if (node == null || node.isRoot())
		{
			if (this._contentId)
			{
				Ext.apply(params, {
					contentId: this._contentId
				});
			}
			else if (this._contentType)
			{
				Ext.apply(params, {
					contentType: this._contentType
				});
			}
		}
		else
		{
			// Loading a node representing an attribute of type content.
			Ext.apply(params, {
				contentType: node.get('contentType'),
				viewName: node.get('viewName') || this._viewName,
                fallbackViewName: node.get('fallbackViewName') || this._fallbackViewName
			});
		}
		
		// Cancel the load request if unable to set mandatory parameters.
		if (!params.contentId && !params.contentType)
		{
			return false;
		}
		
		// Set params
		operation.setParams(params);
	},
	
	/**
	 * Listener called after the store is loaded. 
	 * Auto expand the root node and check for errors.
	 * @param {Ext.data.TreeStore} store The tree store
	 * @param {Ext.data.TreeModel[]} records The loaded 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 node that was loaded.
	 * @private
	 */
	_onLoad: function(store, records, successful, operation, node)
	{
		if (successful && !operation.aborted && node.isRoot())
		{
			node.expand();
			
			var errorInFirstLevel = false;
			Ext.Array.each(records, function(record) {
				errorInFirstLevel = record.get('type') == 'error';
				return errorInFirstLevel; // stops iteration if false.
			});
			
			if (!errorInFirstLevel)
			{
				this._inErrorState = false;
			}
		}
	},
	
	/**
	 * @private
	 * Listen to the beforeappend event of the tree store, to perform additional treatments on the record.
	 * @param {Ext.data.NodeInterface} node The current node
	 * @param {Ext.data.NodeInterface} child The child node to be appended
	 */
	_onBeforeNodeAppend: function(node, child)
	{
		// CMS-5144 remove root title attribute in copy content mode
		if (this._checkMandatory && child.get('contentAncestor') == null && child.get('path') == 'title')
		{
			// Cancel the append.
			return false;
		}
	},
	
	/**
	 * @private
	 * Listen to append event, to perform additional treatments on the record.
	 * This method mainly calls the internal initialization method of the appended node.
	 * @param {Ext.data.NodeInterface} node the parent node
	 * @param {Ext.data.NodeInterface} child the newly appended node
	 * @param {Number} index the index of the newly appended node
	 */
	_onNodeAppend: function(node, child, index)
	{
        if (child.get('type') == 'error')
        {
            child.onError();
        }
        else
        {
			// Search for an ancestor of type 'content'.
			// If found, this attribute does not belong to the main content.
			var contentAncestor = child.getParent({
				role: null,
				type: 'content'
			});
			
			child.set('contentAncestor', contentAncestor);
			
			// check mandatory is ignored if there is a content ancestor because this will involve a creation of a new content.
			// In this case, we want mandatory attribute to always be disabled.
			var forceMandatory = (!!contentAncestor || this._checkMandatory);
            child.set('forceMandatory', forceMandatory);

            child.set('checkByDefault', this._checkByDefault);
            
            if (!this._checkByDefault && this._checkMandatory && child.get('mandatory') && this._parentOrSelfNotAnUnselectedContentOrRepeater(node))
            {
                // When all nodes are not checked by default and the current node is mandatory, we need to check the parents unless content ref
                let cursor = node;
                while (cursor != null)
                {
                    cursor.set('checked', true);
                    cursor = cursor.getParent();
                }
            }
            
			// do not allow creation
			if (this._creationNotAllowed == true && child.get('role') == null && child.get('type') == 'content')
			{
				child.setCannotCreate();
			}
			
			child.initializeCheckStatus();
		}
	},
	
	/**
	 * @private
	 * The node is not a content or a repeater and the same for its parents nodes
	 */
	_parentOrSelfNotAnUnselectedContentOrRepeater: function(node)
	{
        let cursor = node;

        while (cursor != null)
        {
            if (cursor.get('type') == 'content' && cursor.get('mode') != 'create'
                || cursor.get('type') == 'repeater')
            {
                return false;
            }  
            cursor = cursor.getParent();
        }
        return true;
    },
	
	// --------------- Tree panel listeners ----------------------//
	
	/**
	 * On tree view ready listener
	 * Intercept and override the onCheckChange method of the view of the tree to be able to get finer control over the check events.
	 * This allows us to intercept check event before the value of the check box changes. This is used for the disabled node, and the tree states nodes.
	 * @param {Ametys.plugins.cms.content.tree.ViewTree} tree the current tree.
	 * @private
	 */
	_onViewReady: function(tree)
	{
		// Handling load mask...
		if (this._isLoading)
		{
			tree.mask();
		}
		
		// Then, real listener logic.
		var view = tree.getView();
		
		// onCheckChange overriding.
		// Calls the Ametys.plugins.cms.content.tree.ViewTree.ViewNodeModel#setChecked method and fire the checkchange event.
		Ext.override(view, {
			onCheckChange: function(event)
			{
                var node = event.record,
				    checked = node.get('checked');
                    
				if (Ext.isBoolean(checked))
				{
					checked = !checked;
					checked = node.setChecked(checked);
					this.fireEvent('checkchange', node, checked);
				}
			}
		});
		
		// 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)
		{
            var node = event.record;
            
			if (node.get('disabled'))
			{
                let ghostCheck = !node.get('disabledChecked'); // it would be better to check if every child is checked... too complex...
                node.set('disabledChecked', ghostCheck);
                node.setChecked(ghostCheck);
				return false;
			}
			else if (node.get('checked') == node.getNextCheckedState())
			{
				node.onInterceptedCheck();
				return false;
			}
		};
		
		Ext.apply(view, {
			onCheckChange: Ext.Function.createInterceptor(view.onCheckChange, onCheckChangeInterceptor)
		});
	},
	
	/**
	 * @private
	 * afteritemcollapse listener. Used when #adaptTreePanelSize is true to adapt the size of the tree.
	 * @param {Ametys.plugins.cms.content.tree.ViewTree.ViewNodeModel} node The collapsed node.
	 */
	_onItemCollapse: function(node)
	{
		if (this.adaptTreePanelSize)
		{
			var tableEl = this.getView().getEl().down('table');
			if (tableEl)
			{
				// Real height should be table height + toolbar height + x-scrollbar height
				// But this is hardcoded for the moment.
				this.setHeight(tableEl.getHeight() + 60);
			}
		}
	},
	
	/**
	 * @private
	 * afteritemexpand listener. Used when #adaptTreePanelSize is true to adapt the size of the tree.
	 * @param {Ametys.plugins.cms.content.tree.ViewTree.ViewNodeModel} node The expanded node. 
	 */
	_onItemExpand: function(node)
	{
		if (this.adaptTreePanelSize)
		{
			var tableEl = this.getView().getEl().down('table');
			if (tableEl)
			{
				// Real height should be table height + toolbar height + x-scrollbar height
				// But this is hardcoded for the moment.
				this.setHeight(tableEl.getHeight() + 60);
			}
		}
	},
	
	
	
	// --------------- Docked items / Toolbar ----------------------//
	
	/**
	 * @protected
	 * Get the dock items
	 * @param {Object} config The initial tree panel configuration
	 * @return {Object[]} The dock items configuration
	 */
	_getDockItemsCfg: function (config)
	{
		var dockedItems = config.dockedItems || [];
		
		var toolbarCfg = this._getToolbarCfg();
		if (toolbarCfg)
		{
			toolbarCfg.dock = toolbarCfg.dock || 'top';
			dockedItems.push(toolbarCfg);
		}
		
		return dockedItems;
	},
	
	/**
	 * @protected
	 * Retrieves the toolbar config object.
	 */
	_getToolbarCfg: function()
	{
		return {
			dock: 'top',
            xtype: 'toolbar',
            layout: { 
                type: 'hbox',
                align: 'stretch'
            },
            defaultType: 'button',
			items: [/*{
				// Summary
				// FIXME must be implemented? As a button or as a popup that shows up when the user validate its action.
				tooltip: "{{i18n CONTENT_COPY_VIEW_BTN_SUMMARY_TOOLTIP}}",
				handler: Ext.bind (this._displaySummary, this),
				text: "{{i18n CONTENT_COPY_VIEW_BTN_SUMMARY_TEXT}}"
			},*/
			{
                xtype: 'tbspacer',
                flex: 1
            },
			{
				// Select all
				tooltip: "{{i18n CONTENT_COPY_VIEW_BTN_SELECT_ALL}}",
				handler: Ext.bind (this._selectAll, this),
				iconCls: 'ametysicon-check51',
				cls: 'a-btn-light'
			}, {
				// Deselect all
				tooltip: "{{i18n CONTENT_COPY_VIEW_BTN_DESELECT_ALL}}",
				handler: Ext.bind (this._deselectAll, this),
				iconCls: 'ametysicon-blank32',
				cls: 'a-btn-light'
			}]
		};
	},

	/**
	 * Function acting as a handler for the 'summary' button.
	 * Displays some information about the node that are currently checked in the tree.
	 * @param {Ext.button.Button} button The button handling this function
	 * @private
	 */
	_displaySummary: function(button)
	{
		// FIXME displayed information should be user friendly if this feature is implemented. 
		names = Ext.Array.map(this.getChecked(), function(node) {
			var path = node.getPath('name') || '';
			return path.replace(this.self._duplicateSlashes, '/') + ' : ' + node.get('text');
		}, this);
		
		Ext.MessageBox.show({
			title: "{{i18n CONTENT_COPY_VIEW_BTN_SUMMARY_TEXT}}",
			msg: names.join('<br/>'),
			buttons: Ext.Msg.OK,
			icon: Ext.MessageBox.INFO
		});
	},
	
	/**
	 * Function acting as a handler for the 'select all' button.
	 * Checks all the button in tree (except for the button that are restricted).
	 * @param {Ext.button.Button} button The button handling this function
	 * @private
	 */
	_selectAll: function(button)
	{
		this.getRootNode().eachChild(function(child) {
			child.setChecked(true);
		});
	},
	
	/**
	 * Function acting as a handler for the 'deselect all' button.
	 * Unchecks all the button in tree (except for the button that are restricted).
	 * @param {Ext.button.Button} button The button handling this function
	 * @private
	 */
	_deselectAll: function(button)
	{
		this.getRootNode().eachChild(function(child) {
			child.setChecked(false);
		});
	}
});