/*
 *  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 is the model for nodes of a view tree. See {@link Ametys.plugins.cms.content.tree.ViewTree}
 * These node contains an precise internal logic that is strongly tied to the tree.
 * @private
 */
Ext.define('Ametys.plugins.cms.content.tree.ViewTree.ViewNodeModel', { 
	extend: 'Ext.data.Model',
	
	fields: [
		// Common properties
		{name: 'text', mapping: 'label', type: 'string'}, 
		{name: 'expanded', type: 'boolean', defaultValue: false},
		
		// Fields for attribute //
		{name: 'type', defaultValue: null},
		{name: 'name', defaultValue: null},
		{name: 'label', defaultValue: null},
		{name: 'description', defaultValue: null},
        {name: 'path', defaultValue: null},
		{name: 'mandatory', mapping: 'validation.mandatory',  type: 'boolean', defaultValue: false},
		{name: 'contentAncestor', type: 'auto', defaultValue: null} /* A reference to the nearest ancestor which attribute type is content (if any) */,
		// end attribute //
		
		// Fields for CONTENT attribute
		{name: 'contentType', defaultValue: null},
		{name: 'mode', defaultValue: null} /* reference or create */, 
		{name: 'canCreate', type: 'boolean', defaultValue: true},
		{name: 'viewName', defaultValue: null},
        {name: 'fallbackViewName', defaultValue: null},
		{name: 'viewMode', defaultValue: null},
		// end CONTENT attribute
		
		// Field for fieldset
		{name: 'role', defaultValue: null},
		// end fieldset
		
		{name: 'disabled', type: 'boolean', defaultValue: false},
		{name: 'forceMandatory', type: 'boolean', defaultValue: false} /* true if this attribute have to be always checked */,
		
		// Node interface properties with custom logic.
		{
			name: 'checked',
			type: 'auto',
			defaultValue: null,
            depends: ['disabled'],
			convert: function(value, node)
			{
                if (!node) 
                { 
                    return value; 
                }
				// changing checked state is not allowed if the node is disabled.
				return node.get('disabled') ? node.get('checked') : value;
			}
		}, {
			name: 'iconCls', 
            depends: ['type', 'role'],
			convert: function (value, node)
			{
                if (!node) 
                { 
                    return value; 
                }

                if (node.get('role') != null)
                {
                    return 'tree-icon-fieldset';
                }
                else
                {
                    return 'tree-icon-' + node.get('type');
                }
			}
		}, {
			name: 'cls',
            depends: ['type', 'role'],
			// If value is passed (and not falsy), value for 'cls' will be set to value. Else, it will be set to the defaut value for this node type.
			convert: function (value, node)
			{
                if (!node || value)
				{
					return value;
				}
				else
				{
                    var cls = [];
                    
                    if (node.get('role') != null)
                    {
                        cls.push('tree-cell-fieldset');
                    }
                    else if (node.get('type') != 'error')
                    {
                        cls.push('tree-cell-attribute');
                        cls.push('tree-cell-' + node.get('type'));
                    }
                    
                    return cls.join(' ');
				}
			}
		}
	],
	
	/**
	 * Return an object holding the interesting parameters for this node.
	 * @return {Object} A value object for this node.
	 */
	getValues: function()
	{
		var values = null;
		
		var isContentMeta = this._isContentAttribute();
		if (isContentMeta)
		{
			values = {};
			
			if (isContentMeta)
			{	
				values = { '$mode': this.get('mode') };
			}
			
			var viewName = this.get('viewName');
			if (viewName)
			{
				values['$viewName'] = viewName;
			}
            
            var fallbackViewName = this.get('fallbackViewName');
            if (fallbackViewName)
            {
                values['$fallbackViewName'] = fallbackViewName;
            }
			
			if (this.isLoaded())
			{
				values['$loaded'] = true;
			}
		}
		
		return values;
	},

	/**
	 * Retrieves the nearest parent that match with the config object passed as argument.
	 * @param {Object} config To be eligible, a parent should have its properties match each properties of this object.
	 * @return {Ametys.plugins.cms.content.tree.ViewTree.ViewNodeModel} The matching parent or null.
	 */
	getParent: function(config)
	{
		var parent = this.parentNode;
		
		if (!config || Ext.Object.getSize(config) === 0)
		{
			return parent;
		}
		
		var match = false;
		while (!match && parent)
		{
			Ext.Object.each(config, function(key, value) {
				match = parent.get(key) === value;
				return match; // if false, iteration is stopped.
			});
			
			if (!match)
			{
				parent = parent.parentNode;
			}
		}
		
		return parent;
	},
	
	/**
	 * Function to call when a node enter in error state.
	 */
	onError: function()
	{
		if (this.parentNode.get('type') == 'content')
		{
			this.parentNode.setCannotCreate();
		}
		this._updateNodeInfos();
	},
	
	/**
	 * Prevent the 'create' mode for the node
	 * @private
	 */
	setCannotCreate: function()
	{
		// only applicable to node representing a content attribute.
		this.set('canCreate', false);
		
		this.initializeCheckStatus();
	},
	
	/**
	 * Initialize (or re-initialize) a node and the status of its check box.
	 */
	initializeCheckStatus: function()
	{
        if (this.get('checked') != null)
        {
            return;
        }
        
		var checked = this.getInitialCheckValue();
		if (checked != null)
		{
			this.set('checked', checked);
			
			if (this._isContentAttribute())
			{
				if (checked == true)
				{
					// Set 'reference' mode for attribute of type content
					this.setMode('reference');
				}
				else
				{
					this.setMode('');
				}
			}
			
			if (this._isDisabled())
			{
				this.setDisabled();
			}
			
            if (this.parentNode)
            {
                this.parentNode.onChildInitialized();
            }
		}
		
		if (this._isMandatory())
		{
			this.setMandatoryCls();
		}
		
		this._updateNodeInfos();
		// mark as not dirty anymore. This is to avoid a undesired visual feedback when the text property is modified.
		this.commit();
	},
	
	/**
	 * Determines if the node should be marked as mandatory
	 * @return true if the node should be marked as mandatory
	 * @private
	 */
	_isMandatory: function ()
	{
		return (this.get('checked') !== false && this.get('mandatory')) || (this.get('checked') === true && this.get('disabled'))
	},
	
	/**
	 * Determines if it is an attribute of type "content"
	 * @return true if the type is "content"
	 * @private
	 */
	_isContentAttribute: function ()
	{
		return this.get('type') == 'content';
	},
	
	/**
	 * Determines if the content node is in create mode
	 * @return true if the content node is in create mode 
	 */
	_isCreateMode: function ()
	{
		return this.get('mode') == 'create';
	},
	
	/**
	 * Indicates if the node should be disabled or node, given its properties.
	 * @return {Boolean} true if the node must be disabled.
	 * @private
	 */
	_isDisabled: function()
	{
		if (this._isContentAttribute() && this.get('canCreate'))
		{
			// Attribute node of type 'content' is never disabled except if create mode is not allowed
			return false;
		}
		
		if (this.get('path') == 'title' && this.parentNode == this.get('contentAncestor')) // (this.parentNode.isRoot() && forceMandatory) || this.parentNode == this.get('contentAncestor'))
		{
			// Title attribute must be always disabled when a content creation is involved.
			return true;
		}
		
		return this.get('forceMandatory') && this.get('mandatory');
	},
	
	/**
	 * Retrieves the initial value for the check box of this node.
	 * @return {Boolean} true/false correspond to check/unchecked value for the box. If null, the box does not have a checkbox in its initial state.
	 * @private
	 */
	getInitialCheckValue: function()
	{
		var checked = this.get('checked');
		if (this.get('canCreate') == false && checked != null)
		{
			// do not change
			return checked;
		}
		
		var contentAncestor = this.get('contentAncestor');
		if (contentAncestor && contentAncestor.get('mode') != 'create')
		{
			// Remove checkbox
			return null;
		}
		
        if (this.get('contentAncestor') == null && this.get('path') == 'title')
        {
            return false;
        }
        
        if (this.get('forceMandatory') && this.get('mandatory'))
        {
            return true;
        }
        
		return this.get('checkByDefault');
	},
	
	/**
	 * Setter for the disabled attribute.
	 * The set('disabled, true/false) method should never be used on a node. This method must be used.
	 * @param {Boolean} value the disable value, true to disable, false to enable.
	 */
	setDisabled: function(value)
	{
		var oldDisabled = this.get('disabled');
		
		var disable = value !== false;
		this.set('disabled', disable);
		
		if (oldDisabled != this.get('disabled'))
		{
			// Update css classes
			var disabledCls = 'tree-cell-disabled';
			if (disable === true)
			{
				var cls = this.get('cls');
				cls += (cls ? ' ' : '') + disabledCls;
				this.set('cls', cls);
			}
			else
			{
				clsArray = this.get('cls').split(/\s+/);
				Ext.Array.remove(clsArray, disabledCls);
				this.set('cls', clsArray.join(' '));
			}
		}
	},
	
	/**
	 * Setter for the CSS class that denote a mandatory node.
	 * @param {Boolean} value true if mandatory, false to remove this class.
	 */
	setMandatoryCls: function(value)
	{
		var set = value !== false;
		
		// Update css classes
		var mandatoryCls = 'tree-cell-mandatory';
		var cls = '';
		
		if (set === true)
		{
			cls = this.get('cls');
			cls += (cls ? ' ' : '') + mandatoryCls;
		}
		else
		{
			clsArray = this.get('cls').split(/\s+/);
			Ext.Array.remove(clsArray, mandatoryCls);
			cls = clsArray.join(' ');
		}
		
		this.beginEdit();
		this.set('cls', cls);
		this.endEdit();
		
		// mark as not dirty anymore. This is to avoid a undesired visual feedback when the text property is modified.
		this.commit();
	},
	
	/**
	 * Update the node informations (text and tooltip)
	 * @private
	 */
	_updateNodeInfos: function()
	{
		this._updateNodeText();
		this.commit();
	},
	
	/**
	 * Update the node text
	 * @private
	 */
	_updateNodeText: function ()
	{
		if (this.get('type') == 'error')
		{
			this.set('text', "{{i18n plugin.cms:CONTENT_COPY_VIEW_TREE_NODE_ERRORTYPE}}");
		}
		else
		{
			var text = this.get('label');
			
			if (this._isMandatory())
			{
				text += ' <span class="mandatory-highlight">*</span>';
			}
			
			// details depending on the mode
			if (this.get('mode') == 'create')
			{
				text += ' <span class="title-mode">' + "{{i18n plugin.cms:CONTENT_COPY_VIEW_TREE_NODE_TITLE_MODE_CREATE}}" + '</span>';
			}
			else if (this.get('mode') == 'reference')
			{
				text += ' <span class="title-mode">' + "{{i18n plugin.cms:CONTENT_COPY_VIEW_TREE_NODE_TITLE_MODE_REFERENCE}}" + '</span>';
			}
			
			this.set('text', text);
		}
	},
	
	/**
	 * Function called on the parent when one of its child is initialized.
	 * Ensure the consistency of the disabled attribute of the parent.
	 * The parent will bubble the method call to its own parent if its status has changed
	 * @private
	 */
	onChildInitialized: function()
	{
		if (this.get('checked') && (this.get('type') == 'composite' || this.get('role') != null))
		{
			if (this.hasChildNodes())
			{
				var oldDisabled = this.get('disabled');
				var hasChildLocked = false;
				
				this.eachChild(function(child) {
					if (!child._checkedStateCanChange())
					{
						hasChildLocked = true;
						return false; // stop iteration
					}
				});
				
				this.setDisabled(hasChildLocked);
				
				if (oldDisabled != this.get('disabled') && this.parentNode)
				{
					this.parentNode.onChildInitialized();
				}
			}
		}
	},
	
	/**
	 * Retrieves the next checked value for the current node.
	 * For a lot of node, the cycle is true -> false -> true -> false etc..
	 * But the content attribute can have a tree state cycle of the type true -> true -> false etc... (see 'mode' attribute)
	 * @return {Boolean} its next checked value.
	 */
	getNextCheckedState: function()
	{
		var checked = this.get('checked');
		
		// Attribute node of type content have tree states:
		// false / true by reference / true create new content.
		if (checked
			&& this.get('type') == 'content'
			&& this.get('canCreate')
			&& (this.get('mode') == 'reference' || (this.get('mode') == 'create' && this.get('forceMandatory') && this.get('mandatory'))))
		{
			// When the node is in mode true by reference, its next checked is
			// still true (and its mode will be set to 'create')
			// When the node is in mode 'create', its next checked state is false
			// except if the node marked as mandatory, in which case it should 
			// switch to the reference mode
			return checked;
		}
		
		return !checked;
	},
	
	/**
	 * Called when a check as been intercepted. This is used to update the internal attributes of the node with changing its state value.
	 */
	onInterceptedCheck: function()
	{
		this.goToNextState();
	},
	
	/**
	 * Update the mode of the node.
	 * Only applicable to content attribute.
	 * Standard cycle is : 'reference' -> 'create' -> ''. Where empty mode correspond to the unchecked check box.
	 * But there are exception when a node cannotCreate ('create' mode is not allowed)
	 * or when a mode is mandatory and mandatory are restricted ('' mode is not allowed).
	 * @private
	 */
	goToNextState: function()
	{
		// Currently only attribute node of type content manage an internal state.
		if (this.get('type') == 'content')
		{
			var oldMode = this.get('mode');
			
			switch(oldMode)
			{
				case 'reference':
					if (this.get('canCreate'))
					{
						this.setMode('create');
					}
					else
					{
						this.setMode('');
					}
					break;
				case 'create':
					if (this.get('forceMandatory') && this.get('mandatory'))
					{
						this.setMode('reference');
					}
					else
					{
						this.setMode('');
					}
					break;
				case '':
					this.setMode('reference');
					break;
			}
			
			this.onModeChanged(oldMode, this.get('mode'));
		}
	},
	
	/**
	 * Setter for the mode attribute. Only this method should be used.
	 * Also update the CSS classes of the node.
	 * @param {String} value the mode to set. 
	 */
	setMode: function(value)
	{
		var oldMode = this.get('mode');
		this.set('mode', value);
		
		if (oldMode != this.get('mode'))
		{
			// Update css classes
			var clsPrefix = 'tree-cell-mode-';
			
			clsArray = this.get('cls').split(/\s+/);
			clsToRemove = Ext.Array.filter(clsArray, function(cls, index) {
				return Ext.String.startsWith(cls, clsPrefix);
			});
			
			Ext.Array.forEach(clsToRemove, function(cls) {
				Ext.Array.remove(clsArray, cls);
			});
			
			if (value)
			{
				clsArray.push(clsPrefix + value);
			}
			
			this.set('cls', clsArray.join(' '));
		}
		
		this._updateNodeInfos();
		
		// mark as not dirty anymore. This is to avoid a undesired visual feedback when the text property is modified.
		this.commit();
	},
	
	/**
	 * Called when the mode of a node change.
	 * Cascade into the children of the node to update their current state (add/remove checkbox, initialization etc...)
	 * @param {String} oldMode the previous mode of the node
	 * @param {String} newMode the new mode of the node.
	 * @private
	 */
	onModeChanged: function(oldMode, newMode)
	{
		// Currently only attribute node of type content manage an internal state.
		if (this.get('type') == 'content')
		{
			var me = this;
			
			if (oldMode == 'create')
			{
				this.cascadeBy(function(child) {
					if (child == me) return; // ignore current node
					
					if (child.get('checked') != null)
					{
						child.setDisabled(false);
						child.set('checked', null);
						
						// mode management for attribute of type content.
						if (this._isContentAttribute())
						{
							child.setMode('');
						}
					}
					else
					{
						return false; // stop cascading in that branch.
					}
				});
			}
			
			if (newMode == 'create')
			{
				this.cascadeBy(function(child) {
					if (child == me) return; // ignore current node

					
					if (child.get('checked') == null)
					{
                        child.getOwnerTree()._onNodeAppend(child.getParent(), child);
						if (this._isContentAttribute())
						{
							return false; // stop cascading in that branch.
						}
					}
					else
					{
						return false; // stop cascading in that branch.
					}
				});
			}
		}
	},
	
	/**
	 * Setter for the 'checked' attribute of the node. Only this method should be used.
	 * Update the current value of the node, propage the changes in its childre and bubble the change in the parents.
	 * @param {Boolean} checked The checked value of the checkbox. null to not display a checkbox.
	 */
	setChecked: function(checked)
	{
		var oldChecked = this.get('checked');
		var newChecked;
		
		if (oldChecked == null)
		{
			return oldChecked;
		}
		
		// propagate in children
		if (checked == false && this._isContentAttribute() && this._isCreateMode())
		{
			// nothing to propagate, as child nodes won't be checkable anymore.
			this._updateCheckedState(checked);
		}
		else
		{
			this.cascadeBy(function(child) {
				newChecked = child._updateCheckedState(checked);
				
				if (newChecked == null)
				{
					// stop cascading in that branch.
					return false;
				}
                
                /*
                Since CMS-10958, this part is commented because it prevents to uncheck a parent node having a checked and disabled child (like mandatory attribute).
                Then all children keep their checked state.
                It seems to work correctly for now.
                
                // If checked state didn't change for this child, parents checked state must be restored.
                // So, bubble in parent.
                if (child.parentNode.lastChild === child)
                {
                    child._bubbleCheck();
                }
                */
			});
		}
		
		newChecked = this.get('checked');
		if (newChecked != oldChecked)
		{
			this._bubbleCheck();
		}
		
		return this.get('checked');
	},
	
	/**
	 * Internal method used to bubble check change in parent nodes.
	 * @private
	 */
	_bubbleCheck: function()
	{
		// ParentNode must be checkable and its checked state must not be locked.
		var parentNode = this.parentNode;
		if (parentNode && parentNode.get('checked') != null && parentNode._checkedStateCanChange())
		{
			var oldChecked = parentNode.get('checked');
			var hasChildChecked = false;
			
			var oneCheckedChildCanChange = false; 
			
			parentNode.eachChild(function(child) {
				if (child.get('checked'))
				{
					hasChildChecked = true;
					
					if (child._checkedStateCanChange()) 
                    { 
                        oneCheckedChildCanChange = true; 
                        return false; // stop iteration 
                    }
				}
			});
			
			var parentType = parentNode.get('type');
			
			// Also do not bubble an uncheck if non mandatory repeater or attribute of type content
			// because the user might want to select those without any child (ie. empty repeater or empty content).
		    if (oneCheckedChildCanChange || (parentType != 'repeater' && parentType != 'content'))
			{
				var newChecked = parentNode._updateCheckedState(hasChildChecked);
				
				if (oldChecked != newChecked)
				{
					parentNode._bubbleCheck();
				}
			}
		}
	},
	
	/**
	 * Internal helper method indicated if a node can change its checked value.
	 * Disabled and (sometimes) mandatory cannot change there checked value.
	 * @return {Boolean} true if the checked attribute of the node can change.
	 * @private
	 */
	_checkedStateCanChange: function()
	{
		return !this.get('disabled') && !(this.get('forceMandatory') && this.get('mandatory'));
	},
	
	/**
	 * Internal method to update the current value of the checked attribute of a node.
	 * Ensure the integrity of the 'checked' and 'mode' attributes of this node.
	 * @param {Boolean} checked the desired checked value for the node.
	 * @return {Boolean} The new checked value of the node.
	 * @private
	 */
	_updateCheckedState: function(checked)
	{
		var oldChecked = this.get('checked');
		if (oldChecked == null)
		{
			return oldChecked;
		}
		
		if (this._checkedStateCanChange())
		{
			this.set('checked', checked);
		}
		
		var newChecked = this.get('checked');
		this._checkNodeStateIntegrity(checked, newChecked);
		
		return newChecked;
	},
	
	/**
	 * Ensure the integrity of the 'mode' attribute of this node when its 'checked' attribute has recenlty changed.
	 * @param {Boolean} desiredCheck the desired checked value for the node during its last update.
	 * @param {Boolean} checked The checked value that has been set to the node. Can differs from the desiredCheck under some circumstances (disabled node, tree states etc..).
	 * @private
	 */
	_checkNodeStateIntegrity: function(desiredCheck, checked)
	{
		if (this.get('type') == 'content')
		{
			var oldMode = this.get('mode');
			var newMode = null;
			
			if (checked && oldMode == '')
			{
				newMode = 'reference';
			}
			else if (!checked && oldMode != '')
			{
				newMode = '';
			}
			// If uncheck was desired but the node cannot changed its state, the node mode is always reset to 'reference' (instead of '').
			else if (!this._checkedStateCanChange() && oldMode == 'create' && desiredCheck == false)
			{
				// reset to reference mode
				newMode = 'reference';
			}
			
			if (newMode != null && oldMode != newMode)
			{
				this.setMode(newMode);
				this.onModeChanged(oldMode, newMode);
			}
		}
	}
});