// Copyright (c) 2009-2026 SAP SE, All Rights Reserved

sap.ui.define([
    "sap/base/Log",
    "sap/base/i18n/Localization",
    "sap/base/util/deepClone",
    "sap/base/util/deepExtend",
    "sap/base/util/deepEqual",
    "sap/m/Button",
    "sap/m/Input",
    "sap/m/library",
    "sap/ui/core/CustomData",
    "sap/ui/core/mvc/Controller",
    "sap/ui/comp/smartfield/SmartField",
    "sap/ui/comp/smartfield/SmartLabel",
    "sap/ui/comp/smartform/Group",
    "sap/ui/comp/smartform/GroupElement",
    "sap/ui/comp/smartvariants/PersonalizableInfo",
    "sap/ui/core/Title",
    "sap/ui/model/json/JSONModel",
    "sap/ui/model/odata/v2/ODataModel",
    "sap/ushell/resources",
    "sap/m/MessageBox",
    "sap/ushell/components/shell/Settings/userDefaults/controller/ExtendedValueDialog.controller",
    "sap/ushell/Container"
], (
    Log,
    Localization,
    deepClone,
    deepExtend,
    deepEqual,
    Button,
    Input,
    mobileLibrary,
    CustomData,
    Controller,
    SmartField,
    SmartLabel,
    Group,
    GroupElement,
    PersonalizableInfo,
    Title,
    JSONModel,
    ODataModel,
    ushellResources,
    MessageBox,
    ExtendedValueDialogController,
    Container
) => {
    "use strict";

    // shortcut for sap.m.ButtonType
    const ButtonType = mobileLibrary.ButtonType;

    /**
     * @typedef {object} ParameterValue A value object of the parameter.
     * @property {string|undefined} value the value of the parameter which is shown in the input field
     * @property {object} extendedValue the extended values of the parameter which is shown in the extended dialog
     * @property {object[]} extendedValue.Range array of the SelectOption
     *
     * @private
     */

    /**
     * @typedef {object} BindingInfo Model binding data which used to bind the control.
     * @property {boolean} isOdata Odata model binding or binding to the JSON model
     * @property {string} propertyName the property binding statement , e.g. {xxxx} to attach to the control
     * @property {string} fullPropertyPath path into the model to the property value which is used in the input
     * @property {sap.ui.model.json.JSONModel|sap.ui.model.odata.ODataModel} model the model which used to bind the input
     * @property {sap.ui.model.json.JSONModel} extendedModel the model which used to bind to the extended dialog
     *
     * @private
     */

    /**
     * @typedef {object} ParameterMetadata Metadata of the parameter
     * @property {string} groupId The id of the group which parameter is located
     * @property {string} groupTitle The group title
     * @property {string} displayText The display text. Used for the label when Odata can not be loaded
     * @property {string} description The description text. Used for the label tooltip when Odata can not be loaded
     * @property {boolean} extendedUsage if extended dialog should be used
     * @property {object} editorInfo Odata information
     * @property {string} editorInfo.odataURL Odata URL
     * @property {string} editorInfo.propertyName name of the parameter
     * @property {string} editorInfo.bindingPath Odata binding path
     *
     * @private
     */

    /**
     * @typedef {object} FormattedParameter A prepared parameter.
     * @property {string} parameterName The name of the parameter
     * @property {ParameterValue} valueObject A value object of the parameter
     * @property {BindingInfo} modelBind A model binding of the parameter
     * @property {ParameterMetadata} editorMetadata A metadata of the parameter
     *
     * @private
     */

    /**
     * @typedef {object} ModelRecord A model record.
     * @property {object} model The model used for the OData request.
     * @property {Promise} metadata A promise which resolves if the model is loaded or rejects if the loading failed.
     *
     * @private
     */

    // compare parameters by groupId
    function compareByGroupId (oDefault1, oDefault2) {
        // handle default without metadata
        if (!(oDefault2.editorMetadata && oDefault2.editorMetadata.groupId)) {
            return -1; // keep order
        }
        if (!(oDefault1.editorMetadata && oDefault1.editorMetadata.groupId)) {
            return 1; // move oDefault1 to the end
        }

        if (oDefault1.editorMetadata.groupId < oDefault2.editorMetadata.groupId) {
            return -1;
        }
        if (oDefault1.editorMetadata.groupId > oDefault2.editorMetadata.groupId) {
            return 1;
        }

        return 0;
    }

    // compare parameters by parameterIndex
    function compareByParameterIndex (oDefault1, oDefault2) {
        // handle default without metadata
        if (!(oDefault2.editorMetadata && oDefault2.editorMetadata.parameterIndex)) {
            return -1; // keep order
        }
        if (!(oDefault1.editorMetadata && oDefault1.editorMetadata.parameterIndex)) {
            return 1; // move oDefault1 to the end
        }
        return oDefault1.editorMetadata.parameterIndex - oDefault2.editorMetadata.parameterIndex;
    }

    // compare parameters by groupId and then by parameterIndex
    function compareParameters (oDefault1, oDefault2) {
        // first by groupId
        const iComparisonResult = compareByGroupId(oDefault1, oDefault2);
        if (iComparisonResult === 0) {
            // then by parameterIdx
            return compareByParameterIndex(oDefault1, oDefault2);
        }
        return iComparisonResult;
    }

    return Controller.extend("sap.ushell.components.shell.Settings.userDefaults.controller.UserDefaultsSetting", {
        onInit: function () {
            this.oModelRecords = {}; // a map of oData models
            this.oBlockedParameters = {}; // parameters of odata models which are not yet filled with "our" value
            this.aDisplayedUserDefaults = []; // array of displayed parameters, in order
            this.oStoredParameters = {}; // the original parameters which was loaded from service, updated on save

            this.oDirtyStateModel = new JSONModel({
                isDirty: false,
                selectedVariant: null
            });

            const oView = this.getView();
            oView.setBusy(true);
            oView.setModel(this.oDirtyStateModel, "DirtyState");
            oView.setModel(ushellResources.i18nModel, "i18n");

            this.pInit = this._getSystemContextsModel().then((oSystemContextModel) => {
                oView.setModel(oSystemContextModel, "systemContexts");
                return this._fillGroups();
            })
                .then(this._saveIsSupportedPlatform.bind(this))
                .then(this._initializeSmartVariantManagement.bind(this))
                .then(() => {
                    oView.setBusy(false);
                })
                .catch((oError) => {
                    Log.error(
                        "Error during UserDefaultsSetting controller initialization",
                        oError,
                        "sap.ushell.components.shell.Settings.userDefaults.UserDefaultsSetting"
                    );
                    oView.setBusy(false);
                });
        },

        /**
         * For testing purposes only.
         * @returns {Promise} A promise which resolves when the controller is initialized
         *
         * @since 1.145.0
         * @private
         */
        initialized: function () {
            return this.pInit;
        },

        /**
         * Initializes the systemContexts model
         * The systemContexts and the selectedKey will be written into the model
         * @returns {Promise<sap.ui.model.json.JSONModel>} A Promise which resolves systemContexts model
         *
         * @private
         */
        _getSystemContextsModel: async function () {
            const oSystemContextModel = new JSONModel({
                systemContexts: [],
                selectedKey: ""
            });

            const [
                aContentProviderIds,
                ClientSideTargetResolution,
                UserDefaultParameters
            ] = await Promise.all([
                this._getContentProviderIds(),
                Container.getServiceAsync("ClientSideTargetResolution"),
                Container.getServiceAsync("UserDefaultParameters")
            ]);

            // If there are no content providers we use the local systemContext
            if (aContentProviderIds.length === 0) {
                aContentProviderIds.push("");
            }

            await Promise.all(aContentProviderIds.map(async (sContentProvider) => {
                const oSystemContext = await ClientSideTargetResolution.getSystemContext(sContentProvider);
                const aSystemContexts = oSystemContextModel.getProperty("/systemContexts");

                const bRelevant = await UserDefaultParameters.hasRelevantMaintainableParameters(oSystemContext);
                if (bRelevant) {
                    aSystemContexts.push(oSystemContext);
                }
            }));

            const aSystemContexts = oSystemContextModel.getProperty("/systemContexts");
            if (aSystemContexts.length > 0) {
                oSystemContextModel.setProperty("/selectedKey", aSystemContexts[0].id);
            }

            return oSystemContextModel;
        },

        /**
         * Gets all contentProvider ids.
         * If spaces mode is active the content Providers will be received from the CDM service,
         * else only the default contentProvider id will be returned
         * @returns {Promise<string[]>} A promise which resolves to a list of content provider ids
         *
         * @since 1.80.0
         * @private
         */
        _getContentProviderIds: async function () {
            try {
                const CommonDataModel = await Container.getServiceAsync("CommonDataModel");
                const aContentProviderIds = await CommonDataModel.getContentProviderIds();

                return aContentProviderIds;
            } catch {
                return [""];
            }
        },

        /**
         * Checks if the view is dirty and creates a messageBox to ask the user if they want to discard the changes.
         * Calls _fillGroups if the user wants to discard the changes or the view is not dirty
         *
         * @since 1.81.0
         * @private
         */
        handleSystemContextChanged: function () {
            if (!this.oDirtyStateModel.getProperty("/isDirty")) {
                this._fillGroups();
                return;
            }

            const sMessage = ushellResources.i18n.getText("userDefaultsUnsavedChangesMessage");
            const sDialogTitle = ushellResources.i18n.getText("userDefaultsUnsavedChangesTitle");
            const sSaveText = ushellResources.i18n.getText("userDefaultsSave");
            const sDiscardText = ushellResources.i18n.getText("userDefaultsDiscard");

            MessageBox.show(sMessage, {
                title: sDialogTitle,
                actions: [sSaveText, sDiscardText, MessageBox.Action.CANCEL],
                emphasizedAction: sSaveText,
                icon: MessageBox.Icon.QUESTION,
                onClose: function (sAction) {
                    if (sAction === sDiscardText) {
                        this._fillGroups();
                        this._setDirtyState(false);
                    } else if (sAction === sSaveText) {
                        this.onSave();
                        this._fillGroups();
                        this._setDirtyState(false);
                    } else {
                        this.getView().getModel("systemContexts").setProperty("/selectedKey", this.sLastSelectedKey);
                    }
                }.bind(this)
            });
        },

        /**
         * Creates the groups containing the user defaults of the selected system,
         * and adds them to the form
         * @returns {Promise} A promise which resolves to the current view
         *
         * @since 1.80.0
         * @private
         */
        _fillGroups: async function () {
            const oUserDefaultsForm = this.getView().byId("userDefaultsForm");
            oUserDefaultsForm.removeAllGroups();

            const sKey = this.getView().getModel("systemContexts").getProperty("/selectedKey");
            const oSystemContext = this.getView().getModel("systemContexts").getProperty("/systemContexts").find((oContext) => {
                return oContext.id === sKey;
            });

            const UserDefaultParameters = await Container.getServiceAsync("UserDefaultParameters");
            const oParameters = await UserDefaultParameters.editorGetParameters(oSystemContext);

            // take a deep copy of the original parameters
            this.oStoredParameters = deepClone(oParameters);

            // Used to save extendedValues and value of the plain inputs
            // this is the main model where all data is finally synced to
            // each parameter might have its own model for the input control (e.g. OdataModel for SmartField)
            // on change we sync the data back to this model
            // it can be used to get the full data when saving
            this.oParameterModel = new JSONModel(oParameters);
            this.oParameterModel.setDefaultBindingMode("TwoWay");

            this.getView().setModel(this.oParameterModel, "MdlParameter");

            const aFormattedParameters = await this._getFormattedParameters(this.oParameterModel);

            const aGroups = this._createContent(aFormattedParameters);
            aGroups.forEach((oGroup) => {
                oUserDefaultsForm.addGroup(oGroup);
            });
        },

        /**
         * Format original parameters to have the same structure. Also create modelBind.
         * @param {sap.ui.model.json.JSONModel} oJsonModel json model for extended dialog and not Odata fields
         * @returns {Promise<FormattedParameter[]>} The list of sorted formatted parameters.
         *
         * @private
         */
        _getFormattedParameters: async function (oJsonModel) {
            const oParameters = oJsonModel.getData("/");
            const pFormattedParameters = Object.keys(oParameters).map(async (sParameter) => {
                const oParameter = deepClone(oParameters[sParameter]);

                oParameter.parameterName = sParameter;
                oParameter.editorMetadata = oParameter.editorMetadata || {};
                const oEditorMetadata = oParameter.editorMetadata;

                if (oEditorMetadata.editorInfo?.propertyName) {
                    try {
                        const oBindingInfo = await this._createOdataModelBindingInfo(oParameter, oJsonModel);

                        oParameter.modelBind = oBindingInfo;
                        const oODataMetadataModel = oBindingInfo.model ? oBindingInfo.model.getMetaModel() : undefined;

                        let sODataPropType;
                        if (oODataMetadataModel) {
                            await oODataMetadataModel.loaded();

                            const sNamespace = oODataMetadataModel.getObject("/dataServices/schema/0/namespace");
                            const oType = oODataMetadataModel.getODataEntityType(`${sNamespace}.${oEditorMetadata.editorInfo.entityName}`);
                            const oODataProp = oType.property.find((oObj) => oObj.name === oEditorMetadata.editorInfo.propertyName);
                            sODataPropType = oODataProp?.type;
                        }

                        // normalize the value, in the editor
                        if (sODataPropType === "Edm.DateTime") {
                            // We assume that a SmartField is used
                            // => Date is used to handle this Parameter, therefore we adapt the data accordingly
                            oParameter.valueObject.value = oParameter.valueObject.value ? new Date(oParameter.valueObject.value) : null;
                        } else {
                            // undefined is represented as ""
                            oParameter.valueObject.value ??= "";
                        }

                        return oParameter;
                    } catch (oError) {
                        Log.error(`Metadata loading for parameter ${oParameter.parameterName} failed${JSON.stringify(oEditorMetadata)}`, oError);

                        // When odata loading fails, switch to the normal (not smart) control.
                        this.oBlockedParameters[oParameter.parameterName] = false;
                        // continue with normal model binding
                    }
                }

                // normalize the value, in the editor, undefined is represented as ""
                oParameter.valueObject = deepExtend({ value: "" }, oParameter.valueObject);
                oParameter.modelBind = this._createPlainModelBindingInfo(oParameter, oJsonModel);

                return oParameter;
            });

            const aFormattedParameters = await Promise.all(pFormattedParameters);
            aFormattedParameters.sort(compareParameters);
            this.aDisplayedUserDefaults = aFormattedParameters;

            aFormattedParameters.forEach((oParameter) => {
                const oBindingInfo = oParameter.modelBind;
                oBindingInfo.model.setProperty(oBindingInfo.fullPropertyPath, oParameter.valueObject.value);
                oBindingInfo.model.bindTree(oBindingInfo.fullPropertyPath).attachChange(this._syncInputDataIntoJsonModel.bind(this));
            });

            return aFormattedParameters;
        },

        /**
         * Create model binding for the plain control
         * @param {object} oParameter default parameter
         * @param {sap.ui.model.json.JSONModel} oJsonModel json model for extendedModel and model
         * @returns {BindingInfo} The modal binding for the parameter
         *
         * @private
         */
        _createPlainModelBindingInfo: function (oParameter, oJsonModel) {
            const sFullPropertyPath = `/${oParameter.parameterName}/valueObject/value`;

            // create object if not exist
            if (!oJsonModel.getProperty(`/${oParameter.parameterName}/valueObject`)) {
                oJsonModel.setProperty(`/${oParameter.parameterName}/valueObject`, {});
            }

            const oBindingInfo = {
                isOdata: false,
                model: oJsonModel,
                extendedModel: oJsonModel, // same model!
                fullPropertyPath: sFullPropertyPath,
                propertyName: `{${sFullPropertyPath}}`
            };

            return oBindingInfo;
        },

        /**
         * Create model binding for the smart control
         * @param {object} oParameter Parameter
         * @param {sap.ui.model.json.JSONModel} oJsonModel json model for extendedModel
         * @returns {Promise<BindingInfo>} A modal binding info object for the parameter
         *
         * @private
         */
        _createOdataModelBindingInfo: async function (oParameter, oJsonModel) {
            const oEditorMetadata = oParameter.editorMetadata.editorInfo;
            const oODataServiceData = this._getODataServiceData(oEditorMetadata.odataURL, oParameter);

            await oODataServiceData.metadataLoaded;

            const oBindingInfo = {
                isOdata: true,
                model: oODataServiceData.model,
                extendedModel: oJsonModel,
                propertyName: `{${oEditorMetadata.propertyName}}`,
                fullPropertyPath: `${oEditorMetadata.bindingPath}/${oEditorMetadata.propertyName}`
            };
            return oBindingInfo;
        },

        /**
         * Returns a model record for the OData service.
         * @param {string} sUrl The url for which the model should be created.
         * @param {FormattedParameter} oFormattedParameter default parameter
         * @returns {ModelRecord} The model record for the OData Service.
         *
         * @private
         */
        _getODataServiceData: function (sUrl, oFormattedParameter) {
            if (!this.oModelRecords[sUrl]) {
                // In order to reduce the volume of the metadata response
                // We pass only relevant parameters to oDataModel constructor
                const oModel = this._createODataModel(sUrl, {
                    metadataUrlParams: {
                        "sap-documentation": "heading,quickinfo",
                        "sap-value-list": "none",
                        "sap-lang-cachebuster": Localization.getLanguageTag().toString()
                    },
                    json: true
                });
                oModel.setDefaultCountMode("None");
                oModel.setDefaultBindingMode("TwoWay");

                this.oModelRecords[sUrl] = {
                    attachedListeners: [],
                    model: oModel,
                    metadataLoaded: new Promise((resolve, reject) => {
                        oModel.attachMetadataLoaded(resolve);
                        oModel.attachMetadataFailed(reject);
                    })
                };
            }

            if (this.oModelRecords[sUrl].attachedListeners.indexOf(oFormattedParameter.parameterName) === -1) {
                this.oModelRecords[sUrl].attachedListeners.push(oFormattedParameter.parameterName);
                this.oModelRecords[sUrl].model.attachRequestCompleted(this._overrideOdataModelValue.bind(this, oFormattedParameter));
                // because Odata model is used, block input before request is completed
                this.oBlockedParameters[oFormattedParameter.parameterName] = true;
            }
            return this.oModelRecords[sUrl];
        },

        /**
         * To ease testing
         * @param {string} sUrl OData service URL
         * @param {object} oOptions constructor options of the OData model
         * @returns {sap.ui.model.odata.v2.ODataModel} The created OData model
         *
         * @since 1.145.0
         * @private
         */
        _createODataModel: function (sUrl, oOptions) {
            return new ODataModel(sUrl, oOptions);
        },

        /**
         * @param {FormattedParameter} oFormattedParameter The formatted parameter
         * @param {sap.ui.base.Event} oEvent The request completed event
         *
         * @private
         */
        _overrideOdataModelValue: function (oFormattedParameter, oEvent) {
            const oModel = oEvent.getSource();
            const sUrlSegment = `/${oEvent.getParameter("url").replace(/\?.*/, "")}`;
            const oEditorMetadata = oFormattedParameter.editorMetadata;

            if (oEditorMetadata.editorInfo.bindingPath === sUrlSegment) {
                // if the property value in the model is not the same as the one we got from the service,
                // change the property value accordingly
                const sFullPath = oFormattedParameter.modelBind.fullPropertyPath;
                if (oModel.getProperty(sFullPath) !== oFormattedParameter.valueObject.value) {
                    oModel.setProperty(sFullPath, oFormattedParameter.valueObject.value);
                }
                this.oBlockedParameters[oFormattedParameter.parameterName] = false;
            }
        },

        /**
         * Create smart form content
         * @param {FormattedParameter[]} aFormattedParameters list of the formatted parameters
         * @returns {sap.ui.comp.smartform.Group[]} the list of the groups
         *
         * @private
         */
        _createContent: function (aFormattedParameters) {
            const oBindingContexts = {};
            const aGroups = [];

            let sLastGroupId = "nevermore";
            let oGroup; // the current group;

            for (const oFormattedParameter of aFormattedParameters) {
                const oBindingInfo = oFormattedParameter.modelBind;
                const oEditorMetadata = oFormattedParameter.editorMetadata;

                if (sLastGroupId !== oEditorMetadata.groupId) {
                    // generate a group on group change
                    oGroup = new Group({
                        title: new Title({
                            text: oEditorMetadata.groupTitle || "{i18n>userDefaultsTechnicalDefaultValues}"
                        })
                    });
                    oGroup.addCustomData(new CustomData({
                        key: "help-id",
                        value: oEditorMetadata.groupId || "userDefaultsTechnicalDefaultValues",
                        writeToDom: true
                    }));
                    sLastGroupId = oEditorMetadata.groupId;
                    aGroups.push(oGroup);
                }

                const oGroupElement = this._createGroupElement(oFormattedParameter);
                oGroupElement.setModel(oBindingInfo.model);

                if (oBindingInfo.isOdata) {
                    const sUrl = oEditorMetadata.editorInfo.odataURL;
                    // in order to avoid OData requests to the same URL
                    // we try to reuse the BindingContext that was previously created for the same URL
                    // the call to bindElement creates a new BindingContext, and triggers an OData request
                    if (!oBindingContexts[sUrl]) {
                        const sBindingPath = oEditorMetadata.editorInfo.bindingPath;
                        oGroupElement.bindElement(sBindingPath);
                        oBindingContexts[sUrl] = oBindingInfo.model.getContext(sBindingPath);
                    } else {
                        oGroupElement.setBindingContext(oBindingContexts[sUrl]);
                    }
                }
                oGroup.addGroupElement(oGroupElement);
            }
            return aGroups;
        },

        /**
         * Create GroupElement
         * @param {FormattedParameter} oFormattedParameter default parameter
         * @returns {sap.ui.comp.smartform.GroupElement} GroupElement with a Label and elements
         *
         * @since 1.129.0
         * @private
         */
        _createGroupElement: function (oFormattedParameter) {
            const oBindingInfo = oFormattedParameter.modelBind;
            const oEditorMetadata = oFormattedParameter.editorMetadata;

            const oLabel = new SmartLabel({});
            // If oRecord supports extended values (ranges), we want to add an additional button to it
            // The style of the button depends on whether there are any ranges in the extendedValues object
            const oExtendedParametersButton = new Button({
                text: "{i18n>userDefaultsExtendedParametersTitle}",
                tooltip: "{i18n>userDefaultsExtendedParametersTooltip}",
                visible: !!oEditorMetadata.extendedUsage,
                type: {
                    parts: [`MdlParameter>/${oFormattedParameter.parameterName}/valueObject/extendedValue/Ranges`],
                    formatter: (aRanges) => {
                        return aRanges?.length ? ButtonType.Emphasized : ButtonType.Transparent;
                    }
                },
                press: (oEvent) => {
                    ExtendedValueDialogController.openDialog(oFormattedParameter, this._saveExtendedValue.bind(this));
                }
            });

            let oField;
            if (oBindingInfo.isOdata && oEditorMetadata.editorInfo) {
                oField = new SmartField({
                    value: oBindingInfo.propertyName,
                    name: oFormattedParameter.parameterName,
                    fieldGroupIds: ["UserDefaults"]
                });
                oLabel.setLabelFor(oField);
            } else {
                oField = new Input({
                    name: oFormattedParameter.parameterName,
                    value: oBindingInfo.propertyName,
                    fieldGroupIds: ["UserDefaults"],
                    type: "Text"
                });
                oField.addAriaLabelledBy(oLabel);
                oLabel.setText(oEditorMetadata.displayText || oFormattedParameter.parameterName);
                oLabel.setTooltip(oEditorMetadata.description || oFormattedParameter.parameterName);
            }

            oField.attachChange(this._syncInputDataIntoJsonModel.bind(this));
            oField.attachChange(this._setDirtyState.bind(this, true), this);

            const oGroupElement = new GroupElement({
                elements: [oField, oExtendedParametersButton],
                label: oLabel
            });

            return oGroupElement;
        },

        /**
         * Saves the extended value from the extended value dialog into the model.
         * @param {sap.ui.base.Event} oControlEvent The event object from the extended value dialog
         *
         * @private
         */
        _saveExtendedValue: function (oControlEvent) {
            const sParameterName = oControlEvent.getSource().getModel().getProperty("/parameterName");
            const aTokens = oControlEvent.getParameters().tokens || [];

            // convert the Ranges that are coming from the dialog to the format that should be persisted in the service and that applications can read
            const aFormattedTokensData = aTokens.map((oToken) => {
                const oTokenData = oToken.data("range");
                return {
                    Sign: oTokenData.exclude ? "E" : "I",
                    Option: oTokenData.operation !== "Contains" ? oTokenData.operation : "CP",
                    Low: oTokenData.value1,
                    High: oTokenData.value2 || null
                };
            });

            if (!this.oParameterModel.getProperty(`/${sParameterName}/valueObject/extendedValue`)) {
                this.oParameterModel.setProperty(`/${sParameterName}/valueObject/extendedValue`, {});
            }
            this.oParameterModel.setProperty(`/${sParameterName}/valueObject/extendedValue/Ranges`, aFormattedTokensData);

            if (oControlEvent.getParameter("_tokensHaveChanged")) {
                this._setDirtyState(true);
            }
        },

        /**
         * Sets the dirty state and saves the current selectedKey.
         * This is needed for resetting the selectedKey if the user doesn't want to discard their changes.
         * @param {boolean} isDirty True if the state should be changed to dirty.
         *
         * @since 1.81.0
         * @private
         */
        _setDirtyState: function (isDirty) {
            this.oDirtyStateModel.setProperty("/isDirty", isDirty);
            this._setSmartVariantModified(isDirty);

            if (isDirty) {
                this.sLastSelectedKey = this.getView().getModel("systemContexts").getProperty("/selectedKey");
            }
        },

        /**
         * Sets the key of the selected variant.
         * @param {string} sSelectionKey The key of the selected variant.
         *
         * @private
         */
        _setSelectedVariant: function (sSelectionKey) {
            this.oDirtyStateModel.setProperty("/selectedVariant", sSelectionKey);
            this._syncInputDataIntoJsonModel();
        },

        /**
         * This function is invoked on any model data change in the plain JSON fallback model.
         * As this does not work an odata model, it is called during save
         * and it is called during resetting the smart variant, too.
         * We always run over all parameters and record the ones with a delta.
         * We change *relevant* deltas compared to the data when calling up the dialogue.
         * Note:
         *  the valueObject may contain other relevant metadata,
         *  which is *not* altered by the Editor Control!
         *  Thus it is important not to overwrite or recreate the valueObject, but only set the value property.
         *
         * @private
         */
        _syncInputDataIntoJsonModel: function () {
            // sync all parameters from their models into the JSON model
            for (const oFormattedParameter of this.aDisplayedUserDefaults) {
                const sParameterName = oFormattedParameter.parameterName;
                const oBindingInfo = oFormattedParameter.modelBind;

                if (this.oBlockedParameters[sParameterName]) {
                    continue;
                }

                const oOldValues = {
                    value: this.oParameterModel.getProperty(`/${sParameterName}/valueObject/value`),
                    extendedValue: this.oParameterModel.getProperty(`/${sParameterName}/valueObject/extendedValue`) || {}
                };

                const oCurrentValues = {
                    value: undefined,
                    extendedValue: oBindingInfo.extendedModel.getProperty(`/${sParameterName}/valueObject/extendedValue`) || {}
                };
                if (oBindingInfo.fullPropertyPath) {
                    oCurrentValues.value = oBindingInfo.model.getProperty(oBindingInfo.fullPropertyPath);
                }

                if (deepEqual(oCurrentValues, oOldValues)) {
                    continue;
                }

                this.oParameterModel.setProperty(`/${sParameterName}/valueObject/value`, oCurrentValues.value);
                this.oParameterModel.setProperty(`/${sParameterName}/valueObject/extendedValue`, deepClone(oCurrentValues.extendedValue || {}));
            }
        },

        /**
         * Calculates the changed parameters compared to the stored original parameters.
         * @returns {FormattedParameter[]} The list of parameters
         *
         * @since 1.145.0
         * @private
         */
        _getChangedParameters: function () {
            const aChangedParameters = [];

            for (const oFormattedParameter of this.aDisplayedUserDefaults) {
                const sParameterName = oFormattedParameter.parameterName;

                if (this.oBlockedParameters[sParameterName]) {
                    continue;
                }

                const oStoredValue = {
                    value: this.oStoredParameters[sParameterName]?.valueObject?.value,
                    extendedValue: this.oStoredParameters[sParameterName]?.valueObject?.extendedValue
                };

                const oCurrentValue = {
                    value: this.oParameterModel.getProperty(`/${sParameterName}/valueObject/value`),
                    extendedValue: this.oParameterModel.getProperty(`/${sParameterName}/valueObject/extendedValue`)
                };

                if (!deepEqual(oCurrentValue, oStoredValue)) {
                    aChangedParameters.push(oFormattedParameter);
                }
            }

            return aChangedParameters;
        },

        /**
         * Event handler for cancel button press.
         * Resets all changed parameters to their original values.
         *
         * @private
         */
        onCancel: function () {
            this._setDirtyState(false);

            this._getChangedParameters().forEach((oFormattedParameter) => {
                const oBindingInfo = oFormattedParameter.modelBind;
                const oEditorMetadata = oFormattedParameter.editorMetadata;
                const sParameterName = oFormattedParameter.parameterName;

                const oStoredParameter = deepClone(this.oStoredParameters[sParameterName]);
                const oStoredValue = oStoredParameter?.valueObject || {};

                const oODataMetadataModel = oBindingInfo.model ? oBindingInfo.model.getMetaModel() : undefined;

                let sODataPropType;
                if (oODataMetadataModel) {
                    const sNamespace = oODataMetadataModel.getObject("/dataServices/schema/0/namespace");
                    const oType = oODataMetadataModel.getODataEntityType(`${sNamespace}.${oEditorMetadata.editorInfo.entityName}`);
                    const oODataProp = oType.property.find((oObj) => oObj.name === oEditorMetadata.editorInfo.propertyName);
                    sODataPropType = oODataProp?.type;
                }

                if (sODataPropType === "Edm.DateTime") {
                    // We assume that a SmartField is used
                    oBindingInfo.model.setProperty(oBindingInfo.fullPropertyPath, new Date(oStoredValue.value) || null);
                } else {
                    oBindingInfo.model.setProperty(oBindingInfo.fullPropertyPath, oStoredValue.value || "");
                }

                if (oEditorMetadata?.extendedUsage) {
                    oBindingInfo.extendedModel.setProperty(`/${sParameterName}/valueObject/extendedValue`, oStoredValue.extendedValue || {});
                }
            });

            this._setDefaultVariant();
        },

        /**
         * Event handler for save button press.
         * @returns {Promise} A promise which resolves when the save operation is done.
         *
         * @private
         */
        onSave: async function () {
            this._syncInputDataIntoJsonModel();
            this._setDirtyState(false);

            const aChangedFormattedParameters = this._getChangedParameters();
            if (aChangedFormattedParameters.length === 0) {
                return;
            }

            const ClientSideTargetResolution = await Container.getServiceAsync("ClientSideTargetResolution");
            const oSystemContext = await ClientSideTargetResolution.getSystemContext(this.sLastSelectedKey);

            const aSavePromises = aChangedFormattedParameters.map(async (oFormattedParameter) => {
                const sParameterName = oFormattedParameter.parameterName;

                const oValueToStore = deepClone(this.oParameterModel.getProperty(`/${sParameterName}/valueObject`)) || {};

                // as the editor does not distinguish empty string from deletion, and has no "reset" button
                // we drop functionality to allow to set a value to an empty string (!in the editor!)
                // and map an empty string to an effective deletion!
                // eslint-disable-next-line no-warning-comments
                // TODO: make sure all controls allow to enter an empty string as a "valid" value
                if (oValueToStore.value === null || oValueToStore.value === "") {
                    oValueToStore.value = undefined;
                }

                // we rectify the extended value, as the editor produces empty object
                if (
                    !oValueToStore.extendedValue
                    || Object.keys(oValueToStore.extendedValue).length === 0
                    || (
                        Array.isArray(oValueToStore.extendedValue?.Ranges)
                        && oValueToStore.extendedValue.Ranges.length === 0
                    )
                ) {
                    oValueToStore.extendedValue = undefined;
                }

                const UserDefaultParameters = await Container.getServiceAsync("UserDefaultParameters");
                await UserDefaultParameters.editorSetValue(sParameterName, oValueToStore, oSystemContext);

                this.oStoredParameters[sParameterName].valueObject = oValueToStore;
            });

            await Promise.all(aSavePromises);

            await this._resetSmartVariantManagement();
            await this._setDefaultVariant();
        },

        /**
         * Sets the current variant to modified or not. The control will be displayed with an asterisk.
         * @param {boolean} bIsModified True if the variant has been modified.
         *
         * @private
         */
        _setSmartVariantModified: function (bIsModified) {
            if (!this._bIsSupportedPlatform) {
                return;
            }
            const oSmartVariantManagement = this.getView().byId("defaultSettingsVariantManagement");
            oSmartVariantManagement.currentVariantSetModified(bIsModified);
        },

        /**
         * Sets the selected view to the default view in the SmartVariant control.
         *
         * @private
         */
        _setDefaultVariant: function () {
            if (!this._bIsSupportedPlatform) {
                return;
            }
            const oSmartVariantManagement = this.getView().byId("defaultSettingsVariantManagement");
            oSmartVariantManagement.setCurrentVariantId(oSmartVariantManagement.getDefaultVariantKey());
            this._setSelectedVariant(oSmartVariantManagement.getDefaultVariantKey());
        },

        /**
         * Removes all personalizable controls from the SmartVariantManagement control
         * and re-initializes them.
         * This is required to update the "Standard" control after the UserDefaults have been saved.
         * @returns {Promise} Resolves when the component has been initialized.
         *
         * @private
         */
        _resetSmartVariantManagement: async function () {
            if (!this._bIsSupportedPlatform) {
                return;
            }
            const oSmartVariantManagement = this.getView().byId("defaultSettingsVariantManagement");
            oSmartVariantManagement.removeAllPersonalizableControls();
            return this._initializeSmartVariantManagement();
        },

        /**
         * Initializes the PersonalizableInfo instance to introduce the SmartVariantManagement
         * to the UserDefaultsForm control.
         * @returns {Promise} Resolves when the component has been initialized.
         *
         * @private
         */
        _initializeSmartVariantManagement: async function () {
            if (!this._bIsSupportedPlatform) {
                return;
            }
            const oSmartVariantManagement = this.getView().byId("defaultSettingsVariantManagement");
            const fnSetVariant = function () {
                this._setSelectedVariant(oSmartVariantManagement.getSelectionKey());
            }.bind(this);

            return new Promise((resolve, reject) => {
                try {
                    const oUserDefaultsForm = this.getView().byId("userDefaultsForm");
                    const oPersInfo = new PersonalizableInfo({
                        type: "wrapper",
                        keyName: "persistencyKey",
                        dataSource: "none",
                        control: oUserDefaultsForm
                    });

                    oSmartVariantManagement.detachSelect(fnSetVariant)
                        .detachAfterSave(fnSetVariant)
                        .addPersonalizableControl(oPersInfo)
                        .attachSelect(fnSetVariant)
                        .attachAfterSave(fnSetVariant)
                        .initialise(() => {
                            oSmartVariantManagement.setVisible(true);
                            resolve();
                        }, oUserDefaultsForm);
                } catch (oError) {
                    Log.error("Error initializing SmartVariantManagement for UserDefaultsSetting", oError);
                    reject(oError);
                }
            }).then(fnSetVariant);
        },

        /**
         * Checks and saves a flag to disable smartvariant management on not supported platforms.
         * Currently not supported: CDM
         * @returns {Promise<boolean>} A promise resolving when the flag has been set.
         *
         * @private
         */
        _saveIsSupportedPlatform: async function () {
            const sPlatform = Container.getLogonSystem().getPlatform();
            this._bIsSupportedPlatform = sPlatform !== "cdm";
            return this._bIsSupportedPlatform;
        },

        /**
         * Returns true if a diff between the standard variant and the currently chosen variant exists.
         * @returns {boolean} The result.
         *
         * @private
         */
        displayDiffText: function () {
            if (!this._bIsSupportedPlatform) {
                return false;
            }
            const oSmartVariantManagement = this.getView().byId("defaultSettingsVariantManagement");
            const oUserDefaultsForm = this.getView().byId("userDefaultsForm");
            const sStandardKey = oSmartVariantManagement.getStandardVariantKey();
            const sSelectionKey = oSmartVariantManagement.getSelectionKey();

            if (sSelectionKey === sStandardKey) { return false; }

            const oStandardContent = oSmartVariantManagement.getVariantContent(oUserDefaultsForm, sStandardKey);
            const oVariantContent = oSmartVariantManagement.getVariantContent(oUserDefaultsForm, sSelectionKey);

            // workaround to get a deepEqual === true
            delete oVariantContent?.executeOnSelection;

            return !deepEqual(oStandardContent, oVariantContent);
        }
    });
});
