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

/**
 * @fileoverview This file contains the ApplicationContainerCache class.
 * It manages active and reusable application containers.
 */
sap.ui.define([
    "sap/base/Log",
    "sap/ushell/utils"
], (
    Log,
    ushellUtils
) => {
    "use strict";

    /**
     * @alias sap.ushell.appIntegration.ApplicationContainerCache
     * @class
     * @classdesc Manages a cache of application containers.
     * Tracks active containers and maintains a pool of reusable containers.
     *
     * @private
     */
    class ApplicationContainerCache {
        #aActiveContainers = [];
        #aReusableContainers = {};

        /**
         * Creates a new ApplicationContainerCache instance.
         *
         * @private
         */
        constructor () {
            this.init();
        }

        /**
         * Initializes or resets the container cache.
         *
         * @private
         */
        init () {
            // any active application containers
            this.#aActiveContainers = [];
            // containers that are ready for reuse
            // ... also contains appruntime
            this.#aReusableContainers = [];
        }

        /**
         * Marks a container as active.
         * @param {sap.ushell.appIntegration.ApplicationContainer} oApplicationContainer The container to activate.
         *
         * @private
         */
        setContainerActive (oApplicationContainer) {
            if (!oApplicationContainer) {
                return;
            }

            if (this.#aActiveContainers.includes(oApplicationContainer)) {
                Log.warning("The container is already active");
                return;
            }

            this.removeByContainer(oApplicationContainer);

            this.#aActiveContainers.push(oApplicationContainer);
        }

        /**
         * Marks a container as ready for reuse and stores it in the pool.
         * @param {sap.ushell.appIntegration.ApplicationContainer} oApplicationContainer The container to store.
         * @param {string} sUrl The URL associated with the container.
         *
         * @private
         */
        setContainerReadyForReuse (oApplicationContainer, sUrl) {
            if (!oApplicationContainer) {
                return;
            }

            if (!this.#aActiveContainers.includes(oApplicationContainer) && this.getById(oApplicationContainer.getId())) {
                Log.warning("The container is already in pool");
                return;
            }

            this.removeByContainer(oApplicationContainer);

            this.#aReusableContainers.push({
                url: sUrl,
                key: this.#getKeyFromUrl(sUrl),
                container: oApplicationContainer
            });
        }

        /**
         * Finds a free container by URL from the reusable pool.
         * @param {string} sUrl The URL to match.
         * @returns {sap.ushell.appIntegration.ApplicationContainer|undefined} The matching container or undefined.
         *
         * @private
         */
        findFreeContainerByUrl (sUrl) {
            const sKey = this.#getKeyFromUrl(sUrl);

            // use latest free container
            const oFreeContainer = this.#aReusableContainers.reverse().find((oContainer) => {
                return oContainer.key === sKey;
            });

            if (oFreeContainer) {
                return oFreeContainer.container;
            }
        }

        /**
         * Checks if a container can be reused for a given URL.
         * @param {sap.ushell.appIntegration.ApplicationContainer} [oApplicationContainer] The container to check.
         * @param {string} sUrl The URL to match.
         * @returns {boolean} True if the container can be reused.
         *
         * @private
         */
        canBeReused (oApplicationContainer, sUrl) {
            if (!oApplicationContainer) {
                return false;
            }

            if (this.#aActiveContainers.includes(oApplicationContainer)) {
                return false;
            }

            const sKey = this.#getKeyFromUrl(sUrl);
            const oEntry = this.#aReusableContainers.find((oContainer) => {
                return oContainer.container === oApplicationContainer;
            });

            return !!oEntry && oEntry.key === sKey;
        }

        /**
         * Returns the number of active containers.
         * @returns {int} The number of active containers.
         *
         * @private
         */
        getLength () {
            return this.#aActiveContainers.length;
        }

        /**
         * Returns the number of reusable containers in the pool.
         * @returns {int} The number of reusable containers.
         *
         * @private
         */
        getPoolLength () {
            return this.#aReusableContainers.length;
        }

        /**
         * Removes a container from both the active and reusable lists.
         * @param {sap.ushell.appIntegration.ApplicationContainer} [oApplicationContainer] The container to remove.
         *
         * @private
         */
        removeByContainer (oApplicationContainer) {
            if (!oApplicationContainer) {
                return;
            }

            this.#aActiveContainers = this.#aActiveContainers.filter((oContainer) => {
                return oContainer !== oApplicationContainer;
            });

            this.#aReusableContainers = this.#aReusableContainers.filter((oContainer) => {
                return oContainer.container !== oApplicationContainer;
            });
        }

        /**
         * Retrieves a container by its ID.
         * @param {string} sId The container ID.
         * @returns {sap.ushell.appIntegration.ApplicationContainer|undefined} The matching container or undefined.
         *
         * @private
         */
        getById (sId) {
            let oApplicationContainerFound;
            this.forEach((oApplicationContainer) => {
                if (oApplicationContainer.getId() === sId) {
                    oApplicationContainerFound = oApplicationContainer;
                }
            });

            return oApplicationContainerFound;
        }

        /**
         * Returns all containers (both active and reusable).
         * @returns {sap.ushell.appIntegration.ApplicationContainer[]} Array of all containers.
         *
         * @private
         */
        getAll () {
            return [
                ...this.#aActiveContainers,
                ...this.#aReusableContainers.map((oEntry) => oEntry.container)
            ];
        }

        /**
         * Iterates over all containers and calls the callback for each.
         * @param {function(sap.ushell.appIntegration.ApplicationContainer):Promise|undefined} fnCallback The callback to execute.
         * @returns {Promise} A promise that settles when all callbacks complete.
         *
         * @private
         */
        forEach (fnCallback) {
            const aPromises = [];
            for (const oApplicationContainer of this.#aActiveContainers) {
                aPromises.push(fnCallback(oApplicationContainer));
            }

            for (const oEntry of this.#aReusableContainers) {
                aPromises.push(fnCallback(oEntry.container));
            }

            return Promise.allSettled(aPromises);
        }

        /**
         * Builds a key of a reusable iframe that is not destroyed and can be reused.
         * When a new application is opened, and it is hosted in an iframe, in order to
         * check if an iframe for this app already exists, we will build the iframe key based
         * on hard coded keys (The keys are always taken from the URL parameters).
         *
         * The structure of the string key:
         * [url-origin]@hint:[sap-iframe-hint]@uiver:[sap-ui-version]@
         *
         * Example of an iframe key:
         * http://www.test.com@hint:ABC@uiver:1.84.0@async:false@ka:[sap-keep-alive]@async:sap-async-loading@fesr:[sap-calm]@testid:[sap-testcflp-iframeid]
         *
         * NOTE: reusable iframes are kept in in "oCacheStorage" declared above.
         * @param {string} sUrl application url
         * @returns {string} iframe key.
         *
         * @private
         */
        #getKeyFromUrl (sUrl) {
            let sIframeHint = "";
            let sUI5Version = "";
            let sUI5Async = "";
            let sFESR = "";
            let sTestUniqueId = "";

            const oURL = new URL(sUrl || "", ushellUtils.getLocationHref());

            const oSearchParams = oURL.searchParams;
            if (oSearchParams.has("sap-iframe-hint")) {
                sIframeHint = `@hint:${oSearchParams.get("sap-iframe-hint")}`;
            }
            if (oSearchParams.has("sap-ui-version")) {
                sUI5Version = `@uiver:${oSearchParams.get("sap-ui-version")}`;
            }
            if (oSearchParams.has("sap-async-loading") && oSearchParams.get("sap-async-loading") !== "true") {
                sUI5Async = `@async:${oSearchParams.get("sap-async-loading")}`;
            }
            if (oSearchParams.has("sap-enable-fesr") && oSearchParams.get("sap-enable-fesr") !== "false") {
                sFESR = `@fesr:${oSearchParams.get("sap-enable-fesr")}`;
            }
            if (oSearchParams.has("sap-testcflp-iframeid")) {
                sTestUniqueId = `@testid:${oSearchParams.get("sap-testcflp-iframeid")}`;
            }

            return this.#getUrlKeyPart(oURL) + sIframeHint + sUI5Version + sUI5Async + sFESR + sTestUniqueId;
        }

        /**
         * Returns the URL part of the container key:
         * - Only the origin for classic UI frameworks
         * - Origin and pathname for everything else
         *
         * Note: This method does not implement special logic to handle URL path variations
         * that might resolve to the same resource on the web server (e.g., 'same/path',
         * 'same/path/', 'same/path/index.html'). The assumption is that applications use
         * consistent URL templates, so such variations should not occur in practice.
         * @param {URL} oURL The URL object of the application URL
         * @returns {string} The URL key part
         *
         * @private
         */
        #getUrlKeyPart (oURL) {
            let sOrigin = oURL.origin;
            if (oURL.port === "") {
                if (oURL.protocol === "https:") {
                    sOrigin = `${sOrigin}:443`;
                } else if (oURL.protocol === "http:") {
                    sOrigin = `${sOrigin}:80`;
                }
            }

            const aClassicUiFrameworkHints = [
                "GUI",
                "WDA",
                "NWBC",
                "WCF"
            ];
            const bIsClassicUiFramework = aClassicUiFrameworkHints.includes(oURL.searchParams.get("sap-iframe-hint"));
            if (bIsClassicUiFramework) {
                return sOrigin;
            }

            return sOrigin + oURL.pathname;
        }

        /**
         * For Debugging purposes only.
         * @returns {object} The current state of the cache including active and reusable containers.
         *
         * @private
         */
        _getStorageForDebug () {
            return {
                activeContainers: this.#aActiveContainers,
                reusableContainers: this.#aReusableContainers
            };
        }

        /**
         * For testing purposes only.
         * Destroys all the application containers.
         *
         * @since 1.130.0
         * @private
         */
        destroyAllContainers () {
            this.forEach((oApplicationContainer) => {
                this.removeByContainer(oApplicationContainer);
                oApplicationContainer.destroy();
            });
        }
    }

    return new ApplicationContainerCache();
});
