/*
 *  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 abstract class is used by fields providing a combo box for single or multiple selections with querying and type-ahead support.
 * Implement the #getStore method. 
 */
Ext.define('Ametys.form.AbstractQueryableComboBox', {
    extend: 'Ametys.form.AbstractFieldsWrapper',

    canDisplayComparisons: true,
      
    /**
     * @cfg {Boolean} [multiple=false] True to allow multiple selection.
     */
    multiple: false,
    /**
     * @cfg {Number} [minChars=2] The minimum number of characters the user must type before autocomplete activates.
     */
    minChars: 2,
    /**
     * @cfg {Number} [pageSize=0]  If greater than 0, a Ext.toolbar.Paging is displayed in the footer of the dropdown list and the filter queries will execute with page start and limit parameters.
     */
    pageSize: 0,
    /**
     * @cfg {Number} [maxResult=50]  The maximum number of records to display in the dropdown list.
     */
    maxResult: 50,
    /**
     * @cfg {String} noResultText The text when there is no result found.
     */
	noResultText: "{{i18n PLUGINS_CORE_UI_FORM_QUERYABLE_COMBOBOX_NO_RESULT}}",
	/**
     * @cfg {String} loadingText The text while loading results
     */
	loadingText: "{{i18n PLUGINS_CORE_UI_FORM_QUERYABLE_COMBOBOX_LOADING}}",
	
	/**
     * @cfg {String} emptyText The default text to place into an empty field.
     */
    
    /**
     * @cfg {Number} [valueField=id] The underlying data value name to bind to the ComboBox.
     */
    valueField: 'id',
    /**
     * @cfg {Number} [displayField=label] The underlying data field name to bind to the ComboBox.
     */
	displayField: 'label',
	
	/**
     * @cfg {Boolean/String} [stacked=false] If set to `true`, the labeled items will fill to the width of the list instead of being only as wide as the displayed value.
     */
	/**
     * @cfg {String} [growMin=false] If not set to `false`, the min height in pixels of the box select
     */
	/**
     * @cfg {String} [growMax=false] If not set to `false`, the max height in pixels of the box select
     */
    /**
     * @cfg {String} [queryMode=remote] The query mode in which the ComboBox uses the configured Store
     */
	
    /**
     * @cfg {Boolean} [anyMatch=true] True to allow matching of the typed characters at any position in the valueField's value (with {@link #cfg-queryMode} local only)
     */
    anyMatch: true,
    
	/**
	 * @cfg {Boolean} [triggerOnClick=true] Set to `true` to activate the trigger when clicking in empty space in the field.
	 */
	triggerOnClick: true,
	
	/**
	 * @cfg {Boolean} [hideTrigger=false] Set to `true` to hide the trigger
	 */

    /**
     * @cfg {Boolean} [lines=2] When multiple, the number of line to display
     */
     	
    /**
     * @property {Ext.form.field.Tag} combobox The queryable combobox
     * @private
     */
    
    /**
     * @cfg {Object} [listConfig] A set of properties that will be passed to the boundlist configuration
     */
     
    constructor: function(config)
    {
        config.height = config.height || (config.multiple ? 22 * (config.lines || (config.searchTool ? 1 : 2)) + 2: 24) + this._getHeightDiff(config.labelAlign);
        config.minHeight = config.minHeight || config.height;
        
        config.resizable = (config.resizable === false || config.resizable === "false") ? false : true;
        this._isResizable = config.resizable;
        config.resizable = null; // The default resizable config is for the component version  

        config.editable = (config.editable === false || config.editable === "false") ? false : true;
                
        config.cls = config.cls || "";
        config.cls += " ametys-abqc-field";
        
        this.callParent(arguments);  
    },
    
    /**
     * @private
     * Compute the diff for height
     * @param {String} labelAlign The new label position 
     */
    _getHeightDiff: function(labelAlign)
    {
        var size = 24;
        if (labelAlign == 'top' && !this.heightIncludeLabel)
        {
            this.heightIncludeLabel = true;
            return size;
        } 
        else if (labelAlign != 'top' && this.heightIncludeLabel)
        {
            this.heightIncludeLabel = false;
            return - size;   
        }
        else
        {
            return 0;
        }
    },
    
    setConfig: function(config) 
    {
        var val = this._getHeightDiff(config.labelAlign);
        if (val)
        {
            config.height = (config.height || (this.rendered ? this.getHeight() : this.getInitialConfig('height'))) + val;
            config.minHeight = config.minHeight || config.height;
            
            this.setHeight(config.height);
            this.setMinHeight(config.minHeight);
        }
        
        this.callParent(arguments);
    },
    
    initComponent: function()
    {
        this.items = this.getItems();

        this.callParent(arguments);
    },
    
    /**
     * Get the items composing the fields
     * @return {Object[]} The items
     */
    getItems: function ()
    {
    	this.combobox = Ext.create('Ext.form.field.Tag', this.getComboBoxConfig());
    	
        // https://issues.ametys.org/browse/CMS-8760 [Widget] Typing a comma in the select-referencetable-content widget
        // forcing delimiterRegexp to `null` will prevent a call to #setValue on the input before the comma and to send a wrong request to the server
        this.combobox.delimiterRegexp = null;
        this.combobox.on("afterrender", this._onComboboxRender, this);
        this.combobox.getStore().on("load", this._onComboboxStoreLoaded, this);
        
        var items = [this.combobox];
        
        if (this._isResizable)
        {
            items.push(
                { 
                    xtype: 'splitter',
                    cls: "x-field-aqcb-splitter",
                    height: 0,
                    border: true,
                    performCollapse: false, 
                    collapseDirection: 'top', 
                    collapseTarget: 'prev', 
                    width: 40, 
                    size: '100%', 
                    tracker: { xclass: 'Ametys.form.AbstractQueryableComboBox.SplitterTracker', componentToResize: this } 
                }
            );
        }
        
        return [{
            xtype: 'container',
            flex: 1,
            layout: {
                type: 'vbox',
                align: 'stretch'
            },
            itemId: 'items',
            items: items
        }];
    },
    
    /**
     * @private
     * Listener after render
     */
    _onComboboxRender: function()
    {
        if (this.multiple)
        {
            var me = this.combobox,
                ddGroup = 'ametys-box-select-' + me.getId();

            new Ext.dd.DragZone(me.listWrapper, { 
                ddGroup: ddGroup,
                
                getDragData: function(e) 
                {
                    var sourceEl = e.getTarget(".x-tagfield-item", 10), d;
                    if (sourceEl) 
                    {
                        d = sourceEl.cloneNode(true);
                        d.id = Ext.id();
                        return (me.dragData = {
                                sourceEl: sourceEl,
                                repairXY: Ext.fly(sourceEl).getXY(),
                                ddel: d,
                                rec: me.getRecordByListItemNode(sourceEl)
                        });
                    }               
                },
                getRepairXY: function() 
                {
                    return me.dragData.repairXY;
                }
            });
            
            new Ext.dd.DropZone(me.listWrapper, { 
                ddGroup: ddGroup,
                
                getTargetFromEvent: function(e) 
                {
                    return e.getTarget('.x-tagfield-item') || e.getTarget('.x-tagfield-input') || e.getTarget('.x-tagfield-list');
                },
                
                onNodeEnter : function(target, dd, e, data)
                {
                    var t = Ext.fly(target);
                    var r = t.getRegion();
                    
                    if (t.hasCls('x-tagfield-item') && e.getX() > r.left + (r.right - r.left) / 2)
                    {
                        t.removeCls('x-tagfield-target-hoverbefore');
                        t.addCls('x-tagfield-target-hoverafter');
                    }
                    else
                    {
                        t.addCls('x-tagfield-target-hoverbefore');
                        t.removeCls('x-tagfield-target-hoverafter');
                    }
                },
                
                onNodeOut : function(target, dd, e, data)
                {
                    Ext.fly(target).removeCls(['x-tagfield-target-hoverbefore', 'x-tagfield-target-hoverafter']);
                },
                
                onNodeOver : function(target, dd, e, data)
                {
                    this.onNodeEnter(target, dd, e, data);
                    
                    return Ext.dd.DropZone.prototype.dropAllowed;
                },
                
                onNodeDrop : function(target, dd, e, data)
                {
                    var targetRecord;
                    var t = Ext.get(target);
                    if (t.hasCls("x-tagfield-item"))
                    {
                        targetRecord = me.getRecordByListItemNode(target)
                    }
                    
                    var currentValue = me.getValue();

                    var movedValue = data.rec.get(me.valueField);
                    var newPosition = targetRecord != null ? currentValue.indexOf(targetRecord.get(me.valueField)) : currentValue.length;
                    var currentPosition = currentValue.indexOf(movedValue);
                    
                    currentValue = Ext.Array.remove(currentValue, movedValue);
                    newPosition += (newPosition <= currentPosition ? 0 : -1) + (newPosition != currentPosition && t.hasCls('x-tagfield-target-hoverafter') ? 1 : 0);
                    currentValue = Ext.Array.insert(currentValue, newPosition, [movedValue]);
                    
                    // This is to avoid setValue to be pointless
                    me.suspendEvents(false);
                    me.setValue(null);
                    me.resumeEvents();
                    
                    me.setValue(currentValue);
                    
                    return true;
                }
            });
        }
    },
    
    /**
     * Get select combo box
     * @return {Ext.form.field.Tag} The box select
     * @private
     */
    getComboBoxConfig: function ()
    {
    	var minChars = this.minChars || 3;
        if (Ext.isString(minChars))
        {
            minChars = parseInt(this.minChars);
        }
        
        return {
            queryMode: this.queryMode || 'remote',
            anyMatch: this.anyMatch,
            minChars: minChars,
            delimiter: ',',
            
            style: {
                marginBottom: 0
            },
            
            autoLoadOnValue: true,
            encodeSubmitValue: true,
            editable: this.editable,
            selectOnFocus: this.editable,
            autoSelect: false,
            multiSelect: this.multiple,
            
            pageSize: this.pageSize,
            
            growMin: this.growMin && !isNaN(parseInt(this.growMin)) ? parseInt(this.growMin) : null, 
            growMax: this.growMax && !isNaN(parseInt(this.growMax)) ? parseInt(this.growMax) : null, 
            grow: false,
            
            stacked: this.stacked || this.stacked == 'true',
            store: this.getStore(),
            valueField: this.valueField,
            displayField: this.displayField,
            
            listeners: {
                'beforedestroy': function(combo) {
                    combo.store.destroy();
                }
            },
            
            labelTpl: this.getLabelTpl(),
            tipTpl: this.getTipTpl(),
            labelHTML: true,
            flex: 1,
            allowBlank: this.allowBlank,
            
            emptyText: this.emptyText,
            
            listConfig: Ext.applyIf(this.listConfig || {}, {
	            loadMask: true,
	            loadingText: this.loadingText,
	            emptyText: '<span class="x-tagfield-noresult-text">' + this.noResultText + '<span>',
	            enterCanDeselect: false
            }),
            
            readOnly: this.readOnly || false,
            triggerOnClick: !this.editable ? true : this.triggerOnClick,
            hideTrigger: this.hideTrigger
        };
    },
    
    /**
     * Get the remote store. 
     * Should ALWAYS be a newly created store, since destroying this component will destroy the store.
     * @return {Ext.data.Store} The remote store.
     * @protected
     * @template
     */
    getStore: function ()
    {
    	throw new Error("The method #getStore is not implemented in " + this.self.getName());
    },
    
    /**
     * Get the template for selected field
     * @protected
     * @template
     */
    getLabelTpl: function ()
    {
    	return null;
    },
    
    /**
     * Get the tooltip template for selected field
     * @protected
     * @template
     */
    getTipTpl: function()
    {
        return undefined; // no tip
    },
       
    markInvalid: function (msg)
    {
        this.callParent(arguments);
        
         // Some widgets that inherit from this class might have the combobox set to null depending on the context
        if (this.combobox)
        {
            this.combobox.markInvalid(msg);
        }
    },
    
    /**
     * Specifically focus the box select field when an item is selected.
     * @param {Ext.form.field.Tag} field The combo box field.
     * @param {Ext.data.Model[]} records The selected records.
     * @private
     */
    _onValueSelectionChange: function(field, records)
    {
        // Focus the field when a box item is selected, so that we always get the blur event.
        if (records.length > 0)
        {
            this.focus();
        }
    },
    
    /**
     * @inheritdoc
     * Sets a data value into the field and update the comboxbox field
     */
    setValue: function (value)
    {
        var me = this;
        
    	value = Ext.isEmpty(value) ? [] : Ext.Array.from(value);
        
        // Values can be either String (of id) or the model
        var ids = [];
        Ext.Array.each(value, function(v) {
            if (Ext.isString(v))
            {
                ids.push(v);
            }
            else
            {
                ids.push(v[me.valueField]);
                me.combobox.getStore().add(v);
            }
        });

    	me.callParent([ids]);
    	me.combobox.setValue(ids);
    },
    
    /**
     * @protected
     * Convert a value to be comparable
     */
    _convertToComparableValue: function(item)
    {
        if (!item)
        {
            return "";
        }
        else if (Ext.isString(item))
        {
            return item;
        }
        else
        {
            return item[this.valueField]; // this is a guess since value are never records
        } 
    },
    
    _updateComparisonRendering: function()
    {
        if (!this.combobox || !this.combobox.bodyEl)
        {
            return;
        }
        
        if (this._baseValue !== undefined || this._futureValue !== undefined)
        {
            var base = this._baseValue !== undefined;
            
            var comparison = Ametys.form.widget.Comparison.compareCanonicalValues(this.getValue(), base ? this._baseValue : this._futureValue, base, Ext.bind(this._convertToComparableValue, this));
            for (var c = 1; c <= comparison.length; c++)
            {
                var elt = this.combobox.bodyEl.query('.x-tagfield-item:nth-child(' + c + ')', false, true);
                if (elt == null)
                {
                    // not really rendered
                    return;
                }
                elt.removeCls(["ametys-tagfield-new", "ametys-tagfield-old", "ametys-tagfield-mod"]);
                switch (comparison[c-1])
                {
                    case "added": elt.addCls("ametys-tagfield-new"); break;
                    case "moved": elt.addCls("ametys-tagfield-mod"); break;
                    case "deleted": elt.addCls("ametys-tagfield-old"); break;
                    default:
                    case "none": break;
                }
            }
        }
    },
    
    afterRender: function()
    {
        this.callParent(arguments);
        
        this._updateComparisonRendering();
    },
    
    /**
      * When used in readonly mode, settting the comparison value will display ins/del tags
      * @param {String} otherValue The value to compare the current value with
      * @param {boolean} base When true, the value to compare is a base version (old) ; when false it is a future value
      */
    setComparisonValue: function(otherValue, base)
    {
         if (base)
         {
             this._baseValue = otherValue || null;
             this._futureValue = undefined;
         }
         else
         {
             this._baseValue = undefined;
             this._futureValue = otherValue || null;
         }
         this.combobox.updateValue();
    },    
    
    _onComboboxStoreLoaded: function()
    {
        window.setTimeout(Ext.bind(this._updateComparisonRendering, this), 1);
    },
    
    getValue: function ()
    {
        if (this.combobox.getStore().isLoading() || !this.combobox.getStore().isLoaded())
        {
            return this.multiple ? this.value : (this.value && this.value.length > 0 ? this.value[0] : null);
        }
        else
        {
            // tag field is now always return multiple values, but that's not fine for ours widgets
            var value = this.combobox.getValue();
    	    return this.multiple ? value : (value && value.length > 0 ? value[0] : null);
        }
    },
    
    getErrors: function (value) 
    {
    	value = value || this.getValue();

    	// Some widgets that inherit from this class might have the combobox set to null depending on the context
        if (this.combobox)
        {
            return Ext.Array.merge(this.callParent(arguments), this.combobox.getErrors(value));
        }
        else
        {
			return this.callParent(arguments);
        }
    },
    
    getSubmitValue: function ()
    {
    	return this.multiple ? Ext.encode(this.getValue()) : (this.getValue() || '');
    },
    
    getReadableValue: function (separator)
    {
    	separator = separator || ',';
    	
    	var readableValues= [];
    		
    	var value = this.combobox.getValue();
    	if (value && Ext.isArray(value))
    	{
    		var readableValues= []
    		for (var i=0; i < value.length; i++)
    		{
    			var index = this.combobox.getStore().find(this.valueField, value[i]);
    			var r = this.combobox.getStore().getAt(index);
    			if (r)
    			{
    				readableValues.push(Ext.String.escapeHtml(r.get(this.displayField)));
    			}
    		}
    	}
    	else if (value)
    	{
    		var index = this.combobox.getStore().find(this.valueField, value);
			var r = this.combobox.getStore().getAt(index);
			if (r)
			{
				readableValues.push(Ext.String.escapeHtml(r.get(this.displayField)));
			}
    	}
    	
    	return readableValues.join(separator);
    }
});