// Do not delete this as it allows importing the package with other projects
import "regenerator-runtime/runtime.js";
import { Configurator } from "./configurator";
import { EventObservable } from "./event-observable";
import { IConfigurator } from "./interfaces/configurator.interface";
import {
    EventObservableTypes,
    IEventObservable,
} from "./interfaces/event-observable.interface";
import {
    IPostMessage,
    IPostMessageOrigin,
    MaterializeMeshConfig,
    IViewerCommunicator,
    MeshData,
    IViewerCommunicatorOptions,
    ICreateInstanceFromUrlOptions,
    Collision,
    IImagesByTourResponse,
    ILight,
    IShadowPlaneOptions,
    IHdriOptions,
    IAdjustmentsPresetJson,
    IBroadcastSceneSummaryOption,
    ISceneSummary,
    IMeshProps,
    JsonToHtmlObject,
    EventSelector,
    TutorialType,
    IExportOptions,
    IExpotedModel,
    ExportFileType,
    IGifGenOptions,
    MediaFormat,
    IMatcapOptions,
    IApplyTextureOptions,
    TextureMimeType,
    IBoundingBox,
    IAnimationOptions,
    ICameraControlsStateAnimation,
    IMaterialPropsOptions,
    ISwapMaterialType,
    IDiamondOptions,
} from "./interfaces/viewer-communicator.interface";
import { ImageToVideo } from "./image-to-video";

export class ViewerCommunicator implements IViewerCommunicator {
    public configurator: IConfigurator;
    private _hexaViewer: HTMLElement;
    private _frameID: string;
    private _isViewerLoaded: boolean;
    private _isModelLoaded: boolean;
    private _isAnimateEnterEnd: boolean;
    private _isViewerListening: boolean;
    // private _onGetMeshesData: Array<Function>;
    // private _onCollisions = [] as Array<Function>;
    private _onMessageBind: any;
    private _meshesData: { [id: string]: MeshData };
    private _mesheAnimations: { [id: string]: IAnimationOptions };
    private _xrSupport: boolean;
    private _eventObservable: IEventObservable;
    private _hasDestroyed: boolean;
    private _onLoadingProgress: Array<any>;
    constructor(options?: IViewerCommunicatorOptions) {
        this._hasDestroyed = false;
        options = options || {};
        this._hexaViewer = options.hexaViewer;
        this._isViewerLoaded = false;
        this._isModelLoaded = false;
        this._isAnimateEnterEnd = false;
        this._onLoadingProgress = [];
        this.initFrameID();
        this.attachEvents();
        this.initEventObservable();
        this.configurator = new Configurator(this);
        // Update the current viewer state.
        // In case it's too soon this won't even get to the viewer because he's not listening.
        // In case the viewer is already listening and a model has already been loaded the
        // await this.onModelLoaded() will return instead of never returning and preventing
        // all communicator functionality.
        this._updateViewerFullyLoaded();
    }

    get hexaViewer() {
        return this._hexaViewer;
    }

    set hexaViewer(hv: HTMLElement) {
        this.attachInstance(hv);
    }

    get eventObservable() {
        return this._eventObservable;
    }

    get isModelLoaded() {
        return this._isViewerLoaded;
    }

    get hasDestroyed() {
        return this._hasDestroyed;
    }

    get isViewerListening() {
        return this._isViewerListening;
    }

    private set isViewerListening(value: boolean) {
        this._isViewerListening = value;
        if (this._isViewerListening)
            this._eventObservable.invoke(
                EventObservableTypes.ON_VIEWER_LISTENING,
                false
            );

    }

    private set isModelLoaded(value: boolean) {
        if (value) this._onViewerFullyLoaded();
        else this._isViewerLoaded = value;
    }

    private initEventObservable() {
        this._eventObservable = new EventObservable();
    }

    private attachInstance(hexaViewer: HTMLElement) {
        this._hexaViewer = hexaViewer;
        this.initFrameID();
    }

    private initFrameID(elem?: HTMLElement) {
        elem = elem || this._hexaViewer;
        if (elem) {
            this._frameID = elem.getAttribute("frame-id");
            if (!this._frameID) {
                this._frameID = this.generateUUID();
                elem.setAttribute("frame-id", this._frameID);
            }
        }
    }

    private attachEvents() {
        this._onMessageBind = this.onMessage.bind(this);
        self.addEventListener("message", this._onMessageBind, false);
    }

    private onMessage(event: MessageEvent) {
        const obj = this.safeParse(event.data) as IPostMessage;
        if (obj) {
            if (this._frameID && obj.id && this._frameID !== obj.id) return;
            // if (obj.to === IPostMessageOrigin.TOP) {
            switch (obj.action) {
                case "viewerListening": {
                    this.isViewerListening = true;
                    break;
                }
                case "viewerFullyLoaded": {
                    this._onViewerFullyLoaded();
                    break;
                }
                case "onModelLoaded": {
                    this._onModelLoaded();
                    break;
                }
                case "onAnimateEnterEnd": {
                    this._onAnimateEnterEnd();
                    break;
                }
                case "setMeshesData": {
                    this._onMeshesData(obj.data);
                    break;
                }
                case "setMeshAnimations": {
                    this._onMeshAnimations(obj.data);
                    break;
                }
                case "setCollisions": {
                    this._eventObservable.invoke(
                        EventObservableTypes.ON_COLLISIONS,
                        false,
                        [obj.data]
                    );
                    break;
                }
                case "setSceneSummary": {
                    if (obj.data?.autoAdjustScene)
                        this._eventObservable.invoke(
                            EventObservableTypes.ON_ADJUDT_SCENE,
                            true,
                            [obj.data]
                        );
                    this._eventObservable.invoke(
                        EventObservableTypes.ON_SET_SCENE_SUMMARY,
                        true,
                        [obj.data]
                    );
                    break;
                }
                case "setAttachJsonScene": {
                    this._eventObservable.invoke(
                        EventObservableTypes.ON_APPLY_PRESET,
                        true
                    );
                    break;
                }
                case "create_images_by_tour": {
                    this._eventObservable.invoke(
                        EventObservableTypes.ON_CREATE_IMAGES_BY_TOUR,
                        true,
                        [obj.data]
                    );
                    break;
                }
                case "onLightsSummary": {
                    this._eventObservable.invoke(
                        EventObservableTypes.ON_LIGHTS_SUMMARY,
                        true,
                        [obj.data]
                    );
                    break;
                }
                case "onConfiguratorSelectDone": {
                    this._eventObservable.invoke(
                        EventObservableTypes.ON_CONFIGURATOR_SELECT_DONE,
                        true,
                        [obj.data]
                    );
                    break;
                }
                case "onModelInteraction": {
                    this._eventObservable.invoke(
                        EventObservableTypes.ON_MODEL_INTERACTION,
                        false,
                        [obj.data.type]
                    );
                    break;
                }
                case "setViewerFullyLoaded": {
                    this._eventObservable.invoke(
                        EventObservableTypes.ON_SET_VIEWER_FULLY_LOADED,
                        true,
                        [obj.data]
                    );
                    break;
                }
                case "setCurrentScreenshot": {
                    this._eventObservable.invoke(
                        EventObservableTypes.ON_SCREENSHOT,
                        true,
                        [obj.data]
                    );
                    break;
                }
                case "onScreenshotsSequence": {
                    this._eventObservable.invoke(
                        EventObservableTypes.ON_SCREENSHOTS_SEQUENCE,
                        true,
                        [obj.data.images]
                    );
                    break;
                }
                case "setModel": {
                    this._eventObservable.invoke(
                        EventObservableTypes.ON_EXPORT,
                        true,
                        [obj.data]
                    );
                    break;
                }
                case "setBoundingBox": {
                    this._eventObservable.invoke(
                        EventObservableTypes.ON_BOUNDING_BOX,
                        true,
                        [obj.data]
                    );
                    break;
                }
                case "setMaterials": {
                    this._eventObservable.invoke(
                        EventObservableTypes.ON_GET_MATERIALS,
                        true,
                        [obj.data]
                    );
                    break;
                }
                case "setDiamondEffectOptions": {
                    this._eventObservable.invoke(
                        EventObservableTypes.ON_GET_DIAMONDS_OPTIONS,
                        true,
                        [obj.data]
                    );
                    break;
                }
                case "setLoadingPercentage": {
                    this._onLoadingProgress.forEach(f => f(obj.data));
                    break;
                }
            }
            // }
        }
    }

    private _checkModelLoaded() {
        if (
            this._isViewerLoaded &&
            (this._isModelLoaded || this._isAnimateEnterEnd)
        )
            this._eventObservable.invoke(
                EventObservableTypes.ON_MODEL_LOADED,
                true
            );
    }

    private _onAnimateEnterEnd() {
        this._isAnimateEnterEnd = true;
        this._checkModelLoaded();
        this._eventObservable.invoke(
            EventObservableTypes.ON_ANIMATE_ENTER_END,
            true
        );
    }

    private _onModelLoaded() {
        this._isModelLoaded = true;
        this._checkModelLoaded();
    }

    private _onViewerFullyLoaded() {
        this._isModelLoaded = true;
        this._isViewerLoaded = true;
        this._eventObservable.invoke(
            EventObservableTypes.ON_VIEWER_LOADED,
            true
        );
        this._checkModelLoaded();
    }

    private _onMeshesData(obj: { [id: string]: MeshData }) {
        this._meshesData = obj;
        this.isModelLoaded = true;
        this._eventObservable.invoke(
            EventObservableTypes.ON_GET_MESHES_DATA,
            true,
            [this._meshesData]
        );
        // if (this._meshesData) {
        //     setTimeout(() =>{
        //         if (this._meshesData) {
        //             Object.values(this._meshesData).forEach(md => {
        //                 if (md.rotation) {
        //                     md.rotationDegree = {} as ThreeVector3int;
        //                     md.rotationDegree.x = parseFloat(md.rotation.x.toString()) / (Math.PI / 180);
        //                     md.rotationDegree.y = parseFloat(md.rotation.y.toString()) / (Math.PI / 180);
        //                     md.rotationDegree.z = parseFloat(md.rotation.z.toString()) / (Math.PI / 180);
        //                 }
        //             });
        //         }
        //     });
        // }
    }

    private _onMeshAnimations(obj: { [id: string]: IAnimationOptions }) {
        this._mesheAnimations = obj;
        this._eventObservable.invoke(
            EventObservableTypes.ON_GET_MESHE_ANIMATIONS,
            true,
            [this._mesheAnimations]
        );
    }

    private _updateViewerFullyLoaded() {
        return new Promise((resolve: any, reject: any) => {
            this._eventObservable.add(
                EventObservableTypes.ON_SET_VIEWER_FULLY_LOADED,
                (state: boolean) => {
                    this._isViewerLoaded = state;
                    if (this._isViewerLoaded) {
                        this.onModelLoaded();
                        this._isViewerListening = true;
                        this._onViewerFullyLoaded();
                    }
                    resolve();
                }
            );
            const msg = {
                action: "broadcastViewerFullyLoaded",
                to: IPostMessageOrigin.WORKER,
            };
            this.sendToViewer(msg);
        });
    }

    onModelLoaded() {
        return new Promise((resolve: any, reject: any) => {
            if (this._isViewerLoaded) resolve();
            else
                this._eventObservable.add(
                    EventObservableTypes.ON_VIEWER_LOADED,
                    resolve
                );
        });
    }

    // on model ready is a later event then onModelLoaded
    // it will fire after the enter animation is done (in case there is one)
    // in case there isn't it will fire next to the onModelLoaded
    onModelReady() {
        return new Promise((resolve: any, reject: any) => {
            if (this._isModelLoaded) resolve();
            else
                this._eventObservable.add(
                    EventObservableTypes.ON_MODEL_LOADED,
                    resolve
                );
        });
    }

    onViewerListening() {
        return new Promise((resolve: any, reject: any) => {
            if (this._isViewerListening) resolve();
            else
                this._eventObservable.add(
                    EventObservableTypes.ON_VIEWER_LISTENING,
                    resolve
                );
        });
    }



    onAnimateEnterEnd() {
        return new Promise((resolve: any, reject: any) => {
            if (this._isAnimateEnterEnd) resolve();
            else
                this._eventObservable.add(
                    EventObservableTypes.ON_ANIMATE_ENTER_END,
                    resolve
                );
        });
    }

    getMeshesData(): Promise<{ [id: string]: MeshData }> {
        return new Promise((resolve: any, reject: any) => {
            this._eventObservable.add(
                EventObservableTypes.ON_GET_MESHES_DATA,
                resolve
            );
            const msg = {
                action: "getMeshesData",
                to: IPostMessageOrigin.WORKER,
            };
            this.sendToViewer(msg);
        });
    }

    getMeshAnimations(): Promise<{ [id: string]: IAnimationOptions }> {
        return new Promise((resolve: any, reject: any) => {
            this._eventObservable.add(
                EventObservableTypes.ON_GET_MESHE_ANIMATIONS,
                resolve
            );
            const msg = {
                action: "broadcastMeshAnimations",
                to: IPostMessageOrigin.WORKER,
            };
            this.sendToViewer(msg);
        });
    }

    getMaterials(): Promise<{ [id: string]: IAnimationOptions }> {
        return new Promise((resolve: any, reject: any) => {
            this._eventObservable.add(
                EventObservableTypes.ON_GET_MATERIALS,
                resolve
            );
            const msg = {
                action: "broadcastMaterials",
                to: IPostMessageOrigin.WORKER,
            };
            this.sendToViewer(msg);
        });
    }

    updateMeshAnimations(meshAnimations: { [id: string]: IAnimationOptions }): void {
        this.sendToViewer({
            action: "updateMeshAnimations",
            to: IPostMessageOrigin.WORKER,
            value: { meshAnimations }
        });
    }

    private safeParse(p: any) {
        if (typeof p === "string") {
            try {
                return JSON.parse(p);
            } catch (e) { }
        }
        return p;
    }

    private generateUUID(): string {
        var d = new Date().getTime();
        if (
            window.performance &&
            typeof window.performance.now === "function"
        ) {
            d += performance.now();
        }
        var uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
            /[xy]/g,
            (c) => {
                var r = (d + Math.random() * 16) % 16 | 0;
                d = Math.floor(d / 16);
                return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
            }
        );
        return uuid;
    }

    materializeMesh(meshName: string, config: MaterializeMeshConfig) {
        const msg = {
            action: "materializeMesh",
            to: IPostMessageOrigin.WORKER,
            value: {
                name: meshName,
                config: config,
            },
        };
        this.sendToViewer(msg);
    }

    sendToViewer(msg: IPostMessage) {
        // msg.from = IPostMessageOrigin.TOP;
        if (this._frameID) msg.id = this._frameID;
        self.postMessage(msg, location.origin);
    }

    // Creates a <hexa-viewer> instance from a viewer URL (typically the resource_default)
    async createInstanceFromUrl(
        viewerURL: string,
        params = {} as any,
        options?: ICreateInstanceFromUrlOptions
    ): Promise<HTMLElement> {
        options = options || {};
        var hv = document.createElement("hexa-viewer");
        var p = this.getUrlParams(viewerURL);
        var allP = Object.assign(p, params);
        if (typeof allP.server === "undefined") allP.server = "1";
        if (typeof allP["frame-id"] === "undefined")
            allP["frame-id"] = this.generateUUID();
        if (options.enableWebXR) {
            if (await this.isWebXrSupported()) allP["offscreen"] = "false";
        }
        if (options.themeColor) allP["theme-color"] = options.themeColor;
        for (let i in allP)
            i && hv.setAttribute(i, allP[i] === null ? "" : allP[i]);
        this.initFrameID(hv);
        return hv;
    }

    private getUrlParams(url: string) {
        let queryString = {} as any,
            query = "";
        if (url.indexOf("?") > -1) query = url.substring(url.indexOf("?") + 1);
        if (!query) return queryString;
        query = query.split("+").join(" ");
        let vars = query.split("&");
        for (let i = 0; i < vars.length; i++) {
            let pair = vars[i].split("=");
            if (typeof queryString[pair[0]] === "undefined") {
                queryString[pair[0]] = pair[1]
                    ? decodeURIComponent(pair[1])
                    : null;
            } else if (typeof queryString[pair[0]] === "string") {
                let arr = [queryString[pair[0]], decodeURIComponent(pair[1])];
                queryString[pair[0]] = arr;
            } else {
                queryString[pair[0]].push(decodeURIComponent(pair[1]));
            }
        }
        return queryString;
    }

    togglePicInPic(state: boolean) {
        const msg = {
            action: "togglePicInPic",
            to: IPostMessageOrigin.MAIN,
            value: state,
        };
        this.sendToViewer(msg);
    }

    toggleWireframe(state: boolean) {
        const msg = {
            action: "toggleWireframe",
            to: IPostMessageOrigin.WORKER,
            value: state,
        };
        this.sendToViewer(msg);
    }

    toggleUvMode(state: boolean) {
        const msg = {
            action: "toggleUvMode",
            to: IPostMessageOrigin.WORKER,
            value: state,
        };
        this.sendToViewer(msg);
    }

    toggleMatcapMode(state: boolean, options?: IMatcapOptions) {
        const msg = {
            action: "toggleMatcapMode",
            data: options,
            to: IPostMessageOrigin.WORKER,
            value: state,
        };
        this.sendToViewer(msg);
    }

    toggleHideBottom(state: boolean) {
        const msg = {
            action: "toggleHideBottom",
            to: IPostMessageOrigin.WORKER,
            value: state,
        };
        this.sendToViewer(msg);
    }

    isWebXrSupported(): Promise<boolean> {
        return new Promise((resolve: any, reject: any) => {
            if (typeof this._xrSupport === "boolean") {
                resolve(this._xrSupport);
                return;
            }
            const navigatorAny = navigator as any;
            if (!navigatorAny.xr || !navigatorAny.xr.isSessionSupported) {
                this._xrSupport = false;
                resolve(this._xrSupport);
                return;
            }
            try {
                navigatorAny.xr.isSessionSupported("immersive-ar").then(
                    (res: boolean) => {
                        this._xrSupport = res;
                        resolve(this._xrSupport);
                    },
                    (e: Error) => {
                        this._xrSupport = false;
                        resolve(this._xrSupport);
                    }
                );
            } catch (e) {
                this._xrSupport = false;
                resolve(this._xrSupport);
            }
        });
    }

    toggleWebXR(state: boolean, invokeWhenReady = true) {
        const msg = {
            action: "toggleXr",
            obj: {
                state: state,
                value: invokeWhenReady,
                showIcon: true,
            },
        } as IPostMessage;
        this.sendToViewer(msg);
    }

    toggleAR(state: boolean, invokeWhenReady = true) {
        const msg = {
            action: "toggleAR",
            obj: {
                state: state,
                value: invokeWhenReady
            },
        } as IPostMessage;
        this.sendToViewer(msg);
    }

    waitForCollisions(): Promise<Array<Collision>> {
        return new Promise((resolve: any, reject: any) => {
            this._eventObservable.add(
                EventObservableTypes.ON_COLLISIONS,
                resolve
            );
        });
    }

    toggleCollision(collisionMode: boolean, color?: string) {
        this.sendToViewer({
            action: "toggleCollision",
            to: IPostMessageOrigin.WORKER,
            value: {
                value: collisionMode,
                color,
            },
        });
    }

    deleteCollision(position: number, count: number) {
        this.sendToViewer({
            action: "spliceCollision",
            to: IPostMessageOrigin.WORKER,
            value: {
                value: position,
                count,
            },
        });
    }

    removeAllCollisions() {
        this.sendToViewer({
            action: "removeAllCollisions",
            to: IPostMessageOrigin.WORKER,
        });
    }

    adjustScene() {
        return new Promise(async (resolve: any, reject: any) => {
            this._eventObservable.add(
                EventObservableTypes.ON_ADJUDT_SCENE,
                resolve
            );
            await this.onModelLoaded();
            this.sendToViewer({
                action: "adjustScene",
                to: IPostMessageOrigin.WORKER,
            });
        });
    }

    // Not going to work without reloading instance or supporting each parameter on the viewer level
    // private applyPresetSrc(params: any): void {
    //     for (let i in params) {
    //         this.hexaViewer.setAttribute(i, params[i]);
    //     }
    // }

    applyPreset(json: IAdjustmentsPresetJson) {
        return new Promise(async (resolve: any, reject: any) => {
            this._eventObservable.add(
                EventObservableTypes.ON_APPLY_PRESET,
                resolve
            );
            // Make sure model has loaded
            await this.onModelLoaded();
            this.sendToViewer({
                action: "attachJsonScene",
                to: IPostMessageOrigin.WORKER,
                data: json,
            });
            // // Make sure all viewer parameters has taken effect
            // await this.broadcastSceneSummary();
            // if (preset?.preset_json) {
            //     if (preset.preset_json.shadowPlane)
            //         this.applyShadowPlane(preset.preset_json.shadowPlane);
            //     if (preset.preset_json.hdri) {
            //         if (!preset.preset_json.hdri.type)
            //             preset.preset_json.hdri.intensity = 0;
            //         this.applyHDRI(preset.preset_json.hdri);
            //     }
            //     if (preset.preset_json.lights)
            //         await this.setLightsByJson(preset.preset_json.lights);
            //     if (preset.preset_json.params)
            //         this.assetAdjustmentsService.applyParams();
            // }
        });
    }

    applyHDRI(hdri: IHdriOptions) {
        this.sendToViewer({
            action: "setHDR",
            to: IPostMessageOrigin.WORKER,
            value: hdri.type,
            options: hdri,
        });
    }

    applyShadowPlane(shadowPlane: IShadowPlaneOptions) {
        this.sendToViewer({
            action: "togglePlane",
            to: IPostMessageOrigin.WORKER,
            value: shadowPlane.opacity,
            options: {
                active: shadowPlane.active,
                color: shadowPlane.color,
                opacity: shadowPlane.opacity,
                physical: shadowPlane.physical,
                physicalOptions: shadowPlane.physicalOptions,
                reflector: shadowPlane.reflector,
                side: shadowPlane.side,
            },
        });
    }

    broadcastSceneSummary(
        params?: IBroadcastSceneSummaryOption
    ): Promise<ISceneSummary> {
        return new Promise((resolve: any, reject: any) => {
            this._eventObservable.add(
                EventObservableTypes.ON_SET_SCENE_SUMMARY,
                resolve
            );
            this.sendToViewer({
                action: "broadcastSceneSummary",
                to: IPostMessageOrigin.WORKER,
                value: params,
            });
        });
    }

    setLightsByJson(lights: { [id: string]: Array<ILight> }) {
        return new Promise((resolve: any, reject: any) => {
            this._eventObservable.add(
                EventObservableTypes.ON_LIGHTS_SUMMARY,
                resolve
            );
            this.sendToViewer({
                action: "setLightsByJson",
                value: lights,
                to: IPostMessageOrigin.WORKER,
            });
        });
    }

    onCreateImagesByTour(): Promise<IImagesByTourResponse> {
        return new Promise(async (resolve: any, reject: any) => {
            this._eventObservable.add(
                EventObservableTypes.ON_CREATE_IMAGES_BY_TOUR,
                resolve
            );
        });
    }

    setMeshProps(optins: IMeshProps) {
        const obj = {
            name: optins.mesh.name,
            props: {},
        } as any;
        // if (optins.key === 'rotation') {
        //     // radians to degrees
        //     optins.mesh.rotation.x = optins.mesh.rotationDegree.x * (Math.PI / 180);
        //     optins.mesh.rotation.y = optins.mesh.rotationDegree.y * (Math.PI / 180);
        //     optins.mesh.rotation.z = optins.mesh.rotationDegree.z * (Math.PI / 180);
        //     optins.value = optins.mesh.rotation;
        // }
        obj["props"][optins.key] = optins.value;
        if (!this._meshesData[obj.name])
            this._meshesData[obj.name] = {} as MeshData;
        (this._meshesData[obj.name] as any)[optins.key] = optins.value;
        this.sendToViewer({
            action: "setMeshProps",
            to: IPostMessageOrigin.WORKER,
            value: obj,
        });
    }

    appendDynamicElement(
        obj: JsonToHtmlObject,
        events: Array<EventSelector>,
        selectorToAppend = ".body"
    ) {
        this.sendToViewer({
            action: "appendElement",
            to: IPostMessageOrigin.MAIN,
            obj: {
                obj,
                events,
                selectorToAppend,
            },
        });
    }

    controlsTutorial(types?: Array<TutorialType>) {
        this.sendToViewer({
            action: "startZoomTutorial",
            to: IPostMessageOrigin.WORKER,
            value: types,
        });
    }

    toggleAutoRotate(state: boolean) {
        this.sendToViewer({
            action: "toggleAutoRotate",
            to: IPostMessageOrigin.WORKER,
            data: state,
        });
    }

    onModelInteraction(cb?: Function): Promise<string> {
        return new Promise((resolve: any, reject: any) => {
            this._eventObservable.add(
                EventObservableTypes.ON_MODEL_INTERACTION,
                (type: string) => {
                    if (cb) cb(type);
                    resolve(type);
                }
            );
        });
    }

    goToInitialCamPos() {
        this.sendToViewer({
            action: "goToInitialCamPos",
            to: IPostMessageOrigin.WORKER,
        });
    }

    getCurrentScreenshot(): Promise<string> {
        return new Promise(async (resolve: any, reject: any) => {
            this._eventObservable.add(
                EventObservableTypes.ON_SCREENSHOT,
                resolve
            );
            // await this.onModelReady();
            await this.onAnimateEnterEnd();
            this.sendToViewer({
                action: "getCurrentScreenshot",
                to: IPostMessageOrigin.WORKER,
            });
        });
    }

    expotModel(options: IExportOptions): Promise<IExpotedModel> {
        return new Promise(async (resolve: any, reject: any) => {
            if (!options.downloadFile)
                this._eventObservable.add(
                    EventObservableTypes.ON_EXPORT,
                    resolve
                );
            await this.onModelReady();
            let action = "",
                value = null;
            switch (options.type) {
                case ExportFileType.GLB:
                case ExportFileType.glTF: {
                    action = "broadcastGLTF";
                    value = {
                        binary: options.type === ExportFileType.GLB,
                        trs: false,
                        onlyVisible: true,
                        truncateDrawRange: false,
                        embedImages: true,
                        // animations: []
                        forceIndices: true,
                        forcePowerOfTwoTextures: true,
                        imagesFileType: null,
                        normalImagesFileType: null,
                        mroImagesFileType: null,
                        imagesCompressionFactor: null,
                        downloadFile: options.downloadFile,
                        maxTexturesSize: null,
                        maxDiffuseTexturesSize: null,
                        maxNormalTexturesSize: null,
                        maxMroTexturesSize: null,
                        optipng: options.compressPNG,
                        simplify: false,
                        deleteUV2: false,
                    };
                    break;
                }
                case ExportFileType.USDZ: {
                    action = "broadcastUSDZ";
                    break;
                }
                case ExportFileType.OBJ: {
                    action = "broadcastOBJ";
                    break;
                }
                default: {
                    action = "broadcastGLTF";
                    break;
                }
            }
            this.sendToViewer({
                action,
                value,
                to: IPostMessageOrigin.WORKER,
            });
            if (options.downloadFile) resolve(null);
        });
    }

    getScreenshotsSequence(
        options?: IGifGenOptions
    ): Promise<Array<string> | Blob> {
        return new Promise(async (resolve: any, reject: any) => {
            this._eventObservable.add(
                EventObservableTypes.ON_SCREENSHOTS_SEQUENCE,
                async (images: Array<string>) => {
                    if (options.format === MediaFormat.VIDEO) {
                        const i2v = new ImageToVideo(images);
                        if (options.codec)
                            i2v.codecs = options.codec;
                        resolve((await i2v.getVideo()) as Blob);
                    } else resolve(images);
                }
            );
            await this.onModelReady();
            await this.onAnimateEnterEnd();
            const finalOptions = {} as any;
            if (options) {
                finalOptions.ggNumOfFrames = options.numOfFrames;
                // finalOptions.ggInterval = options.interval;
                // finalOptions.ggSampleInterval = options.sampleInterval;
                // finalOptions.ggRotateSpeen = options.rotateSpeen;
            }
            finalOptions.msgObj = {
                data: {},
                action: "onScreenshotsSequence",
                to: IPostMessageOrigin.TOP,
                from: IPostMessageOrigin.WORKER,
            };
            this.sendToViewer({
                action: "broadcastGifSequence",
                to: IPostMessageOrigin.WORKER,
                data: {
                    options: finalOptions,
                },
            });
        });
    }

    applyTexture(options: IApplyTextureOptions) {
        this.sendToViewer({
            action: "mapTextureToMaterial",
            to: IPostMessageOrigin.WORKER,
            value: {
                texture: options.src,
                material: options.materialName,
                options: {
                    map: options.mapType,
                    intensity: options.intensity,
                    textureSrc: options.src,
                    srcChange: !!options.src,
                    color: options.color,
                    videoSrc:
                        options.mimeType === TextureMimeType.VIDEO
                            ? options.src
                            : null,
                },
            },
        });
    }

    setMaterialProps(options: IMaterialPropsOptions) {
        this.sendToViewer({
            action: "setMaterialProps",
            to: IPostMessageOrigin.WORKER,
            value: {
                name: options.materialName,
                props: options.props
            },
        });
    }

    swapMaterialType(options: ISwapMaterialType) {
        this.sendToViewer({
            action: "swapMaterialType",
            to: IPostMessageOrigin.WORKER,
            value: {
                name: options.materialName,
                newType: options.type
            },
        });
    }

    displayFiles(files: Array<Blob>) {
        this.sendToViewer({
            action: "displayFiles",
            to: IPostMessageOrigin.MAIN,
            value: {
                files: files,
            },
        });
    }

    getBoundingBox(): Promise<IBoundingBox> {
        return new Promise(async (resolve: any, reject: any) => {
            await this.onModelReady();
            this._eventObservable.add(
                EventObservableTypes.ON_BOUNDING_BOX,
                resolve
            );
            this.sendToViewer({
                action: "broadcastBoundingBox",
                to: IPostMessageOrigin.WORKER,
            });
        });
    }

    async toggleNoDistanceLimit(state: boolean) {
        await this.onModelReady();
        this.sendToViewer({
            action: "toggleNoDistanceLimit",
            to: IPostMessageOrigin.WORKER,
            value: state
        });
    }

    async toggleCloseup(state: boolean) {
        await this.onModelReady();
        this.sendToViewer({
            action: "toggleCloseup",
            to: IPostMessageOrigin.WORKER,
            value: state
        });
    }

    async setCameraPosition(pos: ICameraControlsStateAnimation) {
        await this.onModelReady();
        this.sendToViewer({
            action: "setCameraPosition",
            to: IPostMessageOrigin.WORKER,
            value: pos
        });
    }

    async setDiamonds(state: boolean, meshesNames: Array<string>, options: IDiamondOptions) {
        await this.onModelReady();
        this.sendToViewer({
            action: "toggleDiamond",
            to: IPostMessageOrigin.WORKER,
            value: {
                state,
                meshes: meshesNames,
                options
            }
        });
    }

    getDiamondsOptions(): Promise<IDiamondOptions> {
        return new Promise((resolve: any, reject: any) => {
            this._eventObservable.add(
                EventObservableTypes.ON_GET_DIAMONDS_OPTIONS,
                resolve
            );
            const msg = {
                action: "broadcastDiamondEffectOptions",
                to: IPostMessageOrigin.WORKER,
            };
            this.sendToViewer(msg);
        });
    }

    registerForLoadingProgress(callback: Function | any) {
        this._onLoadingProgress.push(callback);
    }

    destroy() {
        this._hasDestroyed = true;
        this._eventObservable.destroy();
        this._isViewerListening = false;
        this._isViewerLoaded = false;
        this._isModelLoaded = false;
        this._isAnimateEnterEnd = false;
        this._onLoadingProgress = [];
        self.removeEventListener("message", this._onMessageBind, false);
    }
}
