Source: widget/help/project/SEN.js

(function() {
    const ATTRIBUTES_MAPPING = {
        caption: 'title',
        h4_loio: 'loio',
        h4_product: 'product',
        h4_product_version: 'version',
        h4_system: 'system'
    }

    const TRANSFORM = {
        /**
         * @param {string} value
         * @returns {string}
         */
        getTileType: value => value === 'guide_click' ? 'tour' : value.replace(/_tile/, ''),
        /**
         * @param {string} value
         * @param {Object} params
         * @param {boolean} params.isExtension
         * @returns {string}
         */
        processText: (value, {isExtension}) => typeof value === 'string' || isExtension ? value : '',
        /**
         * @param {string} value
         * @param {Object} params
         * @param {boolean} params.isExtension
         * @returns {string}
         */
        getHotspotId: (value, {isExtension}) => !isExtension ? `${value}-hs` : undefined,
        /**
         * @param {boolean} value
         * @param {Object} params
         * @param {Help4.widget.help.project.SEN.LessonJsMacro} params.macro
         * @param {boolean} params.isExtension
         * @returns {boolean}
         */
        getShowArrow: (value, {macro, isExtension}) => !isExtension && macro.macro_template !== 'guide_click' ? true : value,
        /**
         * @param {string} value
         * @param {Object} params
         * @param {Help4.widget.help.project.SEN.LessonJsMacro} params.macro
         * @param {boolean} params.isExtension
         * @returns {boolean}
         */
        getInstantHelp: function(value, {macro, isExtension}) {
            return value && macro.hotspot_style === 'CIRCLE' && !isExtension ? false : value;  // XRAY-4174
        }
    }

    /**
     * @typedef {Object} Help4.widget.help.project.SEN.ProjectUrlConfig
     * @property {string} serverBaseUrl - base URL of the content server
     * @property {string} pubUrl - URL extension to public end-user content
     * @property {string} [headUrl = null] - URL extension to non-public author only content
     */

    /**
     * @typedef {Object} Help4.widget.help.project.SEN.TagInfo
     * @property {string} id - tag ID
     * @property {string} display_name
     * @property {string} description
     */

    /**
     * @typedef {Help4.widget.help.project.SEN.TagInfo[]} Help4.widget.help.project.SEN.TagInfoList
     */

    /**
     * @typedef {Object} Help4.widget.help.project.SEN.ProjectMeta
     * @property {Object[]} Commits
     * @property {Object[]} Streams
     * @property {number} wt - write token status
     */

    /**
     * @typedef {Object} Help4.widget.help.project.SEN.ProjectTag
     * @property {string} tag
     * @property {string} version
     */

    /**
     * @typedef {Help4.widget.help.project.SEN.ProjectTag[]} Help4.widget.help.project.SEN.ProjectTags
     */

    /**
     * @typedef {Object} Help4.widget.help.project.SEN.ProjectVersion
     * @property {string} version
     * @property {string} user
     * @property {string} date
     */

    /**
     * @typedef {Help4.widget.help.project.SEN.ProjectVersion[]} Help4.widget.help.project.SEN.ProjectVersions
     */

    /**
     * @typedef {Object} Help4.widget.help.project.SEN.Project
     * @property {Help4.widget.help.project.SEN.ProjectMeta} Meta
     * @property {Help4.widget.help.project.SEN.ProjectTags} Tags
     * @property {Help4.widget.help.project.SEN.ProjectVersions} Versions
     * @property {boolean} active
     * @property {Array<*>} assets
     * @property {string} assignee
     * @property {string} caption
     * @property {string} conditions
     * @property {string} context_id
     * @property {string} created_by
     * @property {string} creation_time
     * @property {string} creator
     * @property {string} cwa_recommended_sync_mode
     * @property {string} description
     * @property {*} entity_published_path
     * @property {boolean} fallback_has_entity
     * @property {string} h4_loio
     * @property {string} h4_product
     * @property {string} h4_product_version
     * @property {string} h4_system
     * @property {number} hidden
     * @property {string} language
     * @property {string} macroset
     * @property {string} mastery_score_percent
     * @property {string} max_score_percent
     * @property {string} maxversion
     * @property {string} modification_time
     * @property {string} modified_by
     * @property {string} ref_status
     * @property {string} status
     * @property {string} sub_type
     * @property {string} uid
     * @property {string} workflow_uid
     */

    /**
     * @typedef {Object} Help4.widget.help.project.SEN.EntityTxt
     * @property {*} assets
     * @property {string} caption
     * @property {string} conditions
     * @property {string} context_id
     * @property {string} created_by
     * @property {string} creation_time
     * @property {string} creator
     * @property {string} cwa_recommended_sync_mode
     * @property {string} description
     * @property {string} h4_loio
     * @property {string} h4_product
     * @property {string} h4_product_version
     * @property {string} h4_system
     * @property {boolean} [hidden]
     * @property {string} language
     * @property {string} macroset
     * @property {string} mastery_score_percent
     * @property {string} max_score_percent
     * @property {string} modification_time
     * @property {string} modified_by
     * @property {string} sub_type
     * @property {string} tclass
     * @property {string} uid
     * @property {Object} [info]
     * @property {Help4.widget.help.PUBLISHED_STATUS} info.published
     * @property {Help4.widget.help.PUBLISHED_STATUS} info.playbackTag
     */

    /**
     * @typedef {Object} Help4.widget.help.project.SEN.LessonJsMacroStartUnit
     * @property {number} duration
     * @property {'start_unit'} macro_template
     * @property {number} time
     * @property {string} uid
     */

    /**
     * @typedef {Object} Help4.widget.help.project.SEN.LessonJsMacroDefineTarget
     * @property {number} duration
     * @property {string} key - screen ID
     * @property {'define_target'} macro_template
     * @property {number} time
     * @property {string} uid
     */

    /**
     * @typedef {Object} Help4.widget.help.project.SEN.LessonJsMacroGuideClick
     * @property {boolean} bubble_centered
     * @property {string} bubble_orientation
     * @property {string} bubble_size
     * @property {string} bubble_text
     * @property {string} bubble_type
     * @property {string} caption
     * @property {string} conditions
     * @property {number} duration
     * @property {string} hotspot
     * @property {string} hotspot_element_type
     * @property {Object} manual_manual_offset
     * @property {number} manual_manual_offset.top
     * @property {number} manual_manual_offset.left
     * @property {boolean} hotspot_offset
     * @property {Object} hotspot_rect_delta
     * @property {number} hotspot_rect_delta.width
     * @property {number} hotspot_rect_delta.height
     * @property {boolean} hotspot_spotlight
     * @property {string} hotspot_style
     * @property {'guide_click'} macro_template
     * @property {boolean} show_arrow
     * @property {boolean} show_title_bar
     * @property {number} time
     * @property {string} tourstep_auto_progress
     * @property {boolean} tourstep_auto_skip
     * @property {string} uid
     */

    /**
     * @typedef {Object} Help4.widget.help.project.SEN.LessonJsMacroHelpTile
     */

    /**
     * @typedef {Object} Help4.widget.help.project.SEN.LessonJsMacroLinkTile
     */

    /**
     * @typedef {Help4.widget.help.project.SEN.LessonJsMacroStartUnit|Help4.widget.help.project.SEN.LessonJsMacroLinkTile|Help4.widget.help.project.SEN.LessonJsMacroHelpTile|Help4.widget.help.project.SEN.LessonJsMacroDefineTarget|Help4.widget.help.project.SEN.LessonJsMacroGuideClick} Help4.widget.help.project.SEN.LessonJsMacro
     */

    /**
     * @typedef {Array<Help4.widget.help.project.SEN.LessonJsMacro>} Help4.widget.help.project.SEN.LessonJsMacroArray
     */

    /**
     * @typedef {Object} Help4.widget.help.project.SEN.LessonJsTourstop
     * @property {string} audio
     * @property {number} audio_duration
     * @property {boolean} callable
     * @property {number} duration
     * @property {index} index
     * @property {boolean} jumpable
     * @property {Help4.widget.help.project.SEN.LessonJsMacroArray} macros
     * @property {string} title
     * @property {string} uid
     * @property {boolean} visible
     */

    /**
     * @typedef {Object} Help4.widget.help.project.SEN.LessonJs
     * @property {Object} global_params
     * @property {Help4.widget.help.project.SEN.LessonJsTourstop[]} tourstops
     * @property {Object} user_header
     * @property {string} user_header.audio_ext
     * @property {string} user_header.author
     * @property {string} user_header.comment
     * @property {string} user_header.language
     * @property {string} user_header.mastery_score_percent
     * @property {string} user_header.max_score_percent
     * @property {string} user_header.shelftype
     * @property {string} user_header.title
     * @property {string} user_header.version
     */

    /**
     * this backend connector is able to handle SEN project content for a RO model configuration
     * - in case of SEN config: will download published and head data
     * - in case of EXT config: will only download published data
     */
    Help4.widget.help.project.SEN = class {
        /**
         * @param {string} projectId - project ID
         * @param {Help4.typedef.SystemConfiguration} config - the system configuration
         * @param {Help4.widget.help.Data} data
         * @returns {Promise<Help4.widget.help.Projects|null>}
         */
        static async load(projectId, config, data) {
            const {wpb, sen, ext} = Help4.SERVICE_LAYER;
            const {roModel, serviceUrl} = config.help;
            if (!serviceUrl || roModel !== wpb && roModel !== sen) return null;

            const {help: {serviceLayer}, core: {editor}} = config;
            /** @type {Help4.widget.help.project.SEN.ProjectUrlConfig} */
            const {serverBaseUrl, pubUrl, headUrl} = this._getUrls(projectId, config, serviceUrl);

            /** @type {Help4.widget.help.Projects} */
            const projects = {pub: null};

            // important: in case of EXT - always load and use published data for RO source!
            const {SEN} = Help4.widget.companionCore;
            if (editor && serviceLayer !== ext) {
                // editor: load PUB and HEAD
                /** @type {?Help4.widget.companionCore.SEN.ProjectResponse} */
                const response = await SEN.doProjectRequest(config, serviceUrl, {serverBaseUrl, pubUrl, headUrl});
                const {pub, head, tagInfo} = response || {};
                if (!tagInfo) return null;

                if (pub) {
                    /** @type {?Help4.widget.companionCore.SEN.PubProjectResponse} */
                    const assembled = this._assemblePub(pub, tagInfo, config);
                    if (assembled) projects.pub = this._parse(assembled, config);
                }
                if (head) {
                    /** @type {?Help4.widget.companionCore.SEN.PubProjectResponse} */
                    const assembled = this._assembleHead(head, tagInfo, config);
                    if (assembled) projects.head = this._parse(assembled, config);
                }
            } else {
                // non-editor or EXT: load PUB only
                /** @type {?Help4.widget.companionCore.SEN.PubProjectResponse} */
                const response = await SEN.doProjectRequest(config, serviceUrl, {serverBaseUrl, pubUrl});
                projects.pub = response ? this._parse(response, config) : null;
            }

            return projects;
        }

        /**
         * @protected
         * @param {string} projectId - the project id
         * @param {Help4.typedef.SystemConfiguration} config - the system configuration
         * @param {string} serviceUrl - the server base URL
         * @returns {Help4.widget.help.project.SEN.ProjectUrlConfig}
         */
        static _getUrls(projectId, config, serviceUrl) {
            const {SEN} = Help4.widget.companionCore;
            const serverBaseUrl = SEN.getServerBaseUrl(serviceUrl);

            const {playbackTag: tag} = config.core;
            const context = encodeURIComponent(Help4.JSON.stringify({id: projectId}));
            const pubUrl = SEN.createPubUrl({serverUrl: serviceUrl, tag}) + `/.context?${context}`;
            const headUrl = `${serviceUrl}/project/${projectId}`;
            return {serverBaseUrl, pubUrl, headUrl};
        }

        /**
         * @protected
         * @param {Help4.widget.companionCore.SEN.ProjectResponsePub} pub
         * @param {Help4.widget.companionCore.SEN.PubProjectResponse} pub.context
         * @param {Help4.widget.help.project.SEN.Project} pub.project
         * @param {Help4.widget.help.project.SEN.TagInfoList} tagInfo
         * @param {Help4.typedef.SystemConfiguration} config
         * @returns {Help4.widget.companionCore.SEN.PubProjectResponse|null}
         */
        static _assemblePub({context, project}, tagInfo, config) {
            /** @type {Help4.widget.help.project.SEN.EntityTxt} */
            const {entityTxt} = context;
            if (entityTxt) {
                /** @type {boolean} */
                entityTxt.hidden = Help4.clampBoolean(project.hidden, false);
                /** @type {{published: Help4.widget.help.PUBLISHED_STATUS, playbackTag: Help4.widget.help.PUBLISHED_STATUS}} */
                entityTxt.info = _getPublishedTagInfo(project, tagInfo, config);
                return context;
            }
            return null;
        }

        /**
         * @protected
         * @param {Help4.widget.companionCore.SEN.ProjectResponseHead} head
         * @param {Document} head.entityXml
         * @param {Document} head.projectDpr
         * @param {Help4.widget.help.project.SEN.Project} head.project
         * @param {Help4.widget.help.project.SEN.TagInfoList} tagInfo
         * @param {Help4.typedef.SystemConfiguration} config
         * @returns {Help4.widget.companionCore.SEN.PubProjectResponse|null}
         */
        static _assembleHead(head, tagInfo, config) {
            const {entityXml, projectDpr, project} = head;
            const {XmlHelper} = Help4.widget.help;

            /** @type {Help4.widget.help.project.SEN.EntityTxt|null} */
            const entityTxt = XmlHelper.toEntityTxt(entityXml, project.uid);
            if (entityTxt && projectDpr) {
                /** @type {Help4.widget.help.ContextTypes} */
                const contextType = entityTxt.sub_type.toUpperCase();
                /** @type {Help4.widget.help.project.SEN.LessonJs} */
                const lessonJs = XmlHelper.toLessonJs(projectDpr, entityTxt);
                /** @type {{published: Help4.widget.help.PUBLISHED_STATUS, playbackTag: Help4.widget.help.PUBLISHED_STATUS}} */
                entityTxt.info = _getPublishedTagInfo(project, tagInfo, config);
                return {contextType, entityTxt, lessonJs};
            }
            return null;
        }

        /**
         * @protected
         * @param {Help4.widget.companionCore.SEN.PubProjectResponse} data
         * @param {Help4.widget.help.ContextTypes} data.contextType
         * @param {Help4.widget.help.project.SEN.EntityTxt} data.entityTxt
         * @param {Help4.widget.help.project.SEN.LessonJs} data.lessonJs
         * @param {Help4.typedef.SystemConfiguration} config
         * @param {Help4.widget.help.CatalogueTypes} [catalogueType = 'SEN']
         * @param {boolean} [isExtension = false]
         * @returns {Help4.widget.help.Project}
         */
        static _parse(data, config, catalogueType = 'SEN', isExtension = false) {
            const {contextType, entityTxt, lessonJs} = data;
            const {CtxWPB} = Help4;

            /** @param {Help4.widget.help.project.SEN.EntityTxt} entityTxt */
            const setDefaults = entityTxt => {
                for (const [key, {t, f, /** @type {string} */ wpb}] of Object.entries(Help4.PROJECT_DEFAULTS)) {
                    const value = entityTxt[wpb || key];

                    if (value === undefined) {
                        entityTxt[key] = f;
                        continue;
                    }

                    if (t === 'json') {
                        try {
                            entityTxt[key] = Help4.JSON.parse(value);
                        } catch (e) {
                            entityTxt[key] = f;
                        }
                    } else {
                        entityTxt[key] = value;
                    }
                }
            }

            /**
             * @param {Help4.widget.help.project.SEN.LessonJsTourstop[]} tourstops
             * @param {string} screen
             * @param {Help4.typedef.SystemConfiguration} config
             * @param {Help4.widget.help.Project} project
             * @param {boolean} isExtension
             * @returns {Array<Help4.widget.help.ProjectTile>}
             */
            const parseTiles = (tourstops, screen, config, project, isExtension) => {
                /** @type {Array<Help4.widget.help.ProjectTile>} */
                const tiles = [];

                for (const tourstop of tourstops) {
                    const {/** @type {Help4.widget.help.project.SEN.LessonJsMacroArray} */ macros} = tourstop;
                    /** @type {string|null} */
                    let pageUrl = null;

                    for (const macro of macros) {
                        switch (macro.macro_template) {
                            case 'define_target':
                                pageUrl = macro.key;
                                break;
                            case 'link_tile':
                            case 'help_tile': {
                                /** @type {Help4.widget.help.ProjectTile} */
                                const tile = parseTile(macro, pageUrl || screen, config, project, isExtension);
                                tiles.push(tile);
                                break;
                            }
                            case 'guide_click': {
                                /** @type {Help4.widget.help.ProjectTile} */
                                const tile = parseTile(macro, pageUrl || screen, config, project, isExtension);

                                if (!tile.hotspotAnchor && !tile.hotspotCentered) {
                                    // fix for old content, that could have no hotspot but also is not centered
                                    // this constellation is no longer possible
                                    tile.hotspotCentered = true;
                                }

                                tiles.push(tile);
                                break;
                            }
                        }
                    }
                }

                return tiles;
            }

            /**
             * @param {Help4.widget.help.project.SEN.LessonJsMacro} macro
             * @param {string} pageUrl
             * @param {Help4.typedef.SystemConfiguration} config
             * @param {Help4.widget.help.Project} project
             * @param {boolean} isExtension
             * @returns {Help4.widget.help.ProjectTile}
             */
            const parseTile = (macro, pageUrl, config, project, isExtension) => {
                CtxWPB.initContext(true);
                CtxWPB.setScope('macro');
                CtxWPB.setContext('macro', macro.uid);

                const {/** @type {Help4.widget.help.CatalogueTypes} */ _catalogueType, _dataType, language} = project;
                const isExtensionOverlay = _catalogueType === 'SEN2';
                const isEXT = config.help.serviceLayer === Help4.SERVICE_LAYER.ext;

                /** @type {Help4.widget.help.ProjectTile} */
                const tile = {
                    _ctx: CtxWPB.get(),
                    _mac: macro,
                    pageUrl,
                    language,
                    _projectId: project.id,
                    _catalogueType: _catalogueType,
                    _dataType: _dataType
                };

                for (const [key, {t, f, /** @type {string} */ wpb, /** @type {string} */ wpbGet}] of Object.entries(Help4.DEFAULTS)) {
                    let value = macro[wpb || key];

                    if (t === 'json' && value) {
                        try {
                            value = Help4.JSON.parse(value);
                        } catch (e) {
                            value = f;
                        }
                    }

                    const transform = TRANSFORM[wpbGet];
                    if (transform) value = transform(value, {macro, isExtension});

                    value = clampTileValue(value, {t, f}, isExtension);

                    if (value !== undefined) tile[key] = value;
                }

                // in EXT mode, add standard url marker to urls for non-extended RO only tiles;
                // needed to choose resolving url (CtxWPB) with either RO or RW service url
                if (isEXT && !isExtensionOverlay) Help4.control.input.HtmlEditor.addStandardUrlMarker(tile);

                if (tile.loio) {
                    tile._standalone = false;
                    if (!isExtensionOverlay) tile._reference = true;
                } else {
                    tile._standalone = true;
                    tile.loio = tile.id;

                    // XRAY-3291 existing tiles created in extensibility mode does not have showTitleBar value
                    if (isExtension && tile.showTitleBar == null) tile.showTitleBar = Help4.DEFAULTS.showTitleBar.f;
                }

                CtxWPB.destroyContext();

                return tile;
            }

            /**
             * @param {*} value
             * @param {Object} defaults
             * @param {string} defaults.t
             * @param {*} defaults.f
             * @param {boolean} isExtension
             * @returns {*}
             */
            const clampTileValue = (value, {t, f}, isExtension) => {
                if (isExtension) {
                    return value != null && (t === 'boolean' || t === 'array')
                        ? Help4.clampValue(value, t, f)
                        : value;
                }
                return Help4.clampValue(value, t, f);
            }

            const {tclass, uid, context_id, info} = entityTxt;
            CtxWPB.initContext();
            CtxWPB.setScope(tclass);
            CtxWPB.setContext(tclass, uid);

            entityTxt.h4_loio ||= uid;
            entityTxt.alias = entityTxt.id = uid;
            entityTxt.contextType = contextType;
            entityTxt.screen = (context_id || '').replace(/xRay=/, '');
            entityTxt.published = info?.published;
            entityTxt.playbackTag = info?.playbackTag;
            setDefaults(entityTxt);

            const {ACCEPTED_ATTRIBUTES} = Help4.widget.help.Data;
            /** @type {Help4.widget.help.Project} */
            const project = {};
            for (const [key, value] of Object.entries(entityTxt)) {
                 const name = ATTRIBUTES_MAPPING[key] || key;
                 if (ACCEPTED_ATTRIBUTES.includes(name)) {
                     project[name] = value;
                 }
            }

            project._catalogueType = catalogueType;
            project._dataType = 'SEN';

            /** @type {Help4.widget.help.project.SEN.LessonJsTourstop[]} */
            const {tourstops} = lessonJs;
            /** @type {Help4.widget.help.ProjectTile[]} */
            project.tiles = parseTiles(tourstops, entityTxt.screen, config, project, isExtension);

            CtxWPB.destroyContext();

            return project;
        }
    }

    /**
     * @memberof Help4.widget.help.project.SEN
     * @private
     * @param {Help4.widget.help.project.SEN.Project} project
     * @param {Help4.widget.help.project.SEN.TagInfoList} tagInfo
     * @param {Help4.typedef.SystemConfiguration} config
     * @returns {{published: Help4.widget.help.PUBLISHED_STATUS, playbackTag: Help4.widget.help.PUBLISHED_STATUS}}
     */
    function _getPublishedTagInfo(project, tagInfo, config) {
        const {/** @type {Help4.widget.help.project.SEN.ProjectTags} */ Tags = []} = project;
        const {PUBLISHED_STATUS} = Help4.widget.help;

        /**
         * @param {string} tagName
         * @returns {Help4.widget.help.PUBLISHED_STATUS}
         */
        const get = tagName => {
            /**
             * @param {Help4.widget.help.project.SEN.ProjectTag} tag
             * @returns {boolean}
             */
            const find = tag => tag.tag === tagName;
            /** @type {Help4.widget.help.project.SEN.ProjectTag|undefined} */
            const tag = Tags.find(find);
            return tag
                ? tag.version === project.maxversion ? PUBLISHED_STATUS.published : PUBLISHED_STATUS.updated
                : PUBLISHED_STATUS.new;
        }

        /** @type {Help4.widget.help.PUBLISHED_STATUS} */
        const published = get('published');

        let {/** @type {string|Help4.widget.help.PUBLISHED_STATUS} */ playbackTag} = config.core;
        if (playbackTag && playbackTag !== 'published') {
            const valid = !!tagInfo.find(({id}) => id === playbackTag);

            if (valid) {
                /** @type {Help4.widget.help.PUBLISHED_STATUS} */
                playbackTag = get(playbackTag);
                return {published, playbackTag};
            }
        }

        return {published, playbackTag: PUBLISHED_STATUS.invalid};
    }
})();