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

/**
 * Custom form Panel for advanced search.
 */
Ext.define('Ametys.plugins.cms.search.advanced.AdvancedSearchFormPanel', {
    extend: 'Ext.form.Panel',
    
    /**
     * @private
     * @property {Number} _fieldIndex the field index used to identify the fields
     */
    
    /**
     * @private
     * @property {Number} _rowCount the number of rows of this advanced search form panel
     */
    
    /**
     * @private
     * @property {Object} _simpleCriteria the configuration object for simple criteria
     */
    
    /**
     * @private
     * @property {Object} _criteria the configuration object for advanced criteria
     */
    
    /**
     * @private
     * @property {Object} _language the configuration object for the language
     */
    
    /**
     * @private
     * @property {String} _defaultCriterionId the id of the default criterion (which is the first criterion of the model)
     */
    
    /**
     * @private
     * @property {Ext.data.Store} _languagesEnumerationStore the {@link Ext.data.Store} of available languages. Can be null if no language is defined
     */
    
    /**
     * @private
     * @property {Ext.data.Store} _logicalOperatorStore the store of logical operators linking two consecutive criteria
     */
    
    /**
     * @private
     * @property {Ext.data.Store} _andOperatorStore the store containing solely the 'and' operator
     */
    
    /**
     * @private
     * @property {Object[]} _advancedCriteriaFieldNames Array of criteria for the criterion field's store
     */
    
    /**
     * @private
     * @property {Object} _operatorStores the mapping of operator type with the corresponding operator {@link Ext.data.Store}
     */
    
    /**
     * @private
     * @property {Object} _enumerationStores the mapping of criterion id with its corresponding enumeration {@link Ext.data.Store} for the value field
     */
    
    /**
     * @cfg {Object[]} additionalFields The configurations of fields to add in each row.
     * @cfg {String} additionalFields.itemId The item id of the field to add.
     * @cfg {String} additionalFields.after The item id of the field after which the one to add will be inserted.
     * @cfg {Function} [additionalFields.getValueFn] The function used to retrieve the value of the field when calling {@link #getValueTree}. Its passed argument will be the field itself. Can be undefined to not retrieve the value.
     * @cfg {Object/Function} additionalFields.config The configuration (or a function returning a configuration object) of the field to add (including xtype, flex, etc.).
     * @cfg {Number} [additionalFields.config.rowIndex] If config is a function, the first argument is the index of the row
     * @cfg {String} [additionalFields.config.criterionName] If config is a function, the second argument is the name of the criterion field
     */
    
    /**
     * @private
     * @property {Object[]} _additionalFields The configurations of additional fields.
     */
    
    /**
     * @cfg {Boolean} [allowBlank=false] True if a panel with no row is allowed.
     */
    /**
     * @private
     * @property {Boolean} _allowBlank True if a panel with no row is allowed.
     */
    _allowBlank: false,
    
    statics: {
        /**
         * List of logical operators.
         */
        LOGICAL_OPERATORS: [
            {
                value: "and",
                label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_LOGICALOP_AND}}",
                str: " && "
            }, {
                value: "or",
                label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_LOGICALOP_OR}}",
                str: " || "
            }, {
                value: "and-par",
                label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_LOGICALOP_AND_PAR}}",
                str: ") && ("
            }
        ],
        
        /**
         * List of available operators, by criterion type.
         */
        OPERATORS: {
            'string': [{
                    value: "search",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_SEARCH}}",
                    shortLabel: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_SEARCH_SHORT}}"
                }, {
                    value: "searchStemmed",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_SEARCH_STEMMED}}",
                    shortLabel: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_SEARCH_STEMMED_SHORT}}"
                }, {
                    value: "contains",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_CONTAINS}}"
                }, {
                    value: "not-contains",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_NOT_CONTAINS}}"
                }, {
                    value: "eq",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_EQ}}"
                }, {
                    value: "ne",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_NE}}"
                }, {
                    value: "starts-with",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_STARTS_WITH}}"
                }, {
                    value: "not-starts-with",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_NOT_STARTS_WITH}}"
                }, {
                    value: "ends-with",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_ENDS_WITH}}"
                }, {
                    value: "not-ends-with",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_NOT_ENDS_WITH}}"
                }, {
                    value: "is-empty",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_EMPTY}}"
                }, {
                    value: "is-not-empty",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_NOT_EMPTY}}"
            }],
            'rich-text': [{
                    value: "search",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_SEARCH}}",
                    shortLabel: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_SEARCH_SHORT}}"
                }, {
                    value: "searchStemmed",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_SEARCH_STEMMED}}",
                    shortLabel: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_SEARCH_STEMMED_SHORT}}"
                }, {
                    value: "is-empty",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_EMPTY}}"
                }, {
                    value: "is-not-empty",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_NOT_EMPTY}}"
            }],
            'enumeration': [{
                    value: "eq",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_EQ}}",
                    labelMultiple: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_EQ_MULTIPLE}}"
                }, {
                    value: "ne",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_NE}}",
                    labelMultiple: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_NE_MULTIPLE}}"
                }, {
                    value: "is-empty",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_EMPTY}}"
                }, {
                    value: "is-not-empty",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_NOT_EMPTY}}"
            }],
            'date': [{
                    value: "eq",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS}}"
                }, {
                    value: "ne",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_NOT}}"
                }, {
                    value: "ge",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_AFTER_INCLUDED}}"
                }, {
                    value: "le",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_BEFORE_INCLUDED}}"
                }, {
                    value: "is-empty",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_EMPTY}}"
                }, {
                    value: "is-not-empty",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_NOT_EMPTY}}"
            }],
            'boolean': [{
                value: "eq",
                label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS}}"
            }],
            'number': [{
                    value: "eq",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_EQ}}"
                }, {
                    value: "ne",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_NE}}"
                }, {
                    value: "gt",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_GT}}"
                }, {
                    value: "lt",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_LT}}"
                }, {
                    value: "ge",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_GTE}}"
                }, {
                    value: "le",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_LTE}}"
                }, {
                    value: "is-empty",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_EMPTY}}"
                }, {
                    value: "is-not-empty",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_NOT_EMPTY}}"
                }],
            'geocode': [{
                    value: "eq",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_EQ}}"
                }, {
                    value: "ne",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_NE}}"
                },{
                    value: "is-empty",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_EMPTY}}"
                }, {
                    value: "is-not-empty",
                    label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_IS_NOT_EMPTY}}"
            }]
        },
        
        /**
         * Template used for the operator combobox (for non-multiple criterion).
         */
        OPERATOR_TEMPLATE: Ext.create('Ext.XTemplate',
            '<tpl for=".">',
                '{shortLabel}',
            '</tpl>'
        ),
        
        /**
         * Template used for the operator combobox (for multiple criterion).
         */
        OPERATOR_TEMPLATE_MULTIPLE: Ext.create('Ext.XTemplate',
            '<tpl for=".">',
                '{shortLabelMultiple}',
            '</tpl>'
        ),
        
        /**
         * @property {Number} DEFAULT_LOGICAL_OPERATOR_FIELD_WITH The default width of the logical operator field.
         * @static
         * @readonly
         */
       DEFAULT_LOGICAL_OPERATOR_FIELD_WITH: 70,
        /**
         * @property {String} OPENING_PARENTHESIS_FIELD_ITEM_ID The item id of the opening parenthesis field.
         * @static
         * @readonly
         */
       OPENING_PARENTHESIS_FIELD_ITEM_ID: 'opening-parenthesis',
        /**
         * @property {String} LOGICAL_OPERATOR_FIELD_ITEM_ID The item id of the criterion field.
         * @static
         * @readonly
         */
       LOGICAL_OPERATOR_FIELD_ITEM_ID: 'logical-operator',
        /**
         * @property {String} CRITERION_FIELD_ITEM_ID The item id of the criterion field.
         * @static
         * @readonly
         */
        CRITERION_FIELD_ITEM_ID: 'criterion',
        /**
         * @property {String} OPERATOR_FIELD_ITEM_ID The item id of the operator field.
         * @static
         * @readonly
         */
        OPERATOR_FIELD_ITEM_ID: 'operator',
        /**
         * @property {String} MODE_COMBOBOX_FIELD_ITEM_ID The item id of the value field.
         * @static
         * @readonly
         */
        VALUE_FIELD_ITEM_ID: 'value',
        /**
         * @property {Number} VALUE_FIELD_FLEX The flex configuration of the value field.
         * @static
         * @readonly
         */
        VALUE_FIELD_FLEX: 1,
        /**
         * @property {String} REMOVE_BUTTON_ITEM_ID The item id of the remove button.
         * @static
         * @readonly
         */
        REMOVE_BUTTON_ITEM_ID: 'remove-button',
        /**
         * @property {String} ADD_BUTTON_ITEM_ID The item id of the add button.
         * @static
         * @readonly
         */
        ADD_BUTTON_ITEM_ID: 'add-button',
        /**
         * @property {String} MOVEUP_BUTTON_ITEM_ID The item id of the moveup button.
         * @static
         * @readonly
         */
        MOVEUP_BUTTON_ITEM_ID: 'moveup-button',
        /**
         * @property {String} MOVEDOWN_BUTTON_ITEM_ID The item id of the movedown button.
         * @static
         * @readonly
         */
        MOVEDOWN_BUTTON_ITEM_ID: 'movedown-button'
    },
    
    constructor: function(config)
    {
        this._additionalFields = config.additionalFields || [];
        if (config.allowBlank != null)
        {
            this._allowBlank = config.allowBlank === true;
        }
        this.callParent(arguments);
    },
    
    /**
     * Initialize the form, providing the list of advanced criteria.
     */
    initialize: function(model)
    {
        this._fieldIndex = 0;
        
        // Store the simple criteria to be able to initialize the form from a simple values map.
        this._simpleCriteria = model['simple-criteria'] || {fieldsets: []};
        this._criteria = model['advanced-criteria'].criteria;
        this._language = model['advanced-criteria'].language;
        
        this._defaultCriterionId = Ext.Object.getKeys(this._criteria)[0];
        
        // Initialize the criteria stores.
        this._buildCriteriaStores();
        
        // Draw the language criterion if not hidden
        this._drawLanguageLine();
        
        // Draw the defaut line
        if (!this._allowBlank)
        {
            this._drawDefaultLine();
            this._fieldIndex++;
        }
        
        // Draw the last container line (closing parenthesis + hint).
        this._drawClosingLine(this._fieldIndex);
    },
    
    /**
     * Gets the (advanced) criteria
     * @return {Object} the (advanced) criteria
     */
    getCriteria: function()
    {
        return this._criteria;
    },
    
    /**
     * Initialize the search form
     * @param {Object} values The criteria values into logical expressions
     * @param {String} language The language value. Can be null
     */
    initForm: function (values, language)
    {
        try
        {
            Ext.suspendLayouts();
            
            this._rowCount = 0;
            var languageIsPresent = this.items.get('container-language') != null;
            
            if (!this._allowBlank)
            {
                this._removeOldCriteria(languageIsPresent);
            }
            
            this._setLanguageValue(language);
            
            if (this._allowBlank && Ext.Object.isEmpty(values))
            {
                // nothing more to do
                return;
            }
            if (this._allowBlank && this.items.getCount() == 1)
            {
                // need a first row, there is only 1 item which is the closing line
                this.addCriterion();
            }
            
            var firstRowIndex = languageIsPresent ? 1 : 0;
            if (values.expressions)
            {
                // Saved query
                this._logTreeValues(values);
                this._drawExpression(values.expressions, null, values.type, firstRowIndex);
            }
            else if (values.simpleValues)
            {
                this._initSimpleValues(values.simpleValues, firstRowIndex);
            }
            else
            {
                // Only one criterion, fill first row
                this._setRowValues(firstRowIndex, values.id, this._convertOperator(values.op, values.value), values.rawValue, null, values);
                this._rowCount++;
            }
        }
        finally
        {
            Ext.resumeLayouts(true);
        }
    },
    
    /**
     * @private
     * Removes old criteria
     * @param {Boolean} languageIsPresent true if language is present
     */
    _removeOldCriteria: function(languageIsPresent)
    {
        var minIndex = languageIsPresent ? 1 : 0;
        var criterionItemId = this.statics().CRITERION_FIELD_ITEM_ID;
        var me = this;
        
        function initFirstLine()
        {
            var firstLine = me.items.get('container-' + minIndex);
            if (firstLine)
            {
                me._replaceOperatorFieldByOpeningField(firstLine);
                var criterionField = firstLine.items.get(criterionItemId);
                if (criterionField)
                {
                    var firstCriterionValue = criterionField.getStore().getAt(0);
                    criterionField.setValue(null); // force null before in case the first criterion value was already selected
                    criterionField.setValue(firstCriterionValue);
                }
            }
            return firstLine;
        }
        
        // Remove old criteria rows (except language if present)
        for (var i = this._fieldIndex; i > minIndex; i--)
        {
            this.remove('container-' + i);
        }
        var firstLine = initFirstLine();
        
        this._fieldIndex = languageIsPresent ? 2 : 1;
        this._drawClosingLine(this._fieldIndex);
        this._updateRowUi(firstLine);
    },
    
    /**
     * Initialize the language row with the given value
     * @param {String} value the value of the language
     */
    _setLanguageValue: function(value)
    {
        var row = this.items.get('container-language');
        if (row)
        {
            var valueField = row.items.get(this.statics().VALUE_FIELD_ITEM_ID); 
            valueField.setValue(value);
        }
    },
    
    /**
     * Initialize the advanced search form from simple search form values.
     * @param {Object} values The simple search values, indexed by simple criterion ID.
     * @param {Number} firstRowIndex The index of the first row
     */
    _initSimpleValues: function(values, firstRowIndex)
    {
        Ext.Object.each(values, function(simpleCriterionId, value) {
            if (value != null && value !== '' && !(Ext.isArray(value) && value.length == 0))
            {
                // Get the simple criterion definition from the ID.
                var simpleCriterion = this._getSimpleCriterion(simpleCriterionId);
                if (simpleCriterion != null)
                {
                    // Get the advanced criterion definition corresponding to the simple criterion.
                    var advCriterion = this._getCorrespondingAdvancedCriterion(simpleCriterion);
                    
                    if (advCriterion != null)
                    {
                        // Use the advanced criterion ID but the operator from the simple criterion.
                        var id = advCriterion.name;
                        var type = advCriterion.type;
                        var simpleOp = (simpleCriterion.criterionOperator || '').toLowerCase();
                        var operator = this._convertOperator(simpleOp, value, type);

                        if (this._rowCount == 0)
                        {
                            // Fill first row (start after language line if present)
                            this._setRowValues(firstRowIndex, id, operator, value, null, {});
                        }
                        else
                        {
                            // Add 'AND' row.
                            this.addCriterion();
                            this._setRowValues(this._fieldIndex - 1, id, operator, value, 'and', {});
                        }
                        
                    }
                    this._rowCount++;
                }
            }
        }, this);
    },
    
    /**
     * Get a simple criterion definition.
     * @param {String} id The simple criterion ID.
     * @return {Object} The simple criterion definition, or `null` if not found.
     */
    _getSimpleCriterion: function(id)
    {
        for (var uuid in this._simpleCriteria) {
            if (this._simpleCriteria[uuid].elements[id])
            {
                return this._simpleCriteria[uuid].elements[id];
            }
        }
        
        return null;
    },
    
    /**
     * Get the definition of the advanced criterion corresponding to a simple criterion.
     * @param {Object} simpleCriterion The simple criterion definition.
     * @return {Object} The corresponding advanced criterion definition, or `null` if not found.
     */
    _getCorrespondingAdvancedCriterion: function(simpleCriterion)
    {
        var simpleType = simpleCriterion.criterionType;
        var simpleProp = simpleCriterion.criterionProperty;
        
        for (var advId in this._criteria)
        {
            var advCriterion = this._criteria[advId];
            
            var advType = advCriterion.criterionType;
            var advProp = advCriterion.criterionProperty;
            
            // Consider the advanced criterion equal to the other if they are of the same type and target ("property").
            if (simpleType == advType && simpleProp != null && simpleProp == advProp)
            {
                return advCriterion;
            }
        }
        
        return null;
    },
    
    /**
     * Convert the operator value into value for operators combo box
     * @param {String} op The operator's value
     * @param {Object} value The field value
     * @param {String} type The field type
     * @private
     */
    _convertOperator: function(op, value, type)
    {
        if (!op)
        {
            return null;
        }
        
        if (op == 'like' && type == 'string')
        {
            return 'contains';
        }
        else if (op == 'gt' && (type == 'date' || type == 'datetime'))
        {
            return 'ge';
        }
        else if (op == 'lt' && (type == 'date' || type == 'datetime'))
        {
            return 'le';
        }
        else
        {
            return op;
        }
    },
    
    /**
     * Draw criteria in logical expressions
     * @param {Object[]} expressions The criteria into the logical expression or other logical expressions
     * @param {String} outerType The outer type, i.e. the logical operator between expressions as a single expression and its siblings : "AND" or "OR". Can be null if root expressions
     * @param {String} innerType The inner type, i.e. the logical operator between all expressions : "AND" or "OR"
     * @param {Number} firstRowIndex The index of the first row
     * @param {Boolean} [outerPutParentheses=false] True if outer type needs parentheses (used in the case of the first expression among expressions)
     * @private 
     */
    _drawExpression: function(expressions, outerType, innerType, firstRowIndex, outerPutParentheses)
    {
        var me = this;
        var innerPutParentheses = this._oneOperandNeedsParentheses(expressions, innerType); // even though only one needs parentheses, put on every expression as only ') AND (' and ') OR (' operators exist
        
        Ext.Array.each(expressions, function(object, index) {
            var isFirstAmongExpressions = index == 0;
            if (object.type == 'criterion')
            {
                if (me._rowCount == 0)
                {
                    // Fill first row
                    me._setRowValues(firstRowIndex, object.id, me._convertOperator(object.op, object.value), object.rawValue, null, object);
                }
                else
                {
                    var operator = me._computeCorrectOperator(isFirstAmongExpressions, outerPutParentheses, innerPutParentheses, outerType, innerType);
                    // Add row
                    me.addCriterion();
                    me._setRowValues(me._fieldIndex - 1, object.id, me._convertOperator(object.op, object.value), object.rawValue, operator, object);
                }
                me._rowCount++;
            }
            else
            {
                var subOuterType = isFirstAmongExpressions
                        ? outerType
                        : innerType;
                var subOuterPutParentheses = isFirstAmongExpressions
                        ? outerPutParentheses
                        : innerPutParentheses;
                me._drawExpression (object.expressions, subOuterType, object.type, firstRowIndex, subOuterPutParentheses);
            }
        });
    },
    
    /**
     * @private
     * Determines if one operand among thos in expressions needs some parentheses
     * @param {Object[]} expressions The expressions (i.e. operands) 
     * @param {String} operator The logical operator between all expressions
     * @return {Boolean} true if at least one operand needs parentheses
     */
    _oneOperandNeedsParentheses: function(expressions, operator)
    {
        for (var i in expressions)
        {
            var expression = expressions[i];
            if (needsParentheses(expression, operator))
            {
                return true;
            }
        }
        return false;
        
        function needsParentheses(operand, outerOperator)
        {
            // because parenthesis will thus enable for operator OR to have priority on AND
            return operand.type === 'OR' && outerOperator === 'AND';
        }
    },
    
    /**
     * @private
     * Computes the correct logical operator to draw
     * @param {Boolean} isFirstAmongExpressions true if is first expression among siblings
     * @param {Boolean} outerPutParentheses true if outer level needs to put parentheses
     * @param {Boolean} innerPutParentheses true if inner level needs to put parentheses
     * @param {String} outerType The outer type, i.e. the logical operator between expressions as a single expression and its siblings : "AND" or "OR"
     * @param {String} innerType The inner type, i.e. the logical operator between all expressions : "AND" or "OR"
     * @return {String} The correct logical operar (can be 'and', 'or', 'and-par')
     */
    _computeCorrectOperator: function(isFirstAmongExpressions, outerPutParentheses, innerPutParentheses, outerType, innerType)
    {
        var type = isFirstAmongExpressions
            ? outerType
            : innerType;
        
        var putParentheses = isFirstAmongExpressions
            ? outerPutParentheses
            : innerPutParentheses;
        
        return type.toLowerCase() + (putParentheses ? '-par' : '');
    },
    
    /**
     * @private
     * For debug purposes, log the string representation of the given logical tree
     * @param {Object} values The values of the logical tree
     */
    _logTreeValues: function(values)
    {
        var me = this;
        var logger = this.getLogger();
        if (logger.isInfoEnabled())
        {
	        logger.info('String representation of logical tree ');
            console.info(values); // because wannot use syntax logger.info('aa %o zz', obj);
	        logger.info('is: ');
            var criterionIndex = 1;
	        logger.info(traverseNodeOrLeaf(values));
        }
        
        function traverseNodeOrLeaf(node)
        {
            if (node.type === 'criterion')
            {
                return '$' + criterionIndex++;
            }
            else
            {
                return traverseIntermediateNode(node);
            }
        }
        
        function traverseIntermediateNode(node)
        {
            var result = '';
            var expressions = node.expressions;
            var operator = node.type;
            var atLeastOneOperandNeedsParenthesis = me._oneOperandNeedsParentheses(expressions, operator);
            for (var subIndex in expressions)
            {
                var isFirst = subIndex == 0;
                if (!isFirst)
                {
                    result += (' ' + operator + ' ');
                }
                var subNode = expressions[subIndex];
                if (atLeastOneOperandNeedsParenthesis)
                {
                    result += '(';
                }
                var builtNode = traverseNodeOrLeaf(subNode);
                result += builtNode;
                if (atLeastOneOperandNeedsParenthesis)
                {
                    result += ')';
                }
            }
            return result;
        }
    },
    
    /**
     * Initialize the row values
     * @param {Number} index The row index
     * @param {String} criterion The id of criterion (first combobox)
     * @param {String} operator The operator's value (2nd combobox)
     * @param {Object} value The field's value (3rd field)
     * @param {String} logicalOp The logical operator.
     * @param {Object} object The object which can contain additional values.
     * @private
     */
    _setRowValues: function(index, criterion, operator, value, logicalOp, object)
    {
        var row = this.items.get('container-' + index);
        
        var criterionCombo = row.items.get(this.statics().CRITERION_FIELD_ITEM_ID),
            isFoundCriterion = false;
        if (criterionCombo)
        {
            criterionCombo.store.clearFilter(); // clear filter to get all values
        
            var record = criterionCombo.store.findRecord("name", criterion);
            criterionCombo.setValue(criterion);
            isFoundCriterion = record != null;
        }
        
        var operatorCombo = row.items.get(this.statics().OPERATOR_FIELD_ITEM_ID);
        if (operator && operatorCombo)
        {
            operatorCombo.setValue(isFoundCriterion ? operator : null/*force to empty if no criterion found*/);
        }
        
        var valueCombo = row.items.get(this.statics().VALUE_FIELD_ITEM_ID);
        if (valueCombo)
        {
            valueCombo.setValue(isFoundCriterion ? value : null/*force to empty if no criterion found*/);
        }
        
        if (index != 0 && logicalOp)
        {
            var logicalOperatorCombo = row.items.get(this.statics().LOGICAL_OPERATOR_FIELD_ITEM_ID);
            if (logicalOperatorCombo)
            {
                logicalOperatorCombo.setValue(logicalOp);
            }
        }
        
        this._additionalFields.forEach(function(additionalField) {
            var itemId = additionalField.itemId,
                field = row.items.get(itemId),
                val = object[itemId];
            if (field && Ext.isFunction(field.setValue) && val !== undefined)
            {
                field.setValue(isFoundCriterion ? val : null/*force to empty if no criterion found*/);
            }
        });
    },
    
    /**
     * Reset the fields of a row.
     * @param {Number} index The row index
     * @private
     */
    _resetRowValues: function(index)
    {
        var row = this.items.get('container-' + index);
        
        var combo = row.items.get(this.statics().CRITERION_FIELD_ITEM_ID);
        combo.store.clearFilter(); // clear filter to get all values
        combo.reset();
        
        var opCombo = row.items.get(this.statics().OPERATOR_FIELD_ITEM_ID);
        opCombo.select(opCombo.getStore().getAt(0));
        
        row.items.get(this.statics().VALUE_FIELD_ITEM_ID).reset();
    },
    
    /**
     * Build all the stores.
     * @private
     */
    _buildCriteriaStores: function()
    {
        // Build the logical operator store (shared).
        this._logicalOperatorStore = Ext.create('Ext.data.Store', {
            fields: ['value', 'label', 'str'],
            data: Ametys.plugins.cms.search.advanced.AdvancedSearchFormPanel.LOGICAL_OPERATORS
        });
        
        // The store with the 'and' operator exclusively
        this._andOperatorStore = Ext.create('Ext.data.Store', {
            fields: ['value', 'label', 'str'],
            data: [{
                value: "and",
                label: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_LOGICALOP_AND}}",
                str: " && "
            }]
        });
        
        // Build the criteria store and the enumeration stores.
        this._advancedCriteriaFieldNames = [];
        this._enumerationStores = {};
        var advancedCriteria = [];
        for (var name in this._criteria)
        {
            var criterion = this._criteria[name];
            
            this._advancedCriteriaFieldNames.push({
                name: name,
                label: criterion.label
            });
            
            if (criterion.enumeration)
            {
                this._enumerationStores[name] = Ext.create('Ext.data.Store', {
                    fields: ['value', 'label'],
                    data: criterion.enumeration
                });
            }
            
            if (this._language)
            {
                this._languagesEnumerationStore = Ext.create('Ext.data.Store', {
                    fields: ['value', 'label'],
                    data: this._language.enumeration
                });
            }
        }
        
        // Build the operator stores.
        this._operatorStores = {};
        for (var type in Ametys.plugins.cms.search.advanced.AdvancedSearchFormPanel.OPERATORS)
        {
            this._operatorStores[type] = Ext.create('Ext.data.Store', {
                fields: ['value', 'label', 'shortLabel', 'labelMultiple', 'shortLabelMultiple'],
                data: this.getOperators(type)
            });
        }
    },
    
    /**
     * @protected
     * Gets the operators for the given type
     * @param {String} type The type
     * @return {Object[]} An array of objects of operators (with at least the keys 'value', 'label', 'labelMultiple', 'shortLabel' and 'shortLabelMultiple')
     */
    getOperators: function(type)
    {
        return Ametys.plugins.cms.search.advanced.AdvancedSearchFormPanel.OPERATORS[type]
            .map(function(obj) {
                var res = Ext.apply({}, obj);
                if (!res.shortLabel)
                {
                    res.shortLabel = res.label;
                }
                if (!res.labelMultiple)
                {
                    res.labelMultiple = res.label;
                }
                if (!res.shortLabelMultiple)
                {
                    res.shortLabelMultiple = res.labelMultiple;
                }
                
                return res;
            });
    },
    
    /**
     * Get the language criteria submit value
     * @return {String} the language criteria submit value
     */
    getLanguage: function()
    {
        if (this.items.get('container-language'))
        {
            var languageContainer = this.items.get('container-language');
            var valueField = languageContainer.items.get(this.statics().VALUE_FIELD_ITEM_ID);

            return valueField.getSubmitValue();
        }
        return null;
    },
    
    /**
     * Get the value tree.
     * @return {Object} the user advanced search, organized in a criterion tree. 
     */
    getValueTree: function()
    {
        var criteria = {};
        
        // Build the expression to parse and index the criteria in the map.
        var expression = this._buildExpression(criteria);
        
        if (this._allowBlank && expression == '()')
        {
            return {};
        }

        // Parse the expression and build a syntax tree from it.
        var syntaxTree = Ametys.plugins.cms.search.advanced.AdvancedSearchParser.parse(expression);
        
        // Normalize the syntax tree by removing unnecessary levels (nested AND/OR).
        this._normalizeSyntaxTree(syntaxTree);
        
        // Build and return the value tree from the normalized syntax tree and the criteria map.
        return this._getValues(syntaxTree, criteria);
    },
    
    /**
     * Build the expression to parse and index the criteria in the map.
     * The expression will look like '(criterion-1 || criterion-2) && (criterion-3)'.
     * @param {Object} criteria The criteria map to fill.
     * @return {String} the expression.
     * @private
     */
    _buildExpression: function(criteria)
    {
        var minIndex = this.items.get('container-language') ? 1 : 0; // exclude language line if present
        
        var expression = '(';
        
        var me = this,
            logicalOpItemId = this.statics().LOGICAL_OPERATOR_FIELD_ITEM_ID,
            criterionItemId = this.statics().CRITERION_FIELD_ITEM_ID,
            operatorItemId = this.statics().OPERATOR_FIELD_ITEM_ID,
            valueItemId = this.statics().VALUE_FIELD_ITEM_ID;
        this.items.each(function(container, index, length) {
            if (index >= minIndex)
            {
                var containerItems = container.items,
                    logicalOpField = containerItems.get(logicalOpItemId),
                    criterionField = containerItems.get(criterionItemId),
                    operatorField = containerItems.get(operatorItemId),
                    valueField = containerItems.get(valueItemId);
                
                if (criterionField != null)
                {
                    // Generate a local criterion ID.
                    var critId = 'criterion-' + index;
    
                    // handle additional fields
                    var additionalValues = {};
                    me._additionalFields.forEach(function(additionalField) {
                        var itemId = additionalField.itemId,
                            field = containerItems.get(itemId),
                            getValueFn = field && additionalField.getValueFn;
                        if (Ext.isFunction(getValueFn))
                        {
                            var val = getValueFn(field);
                            additionalValues[itemId] = val;
                        }
                    });
                    
                    // Put the criterion ID, operator and value.
                    criteria[critId] = {
                            id: criterionField.getValue(),
                            op: operatorField.getValue(),
                            val: valueField.getJsonValue(),
                            additionalValues: additionalValues
                    }
                    
                    // Build the expression.
                    if (logicalOpField != null)
                    {
                        var record = logicalOpField.findRecordByValue(logicalOpField.getValue());
                        expression = expression + record.get('str');
                    }
                    expression = expression + critId;
                }
            }
        });
        
        expression = expression + ')';
        
        return expression;
    },
    
    /**
     * Normalize the syntax tree by removing unnecessary levels (nested AND/OR).
     * @param {Object} node the syntax tree node to normalize.
     * @private
     */
    _normalizeSyntaxTree: function(node)
    {
        // Normalize only AND/OR nodes.
        if ((node.type == 'and' || node.type == 'or') && node.expressions)
        {
            // Start by recursing (reduce 'bottom-up').
            for (var i = 0; i < node.expressions.length; i++)
            {
                this._normalizeSyntaxTree(node.expressions[i]);
            }
            
            var toRemove = [];
            var count = node.expressions.length;
            for (var i = 0; i < count; i++)
            {
                var childNode = node.expressions[i];
                // Test if the child node is of the same type ('and' or 'or').
                if (childNode.type == node.type && childNode.expressions)
                {
                    // If the child node is of the same type, store its index in the 'to remove' list
                    // and copy its expressions to the current node.
                    toRemove.push(i);
                    for (var j = 0; j < childNode.expressions.length; j++)
                    {
                        node.expressions.push(childNode.expressions[j]);
                    }
                }
            }
            
            // Remove the children node backwards (to avoid messing up indexes).
            for (var i = (toRemove.length - 1); i >= 0; i--)
            {
                node.expressions.splice(toRemove[i], 1);
            }
        }
    },
    
    /**
     * Build the value tree from a normalized syntax tree node and the criteria map.
     * @param {Object} node The syntax tree node.
     * @param {Object} criteria The criteria map.
     * @private
     */
    _getValues: function(node, criteria)
    {
        if (node.type == 'criterion')
        {
            return this._getCriterionValue(criteria[node.id]);
        }
        else if (node.expressions)
        {
            var expressions = [];
            
            for (var i = 0; i < node.expressions.length; i++)
            {
                expressions.push(this._getValues(node.expressions[i], criteria));
            }
            
            return {
                type: node.type.toUpperCase(),
                expressions: expressions
            };
        }
    },
    
    /**
     * Build a criterion value node.
     * @param {Object} criterion The selected criterion data.
     * @param {String} criterion.id The selected criterion ID.
     * @param {String} criterion.op The selected operator.
     * @param {String} criterion.val The user-submitted value.
     * @private
     */
    _getCriterionValue: function(criterion)
    {
        var value = {
            type: 'criterion',
            id: criterion.id,
            op: criterion.op,
            rawValue: criterion.val,
            value: criterion.val
        };
        
        Ext.Object.each(criterion.additionalValues, function(fieldItemId, fieldValue) {
            value[fieldItemId] = fieldValue;
        });
        
        return value;
    },
    
    /**
     * Add a criterion (i.e. draw a new row).
     * @param {Number} [position] the position of the new row. Can be null to insert criterion at last position.
     */
    addCriterion: function(position)
    {
        var container;
        
        if (position != null)
        {
            var index = this._fieldIndex + 1;
            var container = this.insert(position, this._lineConfig(index, [this._getLogicalOperatorField(index)]));
        }
        else
        {
            // Insert to last position
            container = this.items.getAt(this.items.getCount() - 1);
        }
        
        // Draw the new field row.
        this._drawFieldRow(container);
        if (this._allowBlank && this.items.getCount() === 1)
        {
            this._replaceOperatorFieldByOpeningField(container)
        }
        
        // Increment the index 
        this._fieldIndex++;
        
        if (position == null)
        {
            // and draw the new 'closing parenthesis' line.
            this._drawClosingLine(this._fieldIndex);
        }
        
        // previous & added lines => update UI
        this._updateRowUi(container);
        this._updateRowUi(container.previousSibling());
    },
    
    /**
     * Replace the first field of the row (combobox of logical operators) by an opening field (opening parenthesis)
     * @param {Ext.container.Container} container The row container
     * @private
     */
    _replaceOperatorFieldByOpeningField: function(container)
    {
        var index = container.fieldIndex;
        container.remove(this.statics().OPENING_PARENTHESIS_FIELD_ITEM_ID);
        container.remove(this.statics().LOGICAL_OPERATOR_FIELD_ITEM_ID);
        var hasPreviousSibling = container.previousSibling() != null; // if true, there is probably "language line" before
        var value = hasPreviousSibling
            ? "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_LOGICALOP_START_AND_PAR}}"
            : "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_LOGICALOP_START_PAR}}";
        container.insert(0, this._getOpeningField(index, value));
    },
    
    /**
     * Replace an opening field (opening parenthesis) by a combobox of logical operators)
     * @param {Ext.container.Container} container The row container
     * @private
     */
    _replaceOpeningFieldByOperatorField: function(container)
    {
        var index = container.fieldIndex;
        container.remove(container.getComponent(0));
        container.insert(0, this._getLogicalOperatorField(index));
    },
    
    /**
     * Insert additional field configurations in the given array of line items
     * @param {Object[]} items The items where to insert additional field configurations
     * @param {Number} rowIndex The index of the row
     * @param {String} criterionName The criterion name
     * @private
     */
    _insertAdditionalFields: function(items, rowIndex, criterionName)
    {
        var logger = this.getLogger();
        Ext.Array.each(this._additionalFields, function(additionalField) {
            var itemIds = items.map(function(item) {return item['itemId'];}), 
                after = additionalField['after'],
                itemId = additionalField['itemId'],
                cfg = additionalField['config'] || {};
            
            if (Ext.isFunction(cfg))
            {
                cfg = cfg(rowIndex, criterionName);
            }
            
            if (!itemId)
            {
                logger.error("Cannot insert additional field without the configuration 'itemId'");
                return true;
            }
            if (!after)
            {
                logger.error("Cannot insert additional field without the configuration 'after'");
                return true;
            }
            if (Ext.Array.contains(itemIds, itemId))
            {
                logger.error("Cannot insert additional field with itemId '" + itemId + "' as it already exists.");
                return true;
            }
            if (Ext.Array.contains(['type', 'id', 'op', 'rawValue', 'value'], itemId))
            {
                // those ids can conflict and thus are forbidden
                logger.error("Cannot insert additional field with forbidden itemId '" + itemId + "'.");
                return true;
            }
            
            // Find position
            var position = -1;
            Ext.Array.findBy(itemIds, function(currentItemId, currentIndex) {
                var isFound = currentItemId == after;
                if (isFound)
                {
                    position = currentIndex + 1;
                }
                return isFound;
            });
            if (position == -1)
            {
                logger.error("Cannot insert additional field after itemId '" + after + "' as it does not exists.");
                return true;
            }
            
            cfg['itemId'] = itemId;
            cfg['name'] = itemId + '-' + rowIndex;
            items.splice(position, 0, cfg);
        });
    },
    
    /**
     * Draw the language criterion line 
     * @private
     */
    _drawLanguageLine: function()
    {
        if (this._language && !this._language.hidden)
        {
            var items = Ext.Array.flatten([
                this._getOpeningField(this._fieldIndex, ''),
                this._getLanguageCriterionField(),
                this._getLanguageOperatorField(),
                this.getValueField(this._fieldIndex, this._language),
                this._getDeleteButton(this._fieldIndex, true),
                this._getAddRowUpButton(this._fieldIndex, true),
                this._getMoveUpButton(this._fieldIndex, true),
                this._getMoveDownButton(this._fieldIndex, true)
            ]);
            this._insertAdditionalFields(items, this._fieldIndex, null);
            // Display the language criterion (cannot delete it)
            var container = this.add({
                xtype: 'fieldcontainer',
                itemId: 'container-language',
                layout: { type: 'hbox' },
                defaults: {margin:'0 5 5 0'},
                fieldIndex: this._fieldIndex,
                
                items: items
            });
            
            this._fieldIndex++;
        }
        // else, if not present the language will be forced by server side (using the default value if hidden)
    },
    
    /**
     * Draw the first criterion line (with an opening parenthesis instead of the logical operator).
     * @private
     */
    _drawDefaultLine: function()
    {
        // Get the default criterion.
        var criterion = this._criteria[this._defaultCriterionId];

        var index = this._fieldIndex;
        var items = Ext.Array.flatten([
            this._getOpeningField(index, index > 0 ? "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_LOGICALOP_START_AND_PAR}}" : "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_LOGICALOP_START_PAR}}"),
            this._getCriterionField(index, this._defaultCriterionId),
            this._getOperatorField(index, criterion),
            this.getValueField(index, criterion),
            this._getDeleteButton(index, true),
            this._getAddRowUpButton(index, true),
            this._getMoveUpButton(index, true),
            this._getMoveDownButton(index, true)
        ]);
        this._insertAdditionalFields(items, index, this._defaultCriterionId);
        var container = this.add({
            xtype: 'fieldcontainer',
            itemId: 'container-' + index,
            layout: { type: 'hbox' },
            defaults: {margin:'0 5 0 0'},
            fieldIndex: index,
            
            items: items
        });
    },
    
    /**
     * Add the last container line (closing parenthesis + hint).
     * @param {Number} index The line index.
     * @private
     */
    _drawClosingLine: function(index)
    {
        var items;
        if (this.items.getCount() == 0)
        {
            items = [this._getStartButton()];
        }
        else
        {
            items = [
                this._getLogicalOperatorField(index),
                this._getHintField(index)
            ];
        }
        
        this.add(this._lineConfig(index, items));
    },
    
    /**
     * Gets the configuration of a line
     * @param {Number} index The line index.
     * @param {Object[]} items the child items
     * @return {Object} The configuration of a line
     * @private
     */
    _lineConfig: function(index, items)
    {
        return {
            xtype: 'fieldcontainer',
            itemId: 'container-' + index,
            layout: { type: 'hbox' },
            defaults: {margin:'0 5 0 0'},
            fieldIndex: index,
            items: items
        };
    },
    
    /**
     * Draw a new criterion row (from the default criterion).
     * Called when a logical operator was chosen or 'insert row up' button was clicked.
     * @param {Ext.container.Container} container The row container
     * @private
     */
    _drawFieldRow: function(container)
    {
        // Get the default criterion.
        var criterion = this._criteria[this._defaultCriterionId];
        
        // Remove and destroy the hint if exists
        container.remove('hint', true);
        // Remove and destroy the start button if exists
        container.remove('start-button', true);
        
        // Add the other fields.
        var index = container.fieldIndex;
        var items = Ext.Array.flatten([
            this._getCriterionField(index, this._defaultCriterionId),
            this._getOperatorField(index, criterion),
            this.getValueField(index, criterion),
            this._getDeleteButton(index, false),
            this._getAddRowUpButton(index, false),
            this._getMoveUpButton(index, true /* will be shown if necessary after adding, in #_updateRowUi */),
            this._getMoveDownButton(index, true /* will be shown if necessary after adding, in #_updateRowUi */)
        ]);
        this._insertAdditionalFields(items, index, this._defaultCriterionId);
        container.add(items);
    },
    
    /**
     * Re-generate the operator and value field when the criterion is changed.
     * @param {Number} index The line index. 
     * @param {Ext.form.FieldContainer} container The field container.
     * @param {String} name The criterion name.
     * @param {Object} criterion The criterion data. 
     * @private
     */
    _changeCriterion: function(index, container, name, criterion)
    {
        // Remove and destroy the current criterion-dependent fields.
        container.remove(this.statics().OPERATOR_FIELD_ITEM_ID);
        container.remove(this.statics().VALUE_FIELD_ITEM_ID);
        container.remove(this.statics().REMOVE_BUTTON_ITEM_ID);
        container.remove(this.statics().REMOVE_BUTTON_ITEM_ID + '-placeholder');
        container.remove(this.statics().ADD_BUTTON_ITEM_ID);
        container.remove(this.statics().ADD_BUTTON_ITEM_ID + '-placeholder');
        container.remove(this.statics().MOVEUP_BUTTON_ITEM_ID);
        container.remove(this.statics().MOVEUP_BUTTON_ITEM_ID + '-placeholder');
        container.remove(this.statics().MOVEDOWN_BUTTON_ITEM_ID);
        container.remove(this.statics().MOVEDOWN_BUTTON_ITEM_ID + '-placeholder');
        this._additionalFields
            .map(function(additionalField) {return additionalField['itemId'];})
            .filter(function(itemId) {return itemId != null;})
            .forEach(function(itemId) {
                container.remove(itemId);
            });
        
        var minIndex = this.items.get('container-language') ? 1 : 0;
        
        // Add the other fields, corresponding to the selected criterion.
        var hidden = !this._allowBlank && index === minIndex;
        var items = Ext.Array.flatten([
            this._getOperatorField(index, criterion),
            this.getValueField(index, criterion),
            this._getDeleteButton(index, hidden),
            this._getAddRowUpButton(index, hidden),
            this._getMoveUpButton(index, true /* will be shown if necessary after adding, in #_updateRowUi */),
            this._getMoveDownButton(index, true /* will be shown if necessary after adding, in #_updateRowUi */)
        ]);
        this._insertAdditionalFields(items, index, name);
        container.add(items);
        
        this._updateRowUi(container);
    },
    
    /**
     * Get the opening parenthesis field.
     * @param {Number} index the field's index
     * @param {String} value the value to initialize the field with
     * @return {Object} the opening parenthesis field configuration.
     * @private
     */
    _getOpeningField: function(index, value)
    {
        return {
            xtype: 'displayfield',
            cls: 'ametys',
            margin: '0 5 0 5',
            
            itemId: this.statics().OPENING_PARENTHESIS_FIELD_ITEM_ID,
            fieldIndex: index,
            
            value: value,
            width: this.statics().DEFAULT_LOGICAL_OPERATOR_FIELD_WITH
        }
    },
    
    /**
     * Get a logical operator field.
     * @param {Number} index The line index.
     * @return {Object} the field configuration.
     * @private
     */
    _getLogicalOperatorField: function(index)
    {
        return {
            xtype: 'combobox',
            fieldIndex: index,
            
            name: this.statics().LOGICAL_OPERATOR_FIELD_ITEM_ID + '-' + index,
            itemId: this.statics().LOGICAL_OPERATOR_FIELD_ITEM_ID,
            triggerAction: 'all',
            queryMode: 'local',
            editable: false,
            forceSelection: true,
            width: this.statics().DEFAULT_LOGICAL_OPERATOR_FIELD_WITH,
            emptyText: ')',
            cls: 'ametys',
            margin: '0 5 0 5',
            isParenthesis: true,
            
            store: this._logicalOperatorStore,
            valueField: 'value',
            displayField: 'label',
            
            listeners: {
                'select': this._onSelectLogicalOperator,
                'change': this._onChangeLogicalOperator,
                scope: this
            }
        }
    },
    
    /**
     * Get a criterion field.
     * @param {Number} index The line index.
     * @param {String} criterionId The criterion ID to preselect.
     * @return {Object} the field configuration.
     * @private
     */
    _getCriterionField: function(index, criterionId)
    {
        return {
            xtype: 'combobox',
            fieldIndex: index,
            cls: 'ametys',
            
            name: this.statics().CRITERION_FIELD_ITEM_ID + '-' + index,
            itemId: this.statics().CRITERION_FIELD_ITEM_ID,
            flex: 1,
            
            queryMode: 'local',
            triggerAction: 'all',
            allQuery: '',
            editable: true,
            anyMatch: true,
            forceSelection: true,
            clearFilterOnBlur: true,
            
            store: Ext.create('Ext.data.Store', {
                fields: ['name', 'label'],
                data: this._advancedCriteriaFieldNames,
                sorters: [{property: 'label', direction:'ASC'}]
            }),
            valueField: 'name',
            displayField: 'label',
            value: criterionId || '',
            
            listeners: {
                'change': this._onChangeCriterion,
                'focus': function(combo) {
                    // Clear filter on focus.
                    combo.getStore().clearFilter();
                },
                scope: this
            }
        }
    },
    
    /**
     * @private
     * Get the configuration object for the language criterion field
     * @return {Object} the configuration object
     */
    _getLanguageCriterionField: function()
    {
        return {
            xtype: 'textfield',
            fieldIndex: 0,
            margin: '0 5 5 0',
            readOnly: true,
            cls: 'ametys',
            triggerWrapCls: 'border-transparent',

            name: this.statics().CRITERION_FIELD_ITEM_ID + '-0',
            itemId: this.statics().CRITERION_FIELD_ITEM_ID,
            flex: 1,
            
            value: this._language.label
        };
    },
    
    /**
     * Get a criterion operator field.
     * @param {Number} index The line index.
     * @param {Object} criterion The criterion data.
     * @param {String} [opValue] The operator value. Can be null.
     * @return {Object} the field configuration.
     * @private
     */
    _getOperatorField: function(index, criterion, opValue)
    {
        var store;
        if (criterion)
        {
            var id = criterion.name;
            var type = criterion.type.toLowerCase() || 'string';
            var enumerated = criterion.enumeration != null;
            var widget = criterion.widget != null;
            var multiple = criterion.multiple === true;
            
            if (type == 'date' || type == 'datetime')
            {
                store = this._operatorStores['date'];
            }
            else if (type == 'rich-text')
            {
                store = this._operatorStores['rich-text'];
            }
            else if (enumerated || widget)
            {
                store = this._operatorStores['enumeration'];
            }
            else if (type == 'string' || type == 'reference')
            {
                store = this._operatorStores['string'];
            }
            else if (type == 'long' || type == 'double')
            {
                store = this._operatorStores['number'];
            }
            else if (type == 'boolean')
            {
                store = this._operatorStores['boolean'];
            }
            else if (type == 'content' || type == 'sub_content' || type == 'user')
            {
                store = this._operatorStores['enumeration'];
            }
            else if (type == 'geocode')
            {
                store = this._operatorStores['geocode'];
            }
        }
        else
        {
            store = this._operatorStores[null];
        }
        
        return {         
            xtype: 'combobox',
            fieldIndex: index,
            cls: 'ametys',
            
            name: this.statics().OPERATOR_FIELD_ITEM_ID + '-' + index,
            itemId: this.statics().OPERATOR_FIELD_ITEM_ID,
            triggerAction: 'all',
            queryMode: 'local',
            forceSelection: true,
            width: 150,
            
            store: store,
            valueField: 'value',
            displayField: multiple ? 'labelMultiple' : 'label',
            displayTpl: multiple ? this.statics().OPERATOR_TEMPLATE_MULTIPLE : this.statics().OPERATOR_TEMPLATE,
            
            value: opValue,
            
            listeners: {
                'change': this._onChangeOperator,
                'render': function(combo) {
                    if (combo.getSelection() == null)
                    {
                        combo.select(combo.getStore().getAt(0));
                    }
                },
                scope: this
            }
        }
    },
    
    /**
     * Get the language operator field configuration object
     * @return {Object} the configuration object
     */
    _getLanguageOperatorField: function()
    {
        return {
            xtype: 'textfield',
            margin: '0 5 5 0',
            readOnly: true,
            cls: 'ametys',
            triggerWrapCls: 'border-transparent',
            
            name: this.statics().OPERATOR_FIELD_ITEM_ID + '-0',
            itemId: this.statics().OPERATOR_FIELD_ITEM_ID,
            width: 150,
            
            value: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_OP_EQ}}"
        };
    },
    
    /**
     * Get a criterion value field.
     * @param {Number} index The line index.
     * @param {Object} criterion The criterion data.
     * @param {Object} [value] The init value. Can be null.
     * @return {Object} the field configuration or the field itself.
     */
    getValueField: function(index, criterion, value)
    {
        criterion = criterion || {};
        var type = criterion.type && criterion.type.toLowerCase() || 'string';
        
        var cfg = {
            fieldIndex: index,
            
            name: 'value-' + index,
            itemId: this.statics().VALUE_FIELD_ITEM_ID,
            flex: this.statics().VALUE_FIELD_FLEX,
            cls: 'ametys',
            
            value: value,
            
            type: type,
            contentType: criterion.contentType,
            multiple: criterion.multiple,
            widget: criterion.widget,
            hidden: criterion.hidden,
            
            ametysDescription: criterion.description || ''
        };
        
        if (criterion.validation)
        {
            var validation = criterion.validation;
            cfg.regexp = validation.regexp || null;
            
            if (validation.invalidText)
            {
                cfg.invalidText = validation.invalidText;
            }
            if (validation.regexText)
            {
                cfg.regexText = validation.regexText;
            }
        }
        
        if (criterion.enumeration)
        {
            var enumeration = [];
            
            var entries = criterion.enumeration;
            for (var j=0; j < entries.length; j++)
            {
                enumeration.push([entries[j].value, entries[j].label]);
            }
            
            cfg.enumeration = enumeration;
        }
        
        if (criterion['widget-params'])
        {
            Ext.merge(cfg, criterion['widget-params']);
        }
        
        if (cfg.regexp)
        {
            cfg.regex = new RegExp(cfg.regexp);
            cfg.regexText = cfg.regexText || cfg.invalidText || "{{i18n plugin.cms:PLUGINS_CMS_TOOL_CONTENT_FORM_INVALID_REGEXP}}" + cfg.regexp;
        }
        
        return Ametys.form.WidgetManager.getWidget(cfg.widget, cfg.type, cfg);
        
    },
    
    /**
     * @private
     * Gets the configuration of a placeholder
     * @param {Boolean} hidden true to hide
     * @param {String} btnPlaceHolderItemId The item id of the placeholder
     * @return {Object} The configuration
     */
    _getButtonPlaceholderCfg: function(hidden, btnPlaceHolderItemId)
    {
        return {
            xtype: 'displayfield',
            itemId: btnPlaceHolderItemId,
            hidden: hidden
        };
    },
    
    /**
     * @private
     * Hides the button, shows a placeholder
     * @param {Ext.button.Button} button The button to hide
     */
    _hideBtn: function(button)
    {
        var container = button.up(),
            btnPlaceHolderItemId = button.getItemId() + '-placeholder',
            placeHolder = container.getComponent(btnPlaceHolderItemId);
        placeHolder.show();
        button.hide();
    },
    
    /**
     * @private
     * Shows the button, hides the placeholder
     * @param {Ext.button.Button} button The button to show
     */
    _showBtn: function(button)
    {
        var container = button.up(),
            btnPlaceHolderItemId = button.getItemId() + '-placeholder',
            placeHolder = container.getComponent(btnPlaceHolderItemId);
        placeHolder.hide();
        button.show();
    },
    
    /**
     * Get a row delete button configuration
     * @param {Number} index The line index.
     * @param {Boolean} hidden True to hide the button (used for the first line).
     * @return {Object[]} the button configuration(s).
     * @private
     */
    _getDeleteButton: function(index, hidden)
    {
        var itemId = this.statics().REMOVE_BUTTON_ITEM_ID;
        var removeBtnPlaceHolderItemId = itemId + '-placeholder';
        return [{
            xtype: 'button',
            fieldIndex: index,
            
            itemId: itemId,
            iconCls: 'ametysicon-sign-raw-cross',
            tooltip: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_REMOVE_CRITERION}}",
            handler: this._removeRow,
            scope: this,
            hidden: hidden,
            
            listeners: {
                'afterrender': {
                    fn: function(button) {
                        button.up().getComponent(removeBtnPlaceHolderItemId).setWidth(button.getWidth());
                    },
                    scope: this
                }
            }
        }, this._getButtonPlaceholderCfg(!hidden, removeBtnPlaceHolderItemId) ];
    },
    
    /**
     * Get a 'add row' button configuration
     * @param {Number} index The line index.
     * @param {Boolean} hidden True to hide the button (used for the first line).
     * @return {Object[]} the button configuration(s).
     * @private
     */
    _getAddRowUpButton: function(index, hidden)
    {
        var itemId = this.statics().ADD_BUTTON_ITEM_ID;
        var addBtnPlaceHolderItemId = itemId + '-placeholder';
        return [{
            xtype: 'button',
            fieldIndex: index,
            
            itemId: itemId,
            iconCls: 'ametysicon-sign-raw-add',
            tooltip: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_INSERT_CRITERION}}",
            handler: this._insertRowUp,
            scope: this,
            hidden: hidden,
            
            listeners: {
                'afterrender': {
                    fn: function(button) {
                        button.up().getComponent(addBtnPlaceHolderItemId).setWidth(button.getWidth());
                    },
                    scope: this
                }
            }
        }, this._getButtonPlaceholderCfg(!hidden, addBtnPlaceHolderItemId) ];
    },
    
    /**
     * Get a 'move row up' button configuration
     * @param {Number} index The line index.
     * @param {Boolean} hidden True to hide the button.
     * @return {Object[]} the button configuration(s).
     * @private
     */
    _getMoveUpButton: function(index, hidden)
    {
        var itemId = this.statics().MOVEUP_BUTTON_ITEM_ID;
        var btnPlaceHolderItemId = itemId + '-placeholder';
        return [{
            xtype: 'button',
            fieldIndex: index,
            
            itemId: itemId,
            iconCls: 'ametysicon-arrow-up-simple',
            tooltip: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_MOVEUP_CRITERION}}",
            handler: this._moveRowUp,
            scope: this,
            hidden: hidden,
            
            listeners: {
                'afterrender': {
                    fn: function(button) {
                        button.up().getComponent(btnPlaceHolderItemId).setWidth(button.getWidth());
                    },
                    scope: this
                }
            }
        }, this._getButtonPlaceholderCfg(!hidden, btnPlaceHolderItemId) ];
    },
    
    /**
     * Get a 'move row down' button configuration
     * @param {Number} index The line index.
     * @param {Boolean} hidden True to hide the button.
     * @return {Object[]} the button configuration(s).
     * @private
     */
    _getMoveDownButton: function(index, hidden)
    {
        var itemId = this.statics().MOVEDOWN_BUTTON_ITEM_ID;
        var btnPlaceHolderItemId = itemId + '-placeholder';
        return [{
            xtype: 'button',
            fieldIndex: index,
            
            itemId: this.statics().MOVEDOWN_BUTTON_ITEM_ID,
            iconCls: 'ametysicon-arrow-down-simple',
            tooltip: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_MOVEDOWN_CRITERION}}",
            handler: this._moveRowDown,
            scope: this,
            hidden: hidden,
            
            listeners: {
                'afterrender': {
                    fn: function(button) {
                        button.up().getComponent(btnPlaceHolderItemId).setWidth(button.getWidth());
                    },
                    scope: this
                }
            }
        }, this._getButtonPlaceholderCfg(!hidden, btnPlaceHolderItemId) ];
    },
    
    /**
     * Get the hint field configuration.
     * @param {Number} index The line index.
     * @return {Object} the field configuration.
     * @private
     */
    _getHintField: function(index)
    {
        return {
            xtype: 'displayfield',
            itemId: 'hint',
            fieldIndex: index,
            
            cls: 'advanced-search-form-hint',
            value: "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_ADD_HINT}}"
        }
    },
    
    /**
     * Get the 'start button' button configuration
     * @return {Object} The button configuration
     * @private
     */
    _getStartButton: function()
    {
        return {
            xtype: 'button',
            itemId: 'start-button',
            text: '{{i18n PLUGINS_CMS_UITOOL_ADVANCED_SEARCH_START_BUTTON_TEXT}}',
            iconCls: 'ametysicon-sign-raw-add',
            handler: function() {
                this.addCriterion();
                this._rowCount++;
            },
            scope: this
        };
    },
    
    /**
     * The first time a logical operator is selected, add a new line.
     * @param {Ext.form.field.ComboBox} combo The combo box.
     * @param {Array} records The selected records.
     * @param {Object} eOpts The event options.
     * @private
     */
    _onSelectLogicalOperator: function(combo, records, eOpts)
    {
        // Set a marker
        if (combo.isParenthesis === true)
        {
            this.addCriterion();
            this._rowCount++;
            
            Ext.defer(function() {combo.isParenthesis = false}, 1);
        }
    },
    
    /**
     * Triggered when the value of the combo is changed via the setValue method.
     * @param {Ext.form.field.ComboBox} combo The combo box.
     * @param {Object} newValue The new value.
     * @param {Object} oldValue The old value.
     * @param {Object} eOpts The event options.
     * @private
     */
    _onChangeLogicalOperator: function(combo, newValue, oldValue, eOpts)
    {
        // Set a marker
        if (combo.isParenthesis === true)
        {
            Ext.defer(function() {combo.isParenthesis = false}, 1);
        }
    },
    
    /**
     * Re-draw the operator and value fields when a new criterion is selected.
     * @param {Ext.form.field.ComboBox} combo The combo box.
     * @param {Object} newValue The new value.
     * @param {Object} oldValue The old value.
     * @param {Object} eOpts The event options.
     * @private
     */
    _onChangeCriterion: function(combo, newValue, oldValue, eOpts)
    {
        if (newValue != null)
        {
            // Re-draw the fields 
            var criterion = this._criteria[newValue];
            var container = this.items.get('container-' + combo.fieldIndex); //+ combo.up('fieldcontainer');
            this._changeCriterion(combo.fieldIndex, container, newValue, criterion);
        }
    },
    
    /**
     * Remove a line.
     * @param {Ext.button.Button} button The clicked 'remove' button.
     * @param {Object} eOpts The event options.
     * @protected
     */
    _removeRow: function(button, eOpts)
    {
        var container = button.up('fieldcontainer'),
            isFirst = this.items.getAt(0) === container,
            nextContainer = container.nextSibling(),
            previousContainer = container.previousSibling();
        this.remove(container);
        this._rowCount--;
        
        if (this._allowBlank && isFirst && nextContainer && this._rowCount > 0)
        {
            // next container is now the first one, and is not alone
            this._replaceOperatorFieldByOpeningField(nextContainer)
        }
        else if (this._rowCount == 0 && nextContainer)
        {
            // nextContainer is the closing line
            this.remove(nextContainer);
            this._drawClosingLine(this._fieldIndex);
        }
        
        // previous & next lines => update UI
        this._updateRowUi(previousContainer);
        this._updateRowUi(nextContainer);
    },
    
    /**
     * Insert a row up to the current position
     * @param {Ext.button.Button} button The clicked 'insert row up' button.
     * @param {Object} eOpts The event options.
     * @protected
     */
    _insertRowUp: function(button, eOpts)
    {
        var container = button.up('fieldcontainer');
        var position = this.items.indexOf(container);
                
        // Draw the new field row.
        this.addCriterion(position);
        
        // the created row
        var createdRow = this.items.getAt(position);
        // Set logical operator to default value 'and'
        if (this._allowBlank && position == 0)
        {
            this._replaceOperatorFieldByOpeningField(createdRow);
            
            // the row which was clicked in 1st position and which will be in 2nd
            var secondRow = container;
            this._replaceOpeningFieldByOperatorField(secondRow);
            var logicalOpField = secondRow.items.get(this.statics().LOGICAL_OPERATOR_FIELD_ITEM_ID);
            logicalOpField.setValue("and");
        }
        else
        {
            var logicalOpField = createdRow.items.get(this.statics().LOGICAL_OPERATOR_FIELD_ITEM_ID);
            logicalOpField.setValue("and");
        }
        
        // next line => update UI
        this._updateRowUi(createdRow.nextSibling());
        
        this._rowCount++;
    },
    
    /**
     * @private
     * Swaps the logical operators of the two given row containers
     * @param {Ext.form.FieldContainer} firstRow The first row container
     * @param {Ext.form.FieldContainer} secondRow The second row container
     */
    _swapLogicalOperators: function(firstRow, secondRow)
    {
        var firstLogicalOp = firstRow.items.first(),
            secondLogicalOp = secondRow.items.first();
        
        firstRow.insert(0, secondLogicalOp);
        secondRow.insert(0, firstLogicalOp);
        // and that's all, the components are automatically detached from their former container
    },
    
    /**
     * Moves up a row
     * @param {Ext.button.Button} button The clicked button.
     * @protected
     */
    _moveRowUp: function(button)
    {
        var container = button.up('fieldcontainer'),
            prev = container.previousSibling(),
            
            minIndex = this.items.get('container-language') ? 1 : 0,
            prevPosition = this.items.indexOf(prev),
            prevIsFirstCriterion = prevPosition === minIndex;
        
        if (prev)
        {
            this.moveBefore(container, prev);
            if (prevIsFirstCriterion)
            {
                this._swapLogicalOperators(container, prev);
            }
            this._updateRowUi(container);
            this._updateRowUi(prev);
        }
        else
        {
            // unexpected => button should not be there, at least hide it
            this._hideBtn(button);
        }
    },
    
    /**
     * Moves down a row
     * @param {Ext.button.Button} button The clicked button.
     * @protected
     */
    _moveRowDown: function(button)
    {
        var container = button.up('fieldcontainer'),
            next = container.nextSibling(),
            
            minIndex = this.items.get('container-language') ? 1 : 0,
            position = this.items.indexOf(container),
            isFirstCriterion = position === minIndex;
        
        if (next)
        {
            this.moveAfter(container, next);
            if (isFirstCriterion)
            {
                this._swapLogicalOperators(next, container);
            }
            this._updateRowUi(container);
            this._updateRowUi(next);
        }
        else
        {
            // unexpected => button should not be there, at least hide it
            this._hideBtn(button);
        }
    },
    
    /**
     * @private
     * Updates the UI of the given row (show/hide buttons...)
     * @param {Ext.form.FieldContainer} rowContainer The row container to update
     */
    _updateRowUi: function(rowContainer)
    {
        if (!rowContainer)
        {
            return;
        }
        
        var position = this.items.indexOf(rowContainer),
            maxIndex = this.items.length - 2/* last item is not a criterion, so last criterion is at index 'length-2' */;
            isLastCriterion = position === maxIndex,
            
            minIndex = this.items.get('container-language') ? 1 : 0,
            isFirstCriterion = position === minIndex;
            
        // Delete button
        var deleteBtn = rowContainer && !rowContainer.isDestroyed && rowContainer.getComponent(this.statics().REMOVE_BUTTON_ITEM_ID);
        if (deleteBtn)
        {
            var hide = !this._allowBlank && isFirstCriterion;
            this[hide ? '_hideBtn' : '_showBtn'](deleteBtn);
        }
        
        // Insert button
        var addBtn = rowContainer && !rowContainer.isDestroyed && rowContainer.getComponent(this.statics().ADD_BUTTON_ITEM_ID);
        if (addBtn)
        {
            var hide = !this._allowBlank && isFirstCriterion;
            this[hide ? '_hideBtn' : '_showBtn'](addBtn);
        }
        
        // Move Down button
        var rowMoveDownBtn = rowContainer && !rowContainer.isDestroyed && rowContainer.getComponent(this.statics().MOVEDOWN_BUTTON_ITEM_ID);
        if (rowMoveDownBtn)
        {
            var hide = isLastCriterion;
            this[hide ? '_hideBtn' : '_showBtn'](rowMoveDownBtn);
        }
        
        // Move Up button
        var rowMoveUpBtn = rowContainer && !rowContainer.isDestroyed && rowContainer.getComponent(this.statics().MOVEUP_BUTTON_ITEM_ID);
        if (rowMoveUpBtn)
        {
            var hide = isFirstCriterion;
            this[hide ? '_hideBtn' : '_showBtn'](rowMoveUpBtn);
        }
    },
    
    /**
     * Re-draw the value field when a new operator is selected.
     * @param {Ext.form.field.ComboBox} combo The combo box.
     * @param {Object} newValue The new value.
     * @param {Object} oldValue The old value.
     * @private
     */
    _onChangeOperator: function(combo, newValue, oldValue)
    {
        if (newValue != null && oldValue != null)
        {
            var container = this.items.get('container-' + combo.fieldIndex);
            var valueField = container.items.get(this.statics().VALUE_FIELD_ITEM_ID);
            
            //check if widget can be set to read only mode
            if (typeof valueField.setReadOnly !== "undefined") { 
                valueField.setReadOnly(newValue == "is-empty" || newValue == "is-not-empty")
            }
            // clear widget and store value
            if (oldValue != "is-empty" && oldValue != "is-not-empty" && (newValue == "is-empty" || newValue == "is-not-empty"))
            {
                valueField.oldValue = valueField.getValue();
                valueField.setValue(null)
            }
            
            // fill widget with old value
            if (newValue != "is-empty" && newValue != "is-not-empty" && (oldValue == "is-empty" || oldValue == "is-not-empty"))
            {
                valueField.setValue(valueField.oldValue)
            }
            
        }
    }
    
});