/*
 *  Copyright 2015 Anyware Services
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

/**
 * This class controls a ribbon input field.
 * 
 * - It supports handlers on key events
 * - It supports enabling/disabling upon the current selection (see {@link #cfg-selection-target-id}) and associated rights (see {@link #cfg-rights}).
 * - It supports enabling/disabling upon a focused tool (see {@link #cfg-tool-id})
 * 
 * Note that a property "controlId" is available on the created input. This string references this controller id, that can be retrieve with {@link Ametys.ribbon.RibbonManager#getUI}
 */
Ext.define(
	"Ametys.ribbon.element.ui.FieldController",
	{
		extend: "Ametys.ribbon.element.RibbonUIController",
		
		mixins: ['Ametys.ribbon.element.ui.CommonController'],
		
		statics: {
			/**
			 * @readonly
			 * @property {Number} DEFAULT_LABEL_WIDTH The default width for input label.
			 */
			DEFAULT_LABEL_WIDTH: 80,
			
			/**
			 * @readonly
			 * @property {Number} DEFAULT_TEXTAREA_HEIGHT The default height for textarea label.
			 */
			DEFAULT_TEXTAREA_HEIGHT: 60,
			
			/**
			 * @readonly
			 * @property {Number} DEFAULT_INPUT_WIDTH The default width for input.
			 */
			DEFAULT_INPUT_WIDTH: 190
		},
		
		/**
		 * @property {String} _value The current input value
		 * @private
		 */
		/**
		 * @cfg {String} [value=""] The default value to set to input field
		 */
		/**
		 * @cfg {String} [input-xtype="textfield"] The xtype for input field
		 */
		/**
		 * @cfg {String} [input-type="text"] The type attribute for input field -- e.g. radio, text, password, file, url, mail, ...
		 */
		/**
		 * @cfg {Boolean} [readOnly=false] true to prevent the user from changing the field
		 */
		/**
		 * @cfg {Boolean} [allowDecimals=false] false to allow decimal values
		 */
		/**
		 * @cfg {Number} [label-width=80] The width of label field in pixels
		 */
		/**
		 * @cfg {Number} [width=190] The width of input field in pixels
		 */
		/**
		 * @cfg {Number} height The height of input field in pixels
		 */
		/**
		 * @cfg {Number} [width-small] The width of input field in pixels in "small" layout size
		 */
		/**
		 * @cfg {Number} [width-medium] The width of input field in pixels in "medium" layout size
		 */
		/**
		 * @cfg {Number} [width-large] The width of input field in pixels in "large" layout size
		 */
		/**
		 * @cfg {String} empty-text The default text to place if the field is empty
		 */
		/**
		 * @cfg {Object[]/Object} data 
		 * Valid only when used with a ComboxBox field. See #cfg-input-xtype.
		 * The data of local store used in conjunction with the #cfg-model or #cfg-value-field and #cfg-value-field.
		 * If the format is not an array of model configuration, you will have to use #cfg-data-convert to convert it to such a format.
		 */
		/**
		 * @cfg {String} data-convert A function name that will be called when initializing #cfg-data.
		 * Parameters are the current controller instance and the #cfg-data object, and return value is the transformed array.
		 */
		/**
		 * @cfg {String} [value-field="value"]
		 * 		Valid only when used with a ComboxBox field. See #cfg-input-xtype.
		 * The underlying data value name to bind to the ComboBox. If #cfg-model is not null, this will be ignored.
		 */
		/**
		 * @cfg {String} [display-field="label"] 
		 * 		Valid only when used with a ComboxBox field. See #cfg-input-xtype.
		 * The underlying data field name to bind to the ComboBox. If #cfg-model is not null, this will be ignored.
		 */
		/**
		 * @cfg {String} [model] 
		 * 		Valid only when used with a ComboxBox field. See #cfg-input-xtype.
		 * Name of the Model associated with the store of the ComboBox. 
		 * If null a simple stores with the two-field store #cfg-value-field and #cfg-value-field will be used.
		 */
		/**
		 * @cfg {String/String[]} [template]
		 * 		Valid only when used with a ComboxBox field. See #cfg-input-xtype.
		 * A string or an array of strings to form an Ext.XTemplate for the dropdown menu of ComboBox.
		 */
		
		/**
		 * @cfg {Boolean} [html-encode=false]
		 * 		Valid only when used with a Display field. See #cfg-input-xtype. 
		 *  True to escape HTML in text value when rendering it. Defaults to true.
		 */
		constructor: function(config)
		{
			this.callParent(arguments);

            // Has to be done before initialize, so "initialize" function has access to it
            this.createDataStore();
            
			// Initialize input properties
			this._initialize();
		},
		
		createUI: function(size, colspan)
		{
			if (this.getLogger().isDebugEnabled())
			{
				this.getLogger().debug("Creating new UI field for controller " + this.getId() + " in size " + size + " (with colspan " + colspan + ")");
			}

			var labelWidth = this.getInitialConfig('label') == null || this.getInitialConfig('hideLabel') == 'true' ? 0 : (this.getInitialConfig('label-width') ? Number(this.getInitialConfig('label-width')) : Ametys.ribbon.element.ui.FieldController.DEFAULT_LABEL_WIDTH);
			var width = labelWidth + this._getInputWidth (size) ;
			
			// Is the label going to be on top ?
			var labelOnTop = size == 'large' && this.getInitialConfig('label') != null;
			
			var element = Ext.ComponentManager.create(Ext.apply(
			{
				cls: this.getInitialConfig()['cls'],
				readOnly: this.getInitialConfig()['readOnly'] == "true",
				
				colspan: colspan,
				
				inputType: this.getInitialConfig('input-type') || 'text', 
		    	xtype: this.getInitialConfig('input-xtype') || 'textfield', 

		    	labelWidth: labelWidth,
		    	labelAlign: labelOnTop ? 'top' : 'left',
		    	hideLabel: this.getInitialConfig('label') == null || this.getInitialConfig('hideLabel') == 'true',
		    	
		    	fieldLabel: this.getInitialConfig('hideLabel') == 'true' ? null : this.getInitialConfig('label'), // hideLabel above does not seems to work
		    	name: this.getInitialConfig('name'),
		    	value: this.getInitialConfig('value') || '',
		    	
		    	height: this._getInputHeight(size, labelOnTop), 
		    	width: width,
		    	
		    	emptyText: this.getInitialConfig('empty-text'),
		    	controlId: this.getId(),
		    	
		    	enableKeyEvents:  true,
		    	disabled: this.getInitialConfig('disabled') == "true",

		    	listeners: this._getListeners()
			}, 
			this._getTypeConfig(this.getInitialConfig('input-xtype') || 'textfield'))
		);
			
			this._value = this.getInitialConfig('value') || '';
			
			return element;
		},
		
		createMenuItemUI: function ()
		{
			throw new Error("The method #createMenuItemUI is not supported for " + this.self.getName());
		},
		
		createGalleryItemUI: function ()
		{
			throw new Error("The method #createGalleryItemUI is not supported for " + this.self.getName());
		},
		
		/**
		 * Get the specific configuration object according the input xtype
		 * @param {String} xtype The input xtype
		 * @return {Object} The configuration object to add to the main configuration. Can not be null.
		 * @private
		 */
		_getTypeConfig: function (xtype)
		{
			switch (xtype) 
			{
				case 'numberfield':	
					return this.getNumberConfig();
				case 'checkboxfield':	
				case 'checkbox':	
					return this.getCheckboxConfig();
				case 'combobox':
				case 'combo':
					return this.getComboBoxConfig();
				case 'displayfield':	
					return this.getDisplayConfig();
				default:
					return this.getTextConfig();
			}
		},
		
		/**
		 * This function builds the specific configuration for a combobox field to be added to the main configuration of the UI control
		 * Override this function is you need to specific.
		 * @return {Object} The configuration object to add to the main configuration. Can not be null.
		 * @protected
		 * @template
		 */
		getComboBoxConfig: function ()
		{
			var tpl = null;
			if (this.getInitialConfig('template'))
			{
				tpl = Ext.create('Ext.XTemplate',
						'<tpl for=".">',
							this.getInitialConfig('template').replace(/'/g, '"'),
						'</tpl>'
				);
			}
			
			return Ext.apply({
				store: this.getStore(),
		    	queryMode: 'local',

				forceSelection : true,
				triggerAction: 'all',
				
				editable: false,
				valueField: this.getInitialConfig('value-field') || 'value',
				displayField: this.getInitialConfig('display-field') || 'label',
				
				tpl: tpl
			}, this.getInitialConfig('ui-config'));
		},
		
		/**
		 * This function builds the specific configuration for a number field to be added to the main configuration of the UI control
		 * Override this function is you need to specific.
		 * @return {Object} The configuration object to add to the main configuration. Can not be null.
		 * @protected
		 * @template
		 */
		getNumberConfig: function ()
		{
			return Ext.apply({
				allowDecimals: this.getInitialConfig('allowDecimals') != null ? this.getInitialConfig('allowDecimals') == "true" : false
			}, this.getInitialConfig('ui-config'));
		},
		
		/**
		 * This function builds the specific configuration for text fields to be added to the main configuration of the UI control.
		 * Override this function is you need to specific.
		 * @return {Object} The configuration object to add to the main configuration. Can not be null.
		 * @protected
		 * @template
		 */
		getTextConfig: function ()
		{
			return Ext.apply({
				// TODO regexp
			}, this.getInitialConfig('ui-config'));
		},
		
		/**
		 * This function builds the specific configuration for a display field to be added to the main configuration of the UI control
		 * Override this function is you need to specific.
		 * @return {Object} The configuration object to add to the main configuration. Can not be null.
		 * @protected
		 * @template
		 */
		getDisplayConfig: function ()
		{
			return Ext.apply({
				htmlEncode: this.getInitialConfig('html-encode') ? Boolean(this.getInitialConfig('html-encode')) : false
			}, this.getInitialConfig('ui-config'));
		},
		
		/**
		 * This function builds the specific configuration for a checbox field to be added to the main configuration of the UI control
		 * Override this function is you need to specific.
		 * @return {Object} The configuration object to add to the main configuration. Can not be null.
		 * @protected
		 * @template
		 */
		getCheckboxConfig: function ()
		{
			return Ext.apply({
				hideLabel: true,
				boxLabel: this.getInitialConfig('label')
			}, this.getInitialConfig('ui-config'));
		},
		
		/**
		 * This function creates and returns the data store to be used by combobox field.
		 * This implementation fills the data store upon #cfg-data.
		 * Override this function is you need to specific.
		 * @protected
		 * @template
		 */
		createDataStore: function ()
		{
			var initialData = this.getInitialConfig('data') || [];
			var transformFunction = this.getInitialConfig('data-convert');
			var transformedData = (transformFunction && Ametys.executeFunctionByName(transformFunction, null, null, this, initialData)) || initialData;
			
            var displayField = this.getInitialConfig('display-field') || 'label';
			if (this.getInitialConfig('model'))
			{
				this._store = Ext.create('Ext.data.Store', {
					model: this.getInitialConfig('model'),
					data: transformedData,
					
					listeners: this._getStoreListeners()
				});
			}
			else
			{
				this._store =  Ext.create('Ext.data.Store', {
					fields: [
					         this.getInitialConfig('value-field') || 'value',
					         displayField
					],
					data: transformedData,
                    sorters: [{property: displayField, direction: 'ASC'}],
					
					listeners: this._getStoreListeners()
				});
			}
			
			return this._store;
		},
		
		/**
		 * Get the store used in ComboBox
		 * @return {Ext.data.Store} The store
		 */
		getStore: function ()
		{
			return this._store;
		},
		
		_updateUI: function ()
		{
			var me = this;
			this.getUIControls().each(function (elmt) {
				me._setTooltip(elmt);
			});			
		},
		
		/**
		 * Set the tooltip for component. See Ametys.ui.fluent.Tooltip.
		 * @param {Ext.Component} elmt The component
		 * @private
		 */
		_setTooltip: function (elmt)
		{
            if (elmt.rendered)
            {
	           Ext.QuickTips.register(Ext.apply({target: elmt.getId()}, this._getTooltip())); 
            }
            else
            {
                elmt.on('afterrender', Ext.bind(this._setTooltip, this, [elmt], false), this, { single: true });
            }
		},
		
		/**
		 * Sets a raw data value into the all controller fields ui
		 * @param {String} value The value to set
		 */
		setValue: function (value)
		{
			this._value = value;
			this.getUIControls().each(function (input) {
				// Prevent onchange event
				input.suspendEvent('change');
				input.setValue(value);
				input.resumeEvent('change');
			});
		},
		
		/**
		 * Get the input width from initial configuration and control's size
		 * @param {String} size The size required for the control. Can be 'small', 'very-small' or 'large'.
		 * @return {Number/String} the input width
		 * @private
		 */
		_getInputWidth: function (size)
		{
			var width = this.getInitialConfig()['width'] != null ? Number(this.getInitialConfig()['width']) : Ametys.ribbon.element.ui.FieldController.DEFAULT_INPUT_WIDTH;
			width = this.getInitialConfig()['width-' + size] != null ? Number(this.getInitialConfig()['width-' + size]) : width;
			return width;
		},
		
		/**
		 * Get the input height from initial configuration and control's size
		 * @param {String} size The size required for the control. Can be 'small', 'very-small' or 'large'.
		 * @param {Boolean} labelOnTop is the label going to be on top ?
		 * @return {Number/String} the input height
		 * @private
		 */
		_getInputHeight: function (size, labelOnTop)
		{
			var height = this.getInitialConfig('height') ? Number(this.getInitialConfig('height')) : null;
			height = this.getInitialConfig('height-' + size) != null ? Number(this.getInitialConfig('height-' + size)) : height;
			return height;
		},
		
		/**
		 * Get the handlers to be attached to the field.
		 * @return {Object} The listeners configuration object
		 * @private
		 */
		_getListeners: function ()
		{
			var listeners = {};
			
			listeners['afterrender'] = Ext.bind(this._setTooltip, this);
			
			for (var key in this.getInitialConfig())
			{
				if (/^on(.*)$/.test(key))
				{
					var fn = this.getInitialConfig(key);
					listeners[RegExp.$1] = Ametys.getFunctionByName(fn)
				}
			}
			
			return listeners;
		},
		
		/**
		 * Get the handlers to be attached to the store on the field.
		 * @return {Object} The listeners configuration object
		 * @private
		 */
		_getStoreListeners: function ()
		{
			var listeners = {};
			
			for (var key in this.getInitialConfig())
			{
				if (/^store-on(.*)$/.test(key))
				{
					var fn = this.getInitialConfig(key);
					listeners[RegExp.$1] = Ametys.getFunctionByName(fn)
				}
			}
			
			return listeners;
		}
	}
);