/*
 *  Copyright 2020 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.
 */

/**
 * The widget for comparing two values of any type. It displays the reference value on the left, and the value to be compared on the right.
 * This widget works only if it is added into a 
 */
Ext.define('Ametys.form.widget.Comparison', {
    extend: 'Ametys.form.AbstractFieldsWrapper',
    alias: ['widget.comparator'],
    
    layout: { 
        type: 'hbox',
        align: 'stretch'
    },
    
    statics: {
        /**
         * Compare two values and return the array of modifiers to apply on value
         * Values are consider canonical since [2.1, 2.2] compared to [2.2, 2.1] will return a move of [2.2] for example (and not 1=>2 on first line and 2=>1 on the second line)
         * @param {String/String[]} value The current value
         * @param {String/String[]} comparisonValue The comparison value
         * @param {Boolean} base Is the comparisonValue the old value (right part of the comparison widget) or the future value (left part of the comparison widget)?
         * @param {Function} mappingFunction optionnal mapping function to apply to each element of the incoming values to convert them to a string
         * @return {String[]} An array that have the size of the value array that respectively tell for each value its modification.is new, delete or modified
         *                  When base=true, the array values are "none", "moved", "added".
         *                  When base=false, the array values are "none", "moved", "deleted".
         */
        compareCanonicalValues: function(value, comparisonValue, base, mappingFunction) {
            mappingFunction = mappingFunction || function(v) { return v; };
            
            let uniqueCharIndex = {};
            let uniqueCharIndexCount = 0;
            function _uniqueChar(item)
            {
                var v = uniqueCharIndex[item];
                if (!v)
                {
                    uniqueCharIndex[item] = String.fromCharCode(65 + uniqueCharIndexCount); 
                    uniqueCharIndexCount++;
                }
                
                return uniqueCharIndex[item];
            }
            
            // Compare each array value to a single char (to avoid diff inside values)
            let cValues = Ext.Array.from(value).map(mappingFunction).map(_uniqueChar);
            let cCompareValues = Ext.Array.from(comparisonValue).map(mappingFunction).map(_uniqueChar);

            var a;
            // As diff(a,b) is not the same as diff(b,a) we have to call twice with the same order
            if (base)
            {
                a = HtmlDiff.execute(cCompareValues.join('\n'), cValues.join('\n'));
                a = a.replace(/<del [^>]*>.*?<\/del>/gs, "");
            }
            else
            {
                a = HtmlDiff.execute(cValues.join('\n'), cCompareValues.join('\n'));
                a = a.replace(/<ins [^>]*>.*?<\/ins>/gs, "");
            }
            
            // Prepare reponse
            let response = [];
            
            let results = a.split('\n');
            let index = 1;
            let openedTag = 0;
            for (let result of results)
            {
                if (result)
                {
                    let anyChange;
                    do
                    {
                        anyChange = false;
                        
                        if (result.startsWith('</del>') || result.startsWith('</ins>'))
                        {
                            openedTag--;
                            result = result.substring(6);
                            anyChange = true;
                        }
    
                        
                        if (result.startsWith('<del') || result.startsWith('<ins'))
                        {
                            openedTag++;
                            result = result.substring(result.indexOf('>') + 1);
                            anyChange = true;
                        }
                    }
                    while (anyChange);
                    
                    if (openedTag > 0)
                    {    
                        let uniqueChar = result.substring(0, 1);
                        if (cCompareValues.indexOf(uniqueChar) != -1)
                        {
                            response.push("moved");
                        }
                        else
                        {
                            response.push(base ? "added" : "deleted");
                        }
                    }
                    else
                    {
                        response.push("none");
                    }
                    result = result.substring(1);
                    
                    do
                    {
                        anyChange = false;
                        
                        if (result.startsWith('</del>') || result.startsWith('</ins>'))
                        {
                            openedTag--;
                            result = result.substring(6);
                            anyChange = true;
                        }
    
                        
                        if (result.startsWith('<del') || result.startsWith('<ins'))
                        {
                            openedTag++;
                            result = result.substring(result.indexOf('>') + 1);
                            anyChange = true;
                        }
                    }
                    while (anyChange);
                    
                    index++;
                }
            }

            return response;
        }
    },
    
    /**
     * @property {String} externalizableCls The base class for this field
     * @private
     */
    comparatorFieldCls: 'a-form-comparison-field',
    
    /**
     * @property {String} diffCls The CSS class when field is has difference
     * @private
     */
    diffCls: 'a-form-comparison-field-with-diff',
    
     /**
     * @property {String} itemWrapperCls The base class for fields' wrapper
     * @private
     */
    itemWrapperCls: 'a-form-comparison-item-wrapper',
    
    /**
     * @property {String} leftItemWrapperCls The base class for left field
     * @private
     */
    leftItemWrapperCls: 'a-form-comparison-item-wrapper-left',
    
    /**
     * @property {String} leftItemWrapperCls The base class for right field
     * @private
     */
    rightItemWrapperCls: 'a-form-comparison-item-wrapper-right',
    
    /**
     * @property {Boolean} isComparatorField Flag denoting that this component is a Comparator Field. Always true.
     * @readonly
     */
    isComparatorField: true,
    
    /**
     * @protected
     * @property {Object} _leftValue The reference value (left side)
     */
    /**
     * @protected
     * @property {Object} _rightValue The value to be compared with the reference value (right side)
     */
    
    /**
     * @cfg {String} comparator-wrapped-widget The widget to wrap.
     */
    
    constructor: function(config)
    {
        this._leftField =  this._createField(config);
        this._rightField = this._createField(config);
        
        config.items = [
            {
                // left side
                xtype: 'container',
                flex: 1,
                layout: 'fit',
                cls: [this.itemWrapperCls, this.leftItemWrapperCls],
	            items: [this._leftField],
	            style: {
	                padding: '3px 6px',
                    opacity: 0 // A field with no value should remains hidden (such as unexisting repeater entry)
	            }
            },
            {
                // right side
                xtype: 'container',
                flex: 1,
                layout: 'fit',
                cls: [this.itemWrapperCls, this.rightItemWrapperCls],
                items: [this._rightField],
                style: {
                    padding: '3px 6px',
                    opacity: 0 // A field with no value should remains hidden (such as unexisting repeater entry)
                }
            }
        ];
        
        var cls = Ext.Array.from(config.cls);
        Ext.Array.push(cls, this.comparatorFieldCls);
        
        config.cls = cls;
        
        this.callParent(arguments);
    },
    
    /**
     * @private
     * Creates a wrapped field
     * @param {String} itemId The itemId of the field
     * @param {Object} comparatorConfig The configuration of this field
     * @return {Ext.form.field.Field} The created field
     */
    _createField: function(comparatorConfig)
    {
        var fieldConfig = {
            hideLabel: true,
            readOnly: true,
            msgTarget: 'none',
            preventMark: true,
            cls: ['ametys']
        };
        
        fieldConfig = Ext.applyIf(fieldConfig, comparatorConfig);
        
        delete fieldConfig.ametysDescription;
        delete fieldConfig.ametysDescriptionUrl;
        delete fieldConfig.fieldLabel;
        delete fieldConfig.anchor;
        delete fieldConfig.style;
        delete fieldConfig['comparator-wrapped-widget'];
        
        fieldConfig.resizable = false; // richtext for ex won't work, since Comparison does not watch their size
        
        var xtype = comparatorConfig['comparator-wrapped-widget'];
        var type = comparatorConfig.type;
        
        // Let's check if there is a version of this widget that supports comparisons
        if (!xtype)
        {
            xtype = Ametys.form.WidgetManager.getWidgetXType(null, type, fieldConfig.enumeration !== undefined, fieldConfig.multiple || false);
        }
        
        var finalXType = xtype;
        
        var comparisonXType = xtype + "-comparison";
        if (Ametys.form.WidgetManager.hasWidgetXType(comparisonXType, type, fieldConfig.enumeration !== undefined, fieldConfig.multiple || false))
        {
            finalXType = comparisonXType;
        }
        
        var field = Ametys.form.WidgetManager.getWidget(finalXType, type, fieldConfig);
        field.getErrors = function(/* value */) { return []; };
        
        return field;
    },
    
    /**
     * Set the widget diff state
     * @param {Boolean} state true when right value differs from left value
     */
    setInDiffState: function(state)
    {
        if (state)
        {
            this.addCls(this.diffCls);
            this.markWarning("{{i18n PLUGINS_CORE_UI_WIDGET_COMPARATOR_HAS_MODIFICATIONS}}");
        }
        else
        {
            this.removeCls(this.diffCls);
            this.clearWarning();
        }
    },
    
    renderActiveWarning: function()
    {
        // Do not render warnings
    },
    
    setValue: function(value)
    {
        if (this.form.baseMode === true)
        {
            this._leftValue = value;
            this._leftField.setValue(value);
            this._leftField.ownerCt.setStyle('opacity', 'unset');
            
            if (this._rightField.canDisplayComparisons)
            {
                this._rightField.setComparisonValue(value, true);
            }
        }
        else if (this.form.baseMode === false)
        {
            this._rightValue = value;
            this._rightField.setValue(value);
            this._rightField.ownerCt.setStyle('opacity', 'unset');
            
            if (this._leftField.canDisplayComparisons)
            {
                this._leftField.setComparisonValue(value, false);
            }
        }
        else // if (this.form.baseMode === null)
        {
            // Default values should be ignored
        }
    },
    
    getValue: function(value)
    {
        return '';
    },
    
    getErrors: function(value)
    {
        return [];
    }
});