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

/**
 * Helper for grids of {@link Ametys.plugins.cms.search.AbstractSearchTool}s
 * @private
 */
Ext.define('Ametys.plugins.cms.search.SearchGridHelper', {
    singleton: true,
    
    /**
     * @private
     * @property {Ext.XTemplate} _repeaterTpl The template used for rendering repeater entries
     */
    _repeaterTpl: Ext.create('Ext.XTemplate', [
        '<ul class="a-grid-repeater-entries">',
            '<tpl for="entries">',
                '<li class="a-grid-repeater-entry">{label}<tpl if="!tpl"> ({position})</tpl></li>',
            '</tpl>',
         '</ul>'
    ]),
    
    // ----------------------------------------------------------
    // ---------------- Converters and renderers ----------------
    // ----------------------------------------------------------
    
    /**
     * Convert workflow step value for model.
     * @param {String/Object[]} value If it is an object, it will return the 'name' property.  
     * @param {String} value.name The name of workflow step
     * @param {Ext.data.Model} record The record
     * @param {String} dataIndex The model data index
     * @return {String} The name of workflow step
     */
    convertWorkflowStep: function (value, record, dataIndex)
    {
        var properties = {};
        record.data[dataIndex + "_workflowStep"] = properties;
        
        if (Ext.isArray(value) && value.length > 0 && Ext.isObject(value[0]))
        {
            for (var i = 0; i < value.length; i++)
            {
                properties[value[i].name] = value[i];
                value[i] = value[i].name;
            }
        }
        else if (Ext.isObject(value) && !Ext.Object.isEmpty(value))
        {
            properties[value.name] = value;
            value = value.name;
        }
        else
        {
            value = '';
        }
        
        return value;
    },
    
    /**
     * Function to render workflow step in result grid
     * @param {Object} value The data value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     * @param {Number} rowIndex The index of the current row
     * @param {Number} colIndex The index of the current column
     * @param {Ext.data.Store} store The data store
     * @param {Ext.view.View} view The current view
     * @param {String} dataIndex The data index of the column
     *  
     */
    renderWorkflowStep: function (value, metaData, record, rowIndex, colIndex, store, view, dataIndex)
    {
        if (Ext.isEmpty(value))
        {
            return '';
        }
        
        var inGroup = (dataIndex == null);
        
        dataIndex = inGroup ? rowIndex : dataIndex; // When used by grouping feature, data index is the 4th arguments
        
        var properties = record.get(dataIndex + "_workflowStep");
        
        var property = properties ? properties[value] : null;
        if (property)
        {
            return '<img src="' + Ametys.CONTEXT_PATH + property.smallIcon + '" alt="' + property.name + '" title="' + property.name + '" class="a-grid-icon a-grid-icon-workflow"/>'
                + (inGroup ? property.name :'');
        }
        else if (record.get('workflowStepIcon'))
        {
            return '<img src="' + Ametys.CONTEXT_PATH + record.get('workflowStepIcon') + '"  alt="' + value + '" title="' + value + '" class="a-grid-icon a-grid-icon-workflow"/>'
                + (inGroup ? value :'');
        }
        else
        {
            return value;
        }
    },
        
    /**
     * Function to render content's title in result grid
     * @param {Object} value The data value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     */
    renderTitle: function (value, metaData, record)
    {
        var title = Ametys.plugins.cms.search.SearchGridHelper.renderMultilingualString(value, metaData, record) || record.get('name');
        if (record.get('iconGlyph') != null)
        {
            return '<span class="a-grid-glyph ' + record.get('iconGlyph') + (record.get('iconDecorator') != null ? ' ' + record.get('iconDecorator') : '') + '"></span>' + Ext.String.escapeHtml(title);
        }
        else if (record.get('iconSmall'))
        {
            return '<img src="' + Ametys.CONTEXT_PATH + record.get('iconSmall') + '" class="a-grid-icon a-grid-icon-title"/>' + Ext.String.escapeHtml(title);
        }
        
        return title;
    },
    
    /**
     * Function to render content's title in result grid
     * @param {Object} value The data value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     */
    renderMultilingualTitle: function (value, metaData, record)
    {
        console.warn("Ametys.plugins.cms.search.SearchGridHelper.renderMultilingualTitle is obsolete and should be replaced by Ametys.plugins.cms.search.SearchGridHelper.renderTitle that supports both string and multilingual string titles")
        return this.renderTitle(value, metaData, record);
    },
    
    /**
     * Function to render a boolean value as 'yes' or 'no' text
     * @param {Object} value The data value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
	 * @Deprecated Use {@link Ametys.grid.GridColumnHelper.renderBoolean}
     */
    renderBoolean: function (value, metaData, record)
    {
        return Ametys.grid.GridColumnHelper.renderBoolean.apply(this, arguments);
    },
    
    /**
     * Function to render a boolean value with icon
     * @param {Object} value The data value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
	 * @Deprecated Use {@link Ametys.grid.GridColumnHelper.renderBooleanIcon}
     */
    renderBooleanIcon: function (value, metaData, record)
    {
        return Ametys.grid.GridColumnHelper.renderBooleanIcon.apply(this, arguments);
    },
    
    /**
     * Function to render a multiple string (Array) on one line, separated by commas.
     * @param {Object} value The value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     */
    renderMultipleString: function(value, metaData, record)
    {
        if (Ext.isArray(value))
        {
            return value.join(', ');
        }
        
        return value;
    },
    
    /**
     * Convert an multiple String value into a Array of values
     * @param {Object/Object[]} val If it is an Array, it will return the string 'value' separated by comma.  
     * @param {Ext.data.Model} record The record
     * @param {String} dataIndex The model data index
     * @return {String[]} The values into a Array
     */
    convertMultipleValue: function(val, record, dataIndex)
    {
        if (Ext.isArray(val))
        {
            // Keep value as an array
            return val;
        }
        else
        {
            return Ext.isEmpty(val) ? [] : val.split(',');
        }
    },

    /**
     * Function to render a multiple value (Array) with a line for each value
     * @param {Object} value The value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     * @param {Number} rowIndex The index of the current row
     * @param {Number} colIndex The index of the current column
     * @param {Ext.data.Store} store The store
     * @param {Ext.view.View} view The current view
     * @param {String} dataIndex The index of column
     * @param {Object[]} enumeration The enumeration of possible values. The key is the field value, and the value is the associated label.
     * @param {Function} [singleValueRenderer] The renderer of a single value
     */
    renderMultipleValue: function(value, metaData, record, rowIndex, colIndex, store, view, dataIndex, enumeration, singleValueRenderer)
    {
        if (Ext.isEmpty(value))
        {
            return '';
        }
        
        dataIndex = dataIndex || rowIndex; // When used by grouping feature, data index is the 4th arguments
        
        if (singleValueRenderer == null)
        {
            singleValueRenderer = Ext.identityFn;
        }
        
        let values = this._getValuesForMultipleData(dataIndex, record.data);
        return values
                .map(function(v) { return singleValueRenderer(v, metaData, record, rowIndex, colIndex, store, view, dataIndex, enumeration); })
                .join('<br/>');
    },
    
    /**
     * Convert a multilingual value for an object.
     * @param {Object} value The value as a object with the value for each language
     * @param {Ext.data.Model} record The record
     * @param {String} dataIndex The model data index
     * @return {String} The property value or code.
     */
    convertMultilingualString: function (value, record, dataIndex)
    {
        var properties = {};
        record.data[dataIndex + '_multilingual'] = properties;
        
        if (Ext.isObject(value))
        {
            properties = value;
            
            var currentLang = Ametys.cms.language.LanguageDAO.getCurrentLanguage();
            if (value[currentLang])
            {
                value = value[currentLang];
            }
            else if (value.en)
            {
                value = value.en;
            }
            else
            {
                value = '';
                
                Ext.Object.eachValue(properties, function(v) {
                    if (Ext.isEmpty(v))
                    {
                        value = v;
                        return false; // stop iteration
                    }
                });
            }
        }
        return value;
    },
    
     /**
     * Returns the localized value of a multilingual string value.<br>
     * The return localized value will be in order:<br>
     * - the value for given language if not null and value for this language is not empty,<br>
     * - or the value for current language if exists and not empty,<br>
     * - or the value for default language 'en' if exists and not empty,<br>
     * - or the first non-empty value<br>
     * @param {Object} values the multilingual values as a Map of values for each available languages
     * @param {String} [defaultLanguage] The language of preference. Can be null.
     * @return the localized value
     */
    getDefaultLocalizedValue: function (values, defaultLanguage)
    {
        // If not null, first try to get the value for given language
        if (defaultLanguage && values[defaultLanguage])
        {
            return values[defaultLanguage];
        }
        
        // Then try to get the value for current language
        var currentLang = Ametys.cms.language.LanguageDAO.getCurrentLanguage();
        if (values[currentLang])
        {
            return values[currentLang];
        }
        
        // Then try to get value for default language 'en'
        if (values.en)
        {
            return values.en;
        }
        
        // Then return the first non-empty value
        var value = '';
        Ext.Object.eachValue(values, function(v) {
            if (Ext.isEmpty(v))
            {
                value = v;
                return false; // stop iteration
            }
        });
        
        return value;
        
    },
    
    /**
     * Function to render an multilingual value in result grid
     * @param {Object} value The value for each languages
     * @param {Object} metadata A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     * @param {Number} rowIndex The index of the current row
     * @param {Number} colIndex The index of the current column
     * @param {Ext.data.Store} store The store
     * @param {Ext.view.View} view The current view
     * @param {String} dataIndex the record data index
     * @return {String} The value for the default language.
     */
    renderMultilingualString: function(value, metadata, record, rowIndex, colIndex, store, view, dataIndex)
    {
        if (Ext.isObject(value))
        {
            return Ext.String.escapeHtml(this.getDefaultLocalizedValue(value));
        }
        return value;
    },
            
    /**
     * Function to render an enumerated value in result grid
     * @param {Object} value The data value
     * @param {Object} metadata A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     * @param {Number} rowIndex The index of the current row
     * @param {Number} colIndex The index of the current column
     * @param {Ext.data.Store} store The store
     * @param {Ext.view.View} view The current view
     * @param {String} dataIndex the record data index
     * @param {Object[]} enumeration The enumeration of possible values. The key is the field value, and the value is the associated label.
     */
    renderEnumeration: function(value, metadata, record, rowIndex, colIndex, store, view, dataIndex, enumeration)
    {
        value = Ext.Array.from(value);

        var labels = [];
        Ext.Array.forEach(value, function(v) {
            labels.push(Ext.String.escapeHtml(enumeration[v] || v));
        });
        return labels.join('<br/>');
    },
    
    /**
     * Convert an enumerated value for an object.
     * @param {String/Object[]} val If it is an object, it will return the 'value' property.  
     * @param {String} val.value The property value or code.
     * @param {Ext.data.Model} record The record
     * @param {String} dataIndex The model data index
     * @return {String} The property value or code.
     */
    convertEnumeratedData: function(val, record, dataIndex)
    {
        var properties = {};
        record.data[dataIndex + '_enum'] = properties;
        
        if (Ext.isArray(val) && val.length > 0 && Ext.isObject(val[0]))
        {
            for (var i = 0; i < val.length; i++)
            {
                properties[val[i].value] = val[i];
                val[i] = val[i].value;
            }
        }
        else if (Ext.isObject(val))
        {
            properties[val.value] = val;
            val = val.value;
        }
        return val;
    },
    
    /**
     * Function to render a geocode metadata.
     * @param {Object} value The value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     * @param {Number} rowIndex The index of the current row
     * @param {Number} colIndex The index of the current column
     * @param {Ext.data.Store} store The store
     * @param {Ext.view.View} view The current view
     * @param {String} dataIndex The index of column
     */
    renderGeocode: function(value, metaData, record, rowIndex, colIndex, store, view, dataIndex)
    {
        if (value != null && value.latitude != undefined && value.longitude != undefined)
        {
            return parseFloat(value.latitude).toFixed(4) + "°N, " + parseFloat(value.longitude).toFixed(4) + "°E";
        }
        
        return value;
    },
    
    /**
     * Convert a language value for model.
     * @param {String/Object[]} value If it is an object, it will return the 'code' property.  
     * @param {String} value.code The language code
     * @param {Ext.data.Model} record The record
     * @param {String} dataIndex The model data index
     * @return {String} The language code only
     */
    convertLanguage: function (value, record, dataIndex)
    {
        var properties = {};
        record.data[dataIndex + "_language"] = properties;
        
        if (Ext.isArray(value) && value.length > 0 && Ext.isObject(value[0]))
        {
            for (var i = 0; i < value.length; i++)
            {
                properties[value[i].code] = value[i];
                value[i] = value[i].code;
            }
        }
        else if (Ext.isObject(value))
        {
            properties[value.code] = value;
            value = value.code;
        }
        
        return value;
    },
    
    /**
     * Function to render the content language
     * @param {Object} value The data value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     * @param {Number} rowIndex The index of the current row
     * @param {Number} colIndex The index of the current column
     * @param {Ext.data.Store} store The store
     * @param {Ext.view.View} view The current view
     * @param {String} dataIndex concatenated with '_language' will be get the language additional informations. 
     */
    renderLanguage: function (value, metaData, record, rowIndex, colIndex, store, view, dataIndex)
    {
        if (Ext.isEmpty(value))
        {
            return '';
        }
        
        dataIndex = dataIndex || rowIndex; // When used by grouping feature, data index is the 4th arguments
        
        var properties = record.get(dataIndex + "_language");
        
        var property = properties[value];
        
        return property ? property.label : value;
    },
    
    /**
     * Convert a user object to a user light-object. The light version is the same handled by the user widget, so editing such user will not change the object
     * @param {Object/Object[]} value The value as a object with the value for each language
     * @param {Ext.data.Model} record The record
     * @param {String} dataIndex The model data index
     * @return {Object/Object[]} The property value or code.
	 * @Deprecated Use {@link Ametys.grid.GridColumnHelper.convertUser}
     */
    convertUser: function (value, record, dataIndex)
    {
        return Ametys.grid.GridColumnHelper.convertUser.apply(this, arguments);
    },
    
    /**
     * Function to render the user login
     * @param {Object} value The data value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     * @param {Number} rowIndex The index of the current row
     * @param {Number} colIndex The index of the current column
     * @param {Ext.data.Store} store The store
     * @param {Ext.view.View} view The current view
     * @param {String} dataIndex concatenated with '_user' will get the user additional informations. 
	 * @Deprecated Use {@link Ametys.grid.GridColumnHelper.renderUser}
     */
    renderUser: function(value, metaData, record, rowIndex, colIndex, store, view, dataIndex)
    {
        return Ametys.grid.GridColumnHelper.renderUser.apply(this, arguments);
    },
    
    /**
     * Convert a content value for model.
     * Content values can be of two kinds
     * @param {String/String[]/Object/Object[]} value If it is an object, it will return the 'id' property.  
     * @param {String} value.id The id
     * @param {Ext.data.Model} record The record
     * @param {String} dataIndex The model data index
     * @return {String/String[]} The ids only
     */
    convertContent: function(value, record, dataIndex)
    {
        if ((typeof value == "string"
                || Ext.isArray(value) && (value.length == 0 
                                            || (typeof value[0] == "string")
                                            || Ext.isArray(value[0]) && (value[0].length == 0
                                                                        || typeof value[0][0] == "string")))
             && record.data[dataIndex + "_content"])
        {
            // already converted when back from repeater
            return value;
        }
        
        var properties = {};
        if (!record.data[dataIndex + "_content_initial"])
        {
            record.data[dataIndex + "_content_initial"] = Ext.clone(value);
        }
        record.data[dataIndex + "_content"] = properties;
        
        return valueToProp(value, properties);
        
        function valueToProp(value, properties)
        {
            if (Ext.isArray(value))
            {
                let values = [];
                for (var i = 0; i < value.length; i++)
                {
                    values.push(valueToProp(value[i], properties));
                }
                return values;
            }
            else if (Ext.isObject(value))
            {
                properties[value.id] = value;
                return value.id;
            }
            else
            {
                return value; // Should be null when null or undefined when undefined ODF-3560 
            }
        }
    },
    
    /**
     * @protected
     * Creates a content renderer
     * @param {Object} [config] The config
     * @param {String} [config.pluginName=cms] The plugin holding the url to retrieve info
     * @param {String} [config.url=contents/get-info] The plugin holding the url to retrieve info
     * @param {Boolean} [config.notClickable=fals] The plugin holding the url to retrieve info
     */
    createRenderContent: function(config)
    {
        config = Ext.applyIf(config || {}, {
            pluginName: 'cms',
            url: 'contents/get-info',
            notClickable: false
        });
        
        return function (value, metaData, record, rowIndex, colIndex, store, view, dataIndex)
        {
            if (Ext.isEmpty(value))
            {
                return '';
            }
            
            dataIndex = dataIndex || rowIndex; // When used by grouping feature, data index is the 4th arguments
            
            var values = Ext.isArray(value) ? value : [value];
            var html = '';
    
            var properties = record.get((dataIndex) + "_content");
            
            var contentToResolve = [];
            for (var i=0; i < values.length; i++)
            {
                var contentId = values[i];
                var property = properties[contentId];
                if (!property)
                {
                    contentToResolve.push(contentId);
                }
            }
            
            if (contentToResolve.length > 0)
            {
                // To resolve
                var response = Ametys.data.ServerComm.send({
                    plugin: config.pluginName,
                    url: config.url,
                    parameters: {ids: contentToResolve}, 
                    priority: Ametys.data.ServerComm.PRIORITY_SYNCHRONOUS, 
                    responseType: 'text'
                });
                
                if (Ametys.data.ServerComm.isBadResponse(response))
                {
                    for (var i=0; i < values.length; i++)
                    {
                        record.get(dataIndex + "_content")[values[i]] = {
                            id: values[i],
                            isSimple: true,
                            title: values[i]
                        }
                    }
                    
                    return Ext.isArray(value) ? value.join('<br/>') : value;
                }
                else
                {
                    var result = Ext.JSON.decode(Ext.dom.Query.selectValue("", response));
                    
                    var contents = result.contents;
                    for (var i=0; i < contents.length; i++)
                    {
                        record.get(dataIndex + "_content")[contents[i].id] = {
                            id: contents[i].id,
                            isSimple: contents[i].isSimple,
                            title: contents[i].title
                        }
                    }                   
                }
            }
            
            var properties = record.get(dataIndex + "_content");
            for (var i=0; i < values.length; i++)
            {
                var contentId = values[i];
                var property = properties[contentId];
                
                // Already resolved
                if (!Ext.isEmpty(html))
                {
                    html +='<br/>';
                }
                
                if (property.isSimple || config.notClickable)
                {
                    html += Ext.String.escapeHtml(property.title);
                }
                else if (property.title)
                {
                    html += '<a href="javascript:(function(){Ametys.tool.ToolsManager.openTool(\'uitool-content\', {id:\'' + contentId + '\'});})()">' + Ext.String.escapeHtml(property.title) + '</a>';
                }
            }
            
            return html;
        }
    },
    
    /**
     * Function to render a content value
     * @param {Object} value The data value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     * @param {Number} rowIndex The index of the current row
     * @param {Number} colIndex The index of the current column
     * @param {Ext.data.Store} store The store
     * @param {Ext.view.View} view The current view
     * @param {String} dataIndex concatenated with '_content' will be get the content additional informations. 
     */
    
    /**
     * Convert a tag value for model.
     * @param {String/Object[]} value If it is an object, it will return the 'name' property.  
     * @param {String} value.name The tag name
     * @param {Ext.data.Model} record The record
     * @param {String} dataIndex The model data index
     * @return {String} The tag name only
     */
    convertTag: function (value, record, dataIndex)
    {
        var properties = {};
        record.data[dataIndex + "_tag"] = properties;
        
        if (Ext.isArray(value) && value.length > 0 && Ext.isObject(value[0]))
        {
            for (var i = 0; i < value.length; i++)
            {
                properties[value[i].name] = value[i];
                value[i] = value[i].name;
            }
        }
        else if (Ext.isObject(value))
        {
            properties[value.name] = value;
            value = value.name;
        }
        
        return value;
    },
    
    /**
     * Function to render a tag value
     * @param {Object} value The data value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     * @param {Number} rowIndex The index of the current row
     * @param {Number} colIndex The index of the current column
     * @param {Ext.data.Store} store The store
     * @param {Ext.view.View} view The current view
     * @param {String} dataIndex concatenated with '_tag' will be get the tag additional informations. 
     */
    renderTag: function (value, metaData, record, rowIndex, colIndex, store, view, dataIndex)
    {
        if (Ext.isEmpty(value))
        {
            return '';
        }
        
        dataIndex = dataIndex || rowIndex; // When used by grouping feature, data index is the 4th arguments
        
        var values = Ext.isArray(value) ? value : [value];
        
        var labels = [];
        
        var properties = record.get(dataIndex + "_tag");
        for (var i=0; i < values.length; i++)
        {
            var tagName = values[i];
            var property = properties[tagName];
            
            labels.push (property ? Ext.String.escapeHtml(property.label) : tagName);
        }
        
        return labels.join(", ");
    },
    
    /**
     * Function to render a richtext
     * @param {Object} value The data value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     * @param {Number} rowIndex The index of the current row
     * @param {Number} colIndex The index of the current column
     * @param {Ext.data.Store} store The store
     * @param {Ext.view.View} view The current view
     * @param {String} dataIndex concatenated with '_richtext' will get the richtext additional informations. 
     */
    renderRichText: function(value, metaData, record, rowIndex, colIndex, store, view, dataIndex)
    {
        if (Ext.isEmpty(value))
        {
            return '';
        }
        
        var values = Ext.Array.from(value);
        
        var html = '';
        for (var i=0; i < values.length; i++)
        {
            var v = values[i];
            
            if (!Ext.isEmpty(html))
            {
                html += '<br/>';
            }
            
            if (Ext.isObject(v))
            {
                 html += v.value || '';
            }
            else
            {
                 html += v;
            }
        }
        
        return html;
    },
    
    /**
     * Function to render a file
     * @param {Object} value The data value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     * @param {Number} rowIndex The index of the current row
     * @param {Number} colIndex The index of the current column
     * @param {Ext.data.Store} store The store
     * @param {Ext.view.View} view The current view
     * @param {String} dataIndex concatenated with '_file' will get the file additional informations. 
     */
    renderFile: function(value, metaData, record, rowIndex, colIndex, store, view, dataIndex)
    {
        if (Ext.isEmpty(value))
        {
            return '';
        }
        
        dataIndex = dataIndex || rowIndex; // When used by grouping feature, data index is the 4th arguments
        
        var values = Ext.Array.from(value);
        
        var html = '';
        for (var i=0; i < values.length; i++)
        {
            var fileValue = values[i];
            
            if (!Ext.isEmpty(html))
            {
                html += '<br/>';
            }
            
            if (Ext.isObject(fileValue))
            {
                var hint = Ext.String.format("{{i18n plugin.cms:UITOOL_CONTENTEDITIONGRID_COLUMN_DOWNLOAD_FILE}}", fileValue.filename);
                html += fileValue.filename ? '<a href="' + fileValue.downloadUrl + '" target="_blank" title="' + hint + '">' + Ext.String.escapeHtml(fileValue.filename) +'</a>' : '';
            }
            else
            {
                html += fileValue[i];
            }
        }
        
        return html;
    },
    
    /**
     * Function to render a reference
     * @param {Object} value The data value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     * @param {Number} rowIndex The index of the current row
     * @param {Number} colIndex The index of the current column
     * @param {Ext.data.Store} store The store
     * @param {Ext.view.View} view The current view
     * @param {String} dataIndex The date column. 
     */
    renderReference: function(value, metaData, record, rowIndex, colIndex, store, view, dataIndex)
    {
        if (Ext.isEmpty(value))
        {
            return '';
        }
        
        return value.value ? value.value : value;
    },
    
    /**
     * Function to render a display value based upon another record entry.
     * You have to specify as an additional parameter the base field id.
     * 
     *          // record has a entry 'entry' and an entry 'entryDisplay' that show a displayable version of 'entry'
     *          { renderer: Ext.bind(Ametys.plugins.cms.search.SearchGridHelper.renderDisplay, null, ['entry'], true);
     * 
     * @param {Object} value The data value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     * @param {Number} rowIndex The index of the current row
     * @param {Number} colIndex The index of the current column
     * @param {Ext.data.Store} store The store
     * @param {Ext.view.View} view The current view
     * @param {String} dataIndex concatenated with 'Display' will be the name of the dataIndex to display 
     */
    renderDisplay: function (value, metaData, record, rowIndex, colIndex, store, view, dataIndex)
    {
        dataIndex = dataIndex || rowIndex; // When used by grouping feature, data index is the 4th arguments
        
        var fields = Ext.create('Ext.util.MixedCollection', {
            getKey: function(el) {
                return el.name; //the key for this collection is the attribute 'name' of each item
            }
        });
        fields.addAll( record.getFields() );
        var displayField = fields.getByKey(dataIndex).displayField;
        if (displayField)
        {
            var displayValue = record.data[displayField] || value;
            return Ext.isArray(displayValue) ? Ext.Array.map(displayValue, v => Ext.String.escapeHtml(v)).join('<br/>') : Ext.String.escapeHtml(displayValue);
        }
        
        return Ext.isArray(value) ? Ext.Array.map(value, v => Ext.String.escapeHtml(v)).join('<br/>') : Ext.String.escapeHtml(value);
    },

    /**
     * Function to render a repeater
     * @param {Object} value The data value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     * @param {Number} rowIndex The index of the current row
     * @param {Number} colIndex The index of the current column
     * @param {Ext.data.Store} store The store
     * @param {Ext.view.View} view The current view
     * @param {String} dataIndex concatenated with 'Display' will be the name of the dataIndex to display 
     */
    renderRepeater: function (value, metaData, record, rowIndex, colIndex, store, view, dataIndex)
    {
        if (Ext.isEmpty(value))
        {
            return '';
        }

        var repeater = record.get(dataIndex + "_repeater") || record.get(dataIndex);
        if (Ext.isObject(repeater))
        {
            var rawValues = repeater.entries;
            
            if (Ext.isArray(rawValues) && rawValues.length > 0)
            {
                var repeaterLabel = repeater.label;
                var headerTpl = null;
                
                let tplString = repeater["header-label"] || record.fields.filter(f => f.name == dataIndex)[0]['header-label'];
                if (tplString)
                {
                    var headerLabel = tplString;
                    headerTpl = new Ext.Template(headerLabel, {compiled: true});
                }
                var entries = [];
                for (var i in rawValues)
                {
                    var rawValue = rawValues[i];
                    var label = repeaterLabel;
                    var emptyValue = true;
                    if (headerTpl != null)
                    {
                        headerParams = {};
                        while ((result = Ametys.form.ConfigurableFormPanel.Repeater.HEADER_VARIABLES.exec(headerLabel)) != null)
                        {
                            var fieldValue = rawValue.values[result[1]];
                            if (fieldValue != null)
                            {
                                emptyValue = false;
                                
                                if (rawValue.values[result[1] + "_content"] != null) // Content in model (we just have its id)
                                {
                                    let texts = [];
                                    for (let c of Ext.Array.from(fieldValue))
                                    {
                                        if (rawValue.values[result[1] + "_content"][c])
                                        {
                                            texts.push(rawValue.values[result[1] + "_content"][c].title || c);
                                        }
                                    }
                                    fieldValue = texts;
                                }
                                else if (Ext.isObject(fieldValue) || Ext.isArray(fieldValue))  // Content not in model (we have a full object)
                                {
                                    let texts = [];
                                    for (let c of Ext.Array.from(fieldValue))
                                    {
                                        if (Ext.isObject(c))
                                        {
                                            texts.push(c.title || c.id || c);
                                        }
                                    }
                                    fieldValue = texts;
                                }
                                
                                headerParams[result[1]] = fieldValue;
                            }
                        }
    
                        var addTitle = headerTpl.apply(headerParams);
                        if (!emptyValue && addTitle)
                        {
                            label = addTitle;
                        }                    
                    }
    
                    entries.push({
                        label: Ext.String.escapeHtml(label),
                        position: rawValue.position,
                        tpl: headerTpl != null && !emptyValue
                    });
                }
            
                var html = this._repeaterTpl.applyTemplate({
                    entries: entries,
                });
                
                return html;
            }
        }

        return '';
    }, 
    
    _explodeRepeater: function (entries, record, repeater, prefix)
    {
        let me = this;
        let repeaterEntries = repeater.entries;
        Ext.Array.each(repeaterEntries, function (repeaterEntry) {
            entries["_" + prefix.substring(0, prefix.length - 1) + "[" + repeaterEntry.position + "]/previous-position"] = repeaterEntry['previous-position'] != undefined ? repeaterEntry['previous-position'] : repeaterEntry.position;
            
            Ext.Object.each(repeaterEntry.values, function (attributeName, repeaterEntryValue) {
                if (Ext.String.endsWith(attributeName, "_content")
                    || Ext.String.endsWith(attributeName, "_initial")
                    || Ext.String.endsWith(attributeName, "_external_status")
                    || Ext.String.endsWith(attributeName, "_enum")
                    || attributeName == "id"
                    || record.get(repeaterEntry.values[attributeName + "_repeater"]))
                {
                    // We do not expose internal record attributs
                    return;
                }
                
                if (Ext.String.endsWith(attributeName, "_repeater"))
                {
                    attributeName = attributeName.substring(0, attributeName.length - "_repeater".length);
                }
                
                if (Ext.isObject(repeaterEntryValue) && repeaterEntryValue.entries !== undefined && Ext.isArray(repeaterEntryValue.entries))
                {
                    me._explodeRepeater(entries, record, repeaterEntryValue, prefix.substring(0, prefix.length - 1) + "[" + repeaterEntry.position + "]/" + attributeName + "/");
                }
                else
                {
                    entries[prefix.substring(0, prefix.length - 1) + "[" + repeaterEntry.position + "]/" + attributeName] = repeaterEntryValue;
                }
            });
        });

        entries["_" + prefix + "size"] =  repeaterEntries.length;
    },

    /**
     * Convert a repeater value for model.
     * @param {String/Object} value If it is an object, it will return the array of keys.
     * @param {String} value.login The user login
     * @param {Ext.data.Model} record The record
     * @param {String} dataIndex The model data index
     * @return {String} The user login only
     */
    convertRepeater: function(value, record, dataIndex)
    {
        // Non-empty repeater ?
        if (Ext.isObject(value) && Ext.isArray(value.entries))
        {
            record.data[dataIndex + "_repeater"] = value;
            let entries = {};

            
            // Convert the dataformat recursively in repeaters now
            Ametys.plugins.cms.search.SearchGridRepeaterDialog.convert(value.entries, record.getProxy().getModel().getField(dataIndex).subcolumns, dataIndex);

            this._explodeRepeater(entries, record, value, "");
            
            if (!record.data[dataIndex + "_repeater_initial"])
            {
                // First rendering of this repeater, let's store the original objet that contains all data, to be able to know if there is any modified stuff
                record.data[dataIndex + "_repeater_initial"] = Ext.clone(value);
            }

            return entries;
        }
        else if (value && value._size) // Subrepeater already converted
        {
            return value;
        }
        else // Empty repeater
        {
            record.data[dataIndex + "_repeater"] = { entries: [], label: "" };
            return {
                "_size": 0
            }
        }
    },

    /**
     * Function to render a date
     * @param {Object} value The data value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     * @param {Number} rowIndex The index of the current row
     * @param {Number} colIndex The index of the current column
     * @param {Ext.data.Store} store The store
     * @param {Ext.view.View} view The current view
     * @param {String} dataIndex concatenated with 'Display' will be the name of the dataIndex to display 
	 * @Deprecated Use {@link Ametys.grid.GridColumnHelper.renderDate}
     */
    renderDate: function (value, metaData, record, rowIndex, colIndex, store, view, dataIndex)
    {
        return Ametys.grid.GridColumnHelper.renderDate.apply(this, arguments);
    },

    /**
     * Function to render a datetime
     * @param {Object} value The data value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     * @param {Number} rowIndex The index of the current row
     * @param {Number} colIndex The index of the current column
     * @param {Ext.data.Store} store The store
     * @param {Ext.view.View} view The current view
     * @param {String} dataIndex concatenated with 'Display' will be the name of the dataIndex to display 
	 * @Deprecated Use {@link Ametys.grid.GridColumnHelper.renderDateTime}
     */
    renderDateTime: function (value, metaData, record, rowIndex, colIndex, store, view, dataIndex)
    {
        return Ametys.grid.GridColumnHelper.renderDateTime.apply(this, arguments);
    },
    
    /**
     * Function to render an hidden cell
     * @param {Object} value The data value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     * @param {Number} rowIndex The index of the current row
     * @param {Number} colIndex The index of the current column
     * @param {Ext.data.Store} store The store
     * @param {Ext.view.View} view The current view
     * @param {String} dataIndex concatenated with 'Display' will be the name of the dataIndex to display 
     */
    renderHiddenValue: function (value, metaData, record, rowIndex, colIndex, store, view, dataIndex)
    {
        return '';
    },
    
     /**
     * Function to render error if the value is invalid
     * @param {Object} value The value
     * @param {Object} metaData A collection of metadata about the current cell
     * @param {Ext.data.Model} record The record
     * @param {Number} rowIndex The index of the current row
     * @param {Number} colIndex The index of the current column
     * @param {Ext.data.Store} store The store
     * @param {Ext.view.View} view The current view
     * @param {String} dataIndex The index of column
     * @param {Object[]} enumeration The enumeration of possible values. The key is the field value, and the value is the associated label.
     * @param {Function} [wrappedRenderer] The renderer of the value, wrapped by this one
     */
    renderValueError: function(me, value, metaData, record, wrappedRenderer, args)
    {
         // metaData is a empty objet when used in grouping
        if (metaData.column && !me.isValidValue(metaData.column.config, record))
        {
            // Add a class to the cell to indicate that the data is not valid
            metaData.innerCls = ' cell-error';
        }
        
        if (metaData.column)
        {
            let dataIndex = metaData.column.dataIndex;
            
            if (this._evaluateDisableCondition(record, dataIndex))
            {
                metaData.tdCls += ' cell-disabled';
                
                if (this._shouldHideDisabledCell(record, dataIndex))
                {
                    wrappedRenderer = Ext.bind(Ametys.getFunctionByName('Ametys.plugins.cms.search.SearchGridHelper.renderHiddenValue'), null, [dataIndex], true);
                }
            }
        }
        
        let root = this._getRootRecord(record)
        rootRecord = root.record;
        parentMetadataPath = root.parentPath;
        
        if (rootRecord.get('notEditableData') == true || rootRecord.data['notEditableDataIndex'] && Ext.Array.contains(rootRecord.data['notEditableDataIndex'], (parentMetadataPath ? parentMetadataPath + '/' : '') +  metaData.column.dataIndex))
        {
            metaData.tdCls += ' cell-noteditable';
        }
       
        if (wrappedRenderer == null)
        {
            wrappedRenderer = Ext.identityFn;
        }
        
        return wrappedRenderer.apply(me, args);
    },
    
    /**
     * @private
     */
    _evaluateDisableCondition(record, dataIndex)
    {
        let parentMetadataPath = this._getParentMetadataPath(record);
        if (parentMetadataPath)
        {
            parentRecord = this._getParentRecord(record);
            if (this._evaluateDisableCondition(parentRecord, parentMetadataPath))
            {
                return true;
            }
        }
        
        // With imbricated repeaters and composite you may be here with a path including /
        // We are interested only in the top level dataIndex... but we do not know if it is the last part or included in a composite
        // So with a/b/c we have to try b/c then c
        // TODO CMS-12481: get parentMetadataPath of parent record and remove common part instead of this algorithm

        let field = null;
        while (true)
        {
            field = record.getField(dataIndex);
            if (field == null)
            {
                let i = dataIndex.indexOf('/');
                if (i <= 0)
                {
                    return false; // No field found, no disable condition
                }
                dataIndex = dataIndex.substring(i + 1);
            }
            else
            {
                break;
            }
        }
        
        return Ametys.form.Widget.evaluateDisableCondition({
            disableCondition: field.disableCondition,
            dataPath: dataIndex, 
            record: record 
        });
    },

    /**
     * @private
     */
    _shouldHideDisabledCell(record, dataIndex)
    {
        let parentMetadataPath = this._getParentMetadataPath(record);
        if (parentMetadataPath)
        {
            parentRecord = this._getParentRecord(record);
            if (this._shouldHideDisabledCell(parentRecord, parentMetadataPath))
            {
                return true;
            }
        }
        
        // With imbricated repeaters and composite you may be here with a path including /
        // We are interested only in the top level dataIndex... but we do not know if it is the last part or included in a composite
        // So with a/b/c we have to try b/c then c
        // TODO CMS-12481: get parentMetadataPath of parent recode and remove common part instead of this algorithm

        let field = null;
        while (true)
        {
            field = record.getField(dataIndex);
            if (field == null)
            {
                let i = dataIndex.indexOf('/');
                if (i <= 0)
                {
                    return false; // No field found, no disable condition
                }
                dataIndex = dataIndex.substring(i + 1);
            }
            else
            {
                break;
            }
        }
        
        return Ametys.form.Widget.shouldHideDisabledField({
            disableCondition: field.disableCondition,
            disabledItemRendering: field.disabledItemRendering,
            dataPath: dataIndex, 
            record: record 
        });
    },

    // -------------------------------------
    // ---------------- API ----------------
    // -------------------------------------
    
    /**
     * Return default sorters, based on the filled values of search criteria.
     * <br>If one of the criteria uses a SEARCH/SEARCH_STEMMED operator and is not empty, then the returned default sorter
     * will be an empty array (considered by server-side as a sort by score). Otherwise, it returns undefined and it should be other default ones (on your choice)
     * @param {Object[]} criteria The search criteria
     * @return {Object[]} The sorters
     */
    getDefaultSorters: function(criteria)
    {
        var hasNonEmptySearchCriterion = false;
        Ext.Object.each(criteria, function(criterionName, criterionVal) {
            if ((criterionName.endsWith("-search") || criterionName.endsWith("-searchStemmed")) && Ext.isString(criterionVal) && criterionVal)
            {
                hasNonEmptySearchCriterion = true;
                return false;
            }
        });
        return hasNonEmptySearchCriterion ? [] /*would normally be [{property: 'score', direction: 'DESC'}] but it is not possible with current search API. Empty array is equivalent */
                                          : undefined;
    },
    
    /**
     * Return fields configuration from JSON definition
     * @param {Object} data The fields definition
     * @return {Object[]} The fields configuration
     */
    getFieldsFromJson: function(data)
    {
        var fields = [];
        for (var i in data)
        {
            var columnData = data[i];
            var id = data[i].path;
            
            var field = this.getFieldFromJson(id, columnData);
            
            fields.push(field);
        }
        
        return fields;
    },
    
    /**
     * Return a field configuration from a definition described in JSON
     * @param {String} id The id of the field
     * @param {Object} data The field definition
     * @return {Object} The configuration object
     */
    getFieldFromJson: function(id, data)
    {
        var cfg = {
            name: id,
            mapping: "properties['" + id + "']",
            useNull: true
        };
        
        if (data.converter)
        {
            cfg.convert = Ext.bind(Ametys.getFunctionByName(data.converter), null, [id], true);
        }
        else
        {
            let defaultConvertor = this._getDefaultConvertor(id, data);
            if (defaultConvertor != null)
            {
                cfg.convert = defaultConvertor;
            }            
        }
        
        if (data.disableCondition)
        {
            cfg.disableCondition = data.disableCondition;
            if (data.disabledItemRendering)
            {
                cfg.disabledItemRendering = data.disabledItemRendering;
            }
        }
        if (data['widget'])
        {
            cfg.widget = data['widget'];
        }
        if (data['widget-params'] && data['widget-params'].changeListeners)
        {
            cfg.changeListeners = data['widget-params'].changeListeners;
        }
        
        var needsMultipleConverter;
        var extJsTypes = {
            'long': 'int',
            'double': 'number'
        };
        
        var type = data.type.toLowerCase();
        cfg.ftype = type;
        switch (type) 
        {
            case 'string':
                cfg.type = 'string';
                if (data.enumeration && !data.converter)
                {
                    cfg.convert = Ext.bind(Ametys.plugins.cms.search.SearchGridHelper.convertEnumeratedData, null, [id], true);
                }
                else if (data.multiple && !data.converter)
                {
                    needsMultipleConverter = true;
                }
                break;
            case 'long':
            case 'double':
            case 'boolean':
                if (data.enumeration && !data.converter)
                {
                    cfg.convert = Ext.bind(Ametys.plugins.cms.search.SearchGridHelper.convertEnumeratedData, null, [id], true);
                }
                else if (data.multiple && !data.converter)
                {
                    needsMultipleConverter = true;
                }
                else
                {
                    cfg.type = extJsTypes[type] || type;
                }
                cfg.allowNull = true;
                break;
            case 'repeater':
                // repeaters
                cfg.subcolumns = data.columns;
                cfg['min-size'] = data['min-size'];
                cfg['max-size'] = data['max-size'];
                cfg['initial-size'] = data['initial-size'];
                cfg['add-label'] = data['add-label'];
                cfg['del-label'] = data['del-label'];
                cfg['header-label'] = data['header-label'];
                cfg['widget-params'] = data['widget-params'];
                break;
        }
        
        if (needsMultipleConverter)
        {
            // need to keep value as an array (ExtJS default converter for multiple values join value separated by comma)
            cfg.type = 'string';
            cfg.convert = Ext.bind(Ametys.plugins.cms.search.SearchGridHelper.convertMultipleValue, null, [id], true);
        }
        
        // Wraps convertor with SynchronizedValues extractor
        let wrappedConvertor = cfg.convert;
        cfg.convert = function() 
        {
            let args = Ext.Array.clone(arguments);
            
            let originalValue = args[0];
            let isValueAnArray = Ext.isArray(originalValue);
            let values = Ext.Array.from(originalValue)
            
            let outputValues = [];
            
            for (let value of values)
            {
                if (value && (value.status == "external" || value.status == "local"))
                {
                    var record = args[1];
                    record.data[id + "_external_status"] = value.status;
                    
                    value = value[value.status] !== undefined ? value[value.status] : null; // Return the null value if there is no value at the given status because, the caller of convertor will ignore "undefined" but assign "null"
                }
                outputValues.push(value);
            }
            
            if (!isValueAnArray)
            {
                outputValues = outputValues[0];
            }
            
            // args is a copy, we can modify it to remove the args[0] and replace it
            args.splice(0, 1, outputValues);
            
            return wrappedConvertor ? wrappedConvertor.apply(this, args) : outputValues;
        };
        
        return cfg;
    },

    /**
     * Return a convertor depending on the type
     * @param {String} id The id of the field
     * @param {Object} data The field definition
     * @returns {Function} The convertor function or null
     */    
    _getDefaultConvertor: function(id, data)
    {
        var type = data.type.toLowerCase();
        switch (type) 
        {
            case 'user':
                return Ext.bind(Ametys.grid.GridColumnHelper.convertUser, this);
            case 'content':
                return Ext.bind(Ametys.plugins.cms.search.SearchGridHelper.convertContent, this, [id], true);
            default:
                return null;
        }
    },
    
    /**
     * Return a sorters configuration from JSON definition
     * @param {Object[]} columns The columns configuration from #getColumnsFromJson
     * @param {Ext.grid.Panel} [grid] The grid to get state informations
     * @param {Boolean} [forceDataOrder=false] True to make data with a higher priority in state
     * @param {Object} [state] A state (in the Ext.state.Stateful vocabulary) of the grid
     * @return {Object[]} The sorters configuration
     */
    getSortersFromJson: function(columns, grid, forceDataOrder, state)
    {
        var stateSorters;
        if (!state || Ext.Object.isEmpty(state))
        {
            var stateId = grid && grid.stateful ? grid.getStateId() : null;
            if (stateId) 
            {
                var state = Ext.state.Manager.get(stateId);
                if (state && state.columns) 
                {
                    stateSorters = state.storeState ? state.storeState.sorters : null;
                }
            }
        }
        else
        {
            stateSorters = state.storeState.sorters;
        }
        
        if (Ext.Object.isEmpty(stateSorters))
        {
            // Loop on columns to find all with defaultSort or first sortable
            var defaultSorters = [];
            var firstSortable = null;
            Ext.Array.each(columns, function(column) {
                if (column.defaultSorter)
                {
                    defaultSorters.push({
                        property: column.dataIndex,
                        direction: column.defaultSorter == 'DESC' ? 'DESC' : 'ASC'
                    });
                }
                else if (defaultSorters.length == 0 && !firstSortable && column.sortable && !column.hidden)
                {
                    firstSortable = { 
                        property: column.dataIndex, 
                        direction: 'ASC' 
                    };
                }
            });
            return defaultSorters.length > 0 ? defaultSorters : (firstSortable != null ? [firstSortable] : []);
        }
        else
        {
            // Filter stateSorters as it can reference unexisting columns
            var columnIds = Ext.Array.map(columns, function(column) {return column.dataIndex;});
            return stateSorters.filter(function(stateSorter) {return Ext.Array.contains(columnIds, stateSorter.property);});
        }
    },
    
    /**
     * Return columns configuration from JSON definition
     * @param {Object[]} data The columns definition
     * @param {Boolean} withEditor True to enable edition in the columns (then, an editor will be tried to be built from data object).
     * @param {Ext.grid.Panel} [grid] The grid to get state informations
     * @param {Boolean} [forceDataOrder=false] True to make data with a higher priority on state
     * @param {Object} [state] A state (in the Ext.state.Stateful vocabulary) of the grid
     * @return {Object[]} The columns configuration
     */
    getColumnsFromJson: function (data, withEditor, grid, forceDataOrder, state)
    {
        var stateColumns;
        if (!state || Ext.Object.isEmpty(state))
        {
            var stateId = grid && grid.stateful ? grid.getStateId() : null;
            if (stateId) 
            {
                var state = Ext.state.Manager.get(stateId);
                if (state && state.columns) 
                {
                    stateColumns = state.columns;
                }
            }
        }
        else
        {
            stateColumns = state.columns || {};
        }
        
        var columns = [];
        var handledColumns = [];
        
        if (!forceDataOrder)
        {
            // Handle stated
            for (var o in stateColumns)
            {
                var id = stateColumns[o].id;
                var column = this.getColumn(id, data);
                if (column)
                {
                    columns.push(this.getColumnFromJson(id, column, stateColumns[o], withEditor));
                    handledColumns.push(id);
                }
            }
        }
        
        // Handle others
        for (var i = 0; i < data.length; i++)
        {
            var id = data[i].path;
            
            if (!Ext.Array.contains(handledColumns, id))
            {
                var stateCol = this.getColumn(id, stateColumns);
                
                // stateCol can be null for non-stated columns.
                columns.push(this.getColumnFromJson(id, data[i], stateCol, withEditor));
            }
        }
        
        return columns;
    },
    
    /**
     * @private
     * Find the column with a particular id
     * @param {String} id The id to seek
     * @param {Object} columns A non-null object where to seek in. This object contains object with "path" property.
     * @return {Object} The found object or null
     */
    getColumn: function(id, columns)
    {
        for (var i in columns)
        {
            if (columns[i].path == id)
            {
                return columns[i];
            }
        }
        
        return null;
    },
    
    /**
     * Return a column configuration from a definition described in JSON
     * @param {String} id The data index of the column
     * @param {Object} data The field definition
     * @param {Object} state A state (in the Ext.state.Stateful vocabulary) of the grid
     * @param {Boolean} withEditor True to enable edition in the column (then, an editor will be tried to be built from data object).
     * @return {Object} The configuration object
     * @private
     */
    getColumnFromJson: function(id, data, state, withEditor)
    {
        var cfg = {
            stateId: id,
            headerId: id, // FIXME workaround for https://issues.ametys.org/browse/CMS-9008 (see https://www.sencha.com/forum/showthread.php?469623-ExtJS-6-5-3-Grid-reconfigure-methods-generates-a-warning&p=1317295#post1317295)
//            columnPropId: id,
            header: data.label || data.header, 
            width: state && state.width ? state.width : (data.width || (data.subColumns ? undefined : 100)), 
            flex: state ? state.flex : null, 
            sortable: Ext.isBoolean(data.sortable) ? data.sortable : true,
            defaultSorter: data.defaultSorter,
            // Check if state.hidden is defined, as otherwise hidden columns from data side will appear
            hidden: (state && state.hidden !== undefined ) ? state.hidden : (Ext.isBoolean(data.hidden) ? data.hidden : false),
            locked: state ? (state.locked ? state.locked : false) : (false),
            // Group header does not accept a dataIndex
            dataIndex: data.subColumns ? undefined : id,
            // we need to store enumeration, type and multiple into initial config to be able to save and reapply column formatting
            type: data.type, 
            multiple: data.multiple,
            enumeration: data.enumeration,
            groupable: !data.multiple,
            validation: data.validation,
            filter: data.filter
        };
        if (withEditor)
        {
            cfg.editor = this.getEditorFromJson(data);
        }
        
        // we need to store rendererFn configuration to be able to save and reapply column formatting
        if (data.renderer)
        {
            cfg.rendererFn = data.renderer;
        }
        else
        {
            let defaultRenderer = this._getDefaultRenderer(data, cfg);
            if (defaultRenderer != null)
            {
                cfg.rendererFn = defaultRenderer;
            }
        }
        
        cfg.renderer = this.getRenderer(cfg);
        
        if (data.subColumns)
        {
            cfg.columns = data.subColumns.map(element => this.getColumnFromJson(element.id, element, state ? state.columns[element.id] : null, withEditor));
        }
        
        return cfg;
    },
    
    /**
     * Return a rendering function based on the initial config of an ExtJS column build from a search model
     * @param {Object} cfg The config of an Ext.grid.column.Column
     * @param {String} [cfg.rendererFn] The name of the function used to render a single value. Can be null in the case of an enumeration
     * @param {Object} [cfg.enumeration] The enumeration value if the column value is an enumerated
     * @param {boolean} [cfg.multiple] true if the column value is multiple
     * @param {String} [cfg.type] the element type of the value
     * @return {Function} The rendering function
     */
    getRenderer: function(cfg)
    {
        let renderer;
        
        var enumeration = {};
        if (cfg.enumeration)
        {
            for (var i = 0; i < cfg.enumeration.length; i++)
            {
                enumeration[cfg.enumeration[i].value] = cfg.enumeration[i].label;
            }

            if (!cfg.rendererFn)
            {
                // default renderer for enumerated data
                cfg.rendererFn = 'Ametys.plugins.cms.search.SearchGridHelper.renderEnumeration';
            }
        }

        if (cfg.rendererFn)
        {
            renderer = Ext.bind(Ametys.getFunctionByName(cfg.rendererFn), null, [cfg.dataIndex, enumeration], true);
        }

        if (cfg.multiple && cfg.type != 'COMPOSITE')
        {
            var singleValueRenderer = renderer;
            var appendedArgs = [cfg.dataIndex, enumeration, singleValueRenderer];
            renderer = Ext.bind(Ametys.getFunctionByName('Ametys.plugins.cms.search.SearchGridHelper.renderMultipleValue'), null, appendedArgs, true);
        }

        // CMS-10738 - If our renderers are Ext.binded, the final renderer function as no arguments
        // But extjs considers that a good renderer is a renderer with at least 2 arguments
        // Otherwise we are not called after a grid modification (unless when using the rowexpander plugin)

        // wrap the renderer to indicate invalid values
        let wrappedRenderer = renderer;
        let me = this;
        renderer = function(value, metaData, record) {
            return me.renderValueError(me, value, metaData, record, wrappedRenderer, arguments)
        };
        
        return renderer;
    },
    
    /**
     * @private
     * Gets the default renderer
     * @param {Object} data The field definition
     * @param {Object} cfg The configuration object
     * @return {String} The default renderer function name
     */
    _getDefaultRenderer: function(data, cfg)
    {
        var defaultRenderer = data.rendererFn;
        var type = data.type.toLowerCase();
        switch (type) {
            case 'string':
                if (data.enumeration)
                {
                    defaultRenderer = 'Ametys.plugins.cms.search.SearchGridHelper.renderEnumeration';
                }
                else
                {
                    defaultRenderer = 'Ametys.grid.GridColumnHelper.renderWithHtmlEscaping';
                }
                break;
            case 'multilingual-string':
                defaultRenderer = 'Ametys.plugins.cms.search.SearchGridHelper.renderMultilingualString';
                break;
            case 'rich-text':
                defaultRenderer = 'Ametys.plugins.cms.search.SearchGridHelper.renderRichText';
                break;
            case 'date':
                defaultRenderer = 'Ametys.grid.GridColumnHelper.renderDate';
                break;
            case 'datetime':
                defaultRenderer = 'Ametys.grid.GridColumnHelper.renderDateTime';
                break;
            case 'long':
                cfg.xtype = 'numbercolumn';
                cfg.format = data.format || '0';
                break;
            case 'double':
                cfg.xtype = data.format || 'numbercolumn';
                break;
            case 'boolean':
                cfg.xtype = data.format || 'booleancolumn';
                defaultRenderer = 'Ametys.grid.GridColumnHelper.renderBooleanIcon';
                break;
            case 'geocode':
                defaultRenderer = 'Ametys.plugins.cms.search.SearchGridHelper.renderGeocode';
                break;
            case 'content':
            case 'sub_content':
                defaultRenderer = 'Ametys.plugins.cms.search.SearchGridHelper.renderContent';
                break;
            case 'user':
                defaultRenderer = 'Ametys.grid.GridColumnHelper.renderUser';
                break;
            case 'binary':
            case 'file':
                defaultRenderer = 'Ametys.plugins.cms.search.SearchGridHelper.renderFile';
                break;
            case 'reference':
                defaultRenderer = 'Ametys.plugins.cms.search.SearchGridHelper.renderReference';
                break;
            default:
                break;
        }
        
        return defaultRenderer;
    },
    
    /**
     * Retrieves the properties that must persist when the column are in a
     * stateful grid. These data can be lost when a grid is reconfigured. This
     * method can be used in this case in order to force the changes to persist.
     * @param {Ext.grid.column.Column} column The column
     * @return {Object} The properties
     */
    getStateColumnPropertiesToForce: function(column)
    {
        var p = {
            hidden: column.isHidden(),
            locked: column.locked || false
        };
        
        if (column.flex)
        {
            p.flex = column.flex;
        }
        
        if (column.width)
        {
            p.width = column.width;
        }
        
        return p;
    },
    
    /**
     * Returns the editor configuration from a metadata definition described in XML
     * @param {HTMLElement} data The metadata definition
     * @return {Object} The editor configuration object
     */
    getEditorFromJson: function (data)
    {
        if (!data.editable)
        {
            return null;
        }
        
        var widgetCfg = {
            fieldLabel: data.label || data.header,
            hideLabel: true,
            // In grids, we want to let the user set a mandatory field to empty, event if he won't be able to save afterward
            // allowBlank: !(data.validation ? data.validation.mandatory : false),
            allowBlank: true, // true should be the default valeur but CMS-11988
            multiple: data.multiple,
            widget: data.widget,
            contentType: data.contentType,
            msgTarget: 'qtip',
            ftype: data.type,
            defaultValue: data['default-value'],
            listeners: {
                'focus': function() { this.validate() }  
            },
            cls: 'ametys'
        };
        
        if (data.validation)
        {
            var validation = data.validation;
            widgetCfg.regexp = validation.regexp || null;
            
            if (validation.invalidText)
            {
                widgetCfg.invalidText = validation.invalidText;
            }
            if (validation.regexText)
            {
                widgetCfg.regexText = validation.regexText;
            }
        }
        
        if (data.enumeration)
        {
            var enumeration = [];
            
            var entries = data.enumeration;
            for (var j=0; j < entries.length; j++)
            {
                enumeration.push([entries[j].value, entries[j].label]);
            }
            
            widgetCfg.enumeration = enumeration;
        }
        
        if (data['widget-params'])
        {
            widgetCfg = Ext.merge (widgetCfg, data['widget-params']);
        }
        
        var xtype = Ametys.form.WidgetManager.getWidgetXType (data.widget, data.type.toLowerCase(), data.enumeration && data.enumeration.length > 0, data.multiple);
        return Ext.apply (widgetCfg, {xtype: xtype});
    },
    
    _getParentRecord: function(record) 
    {
        if (!record)
        {
            return null;
        }
        return record.parentRecord
                || (record.store || record.getTreeStore()).parentRecord;
    },
    _getParentMetadataPath: function(record) 
    {
        if (!record)
        {
            return null;
        }
        return record.parentMetadataPath 
                || (record.store || record.getTreeStore()).parentMetadataPath;
    },
    
    _getRootRecord: function(record) 
    {
        let rootRecord = record;
        let parentPath = "";
        while (this._getParentRecord(rootRecord))
        {
            parentPath += (parentPath ? "/" : "") + this._getParentMetadataPath(rootRecord);
            rootRecord = this._getParentRecord(rootRecord);
        }
        return {
            record: rootRecord,
            parentPath: parentPath
        }
    },
    
    /**
     * Check if the value in the given record is valid for the given column
     * @param {Object} column the column configuration (containing validation info)
     * @param {Object} record The record
     * @param {Object} recordFields The record fields
     * @param {String} parentMetadataPath When in a sub grid, the parent metadatapath
     * @param {Boolean} notEditableData If not editable at all
     * @param {String[]} notEditableDataIndex The list of absolute metadata path that cannot be edited
     * @return {Boolean} true if the value is valid, false otherwise
     */
    isValidValue: function(column, record, parentMetadataPath, notEditableData, notEditableDataIndex)
    {
        if (!column.editor && !column.editable && column.config && !column.config.editor)
        {
            return true;
        } 
        
        if (record.isModel)
        {
            let root = this._getRootRecord(record)
            rootRecord = root.record;
            parentMetadataPath = root.parentPath;
            
            notEditableData = rootRecord.get('notEditableData');
            notEditableDataIndex = rootRecord.get('notEditableDataIndex');
        }
        
        if (notEditableData === undefined)
        {
            notEditableData = record.get('notEditableData') || null;
        }
        if (notEditableDataIndex === undefined)
        {
            notEditableDataIndex = record.get('notEditableDataIndex') || null;
        }
        
        if (notEditableData)
        {
            return true;
        }
        
        let data = record.data;
        let fields = record.fields;
        let dataIndex = column.dataIndex || column.path || column.name;
        
        if (column.validation)
        {
            if (column.multiple)
            {
                let hasInvalidValue = false;
                let me = this;
                let values = this._getValuesForMultipleData(dataIndex, data);
        
                if (column.validation.mandatory && (!values || values.length ==0))
                {
                    return false;
                }
        
                values.forEach(function (value) {
                    if (!me._isSingleValueValid(column.validation, value))
                    {
                        hasInvalidValue = true;
                        return;
                    }
                });
        
                if (hasInvalidValue)
                {
                    return false;
                }
            }
            else
            {
                let value = data[dataIndex];
                
                if (notEditableDataIndex && parentMetadataPath)
                {
                    let cursor = parentMetadataPath;
                    while (true)
                    {
                        if (notEditableDataIndex.includes(cursor))
                        {
                            return true;
                        }
                        let i = cursor.lastIndexOf('/');
                        if (i == -1)
                        {
                            break;
                        }
                        cursor = cursor.substring(0, i);
                    }
                }
                
                if ((!notEditableDataIndex || !notEditableDataIndex.includes((parentMetadataPath ? parentMetadataPath + "/" : "") + dataIndex))
                     && !this._evaluateDisableCondition(record, dataIndex)
                     && !this._isSingleValueValid(column.validation, value))
                {
                    return false;
                }
            }
        }
        
        if((column.ftype || column.type) == 'repeater')
        {
            for (let entry of data[dataIndex + "_repeater"].entries)
            {
                let f = fields.filter(f => (f.path || f.name) == dataIndex)[0];
                let columns = f.subcolumns || f.columns;
                for (let subcolumn of columns)
                {
                    if (!this.isValidValue(
                        subcolumn, 
                        {
                          data: entry.values,
                          parentMetadataPath: (parentMetadataPath ? parentMetadataPath + "/" : "") + dataIndex,
                          parentRecord: record,
                          fields: columns,
                          getField: function(d){return this.fields.filter(f => (f.path || f.name) == d)[0];},
                          get: function(d){return this.data[d];}
                        },
                        (parentMetadataPath ? parentMetadataPath + "/" : "") + dataIndex, 
                        notEditableData, 
                        notEditableDataIndex
                      ))
                    {
                        return false;
                    }
                }
            }
        }
        
        return true;
    },
    
    /**
     * Check if the given single value is valid
     * @param {Object} validation the validation info
     * @param {Object} value the value to validate
     * @return {Boolean} true if the value is valid, false otherwise
     */
    _isSingleValueValid: function(validation, value)
    {
        let isBlank = Ext.isEmpty(value) || Ext.isObject(value) && Ext.Object.isEmpty(value);
        if (isBlank)
        {
            if (validation.mandatory)
            {
                return false;
            }
        }
        else if (validation.regexp)
        {
            let regexp = new RegExp(validation.regexp);
            if (Ext.isString(value) && !regexp.test(value) // For string type
                || Ext.isObject(value) && Ext.isString(value.value) &&  !regexp.test(value.value)) // For reference type
            {
                return false;
            }
        }
        
        return true;
    },
    
    /**
     * Retrieves the values at the given data index in the record for a multiple data
     * This method always retrieves an array
     * @param {String} dataIndex the data index
     * @param {Object[]} data the record data
     * @return {Array} the values of the multiple data
     */
    _getValuesForMultipleData: function(dataIndex, data)
    {
        let value = data[dataIndex];
        let values = data[dataIndex + "_values"];
        if (Ext.isArray(values))
        {
            return values;
        }
        else if (Ext.isArray(value))
        {
            return value;
        }
        else
        {
            return[value];
        }
    }
        
});


Ametys.plugins.cms.search.SearchGridHelper.renderContent = Ametys.plugins.cms.search.SearchGridHelper.createRenderContent();