Source: widget/help/view2/view2.js

(function() {
    /**
     * @typedef {Object} Help4.widget.help.view2.AllowUpdateResult
     * @property {string} screenId
     * @property {Help4.widget.help.CatalogueKeys} catalogueKey
     * @property {string[]} helpIds
     * @property {boolean} isDataUpdate
     */

    /**
     * @typedef {Object} Help4.widget.help.view2.SerializedStatus
     * @property {string[]} [hotspotAnimation]
     * @property {string[]} [callouts]
     * @property {string[]} [announcementsAlways]
     * @property {string[]} [announcementsOnce]
     */

    /**
     * @namespace view2
     * @memberof Help4.widget.help
     */
    Help4.widget.help.view2 = {
        /** @type {string} */ BUTTON_CSS: 'show-as-button',
        /** @type {string} */ LIGHTBOX_CSS: 'lightbox',
        /** @type {string} */ HIDDEN_CSS: 'hiddenedit',
        /** @type {string} */ GLOBAL_HELP_CSS: 'globalhelp',

        /** @type {RegExp} */ PERSISTENCE_REGEXP: /(.+?):(.+?):(.+?):(.+?):(.+)$/,  // <screenId>:<catalogueType>:<projectId>:<projectVersion>:<tileId>

        /**
         * @param {Help4.widget.help.ProjectTile|Help4.control2.Control} data
         * @returns {Help4.widget.help.view2.Tile.Descriptor}
         */
        extractTileDescriptor(data) {
            if (data instanceof Help4.control2.Control) {
                return data.getMetadata('descriptor');
            }

            const {
                id: tileId,
                _projectId: projectId,
                _catalogueType: catalogueType,
                _catalogueKey: catalogueKey
            } = data;

            return `${catalogueKey}:${catalogueType}:${projectId}:${tileId}`;
        },

        /**
         * @param {Help4.widget.help.view2.Tile.Descriptor|Help4.widget.help.TileDescriptor} descriptor
         * @returns {Help4.widget.help.TileDescriptor|Help4.widget.help.view2.Tile.Descriptor}
         */
        convertTileDescriptor(descriptor) {
            if (typeof descriptor === 'string') {
                const rx = /^.*?:(.*?):(.*?):(.*?)$/;
                const [all, catalogueType, projectId, tileId] = descriptor.match(rx);
                return {tileId, projectId, catalogueType};
            } else {
                const {Core} = Help4.widget.companionCore;
                const configuration = Help4.getConfiguration();
                const catalogueKey = Core.getCatalogueKey({configuration});
                const {tileId, projectId, catalogueType} = descriptor;
                return `${catalogueKey}:${catalogueType}:${projectId}:${tileId}`;
            }
        },

        /**
         * @param {Help4.widget.help.view2.Tile.Descriptor|Help4.widget.help.TileDescriptor} descriptor1
         * @param {Help4.widget.help.view2.Tile.Descriptor|Help4.widget.help.TileDescriptor} descriptor2
         * @returns {boolean}
         */
        equalTileDescriptors(descriptor1, descriptor2) {
            const rx = /^.*?:(.*?):(.*?):(.*?)$/;

            if (typeof descriptor1 === 'object' && typeof descriptor2 === 'object') {
                const {tileId: t1, projectId: p1, catalogueType: c1} = descriptor1;
                const {tileId: t2, projectId: p2, catalogueType: c2} = descriptor2;
                return t1 === t2 && p1 === p2 && c1 === c2;
            } else if (typeof descriptor1 === 'object') {
                const {tileId: t1, projectId: p1, catalogueType: c1} = descriptor1;
                const [all, c2, p2, t2] = descriptor2.match(rx);
                return t1 === t2 && p1 === p2 && c1 === c2;
            } else if (typeof descriptor2 === 'object') {
                const {tileId: t1, projectId: p1, catalogueType: c1} = descriptor2;
                const [all, c2, p2, t2] = descriptor1.match(rx);
                return t1 === t2 && p1 === p2 && c1 === c2;
            }

            return descriptor1 === descriptor2;
        },

        /**
         * @param {Help4.widget.help.view2.Tile.Descriptor[]|Help4.widget.help.TileDescriptor[]} list
         * @param {Help4.widget.help.view2.Tile.Descriptor|Help4.widget.help.TileDescriptor} descriptor
         * @returns {boolean}
         */
        containsTileDescriptor(list, descriptor) {
            return !!list.find(item => this.equalTileDescriptors(item, descriptor));
        },

        /**
         * @deprecated
         * @param {Array<Object>} list
         * @param {Help4.widget.help.view2.Tile.Descriptor|Help4.widget.help.TileDescriptor} descriptor
         * @returns {?Object}
         */
        getByTileDescriptor(list, descriptor) {
            return list.find(item => {
                const d = this.extractTileDescriptor(item);
                return this.equalTileDescriptors(descriptor, d);
            });
        },

        /**
         * @param {Help4.widget.help.Widget} widget
         * @param {Help4.widget.help.ProjectTile} projectTile
         * @returns {boolean}
         */
        hasHotspot(widget, projectTile) {
            const {HotspotData} = Help4.data;
            const widgetActive = widget.isActive();
            const {instantHelp, callout} = projectTile;
            return (widgetActive || instantHelp || callout) && HotspotData.hasHotspot(projectTile);
        },

        /**
         * check whether update from cache is possible
         * @param {Help4.widget.help.view2.View} view
         * @returns {Help4.widget.help.view2.AllowUpdateResult}
         */
        allowUpdateFromCache(view) {
            const {
                /** @type {Help4.widget.help.Widget} */ __widget,
                /** @type {Help4.widget.help.view2.View.Cache} */ _cache: {
                    screenId: cacheSID,
                    catalogueKey: cacheCK,
                    helpIds: cacheHI
                }
            } = view;

            const {
                widget: {help: {
                    /** @type {Help4.widget.help.CatalogueKeys} */ catalogueKey,
                    /** @type {string[]} */ helpIds,
                    /** @type {string} */ screenId
                }}

            } = __widget.getContext();

            const {project} = Help4.widget.help;

            const specialLastUpdate = /** @type {number} */ (() => {
                const {_Special} = project;
                const timestamp = [];

                for (const handler of Object.values(project)) {
                    if (_Special.isPrototypeOf(handler)) {
                        timestamp.push(handler.getUpdateTS());
                    }
                }

                return Math.max(...timestamp);
            })();

            // a special update has occurred (API, Quick Tour, WDA/GUI, ...)
            const isDataUpdate = screenId !== cacheSID ||  // different screen
                catalogueKey !== cacheCK ||   // HEAD vs PUB
                !Help4.equalArrays(helpIds, cacheHI || []);  // help projects have changed

            return {screenId, catalogueKey, helpIds, isDataUpdate};
        },

        /**
         * serialize view2 status
         * @param {Help4.widget.help.view2.View} view
         * @returns {Help4.widget.help.view2.SerializedStatus}
         */
        serialize(view) {
            const {Core} = Help4.widget.companionCore;
            const {_cache, __widget} = view;
            const {
                configuration,
                widget: {help: {data}}
            } = __widget.getContext();
            const {
                animation = {},
                callout = {},
                announcement_always = {},
                announcement_once = {}
            } = _cache.persist || {};
            const {screenId} = configuration.core;
            const output = [[], [], [], []];

            const catalogueKey = /** @type {Help4.widget.help.CatalogueKeys} */ Core.getCatalogueKey({configuration});

            [animation, callout, announcement_always, announcement_once].forEach((source, outputIndex) => {
                Object.entries(source || {})
                .forEach(([sourceScreenId, list]) => {
                    list.forEach(
                        /**
                         * @param {Help4.widget.help.TileDescriptor} descriptor
                         * @param {number} listIndex
                         */
                        (descriptor, listIndex) => {
                            let {catalogueType, projectId, projectVersion, tileId} = descriptor;

                            if (!projectVersion) {
                                if (sourceScreenId === screenId) {
                                    // can only retrieve information for projects from same screen
                                    projectVersion = (data.getCatalogueProject(projectId, catalogueKey) || {}).version;
                                }
                                if (!projectVersion) return;  // invalid

                                // update this._cache.persist['animation' | 'callout'] with version information
                                source[sourceScreenId][listIndex].projectVersion = projectVersion;
                            }

                            output[outputIndex].push(`${sourceScreenId}:${catalogueType}:${projectId}:${projectVersion}:${tileId}`);
                        }
                    );
                });
            });

            const result = {};
            if (output[0].length) result.hotspotAnimation = output[0];
            if (output[1].length) result.callouts = output[1];
            if (output[2].length) result.announcementsAlways = output[2];
            if (output[3].length) result.announcementsOnce = output[3];
            return result;
        },

        /**
         * deserialize view2 status
         * @param {Help4.widget.help.view2.View} view
         * @param {?Help4.widget.help.view2.SerializedStatus} status
         */
        deserialize(view, status) {
            if (!status) return;

            const {PERSISTENCE_REGEXP} = this;
            const {
                hotspotAnimation = [],
                callouts = [],
                announcementsAlways = [],
                announcementsOnce = []
            } = status;
            const output = [{}, {}, {}, {}];

            [hotspotAnimation, callouts, announcementsAlways, announcementsOnce].forEach((source, index) => {
                source.forEach(key => {
                    const [all, screenId, catalogueType, projectId, projectVersion, tileId] = key.match(PERSISTENCE_REGEXP) || [];
                    if (screenId && catalogueType && projectId && projectVersion && tileId) {
                        output[index][screenId] ||= [];
                        output[index][screenId].push(
                            /** @type {Help4.widget.help.TileDescriptor} */
                            {catalogueType, projectId, projectVersion, tileId}
                        );
                    }
                });
            });

            const {_cache} = view;
            _cache.persist ||= {};
            _cache.persist.animation = output[0];
            _cache.persist.callout = output[1];
            _cache.persist.announcement_always = output[2];
            _cache.persist.announcement_once = output[3];
        },

        /**
         * @param {Help4.widget.help.view2.View} view
         * @param {string} name
         * @param {Help4.widget.help.view2.Tile.Descriptor} descriptor
         * @returns {boolean}
         */
        async setPersistenceCacheValue(view, name, descriptor) {
            const {
                widget: {help: {view2, Cookie}},
                COOKIE_KEYS: {HOTSPOT_ANIMATION, CALLOUT, ANNOUNCEMENT}
            } = Help4;
            const {_cache, __widget} = view;
            const {widget: {help: {screenId}}, /** @type {Help4.controller.Controller} */ controller} = __widget.getContext();

            const storageService = controller.getService('storage');
            const {STORAGE_ID: {fiori}} = storageService;
            if (name !== 'announcement_always' && await storageService?.isStorageAvailable(fiori)) {
                const {tileId} = view2.convertTileDescriptor(descriptor);

                let type = '';
                switch (name) {
                    case 'animation':
                        type = HOTSPOT_ANIMATION;
                        break;
                    case 'callout':
                        type = CALLOUT;
                        break;
                    case 'announcement_once':
                        type = ANNOUNCEMENT;
                        break;
                }

                await Cookie.setPageCookie(__widget, {type, key: tileId});
            }

            _cache.persist ||= {};
            const {persist} = _cache;

            persist[name] ||= {};
            const {[name]: container} = persist;
            container[screenId] ||= [];

            const data = view2.convertTileDescriptor(descriptor);
            if (!view2.containsTileDescriptor(container[screenId], data)) {
                container[screenId].push({...data});  /** XRAY-6029: persist data will be transformed into {@link Help4.widget.help.ExtendedTileDescriptor}; this will disturb other routines */
                return true;
            }
            return false;
        },

        /**
         * will fix persist cache after CMP3 -> CMP4 migration
         * @param {Help4.widget.help.view2.View} view
         * @param {Help4.widget.help.ProjectTile[]} tiles
         * @param {string} screenId
         * @returns {boolean}
         */
        fixPersistentCache(view, tiles, screenId) {
            let fixed = false;

            /**
             * @param {Help4.widget.help.TileDescriptor} descriptor
             * @returns {Help4.widget.help.TileDescriptor|null}
             */
            const fix = ({catalogueType, projectId, projectVersion, tileId}) => {
                /** information is incomplete; see {@link Help4.controller.Persistence.migrate} */
                if (catalogueType === '*' && projectId === '*') {
                    fixed = true;

                    const {_catalogueType, _projectId} = /** @type {Help4.widget.help.ProjectTile} */ tiles.find(({id}) => id === tileId) || {};
                    return _catalogueType
                        ? {catalogueType: _catalogueType, projectId: _projectId, projectVersion, tileId}  // tile is within a valid project; complete the information
                        : null;  // tile is invalid; set null
                }

                // information is already complete
                return {catalogueType, projectId, projectVersion, tileId};
            }

            const run = (persist, key) => {
                if (!persist?.[key]?.[screenId]) return;

                persist[key][screenId] = persist[key][screenId]
                .map(fix)
                .filter(descriptor => !!descriptor);
            }

            const {_cache} = view;
            _cache.persist ||= {};
            const {persist} = _cache;

            run(persist, 'announcement_always');
            run(persist, 'announcement_once');
            run(persist, 'animation');
            run(persist, 'callout');

            return fixed;
        },

        /**
         * @param {Help4.widget.help.ProjectTile} projectTile
         * @returns {string}
         */
        getLinkTileUrl({linkTo}) {
            const {Placeholder, CtxWPB} = Help4;
            return Help4.sanitizeUrl(CtxWPB.resolveAddress(Placeholder.resolve(linkTo)));
        }
    };
})();