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

/**
 * @fileOverview Module that stores the dirty state of the application.
 */

sap.ui.define([], () => {
    "use strict";

    /**
     * @alias sap.ushell.navigation.DirtyState
     * @namespace
     * @description Module that stores the dirty state of the application.
     *
     * @since 1.146.0
     * @public
     */
    class DirtyState {
        #oShellNavigationInternal = null;
        #aRegisteredDirtyMethods = [];
        #bIsDirty = false;

        /**
         * Initializes the DirtyState module with required dependencies.
         * Called from sap/ushell/Container during initialization.
         *
         * @param {object} mConfig Configuration object.
         * @param {object} mConfig.shellNavigationInternal The shell navigation internal instance.
         * @since 1.146.0
         * @private
         */
        init ({shellNavigationInternal}) {
            this.#oShellNavigationInternal = shellNavigationInternal;
        }

        /**
         * Resets the DirtyState module to its initial state.
         * For testing purposes only, should not be called in production code.
         *
         * @since 1.146.0
         * @private
         */
        reset () {
            this.#oShellNavigationInternal = null;
            this.#aRegisteredDirtyMethods = [];
            this.#bIsDirty = false;
        }

        /**
         * Synchronous getter for the dirty flag. Required for internal usages like window.onbeforeunload.
         *
         * @returns {boolean} The value of the dirty flag or the determined dirty state returned by the dirty state providers.
         * @since 1.146.0
         * @private
         */
        getDirtyFlagInternal () {
            if (this.#bIsDirty) {
                return true;
            }
            // If no method is registered, we are not dirty
            if (this.#aRegisteredDirtyMethods.length === 0) {
                return false;
            }
            const oNavigationContext = this.#oShellNavigationInternal.getNavigationContext();
            for (const fnProvider of this.#aRegisteredDirtyMethods) {
                if (fnProvider(oNavigationContext)) {
                    return true;
                }
            }
            return false;
        }

        /**
         * If the dirty state was set to 'false' using 'setDirtyFlag' the registered dirty
         * state provider methods get called to determine the actual dirty state. The determined
         * dirty state is then returned.
         *
         * However, if the dirty state was previously set to 'true' using 'setDirtyFlag' the registered dirty
         * state provider methods are ignored and the function simply returns 'true'.
         *
         * @returns {Promise<boolean>} The value of the dirty flag or the determined dirty state returned by the dirty state providers.
         * @since 1.146.0
         * @public
         */
        async getDirtyFlag () {
            return this.getDirtyFlagInternal();
        }

        /**
         * Setter for the isDirty flag value.
         *
         * @param {boolean} [bIsDirty=false] The value of the dirty flag.
         * @returns {Promise<undefined>} Resolves when the dirty flag is set.
         * @throws {TypeError} If bIsDirty is not a boolean or null/undefined.
         * @since 1.146.0
         * @public
         */
        async setDirtyFlag (bIsDirty) {
            bIsDirty = bIsDirty ?? false; // Defaults to false if undefined or null
            if (typeof bIsDirty !== "boolean") {
                throw new TypeError("bIsDirty must be a boolean");
            }
            this.#bIsDirty = bIsDirty;
        }

        /**
         * Register the work protection dirty callback function.
         * In the work protect mechanism, each platform can register their own method in order to check if data
         * was changed during the session, and notify the container about the change.
         * Registering multiple times the same function is allowed.
         *
         * Use <code>Function.prototype.bind()</code> to determine the callback's <code>this</code> or some of its arguments.
         *
         * Note that registered callback functions are not automatically de-registered when an application is closed.
         * Use {@link sap.ushell.navigation.DirtyState.deregisterDirtyStateProvider} to de-register the callback function.
         *
         * @param {function(): boolean} fnDirty
         *  Function for determining the state of the application. The callback is used to determine the current dirty state during a navigation.
         *  The function must return a boolean which determines if the current application is dirty or not. If <code>true</code> is returned the end user is prompted
         *  with a dialog where they need to confirm the potential data loss.
         *  The callback is called with a navigation context as its first parameter which can be used to determine the dirty state:
         *  <pre>
         *  {
         *    isCrossAppNavigation: true, // Boolean which indicates if the navigation is inner app or across two different applications.
         *    innerAppRoute: "&/SalesOrder/11" // If it is an inner app navigation, it describes the inner app route.
         *  }
         *  </pre>
         *
         * @throws {TypeError} If fnDirty is not a function.
         *
         * @since 1.146.0
         * @public
         */
        registerDirtyStateProvider (fnDirty) {
            if (typeof fnDirty !== "function") {
                throw new TypeError("fnDirty must be a function");
            }
            this.#aRegisteredDirtyMethods.push(fnDirty);
        }

        /**
         * Deregister the work protection dirty callback function.
         * See registerDirtyStateProvider for more information.
         * Only the last registered function will be deregistered (in case it was registered multiple times).
         *
         * @param {function(): boolean} fnDirty function for determining the state of the application
         *
         * @throws {TypeError} If fnDirty is not a function.
         *
         * @since 1.146.0
         * @public
         */
        deregisterDirtyStateProvider (fnDirty) {
            if (typeof fnDirty !== "function") {
                throw new TypeError("fnDirty must be a function");
            }
            for (let i = this.#aRegisteredDirtyMethods.length - 1; i >= 0; i--) {
                if (this.#aRegisteredDirtyMethods[i] === fnDirty) {
                    this.#aRegisteredDirtyMethods.splice(i, 1);
                    return;
                }
            }
        }
    }

    return new DirtyState();
});
