Source: MakeupSDK.js

import { SdkAnalytics } from "./Analytics";
import { FrameProvider } from "./FrameProvider";
import Progress from "./Utility/Progress";
import { MakeupEngine } from "./MakeupEngine";
import { CreateModulesManager } from "./ModulesManager";
import { SkinToneAnalyzer } from "./SkinToneAnalyzer";
import Globals from "./Globals";
import { DefaultFeatureAndFinishService } from "./DefaultFeatureAndFinishService";
import { FaceTracker } from "./FaceTracker";
import { FaceShapeAnalyzer } from "./FaceShapeAnalyzer";
import { EyeToneAnalyzer } from "./EyeToneAnalyzer";
import { EyeShapeAnalyzer } from "./EyeShapeAnalyzer";
import { LightConditionsAnalyzer } from "./LightConditionsAnalyzer";
import { HairToneAnalyzer } from "./HairToneAnalyzer";
import { CmsMakeupEngine } from "./CmsMakeupEngine";
import { LipShapeAnalyzer } from "./LipShapeAnalyzer";
import { GlobalEngine } from "./GlobalEngine";

/**
 * @callback onProgressCallback
 * @param {number} progress - a number from 0 to 1 indicating the progress level
 */

/**
 * Sets onProgressCallback.
 * @function SetOnProgressCallback
 * @param {onProgressCallback} onProgressCallback - callback which will be called every time loading progresses
 */
export const SetOnProgressCallback = (onProgressCallback) => {
    Progress.SetOnProgressCallback(onProgressCallback);
};

/**
 * @typedef {number} SkinToneTargetQuality
 **/

/**
 * @enum {SkinToneTargetQuality}
 */
export const SKINTONE_TARGET_QUALITY = Object.freeze({
    MORE_SPEED: 0,
    MORE_ACCURACY: 1,
});

/**
 * @typedef {number} TrackingQuality
 **/

/**
 * @enum {TrackingQuality}
 */
export const TRACKING_QUALITY = Object.freeze({
    HIGH: 0,
    LOW: 1,
});

/** @type {SDKProvider | undefined} */
let defaultProviderSingleton = undefined;
const DEFAULT_CMS_URL = "https://cms.arbelle.ai:3000/api/v2.0";

/**
 * Gets or creates a SDKProvider singleton instance.
 *
 * @function GetProvider
 * @param {string} pathToAssets - url of the folder containing SDK Assets folder
 * @param {Object.<string, { license: string, isRaw: boolean }>} licenses - Map between String key and String value where
 * key is one of SKINTONE or MAKEUP and value is path to appropriate license for type defined by key.
 * @param {TrackingQuality} [trackingQuality=TrackingQuality.HIGH] - sets the quality of internal face tracker used by the SDK. Possible values are: HIGH: 0(default) or LOW: 1
 * @param {string} [accessToken=""] - token used to provide access to the services.
 * @param {string} [cmsURL="https://cms.arbelle.ai:3000/api/v2.0"] - URL on which analytics requests are being sent. If no URL is provided, the default CMS URL will be used.
 * @returns {Promise<SDKProvider>} SDKProvider singleton.
 */
export const GetProvider = async (
    pathToAssets,
    licenses,
    trackingQuality = TRACKING_QUALITY.HIGH,
    accessToken = "",
    cmsURL = DEFAULT_CMS_URL,
) => {
    if (defaultProviderSingleton) {
        return defaultProviderSingleton;
    }

    let sdkAnalytics = null
    if (accessToken && cmsURL) {
        sdkAnalytics = await SdkAnalytics.create(cmsURL, accessToken);
    }

    Globals.pathToAssets = pathToAssets;

    const modulesManager = await CreateModulesManager(licenses);

    const highOrLow = trackingQuality == TRACKING_QUALITY.HIGH ? "high" : "low";
    const trackingQualityCfgPath = `${Globals.pathToAssets}Assets/Plugins/Facial Features Tracker - ${highOrLow}.cfg`;

    const analyticsConfig =
        await modulesManager.modules.visage.module.AnalyticsCollectionConfig.Create(
            cmsURL,
            accessToken,
            (await sdkAnalytics)?.sessionUuid ?? "",
            1000,
        );

    await modulesManager.modules.visage.module.CreateBSCProvider(
        trackingQualityCfgPath,
        analyticsConfig,
    );

    const frameProvider = new FrameProvider(modulesManager);


    defaultProviderSingleton = new SDKProvider(
        modulesManager,
        frameProvider,
        sdkAnalytics
    );

    return defaultProviderSingleton;
};

/**
 * Provides all makeup|SDK modules.
 */
class SDKProvider {
    /**
     * @param {*} modulesManager 
     * @param {FrameProvider} frameProvider 
     * @param {SdkAnalytics | null} sdkAnalytics 
     */
    constructor(
        modulesManager,
        frameProvider,
        sdkAnalytics,
    ) {
        /** @type {MakeupEngine | undefined} */
        this.makeupEngine = undefined;
        /** @type {CmsMakeupEngine | undefined} */
        this.cmsMakeupEngine = undefined;
        
        this.modulesManager = modulesManager;
        this.frameProvider = frameProvider;
        this.sdkAnalytics = sdkAnalytics

        if (this.sdkAnalytics) {
            window.addEventListener("pagehide", () => {
                this.sdkAnalytics?.onUnload();
            });
        }

        // misc services
        this.featuresAndFinishesService = new DefaultFeatureAndFinishService();
    }

    /**
     * Gets the FrameProvider instance.
     * Use this to pass frames to the makeup engine, skin tone analyzer or face tracker.
     *
     * @return FrameProvider instance.
     */
    GetFrameProvider() {
        return this.frameProvider;
    }

    /**
     * Gets the session uuid used by the Analytics API.
     *
     * @return {string} session uuid.
     */
    GetSessionUUID() {
        return this.sdkAnalytics?.sessionUuid ?? "";
    }

    /**
     * @deprecated Please use CreateMakeupEngine method.
     *
     * Gets or creates a MakeUpEngine singleton instance.
     *
     * @function CreateMakeUpEngine
     * @param {number} width - integer width of the output frame
     * @param {number} height - integer height of the output frame
     * @param {boolean} preserveDrawingBuffer - sets the flag on WebGl (default = false)
     * @returns {Promise<MakeupEngine>} MakeUpEngine singleton.
     */
    async CreateMakeUpEngine(width, height, preserveDrawingBuffer = false) {
        return this.CreateMakeupEngine(width, height, preserveDrawingBuffer);
    }

    /**
     * Gets or creates a MakeupEngine singleton instance.
     *
     * @function CreateMakeupEngine
     * @param {number} width - integer width of the output frame
     * @param {number} height - integer height of the output frame
     * @param {boolean} preserveDrawingBuffer - sets the flag on WebGl (default = false)
     * @returns {Promise<MakeupEngine>} MakeupEngine singleton.
     */
    async CreateMakeupEngine(width, height, preserveDrawingBuffer = false) {
        if (!GlobalEngine.instance) GlobalEngine.Initialize(
            {
                frameProvider: this.frameProvider,
                modulesManager: this.modulesManager
            },
            { width, height },
            preserveDrawingBuffer
        )

        if (this.makeupEngine !== undefined) return this.makeupEngine;

        const pathToAssets = Globals.pathToAssets;

        const [features, finishes] = await Promise.all([
            this.featuresAndFinishesService.GetFeatureTemplates(pathToAssets),
            this.featuresAndFinishesService.GetFinishTemplates(pathToAssets),
        ]);

        this.makeupEngine = new MakeupEngine(features,finishes,this.sdkAnalytics)

        return this.makeupEngine;
    }

    /**
     * Gets or creates a CmsMakeupEngine singleton instance.
     *
     * @function CreateCmsMakeupEngine
     * @param {number} width - integer width of the output frame
     * @param {number} height - integer height of the output frame
     * @param {boolean} preserveDrawingBuffer - sets the flag on WebGl (default = false)
     * @returns {Promise<CmsMakeupEngine>} MakeupEngine singleton.
     */
    async CreateCmsMakeupEngine(
        width,
        height,
        preserveDrawingBuffer = false, //TODO(Nino) rename
    ) {
        if (!GlobalEngine.instance) GlobalEngine.Initialize(
            {
                frameProvider: this.frameProvider,
                modulesManager: this.modulesManager
            },
            { width, height },
            preserveDrawingBuffer
        )

        if (this.cmsMakeupEngine !== undefined) return this.cmsMakeupEngine;

        const pathToAssets = Globals.pathToAssets;

        const [features, finishes] = await Promise.all([
            this.featuresAndFinishesService.GetFeatureTemplates(pathToAssets),
            this.featuresAndFinishesService.GetFinishTemplates(pathToAssets),
        ]);

        this.cmsMakeupEngine = new CmsMakeupEngine(
            features, finishes, this.sdkAnalytics
        );

        return this.cmsMakeupEngine;
    }

    /**
     * Configures skin tone quality. Must be set before SkinToneAnalyzer.
     * is created or it will have no effect
     *
     * MORE_SPEED: 0(default)
     *
     * MORE_ACCURACY: 1
     *
     * @param {SkinToneTargetQuality} skinToneTargetQuality
     */
    SetSkinToneTargetQuality(skinToneTargetQuality) {
        this.skinToneTargetQuality = skinToneTargetQuality;
    }

    /**
     * @callback onSkinToneProgressCallback
     * @param {Object}  status                - information on current progress
     * @param {number}  status.contentLength  - the whole length of the content in bytes
     * @param {number}  status.receivedLength - the currently received length in bytes
     * @param {number}  status.progress       - a number from 0 to 1 indicating the progress level
     * @param {boolean} status.done           - true if the creation process is done, false otherwise
     */

    /**
     * Gets or creates a SkinToneAnalyzer singleton instance.
     * <br/>
     *
     * @function CreateSkinToneAnalyzer
     * @param {undefined | onSkinToneProgressCallback} onProgressCallback - callback which will be called every time loading progresses
     * @returns {Promise<SkinToneAnalyzer>}
     */
    async CreateSkinToneAnalyzer(onProgressCallback = undefined) {
        const analyzer = await SkinToneAnalyzer.Create(
            this.modulesManager,
            this.skinToneTargetQuality ?? SKINTONE_TARGET_QUALITY.MORE_SPEED,
            onProgressCallback,
        );
        return analyzer;
    }

    /**
     * Gets or creates a SkinToneAnalyzer singleton instance.
     * <br/>
     *
     * @function CreateEyeToneAnalyzer
     * @param {undefined | onSkinToneProgressCallback} onProgressCallback - callback which will be called every time loading progresses
     * @returns {Promise<EyeToneAnalyzer>}
     */
    async CreateEyeToneAnalyzer(onProgressCallback = undefined) {
        const analyzer = await EyeToneAnalyzer.Create(
            this.modulesManager,
            onProgressCallback,
        );
        return analyzer;
    }

    /**
     * Gets or creates a FaceTracker singleton instance.
     * @function CreateFaceTracker
     * @returns {Promise<FaceTracker>} FaceTracker singleton.
     */
    async CreateFaceTracker() {
        const analyzer = await FaceTracker.Create(this.modulesManager);
        return analyzer;
    }

    /**
     * Starts creating shape analyzer.
     * <br/>
     * Returns a promise which will be resolved when analyzer has been created.
     * @function CreateFaceShapeAnalyzer
     * @param {undefined | onSkinToneProgressCallback} onProgressCallback - callback which will be called every time loading progresses
     * @returns {Promise<FaceShapeAnalyzer>}
     */
    async CreateFaceShapeAnalyzer(onProgressCallback = undefined) {
        const analyzer = await FaceShapeAnalyzer.Create(
            this.modulesManager,
            onProgressCallback,
        );
        return analyzer;
    }

    /**
     * Starts creating hair tone analyzer.
     * <br/>
     * Returns a promise which will be resolved when analyzer has been created.
     * @function CreateHairToneAnalyzer
     * @param {undefined | onSkinToneProgressCallback} onProgressCallback - callback which will be called every time loading progresses
     * @returns {Promise<HairToneAnalyzer>}
     */
    async CreateHairToneAnalyzer(onProgressCallback = undefined) {
        const analyzer = await HairToneAnalyzer.Create(
            this.modulesManager,
            onProgressCallback,
        );
        return analyzer;
    }

    /**
     * Configures face uniformity threshold. Must be set before LightConditionsAnalyzer
     * is created or it will have no effect.
     *
     * @param {number} uniformityThreshold
     */
    SetLightConditionsUniformityThreshold(uniformityThreshold) {
        this.lightConditionsUniformityThreshold = uniformityThreshold;
    }

    /**
     * Configures face uniformity threshold. Must be set before LightConditionsAnalyzer
     * is created or it will have no effect.
     *
     * @param {number} backlightThreshold
     */
    SetLightConditionsBacklightThreshold(backlightThreshold) {
        this.lightConditionsBacklightThreshold = backlightThreshold;
    }

    /**
     * Starts creating light conditions analyzer.
     * <br/>
     * Returns a promise which will be resolved when analyzer has been created.
     * @function CreateLightConditionsAnalyzer
     * @param {undefined | onSkinToneProgressCallback} onProgressCallback - callback which will be called every time loading progresses
     * @returns {Promise<LightConditionsAnalyzer>}
     */
    async CreateLightConditionsAnalyzer(onProgressCallback = undefined) {
        const analyzer = await LightConditionsAnalyzer.Create(
            this.modulesManager,
            this.lightConditionsUniformityThreshold,
            this.lightConditionsBacklightThreshold,
            onProgressCallback,
        );
        return analyzer;
    }

    /**
     * Starts creating lip shape analyzer.
     * <br/>
     * Returns a promise which will be resolved when analyzer has been created.
     * @function CreateLipShapeAnalyzer
     * @param {undefined | onSkinToneProgressCallback} onProgressCallback - callback which will be called every time loading progresses
     * @returns {Promise<LipShapeAnalyzer>}
     */
    async CreateLipShapeAnalyzer(onProgressCallback = undefined) {
        const analyzer = await LipShapeAnalyzer.Create(
            this.modulesManager,
            onProgressCallback,
        );
        return analyzer;
    }

    /**
     * Starts creating eye shape analyzer.
     * <br/>
     * Returns a promise which will be resolved when analyzer has been created.
     * @function CreateEyeShapeAnalyzer
     * @param {undefined | onSkinToneProgressCallback} onProgressCallback - callback which will be called every time loading progresses
     * @returns {Promise<EyeShapeAnalyzer>}
     */
    async CreateEyeShapeAnalyzer(onProgressCallback = undefined) {
        const analyzer = await EyeShapeAnalyzer.Create(
            this.modulesManager,
            onProgressCallback,
        );
        return analyzer;
    }
}