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

/**
 * Dialog to modify a repeater in a form 
 */
Ext.define('Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog', {
    singleton: true,

    /**
     * Open and show a dialog containing all the repeater entries in a form from a content displayed in a content grid, 
     * or from a grid repeater dialog
     */
    showRepeaterDialog: function (title, parentGridId, recordId, contentId, subcolumns, repeaterCfg, dataIndex, metadataPath, contentGridId, callback) 
    {
        var parentRecord = null;
        var parentGrid = null;
        if (parentGridId != '' && recordId != '')
        {
            parentGrid = Ext.getCmp(parentGridId);
            if (parentGrid != '')
            {
                parentRecord = parentGrid.getStore().getById(recordId);
            }
        }

        if (parentRecord == null)
        {
            Ametys.log.ErrorDialog.display({
                title: "{{i18n plugin.cms:UITOOL_SEARCH_ERROR_TITLE}}",
                text: "{{i18n plugin.cms:UITOOL_SEARCH_ERROR_REPEATER}}",
                category: 'Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog'
            });
            return;
        }

        title = title || "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_SEARCH_REPEATER_MODAL_TITLE}}";

        return this._openDialog(title, parentRecord, contentId, subcolumns, repeaterCfg, dataIndex, metadataPath, contentGridId, callback);
    },

    /**
     * @private
     * Create and open a new dialog
     * @param {String} title The title of the dialog
     * @param {Ext.data.Model} parentRecord The record containing the repeater
     * @param {String} contentId The id of the main content.
     * @param {Objet} subcolumns The sub columns to edit 
     * @param {Object} repeaterCfg The repeater info 'min-size', 'max-size', 'initial-size', 'add-label', 'del-label', 'header-label
     * @param {String} dataIndex The name of the repeater metadata
     * @param {String} metadataPath The path to the repeater metadata. Used to get the repeater columns definition
     * @param {String} contentGridId The id of the main grid containing the content.
     * @param {Function} callback a callback function to invoke after the dialog is closed, can be null
     * @return the opened dialog
     */
    _openDialog: function (title, parentRecord, contentId, subcolumns, repeaterCfg, dataIndex, metadataPath, contentGridId, callback)
    {
        function _containsPath(array, path)
        {
            if (!array)
            {
                return false;
            }
            
            if (array.includes(path))
            {
                return true;
            }
            
            let index = path.lastIndexOf('/');
            if (index > 0)
            {
                return _containsPath(array, path.substring(0, index));
            }
            
            return false;
        }
        
        Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._isModified = false;
        Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries = {};
        
        let superParentRecord = Ametys.plugins.cms.search.SearchGridHelper._getParentRecord(parentRecord); 
        let data = superParentRecord.get("mccSession1_repeater")._educationalPath; // mccSession1 is hardcoded, because the value would be the same for mccSession2
        let educationalPath = data ? data[superParentRecord.getId()] : null;

        let rootInfo = Ametys.plugins.cms.search.SearchGridHelper._getRootRecord(parentRecord);

        let form = this._createFormEditionPanel({contentId: contentId, educationalPath: educationalPath}, parentRecord, metadataPath);
        let hintMessage = this._createTopText(rootInfo.record, parentRecord, metadataPath, Ametys.tool.ToolsManager.getFocusedTool().getInitialConfig()['odf-skills-enabled'] !== 'false');
        
        let items = [
            hintMessage,
            form
        ];	
        
        // FIXME disable conditions qui remontent plus haut que la racine du form
        
        
        let disableFirstLevel = false;
        
        if (repeaterCfg.disabled || rootInfo.record.get('notEditableData') === true || _containsPath(rootInfo.record.get('notEditableDataIndex'), rootInfo.parentPath))
        {
            form.setDisabled(true);
        }
        else if (parentRecord.get('__notEditable'))
        {
            disableFirstLevel = true;
        }
        
        let externalValuesRequired = [];
        
        let conf = {};
        conf[dataIndex] = {
            "name": dataIndex,
            "label": repeaterCfg["title"],
            "plugin": null,
            "widget": null,
            "widget-params": repeaterCfg["widget-params"],
            
            "can-not-write": disableFirstLevel,
                            
            "type": "repeater",
            "header-label": repeaterCfg["header-label"],
            "add-label": repeaterCfg["add-label"],
            "del-label": repeaterCfg["del-label"],
            "min-size": repeaterCfg["min-size"],
            "max-size": repeaterCfg["max-size"],
            "initial-size": repeaterCfg["initial-size"],
            "elements": Object.fromEntries(subcolumns.filter(sc => !hintMessage.hideSkills || sc.name != 'skills').map(sc => this._convertColumnToWidget(sc, externalValuesRequired, parentRecord, disableFirstLevel)).map(j => [j.name, j]))
        };
        
        if (Ametys.tool.ToolsManager.getFocusedTool().getInitialConfig()['odf-skills-enabled'] === 'false')
        {
            // Remove skills column if skills management is disabled
            delete conf.notes.elements.skills;
            delete conf.notes.elements.label.width;
            delete conf.notes.elements.label['widget-params'].width;
        }
        
        form.configure(conf);
        
        externalValuesRequired = [...new Set(externalValuesRequired)];
        
        let entries = structuredClone(parentRecord.getData()[dataIndex]);
        let values = {
            values: {
                __externalDisableConditionsValues: Object.fromEntries(externalValuesRequired.map(path => ["__external_NotesFormRepeaterDialog_" + path, Ametys.form.Widget.getRelativeValue(path, {record: rootInfo.record, dataPath: metadataPath + "/fake"})]))
            },
            repeaters: [],
        };
        
        // If parent is not limited to a path, the path in the skills repeater is worthy... so we need to hide the entries that are not mathing the educational path
        if (parentRecord.get("common") !== false)
        {
            let superParentRecord = Ametys.plugins.cms.search.SearchGridHelper._getParentRecord(parentRecord); 
            let data = superParentRecord.get("mccSession1_repeater")._educationalPath; // mccSession1 is hardcoded, because the value would be the same for mccSession2
            let educationalPath = data ? data[superParentRecord.getId()] : null;
            if (educationalPath) // When no educational path we are supposed to be in readonly mode... so let's ignore it
            {
                // Modify the entries to remove the entries that are not matching the educational path and store them to reintroduce them during the save time
                
                // 1) Find the entries that are not matching the educational path
                let toRemoveTemporarily = [];
                let previousPosition = 0;
                for (let entry of Object.keys(entries || {}))
                {
                    previousPosition++;
                    
                    if (entry.endsWith("/path") && entries[entry] != educationalPath)
                    {
                        toRemoveTemporarily.push(entry.substring(0, entry.length - "/path".length));
                    }
                    else if (entry.endsWith("/skills"))
                    {
                        // Remove useless values
                        delete entries[entry];
                    }
                }   
                
                // 2) Remove the entries (and keep them to reintroduce them later)
                let handledSize = [];
                let regex = new RegExp("^_?(\\[[0-9]*\\]/skills)\\[([0-9]*)\\](.*)");
                for (let entry of Object.keys(entries || {}))
                {
                    for (let toRemove of toRemoveTemporarily)
                    {
                        if (entry.startsWith(toRemove + "/") || entry.startsWith("_" + toRemove + "/"))
                        {
                            if (regex.test(entry))
                            {
                                // Save the entry to reintroduce it later (during the save time)
                                Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[RegExp.$1] = Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[RegExp.$1] || {};
                                Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[RegExp.$1][RegExp.$2] = Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[RegExp.$1][RegExp.$2] || {};
                                Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[RegExp.$1][RegExp.$2][RegExp.$3.substring(1)] = entries[entry];
                                // Delete the entry to do not see it while editiing
                                delete entries[entry];
                            }
                        }
                        
                        if (!handledSize.includes(toRemove))
                        {
                            handledSize.push(toRemove);
                            
                            let sizeKey = "_" + toRemove.substring(0, toRemove.lastIndexOf("[")) + "/size";
                            entries[sizeKey] = entries[sizeKey] - 1;
                        }
                    }
                    
                }
                
                // 3) Renumber the entries according to the removed entries
                for (let i = toRemoveTemporarily.length - 1; i >= 0; i--)
                {
                    if (regex.test(toRemoveTemporarily[i]))
                    {
                        let notesPrefix = RegExp.$1;
                        let skillsIndex = parseInt(RegExp.$2);
                        
                        for (let entry of Object.keys(entries || {}))
                        {
                            if (regex.test(entry) && notesPrefix == RegExp.$1 &&parseInt(RegExp.$2) > skillsIndex)
                            {
                                entries[(entry.startsWith("_") ? "_" : "") + notesPrefix + "[" + (parseInt(RegExp.$2) - 1) + "]" + RegExp.$3] = entries[entry];
                                delete entries[entry];
                            }
                        }
                    }
                }
            }
        }
        
        
        for (let entry of Object.keys(entries || {}))
        {
            if (entry.startsWith("_") && entry.endsWith("size")) 
            {
                let parts = entry.split("/");
                
                values.repeaters.push({
                    "prefix": parts.length > 2 ? dataIndex + entry.substring(1, entry.length - (parts[parts.length - 2].length + "/size".length)) : "",
                    "name": parts.length > 2 ? parts[parts.length - 2] : dataIndex,
                    "count": entries[entry] || (entry == "_size" ? repeaterCfg["initial-size"] : 0/* should be this repeater initialsize */) // null repeater is converted to 0 sized... as we want it to have the initial size
                });
            }
            else if (entry.startsWith("["))
            {
                values.values[dataIndex + entry] = entries[entry];
            }
            else if (entry.startsWith("_"))
            {
                values.values["_" + dataIndex + entry.substring(1)] = entries[entry];
            }
        }

        form.setValues(values);
        
        var dialog = this._showDialog(
            title, 
            items, 
            {type: 'vbox', align: 'stretch'},
            function () {
                this._ok(dialog, parentRecord, form, dataIndex, contentId, callback);
            },
            callback
        );
        
        dialog.setReadOnly = function() {
            form.disable()
        }
        
        return dialog;
    },
    
    _createTopText(rootRecord, parentRecord, metadataPath, withSkills)
    {
        let rootsLabels = ""; 
        let roots = (rootRecord.get("mccSession1_repeater")._educationalPathRootLabels || {})[rootRecord.getId()];
        if (withSkills && roots)
        {
            rootsLabels = "<br/><br/>{{i18n PLUGINS_ODF_PILOTAGE_RIGHTS_MCCCOURSE_NOTES_MCCSESSIONS_EDUCATIONAL_PATH_MULTIPLE_GLOBAL_INTRO}}<ul style='margin-top: 0;'>" + roots.map(x => "<li><strong>" + x + "</strong></li>").join("") + "</ul>{{i18n PLUGINS_ODF_PILOTAGE_RIGHTS_MCCCOURSE_NOTES_MCCSESSIONS_EDUCATIONAL_PATH_MULTIPLE_GLOBAL_WARN}}";
        }
        
        let educationalPathLabel = rootRecord.get("mccSession1_repeater")._educationalPathLabel[rootRecord.getId()];

        let sessionLabel = metadataPath.indexOf("mccSession1") != -1 ? "{{i18n PLUGINS_ODF_PILOTAGE_COURSE_MCC_SESSION1_LABEL}}" : "{{i18n PLUGINS_ODF_PILOTAGE_COURSE_MCC_SESSION2_LABEL}}";
        return {
            xtype: "component",
            ui: 'tool-hintmessage',
            hideSkills: withSkills && roots,
            html: (withSkills ? "{{i18n plugin.odf-pilotage:PLUGINS_ODF_PILOTAGE_DIALOG_MCCCOURSE_NOTES2_LABEL}} " : "{{i18n plugin.odf-pilotage:PLUGINS_ODF_PILOTAGE_DIALOG_MCCCOURSE_NOTES_LABEL}} ")  
                + "<strong>" + sessionLabel + " > {{i18n plugin.odf-pilotage:PLUGINS_ODF_PILOTAGE_DIALOG_MCCCOURSE_NOTES_EVAL}} " + (parentRecord.store.indexOf(parentRecord) + 1) + (parentRecord.get("label") ? " - " + parentRecord.get("label") : "") + "</strong> " 
                + "{{i18n plugin.odf-pilotage:PLUGINS_ODF_PILOTAGE_DIALOG_MCCCOURSE_NOTES_IN}} " 
                + "<strong>" + educationalPathLabel + "</strong>" 
                + rootsLabels
        }
    },
    
    /**
     * @private
     */
    _convertColumnToWidget: function(column, externalValuesRequired, parentRecord, disableFirstLevel, rootPath = "")
    {
        let widget = {...column};
        if (disableFirstLevel === true && widget.type != 'repeater')
        {
            widget['can-not-write'] = true;
        }
        if (column.columns)
        {
            widget.elements = Object.fromEntries(column.columns.map(sc => this._convertColumnToWidget(sc, externalValuesRequired, parentRecord, false, rootPath + "/" + sc.name)).map(j => [j.name, j]));
        }
        
        // We need to clone the disable condition since we will modify when calling this._transformConditions
        widget.disableCondition = window.structuredClone(column.disableCondition);
        
        externalValuesRequired.push(...this._transformConditions(widget.disableCondition, rootPath));
        
        return widget;
    },
    
    /**
     * @private
     */
    _transformConditions: function(conditions, rootPath)
    {
        function _normalize(path)
        {
            let paths = path.substring(1).split("/");
            
            let toRemove;
            
            do 
            {
                toRemove = [];
                
                for (let p = 1; p < paths.length; p++)
                {
                    if (paths[p] == ".." && paths[p-1] != "..")
                    {
                        toRemove.push(p-1);
                        toRemove.push(p);
                        break;
                    }
                }

                for (let i = toRemove.length - 1; i >= 0; i--)
                {
                    paths.splice(toRemove[i], 1);
                }
            }
            while (toRemove.length > 0);
                            
            return paths.join("/");
        }
        
        let externalValues = [];
        
        if (conditions)
        {
            if (conditions.conditions)
            {
                for (let c of conditions.conditions)
                {
                    externalValues.push(..._transformConditions(c, rootPath));
                }
            }

            if (conditions.condition)
            {
                for (let c of conditions.condition)
                {
                    // If algo was already done once (since we modify existing object)
                    if (c.id.startsWith("__external_NotesFormRepeaterDialog_"))
                    {
                        externalValues.push(c.id.substring("__external_NotesFormRepeaterDialog_".length));
                    }
                    else if (!c.id.startsWith("__external"))
                    {
                        let path = _normalize(rootPath + "/" + c.id);
                        if (path.startsWith("../"))
                        {
                            // Found a relative path, we cannot handle it => replace it by a "pseudo" external condition
                            c.id = "__external_NotesFormRepeaterDialog_" + path;
                            externalValues.push(path);
                        }
                    }
                    
                }
            }
        }
        
        return externalValues;
    },
    
    /**
     * @private Get the panel used for edit content
     * @param {Object} widgetInfo The widget info
     * @return {Ext.Panel} The form panel
     */
    _createFormEditionPanel: function (widgetInfo, parentRecord, metadataPath)
    {
        let cfp =  Ext.create('Ametys.form.ConfigurableFormPanel', {
                cls: 'content-form-inner',
                flex: 1,
                
                listeners: {
                    'fieldchange': Ext.bind(this._onChange, this)
                },

                additionalWidgetsConf: {
                    contentInfo: widgetInfo
                },
                additionalWidgetsConfFromParams: {
                    contentType: 'contentType', // some widgets require the contentType configuration
                    editableSource: 'editableSource' // for richtext widgets we want to check the right on content
                },
                
                fieldNamePrefix: 'content.input.',
                displayGroupsDescriptions: false
        });
        
        cfp.getRelativeField = function(fieldPath, field, silently)
        {
            // Is fieldPath outside the form?
            let fieldDeepness = field.getName().replace(/[^/]/g,"").length;
            
            let cursor = fieldPath;
            while (cursor.startsWith("../"))
            {
                cursor = cursor.substring("../".length);
                fieldDeepness--;
            }
            
            if (fieldDeepness > 0)
            {
                return Ametys.form.ConfigurableFormPanel.prototype.getRelativeField.call(this, fieldPath, field, silently);
            }
            else
            {
                return {
                    getValue: function() { return this.value; },
                    value: Ametys.form.Widget.getRelativeValue("../" + cursor, {record: parentRecord, dataPath: metadataPath}),
                    doAddListener: function() { /* Ignore the onRelativeFieldChange... it can not happens */ }
                };
            }
        }
        
        return cfp;
    }, 
    
    /**
     * @private
     * Listener when at least one change was detected
     */
    _onChange: function()
    {
        Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._isModified = true;
    },
        
    /**
     * @protected
     * Display the dialog
     */
    _showDialog: function(title, items, layout, okHandler, callback, additionnalButtons)
    {
        var buttons = additionnalButtons || [];
        buttons.push({
            reference: 'okButton',
            text :"{{i18n plugin.cms:PLUGINS_CMS_UITOOL_SEARCH_REPEATER_MODAL_OK}}",
            handler: okHandler,
            scope: this
        }, {
            text :'{{i18n plugin.cms:PLUGINS_CMS_UITOOL_SEARCH_REPEATER_MODAL_CANCEL}}',
            handler: function () {
                dialog.close();
            },
            scope: this
        });
        
        let dialog = Ext.create("Ametys.window.DialogBox", {
            title: title,
            
            cls: ['a-formrepeater', 'a-notes-form-repeater-dialog'],
            
            closeAction : 'destroy',
            width : Math.max(500, window.innerWidth * 0.7),
            height : Math.max(490, window.innerHeight * 0.75),
            layout: layout,
            
            items: items,
            
            defaultButton: 'okButton',
            referenceHolder: true,
            
            // Buttons
            buttons : buttons,
            
            listeners: {
                'beforeclose': function() {
                    if (this._closeForced)
                    {
                        this._closeForced = false;
                        if (callback && !dialog.doNotCallCallback)
                        {
                            callback(null);
                        }
                        return;
                    }
                    else
                    {
                        this._askToDiscard(dialog);
                        return false; // do not close yet
                    }
                },
                scope: this
            }
        });
        dialog.show();
        return dialog;
    },
    
    /**
     * @private
     * Test if there is any modification and ask if it is ok to discard them.
     * If it is ok, or no modification call the callback
     * @param {Ext.Component} dialog The dialog box
     */
    _askToDiscard: function(dialog)
    {
        let me = this;
        
        function finish()
        {
            me._closeForced = true;
            dialog.close();
        }
        
        if (this._anyModificationToDiscard(dialog)) // any modif ?
        {
            Ametys.Msg.confirm("{{i18n plugin.cms:UITOOL_CONTENTEDITIONGRID_SAVEBAR_UNSAVE_CONFIRM_LABEL}}", 
                "{{i18n plugin.cms:UITOOL_CONTENTEDITIONGRID_SAVEBAR_UNSAVE_CONFIRM_DESC}}",
                function(answer) {
                    if (answer == 'yes') {
                        finish();
                    }
                }
            );        
        }
        else
        {
            finish();
        }
    },
   
    /**
     * @protected
     * Test if there is any modification in the dialog
     * @param {Ext.Component} dialog The dialog box
     * @return {Boolean} true if any modification
     */
    _anyModificationToDiscard: function(dialog)
    {
        return Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._isModified;
    },  
    
    /**
     * @private
     * Callback when validating the dialog
     * @param {Ametys.window.DialogBox} dialog The current dialog
     * @param {Ext.data.Model} parentRecord The parent record
     * @param {Ametys.form.ConfigurableFormPanel} form The configurable form panel
     * @param {String} dataIndex The repeater metadata name
     * @param {String} contentId The main content id
     * @param {Function} callback a callback function to invoke after the dialog is closed, can be null
     */
    _ok: function (dialog, parentRecord, form, dataIndex, contentId, callback)
    {
        let originalEntriesValues = this._formToValues(form, dataIndex);
        if (originalEntriesValues == null)
        {
            Ametys.log.ErrorDialog.display({
                title: "{{i18n plugin.cms:UITOOL_CONTENTEDITIONGRID_REPEATER_VALUE_ERROR_TITLE}}",
                text: "{{i18n PLUGINS_ODF_PILOTAGE_GRIDREPEATER_FORM_ERROR}}",
                details: "The repeater has some invalid values",
                category: this.self.getName()
            });
            
            return;
        }
        let entriesValues = originalEntriesValues.entries.map(t => { return {...t.values, "previous-position": t['previous-position']}; });
        
        if (Ametys.tool.ToolsManager.getFocusedTool().getInitialConfig()['odf-skills-enabled'] !== 'false')
        {
            // Set educational path on the entries
            let superParentRecord = Ametys.plugins.cms.search.SearchGridHelper._getParentRecord(parentRecord); 
            let data = superParentRecord.get("mccSession1_repeater")._educationalPath; // mccSession1 is hardcoded, because the value would be the same for mccSession2
            let educationalPath = data ? data[superParentRecord.getId()] : null;
            if (educationalPath)
            {
                for (let entry of entriesValues)
                {
                    // Data is store twice, modifying the first
                    for (let k of Object.keys(entry.skills))
                    {
                        if (k.endsWith("/path"))
                        {
                            entry.skills[k] = educationalPath;
                        }
                    }
                    // Modyfing the second
                    for (let subentry of entry.skills_repeater.entries)
                    {
                        subentry.values.path = educationalPath;
                    }
                }
            }
    
            
            // Reinsert the entries that were removed because they were not matching the educational path
            for (let entry of Object.keys(Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries))
            {
                let originalIndex = parseInt(entry.substring(1, entry.indexOf("]")));
                
                let matchingEntries = originalEntriesValues.entries.filter(e => e['previous-position'] == originalIndex);
                if (matchingEntries.length == 0)
                {
                    // Discarding the skill values associateed to other paths since the note entry has been removed
                    break;
                }

                let entryValue = matchingEntries[0].values;
                
                let cursor = entryValue.skills._size; 
                entryValue.skills._size += Object.keys(Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[entry]).length;
                for (let subentry of Object.keys(Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[entry]))
                {
                    cursor++;
                    
                    let values = {};
                    let previousPosition;
                    for (let attribute of Object.keys(Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[entry][subentry]))
                    {
                        if (attribute == "previous-position")
                        {
                            previousPosition = Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[entry][subentry][attribute];
                        }
                        else
                        {
                            entryValue.skills["[" + cursor + "]/" + attribute] = Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[entry][subentry][attribute];
                            values[attribute] = Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[entry][subentry][attribute];
                        }
                    }
                    entryValue.skills_repeater.entries.push({ position: cursor, values: values, "previous-position": previousPosition });
                    
                    // FIXME we should sort the subentries to avoid generating changes
                }
            }
        }
        
        var repeaterValues = parentRecord.getData()[dataIndex + "_repeater"];
        repeaterValues.entries = [];
        for (var index = 0; index < entriesValues.length; index++)
        {
            let notEditable = entriesValues[index].__notEditable;
            delete entriesValues[index].__notEditable;
            
            repeaterValues.entries[index] = {
                position: index + 1,
                values: entriesValues[index],
                "previous-position": entriesValues[index]['previous-position']
            };
            delete entriesValues[index]['previous-position'];
            
            if (notEditable)
            {
                repeaterValues.entries[index].notEditable = true;
            }
        }

        if (callback)
        {
            callback(repeaterValues);
            dialog.doNotCallCallback = true;
        }
        
        this._closeForced = true;
        dialog.close();
    },  
    
    /**
     * @private
     * Get the form values at the converted format 
     * @param {Ametys.form.ConfigurableFormPanel} form The configurable form panel
     * @parĂ¹m {String} dataIndex The repeater metadata name
     */
    _formToValues: function(form, dataIndex)
    {
        if (!form.isValid())
        {
            return null;
        }
        
        return this._convertRepeaterToValues(form.getValues(), form.fieldNamePrefix + dataIndex);
    },
    
    /**
     * @private
     */
    _convertRepeaterToValues: function(values, prefix)
    {
        let me = this;
        let entriesValues = [];
        
        let size = values["_" + prefix + "/size"];
        for (let pos = 1; pos <= size; pos++)
        {
            let entryValues = {};
            
            let subPrefix = prefix + "[" + pos + "]/";
            
            // Direct subvalues (including composite)
            Object.keys(values).filter(v => v.startsWith(subPrefix)).forEach(v => {
                let subv = v.substring(subPrefix.length);
                if (!subv.includes("["))
                {
                    entryValues[subv] = values[v];
                }
            })
            
            // Direct subrepeaters
            function _getDataIndex(v) { return v.substring(("_" + subPrefix).length, v.length - "/size".length); }
            Object.keys(values).filter(v => v.startsWith("_" + subPrefix) && v.endsWith("/size") && !_getDataIndex(v).includes("[")).forEach(v => {
                let subv = v.substring(1, subPrefix.length + 1);
                let localDataIndex = _getDataIndex(v);
                entryValues[localDataIndex + "_repeater"] = me._convertRepeaterToValues(values, subv + localDataIndex);
                entryValues[localDataIndex] = Object.fromEntries(Object.keys(values).filter(v => v.startsWith(subv + localDataIndex) || v.startsWith("_" + subv + localDataIndex)).map(v => [v.replace(subv + localDataIndex, '').replace('_/', '_'), values[v]]));
            });
            
            let entry = {
                "position": pos,
                "previous-position": parseInt(values["_" + subPrefix + "previous-position"]),
                "values": entryValues
            };
            
            entriesValues.push(entry);
        }
        
        return  { 
            "entries": entriesValues,
            "type": "repeater",
            // "header-label": "", // how to fill it? is it needed?
            // "label": "" // how to fill it? is it needed?
        };
    }
});