import * as React from "react";
import { GlobalState, RuntimeMode } from "../globalState";
import { Engine } from "babylonjs/Engines/engine";
import { Nullable } from "babylonjs/types";
import { Scene } from "babylonjs/scene";
import { Utilities } from "../tools/utilities";
import { DownloadManager } from "../tools/downloadManager";
import { WebGPUEngine } from "babylonjs";

require("../scss/rendering.scss");

interface IRenderingComponentProps {
    globalState: GlobalState;
}

export class RenderingComponent extends React.Component<IRenderingComponentProps> {
    private _engine: Nullable<Engine>;
    private _scene: Nullable<Scene>;
    private _canvasRef: React.RefObject<HTMLCanvasElement>;
    private _downloadManager: DownloadManager;
    private _unityToolkitWasLoaded = false;

    public constructor(props: IRenderingComponentProps) {
        super(props);

        this._canvasRef = React.createRef();

        // Create the global handleException
        (window as any).handleException = (e: Error) => {
            console.error(e);
            this.props.globalState.onErrorObservable.notifyObservers(e);
        };

        this.props.globalState.onRunRequiredObservable.add(() => {
            this._compileAndRunAsync();
        });

        this._downloadManager = new DownloadManager(this.props.globalState);
        this.props.globalState.onDownloadRequiredObservable.add(() => {
            if (!this._engine) {
                return;
            }
            this._downloadManager.download(this._engine);
        });

        this.props.globalState.onInspectorRequiredObservable.add((state) => {
            if (!this._scene) {
                return;
            }

            if (state) {
                this._scene.debugLayer.show({
                    embedMode: true,
                });
            } else {
                this._scene.debugLayer.hide();
            }
            this.props.globalState.inspectorIsOpened = state;
        });

        this.props.globalState.onFullcreenRequiredObservable.add(() => {
            this._engine?.switchFullscreen(false);
        });

        if (this.props.globalState.runtimeMode !== RuntimeMode.Editor) {
            this.props.globalState.onCodeLoaded.add((code) => {
                this.props.globalState.currentCode = code;
                this.props.globalState.onRunRequiredObservable.notifyObservers();
            });
        }

        window.addEventListener("resize", () => {
            if (!this._engine) {
                return;
            }

            this._engine.resize();
        });
    }

    private async _loadScriptAsync(url: string): Promise<void> {
        return new Promise((resolve, reject) => {
            let script = document.createElement('script');
            script.src = url;
            script.onload = () => {
                resolve();
            };
            document.head.appendChild(script);
        });
    }

    private async _compileAndRunAsync() {
        this.props.globalState.onDisplayWaitRingObservable.notifyObservers(false);
        this.props.globalState.onErrorObservable.notifyObservers(null);

        const displayInspector = this._scene?.debugLayer.isVisible();

        let useWebGPU = location.href.indexOf("webgpu") !== -1 && WebGPUEngine.IsSupported;
        let forceWebGL1 = false;
        const configuredEngine = Utilities.ReadStringFromStore("engineVersion", "WebGL2");

        switch (configuredEngine) {
            case "WebGPU":
                useWebGPU = true;
                break;
            case "WebGL":
                forceWebGL1 = true;
                break;
        }
        

        if (this._engine) {
            try {
                this._engine.dispose();
            } catch (ex) {
                // just ignore
            }
            this._engine = null;
        }

        try {
            let globalObject = window as any;
            let canvas = this._canvasRef.current!;
            globalObject.canvas = canvas;

            if (useWebGPU) {
                globalObject.createDefaultEngine = async function() { 
                    var engine = new BABYLON.WebGPUEngine(canvas);
                    await engine.initAsync();
                    return engine;
                }                
            } else {
                globalObject.createDefaultEngine = function () {
                    return new Engine(canvas, true, {
                        disableWebGL2Support: forceWebGL1,
                        preserveDrawingBuffer: true,
                        stencil: true,
                    });
                };
            }

            let zipVariables = "var engine = null;\r\nvar scene = null;\r\nvar sceneToRender = null;\r\n";
            let defaultEngineZip = `var createDefaultEngine = function() { return new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true,  disableWebGL2Support: ${forceWebGL1}}); }`;

            if (useWebGPU) {
                defaultEngineZip = `var createDefaultEngine = async function() { 
                    var engine = new BABYLON.WebGPUEngine(canvas);
                    await engine.initAsync();
                    return engine;
                }`;
            }

            let code = await this.props.globalState.getCompiledCode();

            if (!code) {
                return;
            }

            // Check for Unity Toolkit
            if ((location.href.indexOf("UnityToolkit") !== -1 || Utilities.ReadBoolFromStore("unity-toolkit", false)) && !this._unityToolkitWasLoaded) {
                await this._loadScriptAsync("/libs/babylon.manager.js");
                this._unityToolkitWasLoaded = true;
            }

            let createEngineFunction = "createDefaultEngine";
            let createSceneFunction = "";
            let checkCamera = true;
            let checkSceneCount = true;

            if (code.indexOf("createEngine") !== -1) {
                createEngineFunction = "createEngine";
            }

            // Check for different typos
            if (code.indexOf("delayCreateScene") !== -1) {
                // delayCreateScene
                createSceneFunction = "delayCreateScene";
                checkCamera = false;
            } else if (code.indexOf("createScene") !== -1) {
                // createScene
                createSceneFunction = "createScene";
            } else if (code.indexOf("CreateScene") !== -1) {
                // CreateScene
                createSceneFunction = "CreateScene";
            } else if (code.indexOf("createscene") !== -1) {
                // createscene
                createSceneFunction = "createscene";
            }

            if (!createSceneFunction) {
                this.props.globalState.onErrorObservable.notifyObservers({
                    message: "You must provide a function named createScene.",
                });
                return;
            } else {
                code += `
                var engine;
                var scene;
                initFunction = async function() {               
                    var asyncEngineCreation = async function() {
                        try {
                        return ${createEngineFunction}();
                        } catch(e) {
                        console.log("the available createEngine function failed. Creating the default engine instead");
                        return createDefaultEngine();
                        }
                    }

                    engine = await asyncEngineCreation();`;
                code += "\r\nif (!engine) throw 'engine should not be null.';";

                if (this.props.globalState.language === "JS") {
                    code += "\r\n" + "scene = " + createSceneFunction + "();";
                } else {
                    var startCar = code.search("var " + createSceneFunction);
                    code = code.substr(0, startCar) + code.substr(startCar + 4);
                    code += "\n" + "scene = " + createSceneFunction + "();";
                }

                code += `}`;

                // Execute the code
                Utilities.FastEval(code);

                await globalObject.initFunction();

                this._engine = globalObject.engine;

                if (!this._engine) {
                    this.props.globalState.onErrorObservable.notifyObservers({
                        message: "createEngine function must return an engine.",
                    });
                    return;
                }

                if (!globalObject.scene) {
                    this.props.globalState.onErrorObservable.notifyObservers({
                        message: createSceneFunction + " function must return a scene.",
                    });
                    return;
                }

                let sceneToRenderCode = "sceneToRender = scene";

                // if scene returns a promise avoid checks
                if (globalObject.scene.then) {
                    checkCamera = false;
                    checkSceneCount = false;
                    sceneToRenderCode = "scene.then(returnedScene => { sceneToRender = returnedScene; });\r\n";
                }

                let createEngineZip = createEngineFunction === "createEngine" ? zipVariables : zipVariables + defaultEngineZip;

                this.props.globalState.zipCode = createEngineZip + ";\r\n" + code + ";\r\n" + sceneToRenderCode;
            }

            if (globalObject.scene.then) {
                globalObject.scene.then((s: Scene) => {
                    this._scene = s;
                    globalObject.scene = this._scene;
                });
            } else {
                this._scene = globalObject.scene as Scene;
            }

            this._engine.runRenderLoop(() => {
                if (!this._scene || !this._engine) {
                    return;
                }

                if (this.props.globalState.runtimeMode === RuntimeMode.Editor && window.innerWidth > this.props.globalState.MobileSizeTrigger) {
                    if (canvas.width !== canvas.clientWidth || canvas.height !== canvas.clientHeight) {
                        this._engine.resize();
                    }
                }

                if (this._scene.activeCamera || this._scene.activeCameras && this._scene.activeCameras.length > 0) {
                    this._scene.render();
                }

                // Update FPS if camera is not a webxr camera
                if (!(this._scene.activeCamera && this._scene.activeCamera.getClassName && this._scene.activeCamera.getClassName() === "WebXRCamera")) {
                    if (this.props.globalState.runtimeMode !== RuntimeMode.Full) {
                        this.props.globalState.fpsElement.innerHTML = this._engine.getFps().toFixed() + " fps";
                    }
                }
            });

            if (checkSceneCount && this._engine.scenes.length === 0) {
                this.props.globalState.onErrorObservable.notifyObservers({
                    message: "You must at least create a scene.",
                });
                return;
            }

            if (this._engine.scenes[0] && displayInspector) {
                this.props.globalState.onInspectorRequiredObservable.notifyObservers(true);
            }

            if (checkCamera && this._engine.scenes[0].activeCamera == null) {
                this.props.globalState.onErrorObservable.notifyObservers({
                    message: "You must at least create a camera.",
                });
                return;
            } else if (globalObject.scene.then) {
                globalObject.scene.then(() => {
                    if (this._engine!.scenes[0] && displayInspector) {
                        this.props.globalState.onInspectorRequiredObservable.notifyObservers(true);
                    }
                });
            } else {
                this._engine.scenes[0].executeWhenReady(() => {
                    this.props.globalState.onRunExecutedObservable.notifyObservers();
                });
            }
        } catch (err) {
            this.props.globalState.onErrorObservable.notifyObservers(err);
        }
    }

    public render() {
        return <canvas id="renderCanvas" ref={this._canvasRef}></canvas>;
    }
}
