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

/**
 * @fileOverview URL Parsing
 *
 * URL Parsing util for shell compliant hashes
 *
 * [ SO-Action~[Context]]
 * [ ? [A=B(&C=D)+]
 * &/
 *
 * The parsing functions are deliberately restrictive and fragile, only shell compliant hashes are parsed correctly,
 * invalid or completely empty results ( not silently ignored parts) are returned if the hash is not deemed parseable
 */
sap.ui.define([
    "sap/ui/thirdparty/URI",
    "sap/base/Log",
    "sap/base/util/deepClone",
    "sap/base/util/deepEqual",
    "sap/base/util/merge",
    "sap/ushell/base/TechnicalParameters",
    "sap/ushell/base/requireAsync"
], (
    URI,
    Log,
    deepClone,
    deepEqual,
    merge,
    TechnicalParameters,
    requireAsync
) => {
    "use strict";

    /**
     * @typedef {object} sap.ushell.base.UrlParsing.HashComparison
     * Type for HashComparison
     * @property {boolean} sameIntent The intent is the semantic object, action and contextRaw of the hash
     * @property {boolean} sameParameters The intent parameters are the parameters of the hash that are not technical parameters
     * @property {boolean} sameAppSpecificRoute The app specific route is the part of the hash after the shell part
     *
     * @since 1.128.0
     * @private
     */

    /**
     * @alias sap.ushell.base.UrlParsing
     * @namespace
     * @description The Unified Shell's internal URL parsing utils (platform independent)
     *
     * Methods in this class allow to break down a shell compliant hash into it's respective parts
     * (SemanticObject,Action,Context, Parameters, appSpecificHash) or (ShellPart,appSpecificHash) respectively
     * or construct a hash from its constituents.
     *
     * All methods deal with the *internal* shellHash format.
     *
     * Most of the parse methods are robust w.r.t. a leading "#".
     *
     * Note: The functions were designed with a "truthy" behaviour for not present values,
     * Thus a client should not rely on the difference between null, "undefined", "" when testing for the result of a parse action.
     *
     * The parsing functions are deliberately restrictive and fragile, only shell compliant hashes are parsed correctly,
     * behaviour for non-compliant hashes is undefined and subject to change,
     * notably we do not aim do "degrade" nicefully or support partial parsing of corrupted urls.
     *
     * @since 1.94.0
     * @private
     */
    const UrlParsing = {};
    const reValidShellPart = /^(([A-Za-z0-9_/]+)-([A-Za-z0-9_/-]+)(~([A-Z0-9a-z=+/]+))?)?([?]([^&]|(&[^/]))*&?)?$/;

    /**
     * Extract the Shell hash# part from an URL
     * The application specific route part is removed
     * See {@link #getHash} for a function which retains the app specific route.
     *
     * Shell services shall use this service to extract relevant parts of an URL from an actual URL string
     * (which should be treated as opaque)
     * <p>
     * The URL has to comply with the Fiori-Wave 2 agreed upon format
     * <p>
     * This service shall be used to extract a hash part from an url.
     * The result can be further broken up by parseShellHash
     *
     * Examples<p>
     * http://a.b.c?defhij#SemanticObject-Action~Context?PV1=A&PV2=B&/appspecific
     * <br/>
     * returns : "#SemanticObject-Action~Context?PV1=A&PV2=B&/appspecific"
     *
     * Note: the results when passing an illegal (non-compliant) url are undefined and subject to change w.o. notice.
     * Notably further checks may added.
     * The design is deliberately restrictive and non-robust.
     *
     * @param {string} sShellHashString a valid (Shell) url, e.g. <br/>
     *   <code>http://xx.b.c#Object-name~AFE2==?PV1=PV2&PV4=V5&/display/detail/7?UU=HH</code>
     * @returns {object} the parsed result
     * @since 1.94.0
     * @protected
     */
    UrlParsing.getShellHash = function (sShellHashString) {
        const re = /[^#]*#(([^&]|&[^/])*)(&\/.*)?/;
        const match = re.exec(sShellHashString);
        if (match) {
            return match[1];
        }
    };

    /**
     * Extract a hash part from an URL, including an app-specific part
     *
     * @param {string} sURL any value
     * @returns {string} <code>extracted string</code> if and only if a hash is present, undefined otherwise
     * @since 1.94.0
     * @protected
     */
    UrlParsing.getHash = function (sURL) {
        const re = /#(.*)/;
        const match = re.exec(sURL);
        if (match) {
            return match[1];
        }
    };

    /**
     * Check if a URL has an intent based navigation part which can be parsed into a semantic object and action part.
     * Accepts only a relative URL (must contain #) or fully qualified Urls for which
     * origin and filename must correspond to the running launchpad.
     *
     * Given actual url
     * <code>http://www.mycorp.com/sap/fiori/FioriLaunchpad.html?sap-language=DE#SO-action?P1=value1</code>, the following parts
     * <code>http://www.mycorp.com/sap/fiori/FioriLaunchpad.html</code> must match.
     *
     * The actual test is synchronous and *only* tests whether the hash part can be parsed and contains a semantic object and action.
     * It does not test whether the intent or its parameters are valid for a given user
     *
     * This function does not work properly when used inside the app runtime as it compares the given URL to the app runtime's URL
     * instead of the outer FLP's URL.
     * It can still be used for synchronous use cases in the ushell that do not run inside the app runtime.
     *
     * @param {string} sUrl the URL to test. Note: this url must be in internal format.
     * @returns {boolean} true if the conditions are fulfilled.
     * @since 1.94.0
     * @protected
     */
    UrlParsing.isIntentUrl = function (sUrl) {
        return this._isIntentUrl(sUrl, window.location.href);
    };

    /**
     * Check if a URL has an intent based navigation part which can be parsed into a semantic object and action part.
     * Accepts only a relative URL (must contain #) or fully qualified Urls for which
     * origin and filename must correspond to the running launchpad.
     *
     * Given actual url
     * <code>http://www.mycorp.com/sap/fiori/FioriLaunchpad.html?sap-language=DE#SO-action?P1=value1</code>, the following parts
     * <code>http://www.mycorp.com/sap/fiori/FioriLaunchpad.html</code> must match.
     *
     * This function *only* tests whether the hash part can be parsed and contains a semantic object and action.
     * It does not test whether the intent or its parameters are valid for a given user
     *
     * @param {string} sUrl the URL to test. Note: this url must be in internal format.
     * @returns {Promise<boolean>} true if the conditions are fulfilled.
     * @since 1.94.0
     * @protected
     */
    UrlParsing.isIntentUrlAsync = async function (sUrl) {
        const [Container] = await requireAsync(["sap/ushell/Container"]);
        const sFlpUrl = await Container.getFLPUrlAsync(true);
        return this._isIntentUrl(sUrl, sFlpUrl);
    };

    /**
     * The internal implementation of isIntentUrl. See isIntentUrl and isIntentURLAsync for more information.
     *
     * @param {string} sUrl the URL to test. Note: this url must be in internal format.
     * @param {string} sFlpUrl the FLP URL to test against.
     * @returns {boolean} true if the conditions are fulfilled.
     * @since 1.94.0
     * @private
     */
    UrlParsing._isIntentUrl = function (sUrl, sFlpUrl) {
        const reStartWithHash = /^#/;
        if (typeof sUrl !== "string") {
            return false;
        }
        if (!reStartWithHash.test(sUrl)) {
            const oTestUri = (new URI(sUrl)).normalize();
            const sTestUriFullResource = `${oTestUri.protocol()}://${oTestUri.host()}${oTestUri.pathname()}`;
            const oLaunchpadUri = (new URI(sFlpUrl)).normalize();
            const sLaunchpadUriFullResource = `${oLaunchpadUri.protocol()}://${oLaunchpadUri.host()}${oLaunchpadUri.pathname()}`;
            if (sTestUriFullResource !== sLaunchpadUriFullResource) {
                return false;
            }
        }
        // sUrl is to be processed by our launchpad
        const sHash = this.getHash(sUrl);
        if (!sHash) {
            return false;
        }
        const oParsedHash = this.parseShellHash(sHash);
        if (oParsedHash && oParsedHash.semanticObject && oParsedHash.action) {
            return true;
        }
        return false;
    };

    /**
     * Parses parameters from a URI query string (starting with "?") into a parameter object.
     * Keys are decoded twice. This assumes that literal "%" characters are not used on keys, otherwise parsing will fail.
     *
     * @param {string} sParams Parameter string, e.g. <code>?ABC=1&ABC=1%202DEF=4</code>
     * @returns {object} Any value, e.g. <code>{ ABC: ["1", "1 2DEF=4"] }</code>
     * @since 1.94.0
     * @protected
     */
    UrlParsing.parseParameters = function (sParams) {
        try {
            const oUriParams = new URLSearchParams(sParams);
            const vKeys = oUriParams.keys();
            let oKey = vKeys.next();
            const oParamObject = {};

            while (oKey.done === false) {
                // decodeURIComponent: required when a key has double encoding, e.g. "%2524" (BCP: 2180415300)
                // this extra decode is fine as long as literal "%" characters are not used on keys
                oParamObject[decodeURIComponent(oKey.value)] = oUriParams.getAll(oKey.value);
                oKey = vKeys.next();
            }
            return oParamObject;
        } catch (oError) {
            Log.error(`URL parsing - malformed URL parameters string: ${sParams}`, oError);
            return {};
        }
    };

    /**
     * combine members of a javascript object into a parameter string,
     * note that parameters are ordered in an arbitrary manner which is subject to change
     *
     * @param {object} oParams any value { ABC : [1,"1 2"], DEF : ["4"]}
     * @returns {string} <code>ABC=1&ABC=1%202DEF=4</code>
     *   Note that the result is *not* prefixed with a "?", parameter values are encodeURIComponent encoded.
     * @since 1.94.0
     * @protected
     */
    UrlParsing.paramsToString = function (oParams) {
        return this.urlParametersToString(oParams);
    };

    /**
     * Removes parameters with empty values from it.
     *
     * Parameters with "no" value are expressed as an array with empty string, e.g. {sap-system: [""]}.
     * This is used to express that a parameter is present, but has no value.
     * This function removes such parameters from the object. Thus the object is modified in place.
     * Note that this is used in the URLTemplateProcessor that expects parameters with "no" value to be expressed as undefined.
     * Combined values, e.g an empty value and a normal value are not changed.
     * @param {object} oParams  any value { ABC : [1,"1 2"], DEF : ["4"]}
     * @param {string[]} aParams  an array of parameter names to remove if they have an empty value
     */
    UrlParsing.removeParametersWithEmptyValue = function (oParams, aParams) {
        if (!oParams) {
            return;
        }
        Object.keys(oParams).forEach((key) => {
            if (aParams.includes(key) &&
                oParams[key].length === 1 // must be an array with one element
                && oParams[key][0] === "") {
                delete oParams[key];
            }
        });
    };

    /**
     * Combines members of a JavaScript object into a parameter string.
     * Parameters are ordered in an arbitrary manner which might change.
     *
     * @param {object} oParameters The parameter object, e.g. <code>{ ABC: [1, "1 2"], DEF: ["4"] }</code>.
     * @param {string} sDelimiter The parameter delimiter. Default is "&".
     * @param {string} sAssign The parameter assignment. Default is "=".
     * @returns {string} The result parameter string, e.g. <code>ABC=1&ABC=1%202&DEF=4</code>.
     *   The result is *not* prefixed with a "?". Parameter values are URI encoded.
     * @since 1.94.0
     * @private
     */
    UrlParsing.urlParametersToString = function (oParameters, sDelimiter, sAssign) {
        let sShellPart = "";

        sDelimiter = sDelimiter || "&";
        sAssign = sAssign || "=";

        let sFirst = "";
        const aParameterKeys = [];
        for (const sKey in oParameters) {
            if (oParameters.hasOwnProperty(sKey)) {
                aParameterKeys.push(sKey);
            }
        }
        aParameterKeys.sort();
        for (let k = 0; k < aParameterKeys.length; k = k + 1) {
            const sKey = aParameterKeys[k];
            if (Array.isArray(oParameters[sKey])) {
                for (let i = 0; i < oParameters[sKey].length; i = i + 1) {
                    sShellPart += sFirst + encodeURIComponent(sKey) + sAssign + encodeURIComponent(oParameters[sKey][i]);
                    sFirst = sDelimiter;
                }
            } else {
                sShellPart += sFirst + encodeURIComponent(sKey) + sAssign + encodeURIComponent(oParameters[sKey]);
                sFirst = sDelimiter;
            }
        }
        return sShellPart;
    };

    /**
     * adds parameter to a url
     * which can be relative or a hash
     * @param {string} sUrlOrHash "url" which can be relative or a hash
     * @param {object} oUrlParams Url parameter
     * @returns {string} new url with the parameters
     * @since 1.131.0
     *
     */
    UrlParsing.addParamsToUrl = function (sUrlOrHash, oUrlParams) {
        const oUri = new URI(sUrlOrHash);
        Object.keys(oUrlParams)
            .forEach((sKey)=>{
                oUri.addSearch(sKey, oUrlParams[sKey]);
            });
        return oUri.toString();
    };

    /**
     * add parameters of the shell specific part to the URL or hash
     * makes sure they are before the app specific hash
     * @param {string} sUrlOrHash Url can be relative or a hash
     * @param {object} oShellParams Shell parameters
     * @returns {string} url with added parameters
     *
     * @since 1.131.0
     *
     */
    UrlParsing.addShellParamsToURL = function (sUrlOrHash, oShellParams) {
        const oURI = new URI(sUrlOrHash);

        // split hash into shell part and app specific route
        const sHashPart = oURI.hash();
        const sBeforeHashPart = oURI.toString().replace(sHashPart, "");

        // merge the parameters
        const oParseHash = this.parseShellHash(sHashPart) || {};
        Object.assign(oParseHash.params, oShellParams.params);

        // now put it back together

        // new hash
        // construct Shell Hash
        const sNewHash = `#${this.constructShellHash(oParseHash)}`;

        return sBeforeHashPart + sNewHash;
    };

    /**
     * Decompose a shell hash into the respective parts
     *
     * @param {string} sHash Hash part of a shell compliant URL
     *   <code>#SO-Action~Context?P1=a&P2=x&/route?RPV=1</code> the hash part of an URL, <br/>
     *   e.g. <code>"#Object-name~AFE2==?PV1=PV2&PV4=V5&/display/detail/7?UU=HH</code>
     * @param {boolean} [bReturnEmpty=false] Return an empty parsed hash object instead of undefined
     * @returns {object} <code>undefined</code> if not a parseable hash<br/>
     * <pre>
     *   {
     *     semanticObject : string, <br/>
     *     action : string, <br/>
     *     contextRaw : string, <br/>
     *     params :  MapObject<String,Array[String]>, <br/>
     *     appSpecificRoute : string <br/>
     *   }
     * </pre>
     *   Note that params always has an Array for each parameter value!
     * @since 1.94.0
     * @protected
     */
    UrlParsing.parseShellHash = function (sHash, bReturnEmpty) {
        const re = reValidShellPart;
        if (!sHash) {
            if (bReturnEmpty) {
                return this.getEmptyParsedHash();
            }
            return;
        }
        // split shell-hash and app-specific parts first
        const oSplitHash = this.splitHash(sHash);

        const match = re.exec(oSplitHash.shellPart);
        if (match) {
            const sSemanticObject = match[2];
            const sAction = match[3];
            const sContext = match[5];
            const sParams = match[6];
            const oParsedParams = this.parseParameters(sParams);
            return {
                semanticObject: sSemanticObject,
                action: sAction,
                contextRaw: sContext,
                params: oParsedParams,
                appSpecificRoute: oSplitHash.appSpecificRoute
            };
        }
        if (oSplitHash.appSpecificRoute) {
            return {
                semanticObject: undefined,
                action: undefined,
                contextRaw: undefined,
                params: {},
                appSpecificRoute: oSplitHash.appSpecificRoute
            };
        }
        if (bReturnEmpty) {
            return this.getEmptyParsedHash();
        }
    };

    /**
     * Returns an empty parsed hash.
     * This is useful to have a consistent empty object instead of "undefined"
     * @returns {object} empty parsed hash
     *
     * @since 1.142.0
     * @private
     */
    UrlParsing.getEmptyParsedHash = function () {
        return {
            semanticObject: undefined,
            action: undefined,
            contextRaw: undefined,
            params: {},
            appSpecificRoute: undefined
        };
    };

    /**
     * Internal function
     *
     * @param {string} sHash Shell hash
     * @returns {string} the string without a leading #
     * @since 1.94.0
     * @private
     */
    UrlParsing.stripLeadingHash = function (sHash) {
        if (!sHash.startsWith("#")) {
            return sHash;
        }

        return sHash.substring(1);
    };

    /**
     * Ensures a leading # in a hash.
     * @param {string} sHash Shell hash.
     * @returns {string} the string with a leading #.
     *
     * @since 1.129.0
     * @private
     */
    UrlParsing.ensureLeadingHash = function (sHash) {
        if (!sHash) {
            return "#";
        }
        if (sHash.startsWith("#")) {
            return sHash;
        }

        return `#${sHash}`;
    };

    /**
     * split a Unified Shell compliant hash into an Object containing a shell specific part and an app specific parts</br>
     * for non compliant hash strings, the empty object {} is returned.
     * an optional leading # is stripped
     *
     * @param {string} sHash Hash part of a shell conformant URL
     *   <code>#SO-Action~Context?P1=a&P2=x&/route?RPV=1<code>
     *   the hash part of an URL, e.g. <code>"#Object-name~AFE2==?PV1=PV2&PV4=V5&/display/detail/7?UU=HH<code>
     * @returns {object}
     *   <code>{}</code>(empty object) if not a parseable hash
     * <pre>
     *   {
     *     shellPart : "Object-name~AFE2==?PV1=PV2&PV4=V5",
     *     appSpecificRoute : "display/detail/7?UU=HH"
     *   }
     * </pre> otherwise
     *   Note that params always has an Array for each parameter value!
     * @since 1.94.0
     * @protected
     */
    UrlParsing.splitHash = function (sHash) {
        const re = /^(?:#|)([\S\s]*?)(&\/[\S\s]*)?$/;

        if (sHash === undefined || sHash === null || sHash === "") {
            return {};
        }
        // break down hash into parts
        // "#SO-ABC~CONTXT?ABC=3A&DEF=4B&/detail/1?A=B");
        const aMatch = re.exec(sHash);
        const sShellPart = aMatch[1];
        if (sShellPart !== "" && !reValidShellPart.test(sShellPart)) {
            return {};
        }
        const sAppSpecificRoute = aMatch[2];
        if (sShellPart || sAppSpecificRoute) {
            return {
                shellPart: sShellPart,
                appSpecificRoute: sAppSpecificRoute // ,"&/detail/1?A=B");
            };
        }
        return {};
    };

    function appendIf (sUrl, sPostfix) {
        if (sPostfix) {
            return sUrl + sPostfix;
        }
        return sUrl;
    }

    function prependIf (sPrefix, sUrl) {
        if (sPrefix) {
            return sPrefix + sUrl;
        }
        return sUrl;
    }

    /**
     * Harmonizes the shell hash by ensuring it has all required properties.
     * The result is compliant with {@link #parseShellHash}.
     * Clones the input object.
     *
     * @param {sap.ushell.services.URLParsing.ShellHash} oShellHash The shell hash to harmonize
     * @returns {sap.ushell.services.URLParsing.DecomposedHash} The hash in the decomposed format.
     *
     * @since 1.142.0
     * @private
     */
    UrlParsing._harmonizeShellHash = function (oShellHash) {
        if (!oShellHash) {
            return this.getEmptyParsedHash();
        }
        const oShellHashClone = deepClone(oShellHash);

        // map target to decomposed hash properties
        if (oShellHash.target) {
            if (typeof oShellHash.target.shellHash === "string") {
                const oParsedHash = this.parseShellHash(
                    appendIf(
                        oShellHash.target.shellHash,
                        oShellHash.appSpecificRoute
                    )
                );
                return oParsedHash || this.getEmptyParsedHash();
            }

            oShellHashClone.semanticObject = oShellHash.target.semanticObject;
            oShellHashClone.action = oShellHash.target.action;
            oShellHashClone.contextRaw = oShellHash.target.contextRaw;
        }

        // ensure params values are arrays
        if (oShellHashClone.params) {
            for (const sKey in oShellHashClone.params) {
                const vValue = oShellHashClone.params[sKey];
                if (!Array.isArray(vValue)) {
                    oShellHashClone.params[sKey] = [vValue];
                }
            }
        } else {
            oShellHashClone.params = {};
        }

        if (oShellHashClone.appStateKey) {
            oShellHashClone.params["sap-xapp-state"] = [oShellHashClone.appStateKey];
        }

        // strip unallowed properties
        const aAllowedProperties = [
            "semanticObject",
            "action",
            "contextRaw",
            "params",
            "appSpecificRoute"
        ];
        for (const sKey in oShellHashClone) {
            if (!aAllowedProperties.includes(sKey)) {
                delete oShellHashClone[sKey];
            }
        }

        return oShellHashClone;
    };

    /**
     * compose a shell Hash from it's respective parts
     * Note that it also may append an app specific route !
     * <pre>
     *   {
     *     target: {
     *       semanticObject: string,
     *       action: string,
     *       contextRaw: string
     *     },
     *     params: MapObject<String,Array[String]>,
     *     appStateKey: string
     *     appSpecificRoute: string
     *   }
     * </pre>
     * <code>{ target: { shellHash } }</code>
     * Note: in general it is preferred to add an appStateKey directly to the params object
     *
     * @param {sap.ushell.services.URLParsing.ShellHash} oShellHash
     *   The action must be a valid action, it may not contain "?" or directly a parameter string
     * @param {boolean} [bPrefixWithHash=false] if true, the result is prefixed with a leading #
     * @returns {string} the hash part of an URL, e.g. <code>"Object-name~AFE2==?PV1=PV2&PV4=V5&/display/detail/7?UU=HH</code>
     *   returns "" for an <code>undefined</code> object
     *
     * @since 1.94.0
     * @protected
     */
    UrlParsing.constructShellHash = function (oShellHash, bPrefixWithHash) {
        const oParsedHash = this._harmonizeShellHash(oShellHash);

        if (!oParsedHash.semanticObject || !oParsedHash.action) {
            return prependIf(
                bPrefixWithHash ? "#" : "",
                appendIf("", oParsedHash.appSpecificRoute)
            );
        }

        let sConstructedHash = `${oParsedHash.semanticObject}-${oParsedHash.action.replace(/[?].*/, "")}`;
        if (oParsedHash.contextRaw) {
            sConstructedHash += `~${oParsedHash.contextRaw}`;
        }

        let sParamPrefix = "?";
        const aParameterKeys = Object.keys(oParsedHash.params).sort();
        for (const sParamKey of aParameterKeys) {
            const aParamValues = oParsedHash.params[sParamKey];
            if (aParamValues.length > 1) {
                Log.error("Array startup parameters violate the designed intent of the Unified Shell Intent, use only single-valued parameters!");
            }
            for (const sParamValue of aParamValues) {
                sConstructedHash += `${sParamPrefix + encodeURIComponent(sParamKey)}=${encodeURIComponent(sParamValue)}`;
                sParamPrefix = "&";
            }
        }

        return prependIf(
            bPrefixWithHash ? "#" : "",
            appendIf(sConstructedHash, oParsedHash.appSpecificRoute)
        );
    };

    /**
     * Calculates the stable intent part of the hash
     *
     * Remark: It also sorts the parameters and their values. This can change the interpretation of the hash.
     * Therefore, the stable hash should only be used for comparison or id generation, not for navigation.
     *
     * @param {sap.ushell.services.URLParsing.ShellHash} oShellHash The parsed shell hash
     * @param {boolean} [bPrefixWithHash=false] if true, the result is prefixed with a leading #
     * @returns {string} the stable intent part of the hash
     *
     * @since 1.135.0
     * @private
     */
    UrlParsing.constructStableHash = function (oShellHash, bPrefixWithHash) {
        const oParsedHash = this._harmonizeShellHash(oShellHash);
        const oParamsWithoutTechnicalParameters = this._removeNonIntentParameters(oParsedHash.params);
        // stabilize the "order" of the parameters and values to make the hash comparison stable
        const oSortedParams = Object.keys(oParamsWithoutTechnicalParameters)
            .sort()
            .reduce((oResult, sKey) => {
                const aValue = oParamsWithoutTechnicalParameters[sKey];

                aValue.sort();
                oResult[sKey] = aValue;
                return oResult;
            }, {});

        return this.constructShellHash({
            semanticObject: oParsedHash.semanticObject,
            action: oParsedHash.action,
            contextRaw: oParsedHash.contextRaw,
            params: oSortedParams
            // ignore appSpecificRoute
        }, bPrefixWithHash);
    };

    /**
     * Reduces a hash to the semantic object and action part.
     *
     * @param {string} sShellHash The shell hash, with or without the leading #
     * @param {boolean} [bPrefixWithHash=false] if true, the result is prefixed with a leading #
     * @returns {string} The basic hash.
     *
     * @since 1.129.0
     * @private
     */
    UrlParsing.getBasicHash = function (sShellHash, bPrefixWithHash) {
        const oParsedHash = this.parseShellHash(sShellHash) || this.getEmptyParsedHash();
        return this.constructBasicHash(oParsedHash, bPrefixWithHash);
    };

    /**
     * Reduces a hash to the semantic object and action part.
     *
     * @param {sap.ushell.services.URLParsing.ShellHash} oShellHash The parsed shell hash
     * @param {boolean} [bPrefixWithHash=false] if true, the result is prefixed with a leading #
     * @returns {string} The basic hash.
     *
     * @since 1.129.0
     * @private
     */
    UrlParsing.constructBasicHash = function (oShellHash, bPrefixWithHash) {
        const oParsedHash = this._harmonizeShellHash(oShellHash);

        return this.constructShellHash({
            semanticObject: oParsedHash.semanticObject,
            action: oParsedHash.action
            // ignore contextRaw
            // ignore params
            // ignore appSpecificRoute
        }, bPrefixWithHash);
    };

    /**
     * Calculates the similarities of the hashes
     *
     * @param {sap.ushell.services.Navigation.TargetIntent} sHash1 The first hash
     * @param {sap.ushell.services.Navigation.TargetIntent} sHash2 The second hash
     * @returns {sap.ushell.base.UrlParsing.HashComparison} A object describing the similarities
     *
     * @since 1.128.0
     * @private
     */
    UrlParsing.compareHashes = function (sHash1, sHash2) {
        const oHash1 = this.parseShellHash(sHash1) || {};
        const oHash2 = this.parseShellHash(sHash2) || {};

        return this.compareParsedHashes(oHash1, oHash2);
    };

    /**
     * Calculates the similarities of the hashes
     *
     * @param {sap.ushell.services.URLParsing.DecomposedHash} oHash1 The first hash
     * @param {sap.ushell.services.URLParsing.DecomposedHash} oHash2 The second hash
     * @returns {sap.ushell.base.UrlParsing.HashComparison} A object describing the similarities
     *
     * @since 1.142.0
     * @private
     */
    UrlParsing.compareParsedHashes = function (oHash1, oHash2) {
        return {
            sameIntent: this.haveSameIntent(oHash1, oHash2),
            sameParameters: this.haveSameIntentParameters(oHash1, oHash2),
            sameAppSpecificRoute: oHash1.appSpecificRoute === oHash2.appSpecificRoute
        };
    };

    /**
     * Determines whether two hash fragments have the same intent during an
     * app-to-app navigation.
     *
     * @param {sap.ushell.services.URLParsing.DecomposedHash} oParsedHash1 The first hash split by parseShellHash
     * @param {sap.ushell.services.URLParsing.DecomposedHash} oParsedHash2 The second hash split by parseShellHash
     *
     * @returns {boolean}
     *   Whether two hash fragments have the same semantic object and action
     *   during an app-to-app navigation.
     *
     * @since 1.128.0
     * @private
     */
    UrlParsing.haveSameIntent = function (oParsedHash1, oParsedHash2) {
        oParsedHash1 = oParsedHash1 || {};
        oParsedHash2 = oParsedHash2 || {};
        return oParsedHash1.semanticObject === oParsedHash2.semanticObject
            && oParsedHash1.action === oParsedHash2.action
            && oParsedHash1.contextRaw === oParsedHash2.contextRaw;
    };

    /**
     * Determines whether two sets of parameters have the same intent parameters.
     *
     * @param {sap.ushell.services.URLParsing.DecomposedHash} oParsedHash1 The first hash split by parseShellHash
     * @param {sap.ushell.services.URLParsing.DecomposedHash} oParsedHash2 The second hash split by parseShellHash
     * @returns {boolean} Whether the two given set of parameters have the same intent parameters.
     *
     * @since 1.128.0
     * @private
     */
    UrlParsing.haveSameIntentParameters = function (oParsedHash1, oParsedHash2) {
        oParsedHash1 = oParsedHash1 || {};
        oParsedHash2 = oParsedHash2 || {};
        const oParams1Copy = merge({}, oParsedHash1.params);
        const oParams2Copy = merge({}, oParsedHash2.params);
        return !this._parametersChanged(
            this._removeNonIntentParameters(oParams1Copy),
            this._removeNonIntentParameters(oParams2Copy)
        );
    };

    /**
     * Removes all technical parameters from the hash parameters that are not intent parameters
     * and returns the new hash parameters
     *
     * @param {Object<string, string[]>} oHashParams the hash parameters that should be checked for non-intent parameters
     * @returns {Object<string, string[]>} the given hash parameters with the non-intent parameters
     *
     * @since 1.128.0
     * @private
     */
    UrlParsing._removeNonIntentParameters = function (oHashParams) {
        const aTechnicalParameters = TechnicalParameters.getParameters({ isIntentParameter: false });
        let oTechnicalParam;

        if (oHashParams) {
            for (let i = 0; i < aTechnicalParameters.length; i++) {
                oTechnicalParam = aTechnicalParameters[i];
                if (oHashParams[oTechnicalParam.name]) {
                    delete oHashParams[oTechnicalParam.name];
                }
            }
        }

        return oHashParams || {};
    };

    /**
     * Checks whether shell hash parameters have changed
     *
     * @param {Object<string, string[]>} oNewParameters the new parameters
     * @param {Object<string, string[]>} oOldParameters the old parameters
     * @returns {boolean} <code>true</code> if oNewParameters are not equal to oOldParameters
     *
     * @since 1.128.0
     * @private
     */
    UrlParsing._parametersChanged = function (oNewParameters, oOldParameters) {
        return !deepEqual(oNewParameters, oOldParameters);
    };

    /**
     * Checks whether the given shell hash is empty.
     * @param {sap.ushell.services.URLParsing.ShellHash} oShellHash The shell hash to check
     * @returns {boolean} Whether the shell hash is empty
     *
     * @since 1.142.0
     * @private
     */
    UrlParsing.isEmptyHash = function (oShellHash) {
        return this.constructShellHash(oShellHash) === "";
    };

    return UrlParsing;
});
