Source: API.js

(function() {
    /**
     * @typedef {Object} Help4.API.LinkTileParams
     * @property {Help4.control2.Lightbox.Sizing} sizing
     * @property {Help4.control2.SizeWH} [size] - size is required if sizing is explicit or explicitFull
     * @property {string} [contentUrl] - content or contentUrl is required
     * @property {string} [content] - content or contentUrl is required
     * @property {string} title
     * @property {string} [summaryText]
     * @property {string} [id]
     */

    /**
     * @typedef {Object} Help4.API.HelpTileParams
     * @property {string} summaryText
     * @property {string} content
     * @property {string} title
     * @property {string} [id]
     * @property {Help4.typedef.BubbleAnimationType} [bubbleAnimationType]
     * @property {Help4.control2.PositionLeftTop} [bubbleOffset]
     * @property {Help4.typedef.BubbleOrientation} [bubbleOrientation]
     * @property {string} [bubbleSize]
     * @property {string} [hotspotAnchor]
     * @property {string} [hotspotAnimationType]
     * @property {boolean} [hotspotCentered]
     * @property {Help4.typedef.HotspotIconPosition} [hotspotIconPos]
     * @property {Help4.typedef.HotspotIconType} [hotspotIconType]
     * @property {Help4.control2.PositionLeftTop} [hotspotManualOffset]
     * @property {string} [hotspotSize]
     * @property {string} [hotspotStyle]
     * @property {Help4.control2.SizeWidthHeight} [hotspotRectDelta]
     * @property {string} [hotspotTrianglePos]
     * @property {boolean} [alignWithText]
     * @property {boolean} [showTitleBar]
     * @property {Help4.typedef.TileIcons} [tileIcon]
     */

    /**
     * @typedef {Object} Help4.API.OpenParams
     * @property {Help4.control2.PositionXY|Help4.control2.PositionLeftTop} [position]
     * @property {boolean} [showCarousel = false]
     */

    /**
     * @typedef {Object} Help4.API.TypeProjectInfo
     * @property {Object[]} standard - standard content
     * @property {Object[]} whatsnew - what's new content
     */

    /**
     * @typedef {Object} Help4.API.TypeProjectList
     * @property {Help4.API.TypeProjectInfo} published - the published data
     * @property {Help4.API.TypeProjectInfo} head - the non-published data
     */

    /**
     * @typedef {Object} Help4.API.RemoteControlParams
     * @property {string} type
     * @property {Object} [data]
     * @property {Object} [params]
     * @property {boolean} [visible]
     */

    /** for external use; playback mode only! */
    Help4.API = class {
        /**
         * will open/close the SAP Companion based on its current status
         * @param {Help4.API.OpenParams} [params] - CMP4 only
         * @returns {boolean} - API command executed successfully
         */
        static toggle(params) {
            return this.toggleWA(params);
        }

        /**
         * will open the SAP Companion
         * @param {Help4.API.OpenParams} [params]
         * @returns {boolean} - API command executed successfully
         */
        static open(params) {
            return this.openWA(true, params);
        }

        /**
         * will close the SAP Companion
         * @returns {boolean} - API command executed successfully
         */
        static close() {
            return this.openWA(false);
        }

        /**
         * will open/close the SAP Companion based on its current status
         * @deprecated - please use Help4.API.toggle instead
         * @param {Help4.API.OpenParams} [params] - CMP4 only
         * @returns {boolean} - API command executed successfully
         */
        static toggleWA(params) {
            return this.openWA('toggle', params);
        }

        /**
         * will open/close the SAP Companion based on a parameter
         * @deprecated - please use Help4.API.open instead
         * @param {boolean|'toggle'} [open = true]
         * @param {Help4.API.OpenParams} [params = {}]
         * @returns {boolean} - API command executed successfully
         */
        static openWA(open = true, {showCarousel = false, position} = {}) {
            const controller = Help4.getController();
            const {isEditMode = true, isOpen, CMP4} = controller?.getConfiguration() || {};
            if (isEditMode) return false;

            const handleCmp4Position = () => {
                if ((showCarousel || position) && controller.isMinimized()) {
                    controller.onMinimize(false);
                }

                let {x, y, top, left} = position || {};
                x ??= left;
                y ??= top;
                if (typeof x === 'number' && typeof y === 'number') {
                    const handler = /** @type {Help4.controller.CMP4} */ controller.getCmp4Handler();
                    const {/** @type {Help4.control2.bubble.Panel} */ panel} = handler;
                    panel.docked = false;
                    panel.setPosition(x, y);
                }
            }

            if (open === 'toggle') open = !isOpen;

            if (Boolean(open)) {
                isOpen || controller.toggle(() => {
                    CMP4
                        ? handleCmp4Position()
                        : showCarousel && this.showCarousel(showCarousel);
                });
            } else if (isOpen) {
                controller.close();
            }

            return true;
        }

        /**
         * will wait until SAP Companion is ready and returns boolean
         * @returns {Promise<void>} - will only return if SAP Companion is ready
         */
        static waitCompanionReady() {
            return new Help4.Promise(resolve => {
                const checkCMP = () => {
                    const {STATUS: {done}} = Help4.StartStatus;
                    const statusService = Help4.getController()?.getService('startStatus');
                    statusService?.has(done)
                        ? resolve()
                        : setTimeout(checkCMP, 250);
                }
                checkCMP();
            });
        }

        /**
         * activate the whatsnew mode if available
         * @deprecated - only for internal use; will become private
         * @param {boolean} enable - enable whats new mode
         * @returns {boolean} - API command executed successfully
         */
        static enableWhatsNew(enable) {
            const controller = Help4.getController();
            const {isEditMode = true, hasWhatsNew, isOpen, CMP4} = controller?.getConfiguration() || {};
            if (isEditMode || !isOpen) return false;

            if (CMP4) {
                const instance = /** @type {?Help4.widget.whatsnew.Widget} */ Help4.widget.getInstance('whatsnew');
                if (enable) {
                    return instance && !instance.isActive()
                        ? void instance.activate() || true
                        : false;
                } else {
                    return instance?.isActive()
                        ? void instance.deactivate() || true
                        : false;
                }
            } else {
                if (!hasWhatsNew) return false;

                const h = controller.getHandler();
                if (h) {
                    h.setCarouselTab({
                        tab: enable ? Help4.CAROUSEL_MODES.wn_help : Help4.CAROUSEL_MODES.help,
                        reason: 'API'
                    });
                }
                return !!h;
            }
        }

        /**
         * starts a specific project: help, whatsnew or tour projects are possible
         * @param {string} pidOrAlias - project ID or alias
         * @param {boolean|'all'} [published = false] - show only published, only not-published or all
         * @returns {boolean} - API command executed successfully
         */
        static startProject(pidOrAlias, published = false) {
            const controller = Help4.getController();
            const config = controller?.getConfiguration() || {};
            const {isEditMode = true, isOpen, CMP4} = config;
            if (isEditMode) return false;

            let projects = Help4.API.getProjects();
            if (published === 'all') {
                const {head: {standard: hs, whatsnew: hw}, published: {standard: ps, whatsnew: pw}} = projects;
                projects = {standard: hs.concat(ps), whatsnew: hw.concat(pw)};
            } else {
                projects = projects[published ? 'published' : 'head'];
            }

            let type = 'standard';
            let project = projects.standard.find(p => p.alias === pidOrAlias || p.id === pidOrAlias);
            if (!project) {
                project = projects.whatsnew.find(p => p.alias === pidOrAlias || p.id === pidOrAlias);
                if (!project) return false;

                type = 'whatsnew';
            }

            const mode = project.contextType.toLowerCase();
            if (mode === 'tour') {
                if (CMP4) {
                    const {
                        companionCore: {Core},
                        tourlist: {Widget: TourlistWidget}
                    } = Help4.widget;

                    const catalogueKey = Core.getCatalogueKey({configuration: config});
                    const {id: projectId, _catalogueType: catalogueType, _dataType: dataType} = project;
                    TourlistWidget.startTour(config, {projectId, catalogueKey, catalogueType, dataType, whatsnew: type === 'whatsnew'});
                    return true;
                } else {
                    const params = {dataId: project.id, isWhatsNew: type === 'whatsnew', forceHandler: true};
                    const handler = controller.getHandler();
                    if (!isOpen || handler?.isMinimized()) params.autoTour = 'API';
                    handler?.close();  // XRAY-5134 - allows to start new tour project while an existing tour is being played
                    controller.changeHandler(Help4.controller.MODES[mode], params);
                    return true;
                }
            }

            // will be help or whatsnew
            this.enableWhatsNew(type === 'whatsnew');
            return true;
        }

        /**
         * selects a help tile within the current help project
         * will open the appropriate bubble and highlight the tile within carousel
         * @deprecated - will be removed
         * @param {string} tileId - tile ID
         * @returns {boolean} - API command executed successfully
         */
        static selectHelpTile(tileId) {
            const controller = Help4.getController();
            const config = controller?.getConfiguration() || {};
            const {isEditMode = true, isHelpMode, isOpen, CMP4} = config;
            if (isEditMode || !isHelpMode || !isOpen) return false;

            if (CMP4) {
                const instance = /** @type {?Help4.widget.Widget} */ Help4.widget.getActiveInstance();
                const name = instance?.getName();
                if (name !== 'help' && name !== 'whatsnew') return false;

                const tile = _getTileCMP4(instance, tileId);
                if (!tile) return false;

                controller.getPanel().minimized = false;
                (/** @type {Help4.widget.help.Widget|Help4.widget.whatsnew.Widget} */ instance).selectTile(tile);
            } else {
                const h = controller.getHandler();
                let t = h && h.getCarouselTab();
                if (t !== Help4.CAROUSEL_MODES.help && t !== Help4.CAROUSEL_MODES.wn_help) return false;

                if (!(t = _getTileCMP3(tileId))) return false;

                h.minimize(false);
                t = t.getMetadata('tileId');
                controller.getEngine('carouselSelection').selectTile(t);
            }

            return true;
        }

        /**
         * shows the help bubble for a specific tile within the current help project
         * will not update the carousel
         * if in remote mode, shows help bubble; API version 2.0
         * @deprecated - only to be used for DA/WA; will become private
         * @param {string} param - tile ID
         * @returns {boolean} - API command executed successfully
         */
        // @param {Object} param - tile describing object, only for remote mode
        static showHelpBubble(param) {
            const c = Help4.getController();

            // send to remoteController if remoteControl engine is started
            const rc = c?.getEngine('remoteControl');
            const po = typeof param === 'object';
            if (rc?.isStarted() && po) return _callRemoteControlEngine({type: 'showHelpBubble', data: param});

            const {isEditMode = true, isHelpMode, isOpen, CMP4} = c?.getConfiguration() || {};
            if (isEditMode || !isHelpMode || !isOpen || po) return false;

            if (CMP4) {
                return this.selectHelpTile(param);
            } else {
                const tile = _getTileCMP3(param);
                if (tile && tile.getMetadata('tileType') === 'help') {
                    const h = c.getHandler();
                    h.clean('+bu');
                    // override auto hide mechanics of help bubbles in case they have no connection point
                    h.getBubbles().openHelp(tile.getMetadata('tileId'), {isAPI: true});
                    return true;
                }
            }

            return false;
        }

        /**
         * selects a tour step with a current tour project
         * @param {number|string} sid - step number or step ID
         * @returns {Promise<boolean>} - API command executed successfully
         */
        static async selectTourStep(sid) {
            const controller = Help4.getController();
            const {isEditMode = true, isTourMode, isOpen, CMP4} = controller?.getConfiguration() || {};
            if (isEditMode || !isOpen) return false;

            if (CMP4) {
                const instance = /** @type {?Help4.widget.Widget} */ Help4.widget.getActiveInstance();
                if (instance?.getName() !== 'tour') return false;

                const {widget: {tour: {
                    /** @type {?Help4.widget.tour.View} */ view,
                }}} = /** @type {Help4.widget.tour.Widget.Context} */ instance.getContext();

                if (typeof sid !== 'number') sid = view.getTileIndex(sid);
                return await view.showStepAt(sid);
            } else {
                if (!isTourMode) return false;

                controller.getHandler().setStep(sid);
                return true;
            }
        }

        /**
         * selects the next tour step with a current tour project
         * @returns {Promise<boolean>} - API command executed successfully
         */
        static async tourNextStep() {
            const controller = Help4.getController();
            const {isEditMode = true, isTourMode, isOpen, CMP4} = controller?.getConfiguration() || {};
            if (isEditMode || !isOpen) return false;

            if (CMP4) {
                const instance = /** @type {?Help4.widget.Widget} */ Help4.widget.getActiveInstance();
                if (instance?.getName() !== 'tour') return false;

                const {widget: {tour: {
                    /** @type {?Help4.widget.tour.View} */ view,
                }}} = /** @type {Help4.widget.tour.Widget.Context} */ instance.getContext();

                return await view.nextStep();
            } else {
                if (!isTourMode) return false;

                return controller.getHandler().nextStep();
            }
        }

        /**
         * selects the previous tour step with a current tour project
         * @returns {Promise<boolean>} - API command executed successfully
         */
        static async tourPrevStep() {
            const controller = Help4.getController();
            const {isEditMode = true, isTourMode, isOpen = false, CMP4} = controller?.getConfiguration() || {};
            if (isEditMode || !isOpen) return false;

            if (CMP4) {
                const instance = /** @type {?Help4.widget.Widget} */ Help4.widget.getActiveInstance();
                if (instance?.getName() !== 'tour') return false;

                const {widget: {tour: {
                    /** @type {?Help4.widget.tour.View} */ view,
                }}} = /** @type {Help4.widget.tour.Widget.Context} */ instance.getContext();

                return await view.prevStep();
            } else {
                if (!isTourMode) return false;

                return controller.getHandler().prevStep();
            }
        }

        /**
         * will open the learning app
         * @param {boolean} external - open the panel learning tab or the external learning app
         */
        static openLearningApp(external) {
            const controller = Help4.getController();
            const {isEditMode = true, hasLearningApp, isOpen, CMP4} = controller?.getConfiguration() || {};
            if (isEditMode) return;

            const f = async () => {
                if (CMP4) {
                    // problem: when tour is playing, on deactivate either tourlist or whatsnew widgets
                    // will auto-activate; so we deactivate tour widget manually, wait some time
                    // for tourlist/whatsnew to activate and activate learning afterwards

                    const instance = /** @type {?Help4.widget.Widget} */ Help4.widget.getActiveInstance();
                    if (instance?.getName() === 'tour') await instance.deactivate();

                    // in tourlist mode wait for the tourlist to activate
                    setTimeout(() => {
                        const widget = /** @type {?Help4.widget.learning.Widget} */ Help4.widget.getInstance('learning');
                        widget?.openLearningCenter(external);
                    }, 100);
                } else {
                    const handler = controller.getHandler();
                    if (external) {
                        handler?.openLearningApp();
                    } else if (hasLearningApp) {
                        handler?.setCarouselTab({tab: Help4.CAROUSEL_MODES.learning, reason: 'API'});
                    }
                }
            };

            isOpen ? f() : controller.open(f);
        }

        /**
         * will change the current theme
         * @param {string} theme - e.g. default, hcb, light
         * @returns {boolean} - API command executed successfully
         */
        static setTheme(theme) {
            const shell = Help4.getShell();
            shell?.setTheme(Help4.THEMES[theme]);
            return !!shell;
        }

        /**
         * set the current screen id / app name; alias for setScreen
         * @param {string} screenId - appName or screenId
         * @returns {boolean} - API command executed successfully
         */
        static setAppName(screenId) {
            const shell = Help4.getShell();
            shell?.setAppName(screenId);
            return !!shell;
        }

        /**
         * set the current screen id / app name; alias for Help4.API.setAppName
         * @param {string} screenId - appName or screenId
         * @returns {boolean} - API command executed successfully
         */
        static setScreen(screenId) {
            return this.setAppName(screenId);
        }

        /**
         * sets or unsets a subscreen id
         * @param {string|null} subscreenId - subscreen ID, null for removing
         * @returns {boolean} - API command executed successfully
         */
        static setSubScreen(subscreenId) {
            const shell = Help4.getShell();
            if (!shell) return false;

            let sid = (shell.getScreenId() || '').split(':');
            sid = sid[0] + (subscreenId ? ':' + subscreenId : '');
            shell.setAppName(sid);
            return true;
        }

        /**
         * delivers the SAP Companion configuration
         * @param {?string} [key = null] - a key to access a certain detail
         * @returns {Help4.typedef.SystemConfiguration|null|*} - configuration object or value for key
         */
        static getContext(key = null) {
            let controller = Help4.getController();
            if (!controller) return null;

            const config = controller.getConfiguration();
            return typeof key === 'string' ? config[key] : config;
        }

        /**
         * delivers a list of all available projects for the current screen
         * @returns {?Help4.API.TypeProjectList}
         */
        static getProjects() {
            const controller = Help4.getController();
            const {CMP4} = controller?.getConfiguration() || {};

            if (CMP4) {
                const widget = /** @type {?Help4.widget.help.Widget} */ Help4.widget.getInstance('help');
                const {/** @type {?Help4.widget.help.Data} */ data} = widget?.getContext().widget.help || {};
                const {/** @type {?Help4.widget.help.Catalogues} */ catalogues} = data || {};

                const filterData = (whatsNew, published) => {
                    /** @type {Help4.widget.help.CatalogueProject[]} */
                    const projects =  catalogues?.[published ? 'pub' : 'head'].projects || [];
                    return projects.filter(({_whatsnew}) => whatsNew ? !!_whatsnew : !_whatsnew);
                }

                return {
                    published: {standard: filterData(false, true), whatsnew: filterData(true, true)},
                    head: {standard: filterData(false, false), whatsnew: filterData(true, false)}
                }
            } else {
                const m = controller?.getService('model');
                if (!m) return null;

                const d = {1: {1: [], 0: []}, 0: {1: [], 0: []}};
                const a = [{p: 0, w: 0}, {p: 0, w: 1}, {p: 1, w: 0}, {p: 1, w: 1}];
                for (let i = 0, p, h, t, c; c = a[i++];) {
                    p = {published: Boolean(c.p), whatsNew: Boolean(c.w)};
                    h = m.getScreenHelp(p);
                    t = m.getScreenTours(p);

                    if (h) d[c.p][c.w].push(h);
                    if (t.length) d[c.p][c.w] = d[c.p][c.w].concat(t);
                }

                return {
                    published: {standard: d[1][0], whatsnew: d[1][1]},
                    head: {standard: d[0][0], whatsnew: d[0][1]}
                };
            }
        }

        /**
         * sets the value for profile parameter in UACP
         * @param {string} profile - the feature profile name
         * @returns {boolean} - API command executed successfully
         */
        static setFeatureProfileUACP(profile) {
            return Help4.getController()?.setFeatureProfileUACP(profile) || false;
        }

        /**
         * gets the value for profile parameter in UACP
         * @returns {?string} - the feature profile name
         */
        static getFeatureProfileUACP() {
            return Help4.getController()?.getConfiguration().featureProfileUACP || null;
        }

        /**
         * sets the key and value for placeholders
         * @param {string|Object} key - string or {"key1": "value1", "key2": "value", ...}
         * @param {string} [value]
         */
        static setPlaceholder(key, value) {
            Help4.Placeholder.add(key, value);
        }

        /**
         * sets SAP Companion language
         * @param {string} language
         * @returns {Promise<boolean>} - API command executed successfully
         */
        static async setLanguage(language) {
            const shell = Help4.getShell();
            return shell ? await shell.setLanguage(language) : false;
        }

        /**
         * passes a condition; these conditions will be used in evaluating availability of tours/helps/tour steps
         * @param {string} condition - condition name
         * @param {string} value
         * @returns {boolean} - API command executed successfully, undefined if controller is not ready and CMP needs to wait
         */
        static async setCondition(condition, value) {
            return (await _getConditionService()).setCondition(condition, value);
        }

        /**
         * passes set of conditions (as an object)
         * @param {Object[]} conditions - [{<name:string>: <value:string>}, ... ]
         * @returns {boolean} - API command executed successfully, undefined if controller is not ready and CMP needs to wait
         */
        static async setConditions(conditions) {
            return (await _getConditionService()).setConditions(conditions);
        }

        /**
         * removes a condition key
         * @param {string} condition
         * @returns {boolean} - API command executed successfully, undefined if controller is not ready and CMP needs to wait
         */
        static async removeCondition(condition) {
            return (await _getConditionService()).removeCondition(condition);
        }

        /**
         * delivers list of set conditions
         * @returns {?Object[]}
         */
        static async getConditions() {
            return (await _getConditionService()).getConditions();
        }

        /**
         * shows lightbox; not for remote mode due to promise return
         * @param {Object} data
         * @param {Help4.control2.Lightbox.Sizing} data.sizing
         * @param {Help4.control2.SizeWH} [data.size] - size is required if sizing is explicit or explicitFull
         * @param {string} [data.contentUrl] - content or contentUrl is required
         * @param {string} [data.content] - content or contentUrl is required
         * @param {boolean} data.showDoNotShowAgain - id is required
         * @param {string} [data.id] - id is required when showDoNotShowAgain is given
         * @returns {Promise<boolean>} - API command executed successfully
         */
        static async openLightbox({sizing, size, contentUrl, content, showDoNotShowAgain, id}) {
            const controller = Help4.getController();
            const handler = controller?.getHandler();
            const {isEditMode = true, isOpen, CMP4} = controller?.getConfiguration() || {};

            if (isEditMode || (!CMP4 && !handler)) return false;

            const lightboxService = controller.getService(CMP4 ? 'lightbox4' : 'lightbox');
            // return false if a lightbox is already open
            // # is forbidden
            if (lightboxService.get() || id && id.includes('#')) return false;

            const storageService = controller.getService('storage');
            let savedDoNotShow;
            if (id && showDoNotShowAgain) {
                savedDoNotShow = await storageService.get(Help4.SERIALIZE_LB_API_KEY) || '';

                // marked as do not show again before by user
                if (savedDoNotShow.includes(id)) return false;
            }

            const onEventBusEvent = async ({type, value, control}) => {
                const lbApiId = control.getMetadata('apiId');
                if (lbApiId && type === 'lightboxCheckbox') {
                    if (value) {
                        storageService.set(Help4.SERIALIZE_LB_API_KEY, `${savedDoNotShow}#${lbApiId}`);
                    } else {
                        savedDoNotShow = (await storageService.get(Help4.SERIALIZE_LB_API_KEY))?.replace(`#${lbApiId}`, '') || '';
                        storageService.del(Help4.SERIALIZE_LB_API_KEY);
                        if (savedDoNotShow) storageService.set(Help4.SERIALIZE_LB_API_KEY, savedDoNotShow);
                    }
                }
            }

            if (contentUrl) content = null;

            const {full, client, explicit, explicitFull} = Help4.LIGHTBOX_SIZES;
            if (!isOpen) sizing = sizing === client ? full : (sizing === explicit ? explicitFull : sizing);

            if (CMP4) {
                const panel = controller.getPanel();
                const fullArea = {x: 0, y: 0, w: window.innerWidth, h: window.innerHeight};
                const clientArea = panel.docked
                    ? Help4.reduceRect(fullArea, panel.getArea())
                    : fullArea;

                lightboxService.add({
                    _metadata: {
                        apiId: id,
                        announcement: true,
                    },
                    fullCover: true,
                    sizing,
                    clientArea,
                    size,
                    url: contentUrl,
                    content,
                    showDetach: !content,
                    checkboxText: id && showDoNotShowAgain ? Help4.Localization.getText('label.doNotShowAgain') : '',
                })
                .addListener('lightboxCheckbox', ({type, target, active}) => onEventBusEvent({type, control: target[0], value: active}));
            } else {
                const eventBus = controller.getService('eventBus');
                const observer = new Help4.observer.EventBusObserver(onEventBusEvent).observe(eventBus, {type: eventBus.TYPES.lightboxCheckbox});

                lightboxService.add({
                    _metadata: {
                        apiId: id,
                        announcement: true
                    },
                    fullCover: true,
                    sizing,
                    clientArea: handler.getContentArea(),
                    size,
                    contentUrl,
                    content,
                    showDetach: !content,
                    checkboxText: id && showDoNotShowAgain ? Help4.Localization.getText('label.doNotShowAgain') : '',
                    onclose: () => void observer.disconnect()
                }, 'control');
            }

            return true;
        }

        /**
         * adds a link tile to the carousel
         * @param {Help4.API.LinkTileParams[]} tileParams
         * @returns {Promise<boolean>} - API command executed successfully
         */
        static async addLinkTile(tileParams) {
            const controller = Help4.getController();
            const {isEditMode = true, CMP4} = controller?.getConfiguration() || {};
            if (isEditMode || !Help4.isArray(tileParams)) return false;

            const {API} = Help4.widget.help.project;
            const existingTiles = CMP4
                ? API.getData()
                : controller.getAPILinkTiles();
            const existingIds = new Set(existingTiles.map(({id}) => id));

            const newTiles = [];
            for (const {id, title, summaryText, contentUrl, content, sizing, size} of tileParams) {
                if (existingIds.has(id)) continue;  // check if same id has been added before
                existingIds.add(id);

                newTiles.push({
                    type: 'link',
                    linkLightbox: true,
                    id: id || Help4.createId(),
                    title,
                    summaryText,
                    content: contentUrl ? null : content,
                    linkTo: contentUrl,
                    lightboxSizing: sizing,
                    lightboxSize: size ? {width: size.w, height: size.h} : null,
                    showDetach: !content
                });
            }

            if (!newTiles.length) return false;

            CMP4
                ? await API.addData(newTiles)
                : controller.addAPILinkTiles(newTiles);
            return true;
        }

        /**
         * adds a help tile to the carousel
         * @param {Help4.API.HelpTileParams[]} tileParams
         * @returns {Promise<boolean>} - API command executed successfully
         */
        static async addHelpTile(tileParams) {
            const controller = Help4.getController();
            const {isEditMode = true, CMP4} = controller?.getConfiguration() || {};
            if (!CMP4 || isEditMode || !Help4.isArray(tileParams)) return false;

            const {API} = Help4.widget.help.project;
            const existingTiles = API.getData();
            const existingIds = new Set(existingTiles.map(({id}) => id));

            const newTiles = [];
            for (const tileParam of tileParams) {
                const {id, content, summaryText, title, hotspotStyle = 'CIRCLE'} = tileParam;
                if (existingIds.has(id) || !content || !summaryText || !title) continue;  // check if same id has been added before
                existingIds.add(id);

                newTiles.push({
                    ...tileParam,
                    type: 'help',
                    id: id || Help4.createId(),
                    hotspotStyle
                });
            }

            if (!newTiles.length) return false;

            await API.addData(newTiles);
            return true;
        }

        /**
         * registers command from target application; invoke callbacks on click of help4api link
         * @param {string} command
         * @param {Function} callback
         * @returns {boolean} - API command executed successfully
         */
        static registerCommand(command, callback) {
            if (typeof callback == 'function') {
                Help4.link_commands ||= {};
                Help4.link_commands[command] = callback;
                return true;
            }
            return false;
        }

        /**
         * invoke the callback on click of help4api links
         * callback will be registered by target application via Help4.API.registerCommand
         * @param {string} command
         */
        static runCommand(command) {
            const callback = Help4.link_commands?.[command];
            callback && setTimeout(callback, 1);
        }

        /**
         * XRAY-5098, will add support for Fiori Spaces data-help-id migration
         * @return {boolean}
         */
        static migrateSpacesIds() {
            (new Help4.MigrateFioriSpaces).execute()
            .catch(console.error);
        }

        /**
         * sets new configuration for CMP.
         * {product, version, system} parameters are supported, all are optional and minimum one is required.
         * refresh help content accordingly and defaults to help playback mode.
         * editing possible with new configuration, but doesn't support cross app tour editing.
         * new configuration is applied for the current screen and on navigation to another screen, new configuration will be lost and CMP fallbacks to the original configuration.
         * @param {Object} params
         * @param {string} [params.product] - minimum one parameter is required
         * @param {string} [params.version] - minimum one parameter is required
         * @returns {boolean} - API command executed successfully
         */
        static overrideConfig(params = {}) {
            const controller = Help4.getController();
            if (!controller) return false;

            const {isEditMode, isTourMode, CMP4, product, version} = controller.getConfiguration();
            if (isEditMode) return false;

            // close tour playback
            if (CMP4) {
                const activeWidget = Help4.widget.getActiveInstance();
                if (activeWidget && activeWidget instanceof Help4.widget.tour.Widget) activeWidget.deactivate();
            } else if (isTourMode) {
                controller.changeHandler(Help4.controller.MODES.help);
            }

            return Help4.getShell().setProductVersion(params.product || product, params.version || version);
        }

        /** for remote mode only */

        /**
         * shows, hides or toggles the carousel
         * @private
         * @param {boolean|'toggle'} [show = true] - shows, hides or toggles the carousel
         * @returns {boolean} - API command executed successfully
         */
        static showCarousel(show = true) {
            const controller = Help4.getController();
            const {isEditMode = true, isTourMode, isOpen, isRemoteMode, CMP4} = controller?.getConfiguration() || {};
            if (isEditMode || isTourMode || !isOpen || !isRemoteMode || CMP4) return false;

            const h = controller.getHandler() || controller.getEngine('remoteControl');
            h?.minimize(show === 'toggle' ? !h.isMinimized() : !Boolean(show));
            return !!h;
        }

        /**
         * sets help data to external source; Remote Mode only
         * @param {Object[]} data
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static setHelpObjects(data){
            _callRemoteControlEngine({type: 'setHelpObjects', data});
        }

        /**
         * sets hotspot data from external source; Remote Mode only; API version 2.0
         * @param {Object[]} data
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static setHotspots(data) {
            _callRemoteControlEngine({type: 'setHotspots', data});
        }

        /**
         * updates hotspot data from external source; Remote Mode only; API version 2.0
         * @param {Object[]} data
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static updateHotspots(data){
            _callRemoteControlEngine({type: 'updateHotspots', data});
        }

        /**
         * selects help object in carousel; Remote Mode only; API version 2.0
         * @param {Object} data
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static selectHelpObject(data){
            _callRemoteControlEngine({type: 'selectHelpObject', data});
        }

        /**
         * sets tour data from external source; Remote Mode only
         * @param {Object[]} data
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static setTours(data){
            _callRemoteControlEngine({type: 'setTours', data});
        }

        /**
         * updates help data from external source; Remote Mode only
         * @param {Object[]} data
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static updateHelpObjects(data){
            _callRemoteControlEngine({type: 'updateHelpObjects', data});
        }

        /**
         * starts tour with given id; Remote Mode only
         * if the first tour step given, WA will show it.
         * @param {Object} params
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static startTour(params) {
            _callRemoteControlEngine({type: 'startTour', params});
        }

        /**
         * shows tour step; Remote Mode only
         * @param {Object} data
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static showTourStep(data){
            _callRemoteControlEngine({type: 'showTourStep', data});
        }

        /**
         * updates visible tour step; Remote Mode only
         * @param {Object} data
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static updateTourStep(data) {
            _callRemoteControlEngine({type: 'updateTourStep', data});
        }

        /**
         * stops tour with given id; Remote Mode only
         * @param {Object} params
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static stopTour(params) {
            _callRemoteControlEngine({type: 'stopTour', params});
        }

        /**
         * sets learning object data from external source; Remote Mode only
         * @param {Object[]} data
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static setLearningObjects(data) {
            _callRemoteControlEngine({type: 'setLearningObjects', data});
        }

        /**
         * starts learningObject with given id; Remote Mode only
         * @param {string} id
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static startLearningObject(id) {
            _callRemoteControlEngine({type: 'startLearningObject', id});
        }

        /**
         * stops learningObject with given id; Remote Mode only
         * @param {Object} params
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static stopLearningObject(params) {
            _callRemoteControlEngine({type: 'stopLearningObject', params});
        }

        /**
         * sets search data from external source; Remote Mode only
         * @param {Object[]} data
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static setSearchResults(data) {
            _callRemoteControlEngine({type: 'setSearchResults', data});
        }

        /**
         * opens notification; Remote Mode only
         * @param {Object} params
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static openNotification(params) {
            _callRemoteControlEngine({type: 'openNotification', params});
        }

        /**
         * closes notification with given id; Remote Mode only
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static closeNotification() {
            _callRemoteControlEngine({type: 'closeNotification'});
        }

        /**
         * opens dialog; Remote Mode only
         * @param {Object} data
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static openDialog(data) {
            _callRemoteControlEngine({type: 'openDialog', data});
        }

        /**
         * closes dialog; Remote Mode only
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static closeDialog() {
            _callRemoteControlEngine({type: 'closeDialog'});
        }

        /**
         * set tour visibility; Remote Mode only
         * @param {boolean} visible
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static setTourVisible(visible) {
            _callRemoteControlEngine({type: 'setTourVisible', visible});
        }

        /**
         * set carousel tab; Remote Mode only
         * @param {Object} data
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static setTab(data) {
            _callRemoteControlEngine({type: 'setTab', data});
        }

        /**
         * shows lightbox; Remote Mode only
         * @param {Object} data
         * @returns {boolean} - API command executed successfully
         * @private
         */
        static showLightbox(data) {
            _callRemoteControlEngine({type: 'showLightbox', data});
        }

        /*** for internal use only ***/

        /**
         * starts selector recording
         * @private
         * */
        static record() {
            Help4.selector.methods.Utils.record();
        }

        /**
         * @param {?string} [managerBaseId = null]
         * @param {Object} [params = {}]
         * @returns {Promise<void>}
         * @private
         */
        static async startDebugMode(managerBaseId = null, params = {}) {
            const root = document.getElementsByTagName('head')[0] || document.getElementsByName('body')[0];
            if (!root) throw new Error('No DOM root!');

            // prepare new configuration
            const acceptedBaseIds = {
                dev: 'https://dev-companion.enable-now.cloud.sap',
                help4qa: 'https://help4qa-t66de1374.int.sap.eu2.hana.ondemand.com',
                canary: 'https://webassistant-outlook.enable-now.cloud.sap'
            };
            const managerBaseUrl = acceptedBaseIds[managerBaseId] || acceptedBaseIds.dev;
            const shell = Help4.getShell();
            const currentConfig = shell.constructor._params;
            const newConfig = Help4.extendObject(currentConfig, params);
            newConfig.resourceUrl = managerBaseUrl + '/web_assistant/framework';
            newConfig._isDebugMode = true;
            if (params.noParameters) delete newConfig.parameters;

            const qaF = (params.qaFramework || '').replace(/[^a-zA-Z_0-9]/g, '');
            if (qaF) newConfig.resourceUrl += qaF;

            if (shell instanceof Help4.shell.Fiori) {
                newConfig._button = shell._button;
            }

            // shutdown
            await shell.destroy();
            Help4.cleanOriginalObjects();
            Help4.disconnectMessages();
            window.Help4 = null;

            // reload
            const script = document.createElement('script');
            Object.assign(script, {
                type: 'text/javascript',
                src: newConfig.resourceUrl + '/wpb/Help4.js',
                async: true,
                defer: true,
                onload: () => Help4.init(newConfig)
            });
            root.appendChild(script);
        }

        /**
         * @param {?Object} [params = null]
         * @param {boolean} [params.UI5Update]
         * @param {boolean} [params.ignoreLongText]
         * @param {number} [params.useFixedVersion]
         * @param {Help4.engine.ur.UrHarmonizationEngine.UrTextResponse[]} [params.mockTexts]
         * @private
         */
        static startUrMock(params = null) {
            const engine = Help4.getController().getEngine('urHarmonization');

            if (params) {
                if (params.UI5Update === true) {
                    Help4.engine.ur.Mock.startUI5(engine);
                    return;
                }
                if (typeof params.ignoreLongText === 'boolean') {
                    engine.ignoreLongText(params.ignoreLongText);
                }
                if (typeof params.useFixedVersion === 'number') {
                    engine.useFixedVersion(params.useFixedVersion === 2 ? 2 : 1);
                }
                if (Array.isArray(params.mockTexts)) {
                    Help4.engine.ur.Mock.MOCK_TEXTS = params.mockTexts;
                }
            }
            engine.startMock();
        }

        /**
         * simulate UR hotspot shift / update when no parameter is passed or
         * simulate UR tour event using params
         * @param {?Object} [params = null]
         * @param {string} [params.messageType]
         * @param {string} [params.hotspotId]
         * @param {string|number} [params.key]
         * @param {boolean} [params.shift]
         * @param {boolean} [params.ctrl]
         * @param {boolean} [params.alt]
         * @private
         */
        static updateUrMock(params = null) {
            const engine = Help4.getController().getEngine('urHarmonization');

            const {messageType, hotspotId, key, shift, ctrl, alt} = params || {};
            const valid = typeof messageType === 'string' && (typeof hotspotId === 'string' || typeof key === 'string' || typeof key === 'number');
            if (valid) engine.updateMock({messageType, hotspotId, key, shift, ctrl, alt});
            else engine.updateMock();
        }

        /**
         * initializes UR test
         * @private
         * */
        static initUrTest() {
            Help4.init({
                editor: true,
                product: 'HH_Test',
                version: '2208',
                type: 'fiori',
                isComponent: true,
                serviceLayerVersion: 'SEN',
                resourceUrl: 'https://help4qa-t66de1374.int.sap.eu2.hana.ondemand.com/wa/wa/adaptable/web_assistant_framework/',
                dataUrlSEN: 'https://help4qa-t66de1374.int.sap.eu2.hana.ondemand.com/wa/wa',
                useABAPHelpTexts: true
            });
        }
    }

    /**
     * @param {Help4.widget.help.Widget|Help4.widget.whatsnew.Widget} widget
     * @param {string} tid
     * @returns {?Help4.widget.help.TileDescriptor}
     * @private
     */
    function _getTileCMP4(widget, tid) {
        const {view} = widget.getContext().widget.help;
        const tiles = view?.getTiles() || [];
        const tile = tiles.find(({id}) => id === tid);

        if (tile) {
            const {id: tileId, _projectId: projectId, _catalogueType: catalogueType} = tile;
            return {tileId, projectId, catalogueType};
        }

        return null;
    }

    /**
     * @param {string} tid
     * @returns {?Help4.control.tile.Tile}
     * @private
     */
    function _getTileCMP3(tid) {
        const controller = Help4.getController();
        const handler = controller?.getHandler();
        const carousel = handler?.getCarousel();
        return carousel
            ? carousel.getTile({byMetadata: {tileId: tid}})
            : null;
    }

    /**
     * @param {Help4.API.RemoteControlParams} params
     * @returns {boolean}
     * @private
     */
    function _callRemoteControlEngine(params) {
        const c = Help4.getController();
        const {type} = params;
        const se = c.getEngine('remoteControl');

        return se.isStarted() && se[type] ? se[type](params) : false;
    }

    /**
     * @returns {Promise<Help4.service.ConditionService>}
     * @private
     */
    function _getConditionService() {
        return new Help4.Promise(resolve => {
            const getService = () => {
                const service = Help4.getController()?.getService('condition');
                service
                    ? resolve(service)
                    : setTimeout(getService, 100);
            }
            getService();
        });
    }
})();