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

/**
 * @fileOverview Shell Navigation Hash Changer Service.
 */

sap.ui.define([
    "sap/base/Log",
    "sap/base/util/isEmptyObject",
    "sap/ui/core/routing/HashChanger",
    "sap/ui/performance/trace/Interaction",
    "sap/ui/thirdparty/hasher",
    "sap/ui/thirdparty/jquery",
    "sap/ushell/Container",
    "sap/ushell/navigation/NavigationState",
    "sap/ushell/utils/UrlShortening",
    "sap/ushell/utils/UrlParsing"
], (
    Log,
    isEmptyObject,
    HashChanger,
    Interaction,
    hasher,
    jQuery,
    Container,
    NavigationState,
    UrlShortening,
    UrlParsing
) => {
    "use strict";

    /**
     * Denotes all possible events that can be emitted by the
     * ShellNavigationHashChanger. Each event is described by an object, with
     * the following attributes:
     * <pre>
     * {
     *   name: "...", // the event name that sent via fireEvent
     *   historyEntry: true // or false, to indicate whether or not a browser
     *                      // history entry should be added once the event is fired.
     * }
     * </pre>
     *
     * @enum {object}
     * @private
     */
    const O_EVENT = {
        HASH_SET: {
            name: "hashSet",
            historyEntry: true
        },
        HASH_REPLACED: {
            name: "hashReplaced",
            historyEntry: false
        },
        SHELL_HASH_CHANGED: {
            name: "shellHashChanged",
            historyEntry: true,
            usedByUi5HistoryDetection: true,
            ui5EventParameterNames: {
                // UI5 needs no separator when inner app routes are emitted
                oldHash: "oldAppSpecificRouteNoSeparator",
                newHash: "newAppSpecificRouteNoSeparator"
            },
            // UI5 needs to know whether the event should be forwarded to the Router in existing components
            // or the event is only relevant for the components which are going to be created.
            // With the "shellHashChanged" event, the new hash should only be consumed by the new component which is
            // going to be created with the new intent and the existing components shouldn't get notified by this hash
            // change. Therefore this option needs to be set with true.
            updateHashOnly: true
        },
        SHELL_HASH_PARAMETER_CHANGED: {
            name: "shellHashParameterChanged",
            historyEntry: null // unknown, control may be passed to an arbitrary function in this case
        },
        HASH_CHANGED: {
            name: "hashChanged",
            historyEntry: false,
            usedByUi5HistoryDetection: true,
            ui5EventParameterNames: {
                newHash: "newHash",
                fullHash: "fullHash"
            }
        }
    };

    const sModuleName = "ShellNavigationHashChanger";
    const sLogFesrPrefix = "[FesrFlp]";

    /**
     * @alias sap.ushell.services.ShellNavigationHashChanger
     * @class
     *
     * @extends sap.ui.core.routing.HashChanger
     *
     * @private
     */
    const ShellNavigationHashChanger = HashChanger.extend("sap.ushell.services.ShellNavigationHashChanger", /** @lends sap.ushell.services.ShellNavigationHashChanger.prototype */ {
        constructor: function (oConfig) {
            this.oServiceConfig = oConfig;
            this._oNavigationState = {};

            HashChanger.apply(this);
            this._initializedByShellNav = false; // initialization flag for the shellNavigationService
            this._bReloadApplication = false;
            this._fnShellCallback = null;
            this._appHashPrefix = "&/";
            this._hashPrefix = "#";
            this.aNavigationFilters = [];
            this.NavigationFilterStatus = {
                Continue: "Continue",
                Custom: "Custom",
                Abandon: "Abandon",
                Keep: "Keep"
            };

            // used by UI5 to understand what events affect the history
            this.getRelevantEventsInfo = function () {
                return this._getAllEvents()
                    .filter((oEvent) => {
                        return oEvent.usedByUi5HistoryDetection;
                    })
                    .map((oEvent) => {
                        const oEventInfo = {
                            name: oEvent.name,
                            paramMapping: oEvent.ui5EventParameterNames
                        };

                        if (oEvent.updateHashOnly !== undefined) {
                            oEventInfo.updateHashOnly = oEvent.updateHashOnly;
                        }

                        return oEventInfo;
                    });
            };
        }
    });

    /**
     * Obtains the current shell hash (with #) urlDecoded
     * Shortened(!)
     * @returns {string} the shell hash
     * @private
     */
    ShellNavigationHashChanger.prototype._getCurrentShellHash = function () {
        const oSplitHash = this._splitHash(hasher.getHash());
        return `#${oSplitHash?.shellPart || ""}`;
    };

    /**
     * Internally constructs the next hash, with #
     * shortened(!)
     * @param {string} sAppSpecific Application specific hash
     * @returns {string} constructed full hash
     * @private
     */
    ShellNavigationHashChanger.prototype._constructHash = function (sAppSpecific) {
        const sHash = this._getCurrentShellHash();

        return `${sHash}${sAppSpecific}`;
    };

    /** split a shell hash into app and shell specific part
     *  @private
     *  @returns <code>null</code>, if sHash is not a valid hash (not parsable);
     *      otherwise an object with properties <code>shellPart</code> and <code>appSpecificRoute</code>
     *      the properties are <code>null</code> if sHash is falsy
     */
    // this method is deliberately restrictive to work only on proper hashes
    //  this may be made part of URLParser
    ShellNavigationHashChanger.prototype._splitHash = function (sHash) {
        if (sHash === undefined || sHash === null || sHash === "") {
            return {
                shellPart: null,
                appSpecificRoute: null,
                intent: null,
                params: null
            };
        }
        // break down hash into parts
        // "#SO-ABC~CONTXT?ABC=3A&DEF=4B&/detail/1?A=B");
        const oParsedHash = UrlParsing.parseShellHash(sHash);
        if (oParsedHash === undefined || oParsedHash === null) {
            return null;
        }

        const oShellHashParams = (oParsedHash.params && !isEmptyObject(oParsedHash.params)) ? oParsedHash.params : null;
        const sAppSpecificRoute = oParsedHash.appSpecificRoute;
        oParsedHash.appSpecificRoute = undefined;
        return {
            shellPart: UrlParsing.stripLeadingHash(UrlParsing.constructShellHash(oParsedHash)) || null,
            appSpecificRoute: sAppSpecificRoute || null, // ,"&/detail/1?A=B");
            intent: (oParsedHash.semanticObject && oParsedHash.action
                && (`${oParsedHash.semanticObject}-${oParsedHash.action}${oParsedHash.contextRaw || ""}`)) || null,
            params: oShellHashParams
        };
    };

    /**
     * internal, central navigation hook that trigger hash change
     * triggers events and sets the hash
     * @param {string} sFullHash full shell hash
     * @param {string} sAppHash application specific hash
     * @param {boolean} bWriteHistory whether to create a history record (true, undefined) or replace the hash (false)
     * @private
     */
    ShellNavigationHashChanger.prototype._setHash = function (sFullHash, sAppHash, bWriteHistory) {
        hasher.prependHash = "";
        sFullHash = UrlParsing.stripLeadingHash(sFullHash);
        sAppHash = sAppHash || "";
        if (bWriteHistory === undefined) {
            bWriteHistory = true;
        }
        // don't call method on super class
        // we set the full hash and fire the events for the app-specific part only
        // this is necessary for consistency of all events; hashSet and hashReplaced are
        // evaluated by sap.ui.core.routing.History
        if (bWriteHistory) {
            this.fireEvent(O_EVENT.HASH_SET.name, { sHash: sAppHash });
            hasher.setHash(sFullHash);
        } else {
            this.fireEvent(O_EVENT.HASH_REPLACED.name, { sHash: sAppHash });
            hasher.replaceHash(sFullHash);
        }
    };

    /**
     * For navigation filters using the custom navigation flow, the hash needs to be set
     * without triggering the hash change events.
     * @param {sap.ushell.services.URLParsing.ShellHash} oArgs The ShellHash object
     * @returns {Promise} A promise which resolves once the hash was set
     *
     * @since 1.144
     * @private
     */
    ShellNavigationHashChanger.prototype.setCustomNavigationFilterHash = async function (oArgs) {
        let sFullHash = this._hrefForExternalNoEnc(oArgs);
        sFullHash = UrlParsing.stripLeadingHash(sFullHash);

        hasher.replaceHash(sFullHash);
    };

    ShellNavigationHashChanger.prototype.registerNavigationFilter = function (fnFilter) {
        if (typeof fnFilter !== "function") {
            throw new Error("fnFilter must be a function");
        }
        this.aNavigationFilters.push(fnFilter);
    };

    ShellNavigationHashChanger.prototype.unregisterNavigationFilter = function (fnFilter) {
        if (typeof fnFilter !== "function") {
            throw new Error("fnFilter must be a function");
        }
        this.aNavigationFilters = this.aNavigationFilters.filter((fnRegisteredFilter) => {
            return fnRegisteredFilter !== fnFilter;
        });
    };

    /**
     * @alias sap.ushell.services.StoreContext
     * @class
     * @classdesc This object can generate an arbitrary number of keys
     * and potentially store them in sequence,
     *
     * it is required to call the getNextKey function before
     * calling store(sValue)
     *
     * this.getPromise(), invoked after the last store sequence,
     * returns a promise which will be ok *after* all save sequences are
     * done
     *
     * @param {object} AppStateService The AppState service
     * @param {object} oComponent a ui5 component
     * @param {boolean} [bTransient] whether the AppState is supposed to be transient
     */
    function StoreContext (AppStateService, oComponent, bTransient) {
        this.oAppState = undefined;
        this.oPromise = (new jQuery.Deferred()).resolve();
        this.getNextKey = function () {
            this.oAppState = AppStateService.createEmptyAppState(oComponent, bTransient);
            return this.oAppState.getKey();
        };
        this.store = function (sValue) {
            this.oAppState.setData(sValue);
            const nPromise = this.oAppState.save();
            this.oPromise = jQuery.when(this.oPromise, nPromise);
        };
        this.getPromise = function () {
            return this.oPromise;
        };
    }

    /**
     * Compact a given parameter object if too long,
     * retaining <code> aRetainParameterList if possible
     * a member sap-intent-param with the key will be added to the
     * returned oParams object
     * @param {object} oParams a parameter object
     * @param {Array<*>} [aRetainParameterList] the parameter list to retain
     * @param {object} [oComponent] a ui5 component, should be the root component of your application
     * @param {boolean} [bTransient] indicates a transient appstate should be created
     * @returns {jQuery.Promise} a promise, whose first argument on resolution is a either
     * an equivalent oParams object (or the identical one) or a new
     * parameter object with the retained url parameters and a sap-intent-param with key value
     */
    ShellNavigationHashChanger.prototype.compactParams = function (oParams, aRetainParameterList, oComponent, bTransient) {
        const oDeferred = new jQuery.Deferred();
        const sHash = UrlParsing.constructShellHash({
            target: {
                semanticObject: "SO",
                action: "action"
            },
            params: oParams
        });

        if (oParams === undefined || Object.keys(oParams).length === 0) {
            return oDeferred.resolve(oParams).promise();
        }

        Container.getServiceAsync("AppState")
            .then((AppStateService) => {
                const oSaveContext = new StoreContext(AppStateService, oComponent, bTransient);
                const oResult = UrlShortening.compactHash(sHash, aRetainParameterList, oSaveContext);

                // separate the parameters
                const oCompactedParams = UrlParsing.parseParameters(oResult.hash.match(/\?.*/)[0]);
                oSaveContext.getPromise()
                    .done(() => {
                        oDeferred.resolve(oCompactedParams);
                    })
                    .fail(oDeferred.reject);
            });
        return oDeferred.promise();
    };

    // protected API, only used by shell services
    /**
     * Returns a string which can be put into the DOM (e.g. in a link tag), asynchronously.
     * Please use CrossApplicationNavigation service and do not invoke this method directly
     * if you are an application.
     *
     * @param {object} oArgs
     *     object encoding a semantic object and action
     *     e.g.:
     *     <pre>
     *     {
     *        target: {
     *            semanticObject: "AnObject",
     *            action: "Action"
     *        },
     *        params: {
     *            A: "B"
     *        }
     *     }
     *     </pre>
     *
     *     or
     *
     *     <pre>
     *     {
     *         target: {
     *             shellHash: "SO-36&jumper=postman"
     *         }
     *     }
     *     </pre>
     * @param {boolean} [bVerbose]
     *    whether the response should be returned in verbose format. If
     *    this flag is set to true, this function returns an object
     *    instead of a string.
     * @returns {Promise<object>}
     *    <p>a string that can be put into an href attribute of an
     *    HTML anchor.  The returned string will always start with a
     *    hash character.</p>
     *
     *    <p>
     *    In case the <b>bVerbose</b> parameter is set to true, an
     *    object that wraps the result string will be returned
     *    instead:
     *    <pre>
     *    { hash : {string},
     *      params : {object}
     *      skippedParams : {object}
     *    }
     *    </pre>
     *    </p>
     *
     * where:
     * <ul>
     * <li><code>params</code> is an object containing non-truncated parameters</li>
     * <li><code>skippedParams</code> is an object containing truncated parameters if truncation occurred or undefined if not</li>
     * </ul>
     *
     * @since 1.15.0
     * @private
     */
    ShellNavigationHashChanger.prototype.hrefForExternal = async function (oArgs, bVerbose) {
        let vResult = await this.hrefForExternalNoEncAsync(oArgs, bVerbose);

        // must encode the result
        if (bVerbose) {
            vResult.hash = encodeURI(vResult.hash);
        } else {
            vResult = encodeURI(vResult);
        }

        return vResult;
    };

    /**
     * Returns a string which can be put into the DOM (e.g. in a link tag), synchronously.
     * Please use CrossApplicationNavigation service and do not invoke this method directly
     * if you are an application.
     *
     * @param {object} oArgs
     *     object encoding a semantic object and action
     *     e.g.:
     *     <pre>
     *     {
     *        target: {
     *            semanticObject: "AnObject",
     *            action: "Action"
     *        },
     *        params: {
     *            A: "B"
     *        }
     *     }
     *     </pre>
     *
     *     or
     *
     *     <pre>
     *     {
     *         target: {
     *             shellHash: "SO-36&jumper=postman"
     *         }
     *     }
     *     </pre>
     * @param {boolean} [bVerbose]
     *    whether the response should be returned in verbose format. If
     *    this flag is set to true, this function returns an object
     *    instead of a string.
     * @param {object} [oComponent]
     *    an optional instance of sap.ui.core.UIComponent
     * @returns {object}
     *    <p>a string that can be put into an href attribute of an
     *    HTML anchor.  The returned string will always start with a
     *    hash character.</p>
     *
     *    <p>
     *    In case the <b>bVerbose</b> parameter is set to true, an
     *    object that wraps the result string will be returned
     *    instead:
     *    <pre>
     *    { hash : {string},
     *      params : {object}
     *      skippedParams : {object}
     *    }
     *    </pre>
     *    </p>
     *
     * where:
     * <ul>
     * <li><code>params</code> is an object containing non-truncated parameters</li>
     * <li><code>skippedParams</code> is an object containing truncated parameters if truncation occurred or undefined if not</li>
     * </ul>
     *
     * @since 1.119.0
     * @private
     * @deprecated since 1.119. Please use {@link #hrefForExternal} instead.
     */
    ShellNavigationHashChanger.prototype.hrefForExternalSync = function (oArgs, bVerbose, oComponent) {
        let vResult = this.hrefForExternalNoEnc(oArgs, bVerbose, oComponent);

        // must encode the result
        if (bVerbose) {
            vResult.hash = encodeURI(vResult.hash);
        } else {
            vResult = encodeURI(vResult);
        }

        return vResult;
    };

    /**
     * Behaves as {@link #hrefForExternal} but does not encode the
     * returned intents with encodeURI.
     *
     * @param {object} oArgs
     *     object encoding a semantic object and action
     * @param {boolean} [bVerbose]
     *    whether the response should be returned in verbose format.
     * @param {object} [oComponent]
     *    an optional instance of sap.ui.core.UIComponent
     * @param {boolean} [bAsync]
     *    indicates whether the method should return the result asynchronously.
     *    <code>bAsync=false</code> is deprecated since 1.95.
     *
     * @returns {object}
     *    <p>a string that can be put into an href attribute of an
     *    HTML anchor.</p>
     *
     * @see {@link #hrefForExternal}
     * @since 1.32.0
     * @private
     * @deprecated since 1.108. Please use {@link #hrefForExternalNoEncAsync} instead.
     */
    ShellNavigationHashChanger.prototype.hrefForExternalNoEnc = function (oArgs, bVerbose, oComponent, bAsync) {
        Log.error("Deprecated API call of 'sap.ushell.services.ShellNavigationHashChanger.hrefForExternalNoEnc'. Please use 'hrefForExternalNoEncAsync' instead",
            null,
            "sap.ushell.services.ShellNavigationHashChanger"
        );

        if (bAsync) {
            const oDeferred = new jQuery.Deferred();

            this.hrefForExternalNoEncAsync(oArgs, bVerbose)
                .then(oDeferred.resolve)
                .catch(oDeferred.reject);

            return oDeferred.promise();
        }

        const sHash = this._hrefForExternalNoEnc(oArgs);
        if (bVerbose) {
            return {
                hash: sHash,
                params: undefined,
                skippedParams: undefined
            };
        }

        return sHash;
    };

    /**
     * Behaves as {@link #hrefForExternal} but does not encode the
     * returned intents with encodeURI.
     *
     * @param {object} oArgs
     *     object encoding a semantic object and action
     * @param {boolean} [bVerbose]
     *    whether the response should be returned in verbose format.
     *
     * @returns {Promise<object>}
     *    <p>Promise resolving a string that can be put into an href
     *    attribute of an HTML anchor.</p>
     *
     * @see {@link #hrefForExternal}
     * @since 1.95.0
     * @private
     */
    ShellNavigationHashChanger.prototype.hrefForExternalNoEncAsync = async function (oArgs, bVerbose) {
        let vResult;

        const sHash = this._hrefForExternalNoEnc(oArgs);
        if (bVerbose) {
            vResult = {
                hash: sHash,
                params: undefined,
                skippedParams: undefined
            };
        } else {
            vResult = sHash;
        }

        return vResult;
    };

    /**
     * Shortened(!)
     * @param {object} oArgs arguments
     * @returns {string} THE hash
     */
    ShellNavigationHashChanger.prototype._hrefForExternalNoEnc = function (oArgs) {
        if (oArgs === undefined) {
            return this._getCurrentShellHash();
        }
        // construct url
        if (oArgs && oArgs.target && (typeof oArgs.target.semanticObject === "string" || typeof oArgs.target.shellHash === "string")) {
            return `#${UrlParsing.constructShellHash(oArgs)}`;
        }
        return this._getCurrentShellHash();
    };

    ShellNavigationHashChanger.prototype._getAppHash = function (oArgs) {
        let sAppHash;
        if (oArgs && oArgs.target && (typeof oArgs.target.shellHash === "string")) {
            const oParsedHash = UrlParsing.parseShellHash(oArgs.target.shellHash);
            sAppHash = oParsedHash && oParsedHash.appSpecificRoute;
            sAppHash = sAppHash && sAppHash.substring(2);
        }
        return sAppHash;
    };

    /**
     * returns a string which can be put into the DOM (e.g. in a link tag)
     * given an app specific hash suffix
     *
     * @param {string} sAppHash Application hash
     * @returns {string} a string which can be put into the link tag,
     *          containing the current shell hash as prefix and the
     *          specified application hash as suffix
     *
     * example: hrefForAppSpecificHash("View1/details/0/") returns
     * "#MyApp-Display&/View1/details/0/"
     *
     * @since 1.15.0
     * @private
     */
    ShellNavigationHashChanger.prototype.hrefForAppSpecificHash = function (sAppHash) {
        return encodeURI(this._constructHash(this._appHashPrefix + sAppHash));
    };

    /**
     *
     * Navigate to an external target
     * Please use CrossApplicationNavigation service and do not invoke this method directly!
     *
     * @param {object} oArgs  configuration object describing the target
     *
     *  e.g. { target : { semanticObject : "AnObject", action: "Action" },
     *         params : { A : "B" } }
     *
     * constructs sth. like    http://....ushell#AnObject-Action?A=B ....
     * and navigates to it.
     * @param {object} oComponent runtime - not in use anymore!
     * @param {boolean} bWriteHistory set to false to invoke replaceHash
     *
     * @returns {Promise} A Promise which resolves after the navigation was triggered
     *
     * @private
     */
    ShellNavigationHashChanger.prototype.toExternal = function (oArgs, oComponent, bWriteHistory) {
        // SAP Mobile Start (SMS) runs in-app browsers in the SMS app. Here, explace navigations cannot work, as in-app browsers do not
        // have tab/window support, but run "single-window-only". Therefore, all navigations need to run inplace.
        // SAP Mobile Start is identified through an addition to its user agent string.
        // This was done in "MCOESAPSTART-24515".
        if (this._getUserAgent().indexOf("SAPMobileStart") >= 0) {
            oArgs.params = oArgs.params || {};
            oArgs.params["sap-ushell-navmode"] = "inplace";
        }

        const sHash = this._hrefForExternalNoEnc(oArgs);
        const sAppHash = this._getAppHash(oArgs);
        this._setHash(sHash, sAppHash, bWriteHistory);
        return Promise.resolve();
    };

    ShellNavigationHashChanger.prototype._getUserAgent = function () {
        return window.navigator.userAgent;
    };

    /**
     * constructs the full shell hash and
     * sets it, thus triggering a navigation to it
     * @param {string} sAppHash specific hash
     * @param {boolean} bWriteHistory if true it adds a history entry in the browser if not it replaces the hash
     * @private
     */
    ShellNavigationHashChanger.prototype.toAppHash = function (sAppHash, bWriteHistory) {
        const sHash = this._constructHash(this._appHashPrefix + sAppHash);
        this._setHash(sHash, sAppHash, bWriteHistory);
    };

    /**
     * Initialization for the shell navigation.
     *
     * This will start listening to hash changes and also fire a hash changed event with the initial hash.
     * @param {function} fnShellCallback Shell callback
     * @protected
     * @returns {boolean} false if it was initialized before, true if it was initialized the first time
     */
    ShellNavigationHashChanger.prototype.initShellNavigation = function (fnShellCallback) {
        if (this._initializedByShellNav) {
            Log.info("initShellNavigation already called on this ShellNavigationHashChanger instance.");
            return false;
        }

        this._fnShellCallback = fnShellCallback;

        hasher.changed.add(this.treatHashChanged, this); // parse hash changes

        if (!hasher.isActive()) {
            hasher.initialized.addOnce(this.treatHashChanged, this); // parse initial hash
            hasher.init(); // start listening for history change
        } else {
            this.treatHashChanged(hasher.getHash());
        }
        this._initializedByShellNav = true;
        return true;
    };

    /**
     * Initialization for the application
     *
     * The init method of the base class is overridden, because the hasher initialization (registration for hash changes) is already done
     * in <code>initShellNavigation</code> method. The application-specific initialization ensures that the application receives a hash change event for the
     * application-specific part if set in the  initial hash.
     * @returns {boolean} false if it was initialized before, true if it was initialized the first time
     * @protected
     */
    ShellNavigationHashChanger.prototype.init = function () {
        if (this._initialized) {
            Log.info("init already called on this ShellNavigationHashChanger instance.");
            return false;
        }
        // fire initial hash change event for the app-specific part
        const oNewHash = this._splitHash(hasher.getHash());
        const sAppSpecificRoute = (oNewHash?.appSpecificRoute || "  ").substring(2); // strip &/
        const sDelimiterIfNecessary = sAppSpecificRoute ? "&/" : "";
        const sShellPart = oNewHash?.shellPart || "";
        this.fireEvent(O_EVENT.HASH_CHANGED.name, {
            newHash: sAppSpecificRoute,
            fullHash: sShellPart + sDelimiterIfNecessary + sAppSpecificRoute
        });
        this._initialized = true;
        return true;
    };

    ShellNavigationHashChanger.prototype._removeInnerAppSeparator = function (sInnerAppRoute) {
        return (sInnerAppRoute || "").replace("&/", "");
    };

    ShellNavigationHashChanger.prototype._startFesrInteraction = function () {
        // inform ui5 interaction about pending navigation
        if (!this._fnResolveFesrInteraction) {
            Log.debug(`${sLogFesrPrefix} Interaction Start`, null, sModuleName);
            this._fnResolveFesrInteraction = Interaction.notifyAsyncStep();
        }
    };

    ShellNavigationHashChanger.prototype._endFesrInteraction = function () {
        if (this._fnResolveFesrInteraction) {
            Log.debug(`${sLogFesrPrefix} Interaction End`, null, sModuleName);
            this._fnResolveFesrInteraction();
            this._fnResolveFesrInteraction = null;
        }
    };

    /**
     * Saves the old and new hash of a navigation step.
     *
     * @param {string} newHash the new hash of the browser
     * @param {string} oldHash the previous hash
     * @private
     */
    ShellNavigationHashChanger.prototype._trackNavigation = function (newHash, oldHash) {
        this._oNavigationState.newHash = newHash;
        this._oNavigationState.oldHash = oldHash;
    };

    ShellNavigationHashChanger.prototype.getCurrentNavigationState = function () {
        return this._oNavigationState;
    };

    /**
     * Fires the hashchanged event, may be extended to modify the hash before firing the event
     * @param {string} newHash the new hash of the browser
     * @param {string} oldHash the previous hash
     * @protected
     */
    ShellNavigationHashChanger.prototype.treatHashChanged = function (newHash, oldHash) {
        NavigationState.startNavigation();
        this._trackNavigation(newHash, oldHash);
        this._startFesrInteraction();

        if (this.inAbandonFlow) {
            // in case and navigation was abandon by a navigation filter, we ignore the hash reset event
            this._endFesrInteraction();
            this.setReloadApplication(false);
            NavigationState.endNavigation();
            return;
        }

        let bShouldKeep;
        newHash = UrlShortening.expandHash(newHash); // do synchronous expansion if possible
        oldHash = UrlShortening.expandHash(oldHash); // if not, the parameter remains and is expanded during NavTargetResolutionInternal
        const oNewHash = this._splitHash(newHash);
        let oOldHash = this._splitHash(oldHash);

        if (!oNewHash) {
            // illegal new hash; pass the full string and an error object
            const oError = new Error(`Illegal new hash - cannot be parsed: '${newHash}'`);
            this.fireEvent(O_EVENT.SHELL_HASH_CHANGED.name, {
                newShellHash: newHash,
                newAppSpecificRoute: null,
                fullHash: newHash,
                oldShellHash: (oOldHash ? oOldHash.shellPart : oldHash),
                error: oError
            });
            this._fnShellCallback(newHash, null, (oOldHash ? oOldHash.shellPart : oldHash), (oOldHash ? oOldHash.appSpecificRoute : null), oError);
            this._endFesrInteraction();
            this.setReloadApplication(false);
            return;
        }

        if (!oOldHash) {
            // illegal old hash - we are less restrictive in this case and just set the complete hash as shell part
            oOldHash = {
                shellPart: oldHash,
                appSpecificRoute: null
            };
        }

        // call all navigation filters
        for (let i = 0; i < this.aNavigationFilters.length; i++) {
            try {
                let sFilterHash;
                const vFilterResult = this.aNavigationFilters[i].call(undefined, newHash, oldHash);
                let sFilterResult = vFilterResult;
                if (typeof vFilterResult !== "string") {
                    sFilterResult = vFilterResult.status;
                    sFilterHash = vFilterResult.hash;
                }

                if (sFilterResult === this.NavigationFilterStatus.Custom) {
                    // filter is handling navigation - stop the navigation flow.
                    if (sFilterHash && sFilterHash.length > 0) {
                        this.inAbandonFlow = true;
                        hasher.replaceHash(sFilterHash);
                        this.inAbandonFlow = false;
                    }
                    this._endFesrInteraction();
                    this.setReloadApplication(false);
                    return;
                }
                if (sFilterResult === this.NavigationFilterStatus.Abandon) {
                    // filter abandon this navigation, therefore we need to reset the hash and stop the navigation flow
                    this.inAbandonFlow = true;
                    hasher.replaceHash(oldHash);
                    this.inAbandonFlow = false;
                    this._endFesrInteraction();
                    this.setReloadApplication(false);
                    return;
                }

                if (sFilterResult === this.NavigationFilterStatus.Keep) {
                    bShouldKeep = true;
                }
            } catch (oError) {
                Log.error("Error while calling Navigation filter! ignoring filter...", oError, "sap.ushell.services.ShellNavigationHashChanger");
            }
        }

        if (bShouldKeep) {
            // parameter fullHash is passed twice for consistency
            this.fireEvent(O_EVENT.HASH_CHANGED.name, {
                newHash: newHash,
                oldHash: oldHash,
                fullHash: newHash
            });
            this._endFesrInteraction();
            this.setReloadApplication(false);
            NavigationState.endNavigation();
            return;
        }
        // else - continue with navigation

        const bIsInitialHashChange = oldHash === undefined;
        const oHashComparison = UrlParsing.compareHashes(newHash, oldHash);
        oHashComparison.sameIntent = oHashComparison.sameIntent && !bIsInitialHashChange;

        const bNavigationToExactSameHash = oHashComparison.sameIntent && oHashComparison.sameParameters && oHashComparison.sameAppSpecificRoute;
        if (bNavigationToExactSameHash) {
            // this kind of navigation might happen when the technical parameters are changed.
            // e.g. sap-ui-fl-control-variant-id
            Log.info("Navigation happened but hash stayed the same", null, "sap.ushell.services.ShellNavigationHashChanger");
            // an empty string has to be propagated!
            this.fireEvent(O_EVENT.HASH_CHANGED.name, {
                newHash: this._removeInnerAppSeparator(oNewHash.appSpecificRoute),
                oldHash: this._removeInnerAppSeparator(oOldHash.appSpecificRoute),
                fullHash: newHash
            });
            this._endFesrInteraction();
            this.setReloadApplication(false);

            return;
        }

        const bInnerAppNavigation = oHashComparison.sameIntent && oHashComparison.sameParameters;
        if (bInnerAppNavigation) {
            // In case a UI5 application navigates to itself without an appSpecificRoute, the app should be reloaded.
            // This check gives the option to continue the navigation, as if the navigation was not an app specific navigation.
            // Thereby reloading the app. (calling exit and init functions of the app component)
            if (this.getReloadApplication()) {
                this.setReloadApplication(false);
            } else {
                const sNewAppSpecificRoute = this._removeInnerAppSeparator(oNewHash.appSpecificRoute);
                const sOldAppSpecificRoute = this._removeInnerAppSeparator(oOldHash.appSpecificRoute);
                Log.info(`Inner App Hash changed from '${sOldAppSpecificRoute}' to '${sNewAppSpecificRoute}'`, null, "sap.ushell.services.ShellNavigationHashChanger");
                // an empty string has to be propagated!
                this.fireEvent(O_EVENT.HASH_CHANGED.name, {
                    newHash: sNewAppSpecificRoute,
                    oldHash: sOldAppSpecificRoute,
                    fullHash: newHash
                });
                this._endFesrInteraction();
                NavigationState.endNavigation();

                return;
            }
        }

        if (oHashComparison.sameIntent && this.hasListeners(O_EVENT.SHELL_HASH_PARAMETER_CHANGED.name)) {
            /*
             * This implements a feature requested via FLPINTEGRATION2014-403,
             * where consumers are allowed to change intent parameters at will
             * without triggering navigation.
             *
             * Attached listeners take control over the navigation after the
             * hash fragment changes.
             */
            const sNewParameters = UrlParsing.paramsToString(oNewHash.params);
            const sOldParameters = UrlParsing.paramsToString(oOldHash.params);

            Log.info(`Shell hash parameters changed from '${sOldParameters}' to '${sNewParameters}'`, null, "sap.ushell.services.ShellNavigationHashChanger");
            this.fireEvent(O_EVENT.SHELL_HASH_PARAMETER_CHANGED.name, {
                oNewParameters: oNewHash.params,
                oOldParameters: oOldHash.params
            });
            this._endFesrInteraction();
            this.setReloadApplication(false);
            NavigationState.endNavigation();
            return;
        }

        Log.info(`Outer shell hash changed from '${oldHash}' to '${newHash}'`, null, "sap.ushell.services.ShellNavigationHashChanger");
        // all Shell specific callback -> load other app !
        this.fireEvent(O_EVENT.SHELL_HASH_CHANGED.name, {
            newShellHash: oNewHash.shellPart,
            newAppSpecificRoute: oNewHash.appSpecificRoute,
            fullHash: newHash,
            oldShellHash: oOldHash.shellPart,
            oldAppSpecificRoute: oOldHash.appSpecificRoute,
            error: "",
            oldAppSpecificRouteNoSeparator: this._removeInnerAppSeparator(oOldHash.appSpecificRoute),
            newAppSpecificRouteNoSeparator: this._removeInnerAppSeparator(oNewHash.appSpecificRoute)
        });
        this._fnShellCallback(oNewHash.shellPart, oNewHash.appSpecificRoute, oOldHash.shellPart, oOldHash.appSpecificRoute);

        this._endFesrInteraction();
        this.setReloadApplication(false);
    };

    /**
     * Sets the hash to a certain value, this hash is prefixed by the shell hash if present
     * @param {string} sHash the hash
     *  adds a history entry in the browser if not it replaces the hash
     * @protected
     */
    ShellNavigationHashChanger.prototype.setHash = function (sHash) {
        this.toAppHash(sHash, /* bWriteHistory */true);
    };

    /**
     * Replaces the hash to a certain value. When using the replace function no browser history is written.
     * If you want to have an entry in the browser history, please use set setHash function.
     * this function has a side effect
     * @param {string} sHash the hash
     * @protected
     */
    ShellNavigationHashChanger.prototype.replaceHash = function (sHash) {
        this.toAppHash(sHash, /* bWriteHistory */false);
    };

    /**
     * Gets the current hash
     *
     * Override the implementation of the base class and just return the application-specific hash part
     * @returns {string} returned string
     * @protected
     */
    ShellNavigationHashChanger.prototype.getHash = function () {
        return this.getAppHash();
    };

    /**
     * Gets the current application-specific hash part
     *
     * @returns {string} the current application hash
     * @private
     */
    ShellNavigationHashChanger.prototype.getAppHash = function () {
        const oNewHash = this._splitHash(hasher.getHash());
        const sAppSpecificRoute = (oNewHash?.appSpecificRoute || "  ").substring(2); // strip &/
        return sAppSpecificRoute;
    };

    /**
     * Cleans the event registration
     * @see sap.ui.base.Object.prototype.destroy
     * @protected
     */
    ShellNavigationHashChanger.prototype.destroy = function () {
        hasher.changed.remove(this.treatHashChanged, this);
        HashChanger.prototype.destroy.apply(this, arguments);
    };

    /**
     * Returns all events triggered by this module into an array.
     *
     * @returns {object[]}
     *  the ShellNavigationHashChanger.EVENT enum items wrapped into an array.
     *
     * @private
     */
    ShellNavigationHashChanger.prototype._getAllEvents = function () {
        return Object.keys(O_EVENT).map((sEvent) => {
            return O_EVENT[sEvent];
        });
    };

    /**
     * Returns the name of events that would not cause an entry to be added in
     * the history once triggered.
     *
     * @returns {string[]}
     *   An array containing event names.
     *
     * @protected
     */
    ShellNavigationHashChanger.prototype.getReplaceHashEvents = function () {
        return this._getAllEvents()
            .filter((oEvent) => {
                return oEvent.historyEntry === false;
            })
            .map((oEvent) => {
                return oEvent.name;
            });
    };

    /**
     * Returns the name of events that would cause an entry to be added in the
     * history once triggered.
     *
     * @returns {string[]}
     *   An array containing event names.
     *
     * @protected
     */
    ShellNavigationHashChanger.prototype.getSetHashEvents = function () {
        return this._getAllEvents()
            .filter((oEvent) => {
                return oEvent.historyEntry === true;
            })
            .map((oEvent) => {
                return oEvent.name;
            });
    };

    /**
     * Determines whether an inner app navigation will be made, given the old
     * and the new hash. This method should be called internally by FLP
     * components only, and it's not intended for any other usage.
     * Technical parameters are filtered out before the evaluation.
     *
     * @param {string} sNewHash
     *   The new hash as returned by <code>treatHashChanged</code>
     *
     * @param {string} sOldHash
     *   The old hash as returned by <code>treatHashChanged</code>
     *
     * @returns {boolean}
     *   Whether the hash change is only in the appSpecificRoute.
     *
     * @protected
     */
    ShellNavigationHashChanger.prototype.isInnerAppNavigation = function (sNewHash, sOldHash) {
        const oNewHash = UrlParsing.parseShellHash(sNewHash);
        const oOldHash = UrlParsing.parseShellHash(sOldHash);

        const bIsInitialHashChange = sOldHash === undefined;
        const bHaveSameIntent = UrlParsing.haveSameIntent(oNewHash, oOldHash) && !bIsInitialHashChange;
        const bHaveSameIntentParameters = UrlParsing.haveSameIntentParameters(oNewHash, oOldHash);

        return bHaveSameIntent && bHaveSameIntentParameters;
    };

    /**
     * Returns wether the currently running application should be reloaded during navigation.
     *
     * @returns {boolean} indicating wether the currently running application should be reloaded during navigation.
     *
     * @since 1.106
     * @private
     */
    ShellNavigationHashChanger.prototype.getReloadApplication = function () {
        return this._bReloadApplication;
    };

    /**
     * Sets a flag to indicate that the currently running application should be reloaded during navigation.
     *
     * @param {boolean} value wether the currently running application should be reloaded during navigation.
     *
     * @since 1.106
     * @private
     */
    ShellNavigationHashChanger.prototype.setReloadApplication = function (value) {
        this._bReloadApplication = value;
    };

    return ShellNavigationHashChanger;
});
