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

/**
 * This class provides a repeater container
 * @private
 */
Ext.define('Ametys.form.ConfigurableFormPanel.Repeater',
{
    extend: 'Ext.panel.Panel',
    alias: 'widget.cms.repeater',
    
    statics: {
        /**
         * @property {Number} PADDING The padding of repeater
         * @private
         * @readonly 
         */
        PADDING: 5,
        
        /**
         * @property {Number} NESTED_OFFSET The left offset when nesting repeaters
         * @private
         * @readonly 
         */
        NESTED_OFFSET: 20,
        
        /**
         * @property {RegExp} HEADER_VARIABLES Regular expression used to extract used metadatas from the header template.
         * @private
         * @readonly 
         */
        HEADER_VARIABLES: /\{([^\}\:]+)(?:\:[^}]+)?\}/gi,
        
        /**
         * @property {Number} TOOLS_COLUMN_WIDTH The width for tools column for repeaters in mode table
         * @private
         * @readonly
         */
        TOOLS_COLUMN_WIDTH: 91,
        
        /**
         * @property {Number} FIELD_MARGIN_RIGHT The margin right for fields
         * @private
         * @readonly 
         */
        FIELD_MARGIN_RIGHT: 5,
        
        /**
         * @property {Number} SIMPLE_FIELD_MAX_HEIGHT The max height for simple fieds in table mode
         * @private
         * @readonly 
         */
        SIMPLE_FIELD_MAX_HEIGHT: 24,
        
        /**
         * @protected
         * Retrieves the name of the repeater at the given index
         * @param {String} name The name
         * @param {String} separator The separator
         * @param {Number} index The index
         * @return {String} The name of the repeater at the given index
         */
        getNameAtIndex: function(name, separator, index)
        {
            return name + '[' + index + ']';
        }
    },
    
    /**
     * @cfg {String} label The repeater label.
     */
    /**
     * @cfg {Ametys.form.ConfigurableFormPanel} form The parent form panel
     */
    /**
     * @cfg {String} addLabel The add button label.
     */
    /**
     * @cfg {String} delLabel The delete button label.
     */
    /**
     * @cfg {String} headerLabel The item panel header label template.
     */
    /**
     * @cfg {Boolean} readOnly True to disallow add, delete and move actions
     */
    /**
     * @cfg {Number} minSize The repeater min size.
     */
    /**
     * @cfg {Number} maxSize The repeater max size.
     */
    /**
     * @cfg {HTMLElement} compositionNode The repeater composition as XML node.
     */
    /**
     * @cfg {Object} composition The repeater composition as JSON object.
     */
    /**
     * @cfg {Object/Object[]} fieldCheckers The field checkers of this repeater as a JSON object
     */
    /**
     * @cfg {String} prefix The attribute prefix (to create sub elements)
     */
    /**
     * @cfg {Number} nestingLevel The nesting level of the repeater.
     */

    /**
     * @cfg {String} invalidCls The CSS class to use when marking the repeater invalid.
     */
    invalidCls: 'a-repeater-invalid',
    
    /**
     * @cfg {String} [defaultPathSeparator="/"] The default separator for fields
     */
    defaultPathSeparator: '/',
    
    /**
     * @cfg {String} [mode="panel"] The mode to use for the repeater's modification. Can be panel (default) or table
     */
    mode: 'panel',
    
    /**
     * @cfg {String/String[]/Ext.XTemplate} activeErrorsTpl The template used to format the Array of error messages.
     * It renders each message as an item in an unordered list.
     */
    activeErrorsTpl: [
                          '<tpl if="errors && errors.length">',
                              '<ul class="{listCls}">',
                                  '<tpl for="errors"><li>{.}</li></tpl>',
                              '</ul>',
                          '</tpl>'
                      ],
                      
    
    /**
     * @property {Number} _lastInsertItemPosition The last inserted item was at this position
     * @private
     */
    
    /**
     * @property {Object} _newItemExternalDisableConditionsValues the external disable conditions values to set to new repeater items
     * @private
     */
                      
    /**
     * @property {Boolean} isRepeater
     * Flag denoting that this component is a Repeater. Always true.
     */
    isRepeater : true,
    
    constructor: function(config)
    {
        config = config || {};
        
        if (config.defaultPathSeparator)
        {
            this.defaultPathSeparator = config.defaultPathSeparator;
        }
        
        this._newItemExternalDisableConditionsValues = config.newItemExternalDisableConditionsValues || {};
        
        this.callParent(arguments);  
    },
    
    initComponent: function()
    {
        Ext.apply(this, {
            ui: 'light',
            border: false,
            shadow: false,
            isRepeater: true,
            
            margin: this.nestingLevel > 1 ? ('0 0 5 ' + Ametys.form.ConfigurableFormPanel.Repeater.NESTED_OFFSET) : '0 0 5 0',
            
            items: [{
                hidden: true,
                items: [{
                    xtype: 'numberfield',
                    name: '_' + this.prefix + this.name + this.defaultPathSeparator + 'size',
                    value: 0
                }]
            }]
        });
        
        var header = {
            title: this.label + ' (0)',
            style: "border-width: 1px !important",
        };
        
        var addFirstTool = !this.readOnly ? this._addFirst(this.addLabel) : undefined;
        
        var cls = 'a-repeater a-repeater-level-' + this.nestingLevel + (this.nestingLevel % 2 == 0 ? ' even' : ' odd');
        
        if (this.mode == 'table')
        {
            Ext.applyIf(this, {
                layout: {
                    type: 'anchor'
                }
            });
            
            // Header line with columns' labels is put in dockedItems
            var dockedItemsChildren = [];
            
            // Tool to add a repeater item at first position
            if (addFirstTool)
            {
                Ext.apply(addFirstTool, {
                    width: Ametys.form.ConfigurableFormPanel.Repeater.TOOLS_COLUMN_WIDTH,
                    minWidth: Ametys.form.ConfigurableFormPanel.Repeater.TOOLS_COLUMN_WIDTH
                });
                dockedItemsChildren.push(addFirstTool);
            }
            
            // Columns' labels
            Ext.Array.push(dockedItemsChildren, this._getRepeaterTableColumns());
            
            Ext.apply(this, {
                header: header,
                
                dockedItems: [{
                    xtype: 'container',
                    ui: 'light',
                    cls: 'a-column-header',
                    border: true,
                    layout: {
                        type: 'hbox',
                        align: 'stretch'
                    },
                    
                    items: dockedItemsChildren
                }],
                
                cls: cls + ' a-repeater-table'
            });
        }
        else
        {
            Ext.applyIf(this, {
                layout: {
                    type: 'repeater-accordion',
                    multi: true
                }
            });
            
            this._toolExpandAll = Ext.create('Ext.panel.Tool', {
                type: 'expandall',
                qtip: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_TOOL_EXPAND_ALL_HINT}}",
                handler: this.expandAll,
                hidden: true,
                scope: this
            });
            
            this._toolCollapseAll = Ext.create('Ext.panel.Tool', {
                type: 'collapseall',
                qtip: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_TOOL_COLLAPSE_ALL_HINT}}",
                handler: this.collapseAll,
                hidden: true,
                scope: this
            });
            
            var tools = [
                this._toolExpandAll,
                this._toolCollapseAll
            ];
            
            if (addFirstTool)
            {
                tools.push(addFirstTool);
            }
            
            Ext.apply(header, {
                titlePosition: 2
            });
            
            Ext.apply(this, {
                header: header,
                tools: tools,
                cls: cls
            });
        }
        
        if (this.headerLabel)
        {
            // Compile the header template and extract the metadata names.
            this._headerTpl = new Ext.Template(this.headerLabel, {compiled: true});
            this._headerFields = [];
            while ((result = Ametys.form.ConfigurableFormPanel.Repeater.HEADER_VARIABLES.exec(this.headerLabel)) != null)
            {
                this._headerFields.push(result[1]);
            }
        }
        
        // Monitor when the form is ready, to update panel headers accordingly.
        if (this.form)
        {
            this.form.on('formready', this._onFormReady, this);
            this.form.on('repeaterEntryReady', this._onRepeaterEntryReady, this);
        }
        
        /**
         * @event validitychange
         * Fires when an entry was added or deleted
         * @param {Ametys.form.ConfigurableFormPanel.Repeater} this
         * @param {Boolean} isValid Whether or not the repeater is now valid
         */
        
        this.callParent(arguments);
    },
    
    /**
     * @private
     * Retrieves the items to initialize the header line of repeater as table
     * @return the items to initialize header of repeater
     */
    _getRepeaterTableColumns: function()
    {
        let items = [];
        
        let data = this.composition;
        for (let name in data)
        {
            if (!data[name] || data[name].hidden)
            {
                continue;
            }
            
            let item = data[name];
            let innerItems = [];
            
            let label = item.label;
            let isMandatory = item.validation ? (item.validation.mandatory) || false : false;
            let labelWithMandatory = label + (label && isMandatory ? '* ' : '');
            innerItems.push({
                xtype: 'component',
                html: labelWithMandatory,
                cls: 'a-text'
            });
            
            let description = item.description;
            let help = item.help;
            if (description)
            {
                innerItems.push({
                    xtype: 'component',
                    cls: 'ametys-description',
                    listeners: {
                       afterrender: function(me)
                       {
                           // Register the new tip with an element's ID
                           Ext.tip.QuickTipManager.register({
                               target: me.getId(), // Target button's ID
                               text: description, // Tip content
                               help: help,
                               inribbon: false,
                               fluent: true
                           });
         
                       },
                       destroy: function(me)
                       {
                           Ext.tip.QuickTipManager.unregister(me.getId());
                       }
                   }
                });
            }
            
            let itemConfig = {
                xtype: 'container',
                layout: {
                    type: 'hbox',
                    align: 'stretch'
                },
                cls: 'a-column-header-inner',
                
                minWidth: Ametys.form.ConfigurableFormPanel.FIELD_MINWIDTH + Ametys.form.ConfigurableFormPanel.Repeater.FIELD_MARGIN_RIGHT,
                items: innerItems
            };
            
            let widgetParams = item['widget-params'];
            if (widgetParams && widgetParams.width)
            {
                Ext.apply(itemConfig, {
                    width: Number(widgetParams.width) + Ametys.form.ConfigurableFormPanel.Repeater.FIELD_MARGIN_RIGHT
                });
                if (widgetParams.minWidth) {
                    Ext.apply(itemConfig, {
                        minWidth: Number(widgetParams.minWidth) + Ametys.form.ConfigurableFormPanel.Repeater.FIELD_MARGIN_RIGHT
                    });
                }
            }
            else
            {
                Ext.apply(itemConfig, {
                    flex: widgetParams && widgetParams.flex ? parseFloat(widgetParams.flex) : 1
                });
            }
            
            items.push(itemConfig);
        }
        
        return items;
    },
    
    /**
     * Clear all the repeater items
     */
    reset: function()
    {
        var items = this.getItems();
        for (var i = items.getCount() - 1; i >= 0; i--)
        {
            this.removeItem(items.getAt(i));
        }
    },
    
    /**
     * Get the miminum size ie. the miminum amount of entries
     * @return {Number} The miminum size of the repeater
     */
    getMinSize: function ()
    {
        return this.minSize;
    },
    
    /**
     * Add a new repeater item.
     * @param {Object} options The options.
     * @param {Number} options.position The index at which the Component will be inserted into the Container's items collection. The position is 0-based. Can be null to add at the end.
     * @param {Number} options.previousPosition The panel previous position.
     * @param {Boolean} [options.collapsed=true] Whether to render the panel collapsed or not.
     * @param {Boolean} [options.animate=true] `true` to animate the panel, `false` otherwise.
     * @return The panel corresponding to the new instance
     */
    createRepeaterItemPanel: function (options)
    {
        var opt = options || {};
        
        var pos = Ext.isNumber(opt.position) ? opt.position : this.getItemCount();
        this._lastInsertItemPosition = pos + 1;
        
        var children = [{
            xtype: 'numberfield',
            name: '_' + this.prefix + this._getNameAtIndex(pos + 1) + this.defaultPathSeparator + 'previous-position',
            value: Ext.isNumber(opt.previousPosition) ? opt.previousPosition + 1 : -1,
            hidden: true
        }];
        
        if (this.mode == 'table' && !this.readOnly)
        {
            // Add the tools in the fist column
            children.push({
                xtype: 'container',
                cls: 'a-container-tool',
                width: Ametys.form.ConfigurableFormPanel.Repeater.TOOLS_COLUMN_WIDTH,
                layout: {
                    type: 'hbox',
                    align: 'stretch'
                },
                
                items: [
                    this._deleteTool(this.delLabel),
                    this._addTool(this.addLabel),
                    this._upTool(),
                    this._downTool()
                ]
            });
        }
        
        var item = Ext.create('Ext.Panel', {
            // title: this.label + ' (' + (pos+1) + ')',
            minTitle: this.label,
            
            index: pos,   // Index in the item panels list, 0-based.
            
            ui: 'light',
            border: true,
            
            cls: 'a-repeater-item a-repeater-item-level-' + this.nestingLevel + (this.nestingLevel % 2 == 0 ? ' even' : ' odd'),
            
            anchor: '100%',
            
            _externalDisableConditionsValues: this._newItemExternalDisableConditionsValues,
            
            items: children,
            
            listeners: {
                add: {fn: this._onAddComponent, scope: this}
            }
        });
        
        if (this.mode == 'table')
        {
            Ext.apply(item, {
                layout: {
                    type: 'hbox',
                    align: 'stretch'
                },
                
                header: false,

                defaults: {
                    flex: 1
                }
            });
        }
        else
        {
            // The tools
            var tools = this.readOnly ? null : [
                // collapse tool (automatically added)
                this._upTool(),
                this._downTool(),
                this._addTool(this.addLabel),
                this._deleteTool(this.delLabel)
            ];
        
            Ext.apply(item, {
                bodyPadding: Ametys.form.ConfigurableFormPanel.Repeater.PADDING + ' ' + Ametys.form.ConfigurableFormPanel.Repeater.PADDING + ' 0 ' + Ametys.form.ConfigurableFormPanel.Repeater.PADDING,
                
                header: {
                    titlePosition: 1
                },
                tools: tools,
            
                layout: {
                    type: 'anchor'
                },
                
                titleCollapse: true,
                hideCollapseTool: false,
                collapsible: true,
                collapsed: opt.collapsed !== false  // Render the items collapsed by default
            });
        }
        
        if (Ext.isNumber(opt.position))
        {
            var items = this.getItems();
            
            // Shift the items after the insert position.
            for (var i = items.getCount() - 1; i >= opt.position; i--)
            {
                var itemPanel = items.getAt(i);
                
                this._increaseIndexOfFields(itemPanel.index + 1);
                itemPanel.index = itemPanel.index + 1;
                this._updatePanelHeader(itemPanel);
            }
            
            // Position is 0 -> insert at position #1 to insert after the size field.
            this.insert(opt.position + 1, item);
        }
        else
        {
            this.add(item);
        }
        
        this._updatePanelHeader(item);
        
        this.incrementItemCount();
        
        this._updateGlobalHeader(true);
        
        return item;
    },
    
    /**
     * Draw the repeater fields
     * @param {Ext.Panel} panel The item panel.
     * @param {Object} options Options.
     */
    drawRepeaterItemFields: function(panel, options)
    {
        var opt = options || {};
        
        var index = panel.index + 1;
        
        // Transmit offset + 20 (margin) + 5 (padding) + 1 (border).
        var offset = this.offset 
                    + Ametys.form.ConfigurableFormPanel.Repeater.PADDING 
                    + 1 + (this.nestingLevel > 1 ? Ametys.form.ConfigurableFormPanel.Repeater.NESTED_OFFSET : 0);
        var roffset = this.roffset 
                    + Ametys.form.ConfigurableFormPanel.Repeater.PADDING 
                    + 1;

        // Draw the fields.
        // Hide labels for each fields if mode == table
        if (this.mode == 'table')
        {
            let data = this.composition;
            for (let name in data)
            {
                if (!data[name])
                {
                    continue;
                }
                
                let item = data[name];
                item.hideLabel = true;
                item.hideDescription = true;
                item.style = 'margin-right: ' + Ametys.form.ConfigurableFormPanel.Repeater.FIELD_MARGIN_RIGHT + 'px';
                item.minWidth = Ametys.form.ConfigurableFormPanel.FIELD_MINWIDTH;
                item.repeaterMode = this.mode;
            }
        }
        Ext.defer(this.form._configureJSON, 0, this.form, [this.composition, this.prefix + this._getNameAtIndex(index) + this.defaultPathSeparator, panel, offset, roffset]);
        
        if (this.fieldCheckers)
        {
            this.form._fieldCheckersManager.addFieldCheckers(this.items.get(index), this.fieldCheckers, this.prefix + this._getNameAtIndex(index) + this.defaultPathSeparator, offset, roffset);
        }

        // Default to true
//        panel.expand(opt.animate !== false);
    },
    
    /**
     * Add a new repeater item.
     * @param {Object} options The item panel.
     * @param {Boolean} [scrollTo=false] When true, the form will scroll to this new element
     * @return {Ext.panel.Panel} The newly created repeater item panel.
     */
    addRepeaterItem: function(options, scrollTo) // function(position)
    {
        // Suspend layout update.
        Ext.suspendLayouts();
        
        try
        {
            var options = options || {};
            
            if (options.fireRepeaterEntryReadyEvent)
            {
                this.form.notifyAddRepeaterEntry(true);
            }
            
            // Create the item panel.
            var itemPanel = this.createRepeaterItemPanel(options);
            
            // Draw the item fields.
            this.drawRepeaterItemFields(itemPanel, options);
            
            this._updateGlobalHeader(true);
        
            // Update the tools visibility.
            if (itemPanel.rendered)
            {
                this._updateToolsVisibility();
                if (scrollTo)
                {
                    this._scrollToNewPanel(itemPanel);
                }
            }
            else
            {
                itemPanel.on('render', this._updateToolsVisibility, this);
                if (scrollTo)
                {
                    itemPanel.on('boxready', this._scrollToNewPanel, this);
                }
            }
        }
        finally 
        {
            // Resume layout update and force to recalculate the layout.
            Ext.resumeLayouts(true);
        }
        
        if (options.fireRepeaterEntryReadyEvent)
        {
            this.form.notifyAddRepeaterEntry(false);
            this.form.fireEvent('repeaterEntryReady', this);
        }
        
        return itemPanel;
    },
    
    /**
     * @private
     * Scroll the form to make the new panel visible
     * @param {Ext.panel.Panel} panel The new panel to scroll to
     */
    _scrollToNewPanel: function(panel)
    {
        var newHeight = panel.getHeight();
        var newTop = panel.getPosition()[1]; // get the bottom of the previous
        
        var visibleTop = this.form.getPosition()[1];
        var visibleSize = this.form.getHeight();

        var newPos = Math.min((newTop + newHeight) - (visibleTop + visibleSize) + 10, newTop-visibleTop);
        if (newPos > 0)
        {
            this.form.scrollBy(0, newPos);
        }
    },
    
    /**
     * Remove a repeater entry.
     * @param {Ext.panel.Panel} itemPanel The item panel to remove.
     */
    removeItem: function(itemPanel)
    {
        // Position of the panel to delete
        var position = itemPanel.index;
        
        // Remove the repeater item.
        this.remove(itemPanel);
        
        this._removeFields(position + 1);
        
        // Get the next panel in repeater (the panel has been already removed => position + 1).
        var itemPanel = this.items.getAt(position + 1);
        while (itemPanel != null)
        {
            // Update the repeater fields index.
            this._decreaseIndexOfFields(itemPanel.index + 1);
            itemPanel.index = itemPanel.index - 1;
            this._updatePanelHeader(itemPanel);
            
            itemPanel = itemPanel.nextSibling();
        }
        
        // Decrease the repeater size.
        this.decrementItemCount();
        this._updateToolsVisibility();
        
        // Show the repeater header if there is no more entry.
        if (this.getItemCount() < 1)
        {
            this._updateGlobalHeader(false);
        }
    },
    
    /**
     * Get the repeater items.
     * @return {Ext.util.MixedCollection} the repeater items.
     */
    getItems: function()
    {
        // Filter the hidden panels.
        return this.items.filterBy(function(panel, key) {
            return !panel.isHidden();
        });
    },
    
    /**
     * Get the repeater item count.
     * @return {Number} the repeater item count.
     */
    getItemCount: function()
    {
        // Remove 1 for the hidden count field.
        return this.items.getCount() - 1;
    },
    
    /**
     * Get the repeater's label
     * @return {String} the label
     */
    getLabel: function ()
    {
        return this.label;
    },
    
    /**
     * Returns whether or not the repeater is currently valid
     * @return True if the repeater is valid, else false
     */
    isValid: function ()
    {
        var errors = this.getErrors(),
            isValid = Ext.isEmpty(errors);
        
        if (isValid)
        {
            this.clearInvalid();
        }
        else
        {
            this.markInvalid(errors);
        }
        
        return isValid;
    },
    
    /**
     * Returns whether or not the repeater is currently valid, 
     * and fires the {@link #validitychange} event if the repeater validity has changed since the last validation.
     *
     * @return {Boolean} True if the repeater is valid, else false
     */
    validate : function()
    {
        var me = this,
            isValid = me.isValid();
        
        if (isValid !== me.wasValid) {
            me.wasValid = isValid;
            me.fireEvent('validitychange', me, isValid);
        }
        
        return isValid;
    },

    
    /**
     * Runs repeater's validations and returns an array of any errors
     * @return {String[]} All validation errors for this field
     */
    getErrors: function ()
    {
        var errors = [];
        
        if (this.minSize != null && this.getItemCount() < this.minSize)
        {
            errors.push("{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_INSERT_INVALID_MINSIZE}}: " + this.getItemCount() + '/' +  this.minSize);
        }
        
        if (this.maxSize != null && this.getItemCount() > this.maxSize)
        {
            errors.push("{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_INSERT_INVALID_MAXSIZE}}: " + this.getItemCount() + '/' +  this.maxSize);
        }
        
        return errors;
    },
    
    /**
     * Display one or more error messages associated with this repeater
     * @param {String/String[]} errors The validation message(s) to display.
     */
    markInvalid: function (errors)
    {
        errors = Ext.Array.from(errors);
        
        var me = this,
            hasError = !Ext.isEmpty(errors);
        
        if (me.rendered && !me.isDestroyed) 
        {
            var tpl = me.getTpl('activeErrorsTpl');
            
            var activeError = tpl.apply({
                errors: errors,
                listCls: Ext.plainListCls + ' a-repeater-invalid-tooltip'
            });
            
            if (me.getItemCount() > 0)
            {
                me.getItems().each(function(panel, index, length) {
                    
                    // Remove old errors if exists
                    if (panel.tools && panel.tools.error)
                    {
                        panel.getHeader().remove(panel.tools.error);
                    }
                    
                    if (hasError)
                    {
                        me.clearErrorMessages(panel.getHeader() || panel.header);
                        
                        panel.addTool({type: 'error', qtip: activeError});
                    }
                });
            }
            else
            {
                me.clearErrorMessages(me.getHeader() || me.header);
                me.addTool({type: 'error', qtip: activeError});
            }
        }
        
        this.el[hasError ? 'addCls' : 'removeCls'](this.invalidCls);
    },
    
    /**
     * @private 
     * Clear any error messages
     * @param {Ext.panel.Header} header The panel's header
     */
    clearErrorMessages : function (header)
    {
        var tools = header.tools || [];
        
        var errorTools = header.items.filter('type', 'error');
        if (errorTools != null)
        {
            errorTools.each (function (item) {
                Ext.Array.remove(tools, item);
                header.remove(item);
            });
        }
    },
    
    /**
     * Clear any invalid styles/messages for this repeater.
     */
    clearInvalid: function ()
    {
        var me = this;
        me.removeCls(me.invalidCls);

        if (me.getItemCount() > 0)
        {
            me.getItems().each(function(panel, index, length) {
                 if (panel.tools && panel.tools.error)
                 {
                     panel.getHeader().remove(panel.tools.error);
                 }
            });
        }
        else
        {
            if (me.getHeader().tools && me.getHeader().tools.error)
            {
                me.getHeader().remove(me.getHeader().tools.error);
            }
        }
    },
    
    /**
     * Expand all the repeater items.
     */
    expandAll: function()
    {
        if (this.mode !== 'table')
        {
            Ext.suspendLayouts();
    
            var me = this;
            me.getItems().each(function(panel, index, length) {
                if (panel.rendered)
                {
                    panel.expand();
                }
                else
                {
                    panel.collapsed = false;
                }
            });
            
            Ext.resumeLayouts(true);
        }
    },
    
    /**
     * Collapse all the repeater items.
     */
    collapseAll: function()
    {
        if (this.mode !== 'table')
        {
            Ext.suspendLayouts();
            
            var me = this;
            me.getItems().each(function(panel, index, length) {
                if (panel.rendered && panel.isVisible(true))
                {
                    panel.collapse();
                }
                else
                {
                    panel.collapsed = true;
                }
            });
            
            Ext.resumeLayouts(true);
        }
    },
    
    /**
     * @private
     * Get the field that holds the size of the repeater
     * @return {Ext.form.field.Field} The field
     */
    getSizeField: function()
    {
        return this.items.first().items.first();
    },
    
    /**
     * @private
     * Increment the value of the internal field holding the repeater size
     */
    incrementItemCount: function()
    {
        var sizeField = this.getSizeField();
        sizeField.setValue(sizeField.getValue() + 1);
    },
    
    /**
     * @private
     * Decrement the value of the internal field holding the repeater size
     */
    decrementItemCount: function()
    {
        var sizeField = this.getSizeField();
        sizeField.setValue(Math.max(sizeField.getValue() - 1, 0));
    },
    
    /**
     * Set an item's previous position field.
     * @param {Number} position the item's position in the repeater, 0-based.
     * @param {Number} previousPosition the item's previous position.
     */
    setItemPreviousPosition: function(position, previousPosition)
    {
        // Get the corresponding item.
        var item = this.getItems().getAt(position);
        // The item panel's first element is the previous position field.
        item.items.first().setValue(previousPosition + 1);
    },
    
    /**
     * Mark an item as new by modifying its previous position field.
     * @param {Number} position the item's position in the repeater, 0-based.
     */
    markItemAsNew: function(position)
    {
        // Get the corresponding item.
        var item = this.getItems().getAt(position);
        // The item panel's first element is the previous position field.
        item.items.first().setValue(-1);
    },
    
    /**
     * Set the external disable conditions values of the item at given position
     * @param {Number} position the item's position in the repeater, 0-based.
     * @param {Object} values the external disable conditions values to set
     */
    setItemExternalDisableConditionsValues: function(position, values)
    {
        // Get the corresponding item.
        var item = this.getItems().getAt(position);
        // Set the conditions values
        item._externalDisableConditionsValues = values;
    },
    
    /**
     * Retrieves the item's external disable conditions values 
     * @param {Number} position the item's position in the repeater, 0-based.
     */
    getItemExternalDisableConditionsValues: function(position)
    {
        // Get the corresponding item.
        var item = this.getItems().getAt(position);
        // Set the conditions values
        return item._externalDisableConditionsValues;
    },

    /**
     * Set the external disable conditions values for the new items of the repeater
     * @param {Object} values the external disable conditions values to set
     */
    setNewItemExternalDisableConditionsValues: function(values)
    {
        this._newItemExternalDisableConditionsValues = values;
    },
    
    /**
     * Hide or show the tools
     * @private
     */
    _updateToolsVisibility: function()
    {
        if (!this.readOnly)
        {
            var me = this;
            var items = me.getItems();
                        
            // Iterate on repeater item panels.
            items.each(function(panel, index, length) {
                
                me._setVisible("moveup", index > 0, panel);
                me._setVisible("movedown", index < (length-1), panel);
                me._setVisible("plus", me.maxSize == null || length < me.maxSize, panel);
                me._setVisible("delete", me.minSize == null || length > me.minSize, panel);
            });
            
            // Display global header "plus" button only if the max size is not reached
            window.setTimeout(function(){me._setVisible("plus", me.maxSize == null || items.length < me.maxSize, me)}, 1);
        }
    },
    
    /**
     * Update visibility of a tool
     * @param {String} elementName the name of element to set visible or not.
     * @param {Boolean} visible true to set the element visible.
     * @param {Ext.panel.Panel} panel the panel containing the element.
     * @private
     */
    _setVisible: function(elementName, visible, panel)
    {
        if (this.mode == 'table')
        {
            // In mode table, only set tools as disable, do not hide them
            panel.down("[type='" + elementName + "']").setDisabled(!visible);
        }
        else
        {
            // The panel is rendered: tools is an object, items can be accessed by their name.
            if (panel.tools[elementName])
            {
                panel.tools[elementName].setVisible(visible);
            }
            // The panel is not rendered yet: tools is a configuration array.
            else
            {
                Ext.Array.findBy(panel.tools, function(f) { return f.type == elementName; }).hidden = !visible;
            }
        }
    },
    
    /**
     * Decrease the index of repeater fields 
     * @param {Number} index The start index
     * @private
     */
    _decreaseIndexOfFields: function (index)
    {
        this._shiftIndexOfFields(index, -1);
    },
    
    /**
     * Increase the index of repeater fields 
     * @param {Number} index The start index
     * @private
     */
    _increaseIndexOfFields: function (index)
    {
        this._shiftIndexOfFields(index, 1);
    },
    
    /**
     * Shift the index of repeater fields 
     * @param {Number} index The start index
     * @param {Number} offset The offset to shift
     * @private
     */
    _shiftIndexOfFields: function (index, offset)
    {
        var me = this;
        var fieldsToRename = {};
        
        // Shift standard fields.
        var prefix = me.prefix + me._getNameAtIndex(index);
        var fieldNames = me.form.getFieldNames();
        for (var i = 0; i < fieldNames.length; i++)
        {
            var fieldName = fieldNames[i];
            if (fieldName.indexOf(prefix + me.defaultPathSeparator) == 0)
            {
                var field = me.form.getField(fieldName);
                var newName = me.prefix + me._getNameAtIndex(index + offset) + me.defaultPathSeparator + fieldName.substring(prefix.length + 1);
                me._setFieldName(field, newName);
                
                fieldsToRename[fieldName] = newName;
            }
        }
        
        // Shift subrepeaters recursiverly
        for (let subrepeater of this.form.getRepeaters(this.id))
        {
            if (subrepeater.prefix.indexOf(prefix + me.defaultPathSeparator) == 0)
            {
                subrepeater.prefix = me.prefix + me._getNameAtIndex(index + offset) + subrepeater.prefix.substring(prefix.length);
            }
        }
        
        // Shift hidden fields (which name starts with an underscore).
        prefix = '_' + me.prefix + me._getNameAtIndex(index);
        me.form.getForm().getFields().each(function(formField) {
            if (formField.name && formField.name.indexOf(prefix + me.defaultPathSeparator) == 0)
            {
                var newName = '_' + me.prefix + me._getNameAtIndex(index + offset) + me.defaultPathSeparator + formField.name.substring(prefix.length + 1);
                me._setFieldName(formField, newName);
            }
        });
        
        for (var oldName in fieldsToRename)
        {
            me.form._onRenameField(oldName, fieldsToRename[oldName]);
        }
    },
    
    /**
     * Switch index of two repeater fields
     * @param {Number} index1 Index of first field
     * @param {Number} index2 Index of second field
     * @private
     */
    _switchIndexOfFields: function(index1, index2)
    {
        var me = this;
        var fieldsToRename = [];
        
        // Switch standard fields.
        var prefix1 = me.prefix + me._getNameAtIndex(index1);
        var prefix2 = me.prefix + me._getNameAtIndex(index2);
        var fieldNames = me.form.getFieldNames();
        for (var i = 0; i < fieldNames.length; i++)
        {
            var fieldName = fieldNames[i];
            if (fieldName.indexOf(prefix1 + me.defaultPathSeparator) == 0)
            {
                var field = me.form.getField(fieldName);
                var newName = me.prefix + me._getNameAtIndex(index2) + me.defaultPathSeparator + fieldName.substring(prefix1.length + 1);
                
                fieldsToRename.push({index: i, field: field, newName: newName});
            }
            else if (fieldName.indexOf(prefix2 + me.defaultPathSeparator) == 0)
            {
                var field = me.form.getField(fieldName);
                var newName = me.prefix + this._getNameAtIndex(index1) + me.defaultPathSeparator + fieldName.substring(prefix2.length + 1);
                
                fieldsToRename.push({index: i, field: field, newName: newName});
            }
        }
        
        // Switch subrepeaters recursiverly
        for (let subrepeater of this.form.getRepeaters(this.id))
        {
            if (subrepeater.prefix.indexOf(prefix1 + me.defaultPathSeparator) == 0)
            {
                subrepeater.prefix = prefix2 + subrepeater.prefix.substring(prefix1.length);
            }
            else if (subrepeater.prefix.indexOf(prefix2 + me.defaultPathSeparator) == 0)
            {
                subrepeater.prefix = prefix1 + subrepeater.prefix.substring(prefix2.length);
            }
        }
        
        // Switch hidden fields (which name starts with an underscore).
        prefix1 = '_' + me.prefix + me._getNameAtIndex(index1);
        prefix2 = '_' + me.prefix + me._getNameAtIndex(index2);
        me.form.getForm().getFields().each(function(formField) {
            if (formField.name && formField.name.indexOf(prefix1 + me.defaultPathSeparator) == 0)
            {
                var newName = '_' + me.prefix + me._getNameAtIndex(index2) + me.defaultPathSeparator + formField.name.substring(prefix1.length + 1);
                me._setFieldName(formField, newName);
            }
            else if (formField.name && formField.name.indexOf(prefix2 + me.defaultPathSeparator) == 0)
            {
                var newName = '_' + me.prefix + me._getNameAtIndex(index1) + me.defaultPathSeparator + formField.name.substring(prefix2.length + 1);
                me._setFieldName(formField, newName);
            }
        });
        
        for (var i = 0; i < fieldsToRename.length; i++)
        {
            me._setFieldName(fieldsToRename[i].field, fieldsToRename[i].newName);
            me.form._onRenameField(fieldsToRename[i].index, fieldsToRename[i].newName);
        }
    },
    
    /**
     * Remove the references to field names in the form.
     * @param {Number} index The index of the removed form item.
     * @private
     */
    _removeFields: function(index)
    {
        var fieldNames = this.form.getFieldNames();
        var fieldsToRemove = [];
        
        var prefix = this.prefix + this._getNameAtIndex(index) + this.defaultPathSeparator;
        for (var i = 0; i < fieldNames.length; i++)
        {
            if (fieldNames[i].indexOf(prefix) == 0)
            {
                fieldsToRemove.push(fieldNames[i]);
            }
        }
        
        for (var i = 0; i < fieldsToRemove.length; i++)
        {
            this.form._onRemoveField(fieldsToRemove[i]);
        }
    },
    
    /**
     * @private
     * Change a field name. Used when position of a repeater line has changed.
     * @param {Ext.form.field.Field} field The field to rename
     * @param {String} newName The new name of the field
     */
    _setFieldName: function(field, newName)
    {
        field.name = newName;
        
        if (typeof field.setName == 'function')
        {
            field.setName(newName);
        }
        
        var input = Ext.get(field.getInputId());
        if (input != null)
        {
            input.dom.name = newName;
        }
    },
    
    /**
     * Called when the owner form is ready, all its fields initialized and valued.
     * @param {Ametys.form.ConfigurableFormPanel} form The owner form.
     * @private
     */
    _onFormReady: function(form)
    {
        this._updateAllItemHeaders();
    },
    
    /**
     * Called when the an repeater entry has been added and is ready (all its fields initialized)
     * @param {Ametys.form.ConfigurableFormPanel.Repeater} repeater The repeater containing the entry.
     * @private
     */
    _onRepeaterEntryReady: function(repeater)
    {
        // updates only 
        if (repeater === this)
        {
            this._updateAllItemHeaders();
        }
    },
    
    /**
     * Called when a component is added to an item panel.
     * @param {Ext.panel.Panel} panel The container panel.
     * @param {Ext.Component} component The added component.
     * @param {Number} index The component index.
     * @private
     */
    _onAddComponent: function(panel, component, index)
    {
        // When a specific header label is specified, monitor when field values change.
        if (component.isFormField && this.headerLabel)
        {
            // Monitor only metadata the template is based on.
            var shortName = component.shortName;
            if (shortName && this._headerFields.indexOf(shortName) >= 0)
            {
                // When the field loses focus, update the header of its entry.
                component.on('change', Ext.bind(this._updatePanelHeader, this, [panel]), this);
            }
        }
        
        component.on('resize', function(elt, w, h, oldW, oldH) {
            if (oldH && oldH != h) // non-empty oldH means that is not the first size ; moreover the issue happens only in vertical resize
            {
                // When a subcomponent of this repeater is resized we need to enlarge (not scroll) 
                panel.updateLayout();
            }
        }) 

    },
    
    /**
     * Update all item panel headers from their fields.
     * @private
     */
    _updateAllItemHeaders: function()
    {
        var me = this;
        me.getItems().each(function(panel, index, length) {
            me._updatePanelHeader(panel);
        });
    },
    
    /**
     * Update a panel header from its fields.
     * @param {Ext.panel.Panel} panel The panel which header to update.
     * @private
     */
    _updatePanelHeader: function(panel)
    {
        let addTitle = "";
        if (this.headerLabel)
        {
            var subFields = panel.query('> *[shortName]');
            var emptyValue = true;
            
            // Iterate over all the fields.
            var values = {};
            for (var i = 0; i < subFields.length; i++)
            {
                // Process only the fields which are used in the header template.
                var shortName = subFields[i].shortName;
                if (this._headerFields.indexOf(shortName) >= 0)
                {
                    // Get the value and test if it's empty.
                    var value = Ext.String.escapeHtml(subFields[i].getReadableValue());
                    values[shortName] = value;
                    if (value != null && value != '')
                    {
                        emptyValue = false;
                    }
                }
            }
            
            addTitle = this._headerTpl.apply(values);
        }
        
        // Compute and set the new panel header/title.
        var newTitle = panel.minTitle + ' (' + (panel.index+1) + ')';
        if (!emptyValue && addTitle)
        {
            newTitle = newTitle + '<span class="header-repeater-item-value"> - ' + addTitle + '</span>';
        }
        
        panel.setTitle(newTitle);
    },
    
    /**
     * Update the global header.
     * @param {Boolean} hasEntry True if the repeater has at least one entry
     * @private
     */
    _updateGlobalHeader: function(hasEntry)
    {
        if (this.mode !== 'table')
        {
            if (hasEntry)
            {
                this._toolExpandAll.show();
                this._toolCollapseAll.show();
            }
            else
            {
                this._toolExpandAll.hide();
                this._toolCollapseAll.hide();
            }
        }
        
        var label = this.label + (hasEntry ? '' : ' (0)');
        this.getHeader().setTitle ? this.getHeader().setTitle(label) : this.getHeader().title = label;
    },
    
    // ----------------------------------------------------
    /**
     * Creates the general 'add' tool with the given label
     * @private
     */
    _addFirst: function (label)
    {
        return {
            xtype: 'tool',
            type: 'plus',
            qtip: label, 
            handler: this._add,
            scope: this
        };
    },
    
    /**
     * Creates the 'add' tool with the given label
     * @private
     */
    _addTool: function(label)
    {
        return {
            xtype: 'tool',
            type: 'plus',
            qtip: label, 
            handler: this._insert,
            scope: this
        };
    },
    
    /**
     * Creates the 'delete' tool with the given label
     * @private
     */
    _deleteTool: function(label)
    {
        return {
            xtype: 'tool',
            type: 'delete',
            qtip: label, 
            handler: function(event, toolEl, header, tool) {
                // header.ownerCt returns the item panel.
                this._delete(header.ownerCt);
            },
            scope: this
        };
    },
    
    /**
     * Creates the 'move down' tool with the given label
     * @private
     */
    _downTool: function()
    {
        return {
            xtype: 'tool',
            type: 'movedown',
            qtip: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_MOVE_DOWN}}", 
            handler: this._down,
            scope: this
        };
    },
    
    /**
     * Creates the 'move up' tool with the given label
     * @private
     */
    _upTool: function()
    {
        return {
            xtype: 'tool',
            type: 'moveup',
            qtip: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_MOVE_UP}}", 
            handler: this._up,
            scope: this
        };
    },
    
    // ----------------------------------------------------
    // Tool actions
    /**
     * Add a new repeater instance at the end of the list.
     * @param {Ext.event.Event} event The click event.
     * @param {Ext.Element} toolEl The tool Element.
     * @param {Ext.panel.Header} header The host panel header.
     * @param {Ext.panel.Tool} tool The tool object
     */
    _add: function(event, toolEl, header, tool)
    {
        if (this.maxSize != null && this.getItemCount() >= this.maxSize)
        {
            Ametys.Msg.show({
                title: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_INSERT}}",
                msg: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_INSERT_ERROR_MAXSIZE}}",
                buttons: Ext.Msg.OK,
                icon: Ext.MessageBox.ERROR
            });
            return;
        }
        
        this.addRepeaterItem({position: 0, collapsed: false, fireRepeaterEntryReadyEvent: true});
        this.validate();
    },
    
    /**
     * Insert a new repeater instance after the given panel.
     * @param {Ext.event.Event} event The click event.
     * @param {Ext.Element} toolEl The tool Element.
     * @param {Ext.panel.Header} header The host panel header.
     * @param {Ext.panel.Tool} tool The tool object
     */
    _insert: function(event, toolEl, header, tool)
    {
        if (this.maxSize != null && this.getItemCount() >= this.maxSize)
        {
            Ametys.Msg.show({
                title: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_INSERT}}",
                msg: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_INSERT_ERROR_MAXSIZE}}",
                buttons: Ext.Msg.OK,
                icon: Ext.MessageBox.ERROR
            });
            return;
        }
        
        var panel = header.ownerCt;
        this.addRepeaterItem({position: panel.index + 1, collapsed: false, fireRepeaterEntryReadyEvent: true}, true);
        this.validate();
    },
    
    
    /**
     * Move down the given panel in its repeater
     * @param {Ext.event.Event} event The click event.
     * @param {Ext.Element} toolEl The tool Element.
     * @param {Ext.panel.Header} header The host panel header.
     * @param {Ext.panel.Tool} tool The tool object
     */
    _down: function(event, toolEl, header, tool)
    {
        var itemPanel = header.ownerCt;
        var index = itemPanel.index + 1;
        
        if (index >= this.getItemCount())
        {
            return;
        }
        
        var items = this.getItems();
        var itemPanel2 = items.getAt(itemPanel.index + 1);
        
        this.move(index, index + 1);
        
        // Switch all fields. Position fields will be updated as well.
        this._switchIndexOfFields(index, index + 1);
        
        var tmpIndex = itemPanel.index;
        itemPanel.index = itemPanel2.index;
        itemPanel2.index = tmpIndex;

        // Update title (that depends on position, but also on fields)
        this._updatePanelHeader(itemPanel);
        this._updatePanelHeader(itemPanel2);
        
        itemPanel.expand();
        
        // Update tools
        this._updateToolsVisibility();
        
    },
    
    /**
     * Move up the given panel in its repeater
     * @param {Ext.event.Event} event The click event.
     * @param {Ext.Element} toolEl The tool Element.
     * @param {Ext.panel.Header} header The host panel header.
     * @param {Ext.panel.Tool} tool The tool object
     */
    _up: function(event, toolEl, header, tool)
    {
        var itemPanel = header.ownerCt;
        var index = itemPanel.index + 1;
        
        if (index <= 0)
        {
            return;
        }
        
        var items = this.getItems();
        var itemPanel2 = items.getAt(itemPanel.index - 1);
        
        this.move(index, index - 1);
        
        // Switch all fields. Position fields will be updated as well.
        this._switchIndexOfFields(index, index - 1);
        
        var tmpIndex = itemPanel.index;
        itemPanel.index = itemPanel2.index;
        itemPanel2.index = tmpIndex;
        
        // Update title (that depends on position, but also on fields)
        this._updatePanelHeader(itemPanel);
        this._updatePanelHeader(itemPanel2);
        
        itemPanel.expand();
        
        // Update tools
        this._updateToolsVisibility();
    },
    
    /**
     * @private
     * Removes a repeater entry.
     * @param {Ext.Panel} itemPanel The panel to delete
     */
    _delete: function(itemPanel)
    {
        if (this.minSize != null && this.getItemCount() <= this.minSize)
        {
            Ametys.Msg.show({
                title: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_DELETE}}",
                msg: "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_DELETE_ERROR_MINSIZE}}",
                buttons: Ext.Msg.OK,
                icon: Ext.MessageBox.ERROR
            });
            return;
        }
        
        // Confirm deletion
        Ametys.Msg.confirm(
            "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_DELETE}}",
            "{{i18n PLUGINS_CORE_UI_CONFIGURABLE_FORM_REPEATER_DELETE_CONFIRM}}",
            function (answer)
            {
                if (answer == 'yes')
                {
                    // Remove the entry.
                    this.removeItem(itemPanel);
                    this.validate();
                }
            },
            this
        );
    },
    
    /**
     * Retrieves the name of the repeater at the given index
     * @param {Number} index The index
     * @return {String} The name of the repeater at the given index
     * @private
     */
    _getNameAtIndex: function(index)
    {
        return this.self.getNameAtIndex(this.name, this.defaultPathSeparator, index);
    },
        
    enable: function()
    {
        let me = this;
        
        this.callParent(arguments);
        
        if (this.mode !== 'table')
        {
            this.tools.forEach(function(tool) { if (tool.show) { tool.show() } else { tool.hidden = false; } });
            this.expand();
        }

        this._updateGlobalHeader(this.getItemCount() > 0);
        
    },
    
    disable: function()
    {
        this.callParent(arguments);
        
        if (this.mode !== 'table')
        {
            this.tools.forEach(function(tool) { if (tool.hide) { tool.hide() } else { tool.hidden = true; } });
            this.collapse();
        }
    }
});

/**
 * Repeater-specific accordion layout, which allows to collapse all items.
 * @private
 */
Ext.define('Ametys.cms.form.layout.Repeater', {
    extend: 'Ext.layout.container.Accordion',
    alias: ['layout.repeater-accordion'],
    
    /**
     * Overridden to prevent automatically expanding an item when another is collapsed.
     */
    onComponentCollapse: function(comp)
    {
        // Do nothing.
    }
    
    /*onContentChange: function () {
        this.owner.updateLayout({isRoot: true});
        return true;
    }*/ 
});