import * as THREE from 'three';
import { GLTFLoader, DRACOLoader, OrbitControls } from 'three/examples/jsm/Addons.js';
//import { Controller as OrbitControls } from './3rd-party/phy/3TH/Controller.js';
import { ViewportGizmo } from "three-viewport-gizmo";
import Stats from 'three/examples/jsm/libs/stats.module';
import { AnaglyphEffect } from 'three/addons/effects/AnaglyphEffect.js';
import { StereoEffect } from 'three/addons/effects/StereoEffect.js';
// import { MapControls } from 'three/addons/controls/MapControls.js';
// import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js';
import { TransformControls } from 'three/addons/controls/TransformControls.js';
import { PointerControls } from './PointerControls';
import { ARButton } from 'three/addons/webxr/ARButton.js';
import { XRButton } from 'three/addons/webxr/XRButton.js';
import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js';
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
import { Physics } from './Physics.js';
import { Clickable } from './Clickable.js';
import { DashBoard } from './Dashboard.js';
import { MotionEngine } from './MotionEngine.js';
import { Draggable } from './Draggable.js';
import { EventManager } from "./EventManager";
import { Telemetry } from './Telemetry.js';
import { MeshUtils } from './MeshUtils.js';

THREE.Cache.enabled = true

const assetPath = '/asset/default/';
const defaultLightIntensity = 11;
const defaultInteractionDistance = 10;
const defaultCameraFov = 50;
const sceneScale = 1.33;

class GameEngine extends EventManager{

    async initScene(){
        this.perspectiveCamera = new THREE.PerspectiveCamera(defaultCameraFov, this.aspect, 0.001, 399);
        //this.perspectiveCamera.position.set(0, 0, 10);

        this.camera = this.perspectiveCamera;
        this.defaultCameraFov = defaultCameraFov;

        this.frustumSize = 50;
        this.orthographicCamera = new THREE.OrthographicCamera(
            this.frustumSize * this.aspect / - 2,
            this.frustumSize * this.aspect / 2,
            this.frustumSize / 2,
            this.frustumSize / - 2,
            0.01, 1000
        );

        const scene = new THREE.Scene();

        this.sceneWrapper = new THREE.Group();
        scene.add(this.sceneWrapper);
        this.activeObjects = new THREE.Group();
        this.sceneWrapper.add(this.activeObjects);
        this.activeObjects.scale.setScalar(this.scale);
        //this.camera.scale.setScalar(0.1);

        this.scene = scene;

        this.initCameraPivot()

        this.ambientLight = new THREE.AmbientLight( 0x404040, defaultLightIntensity ); // soft white light
        scene.add( this.ambientLight );

        // var hemiLight = new THREE.HemisphereLight( 0xffffff, 0x444444, 10 );
        // hemiLight.position.set(0, 33, 0);
        //scene.add( hemiLight );

        // scene.fog = new THREE.Fog(0xbbaaaa, 11, 33);
        // scene.fog = new THREE.FogExp2(0xaaaaaa, 0.037);

        const dirLight = new THREE.DirectionalLight(0xffffff, 1);
        dirLight.color.setHSL(0.1, 1, 0.95);
        dirLight.position.set(12, 33, -37);
        dirLight.position.multiplyScalar( 0.20 );
        // hemiLight.position.multiplyScalar( 0.20 );
        scene.add(dirLight);
        dirLight.castShadow = true;
        dirLight.shadow.mapSize.width = 1024;
        dirLight.shadow.mapSize.height = 1024;
        const d = 50;
        dirLight.shadow.camera.left = - d;
        dirLight.shadow.camera.right = d;
        dirLight.shadow.camera.top = d;
        dirLight.shadow.camera.bottom = - d;
        dirLight.shadow.camera.far = 100;
        dirLight.shadow.bias = - 0.001;

        // const spotLight = new THREE.SpotLight(0xffffff);
        // scene.add(spotLight);

        this.listener = new THREE.AudioListener();
        this.camera.add(this.listener);
        this.ambientSound = new THREE.Audio(this.listener);

        if (this.opts.gizmo) {
            this.orbitControls = new OrbitControls(this.camera, this.renderer.domElement);
            //this.orbitControls.enableZoom = false;
            const gizmo = new ViewportGizmo(this.camera, this.renderer, {
                container: '.renderer-gizmo',
                //type:'cube'
            });
            gizmo.attachControls(this.orbitControls);
            this.gizmo = gizmo;
            this.perspectiveCamera.position.set(0, 0, 10);
            this.orthographicCamera.position.set(0, 0, 100);
            this.cameraRig.rotation.y = 0;
        }
        //this.scene.scale.setScalar(1.25);

        //const controls = new MapControls( camera, renderer.domElement );
        
        if (this.opts.mode == 'GameDesigner'){
            const gameEngine = this;
            this.transformControls = new TransformControls(this.camera, this.renderer.domElement);
            this.transformControls.addEventListener('dragging-changed', function (event) {
                if (gameEngine.orbitControls){
                    gameEngine.orbitControls.enabled = !event.value;
                }
            });
            scene.add(this.transformControls.getHelper());
        }else if (['GamePlay', 'GamePreview'].includes(this.opts.mode)){
            const dashboard = new DashBoard(this);
            this.dashboard = dashboard;
            dashboard.enable();
        }

        this.pointerControls = new PointerControls(this);
        
        scene.background = new THREE.Color(1, 1, 1);

        const mixer = new THREE.AnimationMixer(this.scene);
        const timer = new THREE.Timer();

        this.timer = timer;
        this.mixers = [mixer];

        await this.initPhysics();

        if (this.opts.xr){
            this.cameraRig.add(this.xrController1);
            this.cameraRig.add(this.xrController2);
            this.cameraRig.add(this.xrControllerGrip1);
            this.cameraRig.add(this.xrControllerGrip2);

            const geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, - 1)]);

            let line = new THREE.Line(geometry);
            line.scale.z = 7;

            this.controllerLine = line;
        }
    }

    async init(domNode, opts = {}) {
        this.scale = sceneScale;
        this.w = domNode.clientWidth || 1200, this.h = domNode.clientHeight || 800;
        this.aspect = this.w / this.h
        this.opts = opts;
        this.raycaster = new THREE.Raycaster();
        this.meshUtils = new MeshUtils(this);

        const renderer = new THREE.WebGLRenderer({
            antialias: true,
            alpha: false,
            preserveDrawingBuffer: true, //this is important for screenshots capturing
            //powerPreference: "high-performance",
            //precision: 'mediump'
        });
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.toneMapping = THREE.CineonToneMapping;
        renderer.toneMappingExposure = 1.0;
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.VSMShadowMap; // default THREE.PCFShadowMap
        // renderer.toneMapping = THREE.ACESFilmicToneMapping;
        // renderer.toneMappingExposure = 1;
        // renderer.shadowMap.enabled = true;
        // renderer.shadowMap.type = THREE.PCFSoftShadowMap;
        renderer.setSize(this.w, this.h);
        renderer.setViewport(0, 0, this.w, this.h);
        renderer.autoClear = true;
        //renderer.alpha = true
        this.renderer = renderer;
        this.pmremGenerator = new THREE.PMREMGenerator(renderer);

        this.anaglyph = new AnaglyphEffect(renderer);
        this.anaglyph.setSize(this.w, this.h);

        this.stereo = new StereoEffect(renderer);
        this.stereo.setSize(this.w, this.h);

        this.motionQueue = new MotionEngine();
        this.assetPath = assetPath;
        this.tm = new Telemetry(opts.mode);
        // controls.enableDamping = true;
        // controls.screenSpacePanning = true;

        this.renderType = 'ST';
        
        this.handleXrAction = ['ObjectPreview', 'GameDesigner'].includes(opts.mode) ? this.handleXrActionDesignMode : this.handleXrActionGameMode;

        if (opts.stats){
            this.stats = new Stats();
            this.stats.dom.classList.add('engine-stats');
            document.body.appendChild(this.stats.dom);
        }

        if (opts.ar) {
            renderer.xr.enabled = true;
            this.arBtn = ARButton.createButton(renderer, {});
            this.arBtn.classList.add('engine-ar-btn');
            document.body.appendChild(this.arBtn);
        }
        if (opts.xr) {
            renderer.xr.enabled = true;
            this.xrBtn = XRButton.createButton(renderer, opts.depthSense ? {
                requiredFeatures: ['depth-sensing'],
                depthSensing: {
                    usagePreference: ["gpu-optimized"],
                    dataFormatPreference: ["unsigned-short"],
                    matchDepthView: false
                }
            } : {

            })
            this.xrBtn.classList.add('engine-xr-btn');
            document.body.appendChild(this.xrBtn);
            this.initXr();
        }

        this.clickable = new Clickable(this, defaultInteractionDistance);
        this.draggable = new Draggable(this, defaultInteractionDistance);

        await this.initScene();

        function animate() {
            this.timer.update();
            let delta = this.timer.getDelta();
            this.physics?.step();
            this.handleXrAction(this, delta);
            this.hero?.update(delta);
            this.mixers.forEach(m => m.update(delta));
            this.dispatchEvent({type: 'beforeRender'})
            this.processHideIfFar();
            this.motionQueue.update(delta);

            this.render(this.scene, this.camera);
            if (!renderer.xr.isPresenting) {
                this.gizmo?.render();
            }
        }
        renderer.setAnimationLoop(animate.bind(this));

        this.loadedObjects = [];

        domNode.appendChild(renderer.domElement);

        this._wheelEvent = ((event) => {
            event.preventDefault();
            if (this.hero){
                if (!this.pointerControls.isLocked){
                    this.hero.cameraZ += event.deltaY * 0.005;
                }
            }else{
                this.camera.zoom -= event.deltaY / (1000 / this.camera.zoom);
                this.camera.zoom = Math.max(this.camera.zoom, .01);
                //controls.rotateSpeed = 1 / Math.sqrt(gameEngine.camera.zoom);
                this.camera.updateProjectionMatrix();
                this.orbitControls.panSpeed = 1 / this.camera.zoom;
            }
        }).bind(this)

        renderer.domElement.addEventListener('wheel', this._wheelEvent)
        GameEngine.ktxLoader.detectSupport(renderer);
    }

    initXr() {
        this.xrHandlers = {
            onControllerEvent: (event=>{
                const controller = event.target;
                if (!controller.userData?.active) return;
                this.dispatchEvent({type: 'controller', data: event})
                
                if (this.opts.mode != 'GameDesigner'){
                    this.draggable?.handleController(controller, event.type)
                }
            }).bind(this),

            onSelect: (event => {
                const controller = event.target;
                this.xrController1.userData.active = false;
                this.xrController2.userData.active = false;

                if (controller === this.xrController1) {
                    this.xrController1.userData.active = true;
                    this.xrController1.add(this.controllerLine);
                }

                if (controller === this.xrController2) {
                    this.xrController2.userData.active = true;
                    this.xrController2.add(this.controllerLine);
                }

                this.dispatchEvent({type: 'controller', data: event})

                if (this.opts.mode != 'GameDesigner'){
                    this.clickable.handleController(controller, event);
                }
            }).bind(this),

            onConnect: e => {
                e.target.gamepad = e.data.gamepad;
            }
        }
        
        let c1 = this.renderer.xr.getController(0);
        c1.userData.active = false;

        let c2 = this.renderer.xr.getController(1);
        c2.userData.active = true;

        [c1, c2].forEach(c=>{
            c.addEventListener('select', this.xrHandlers.onSelect);
            c.addEventListener('selectstart', this.xrHandlers.onControllerEvent);
            c.addEventListener('selectend', this.xrHandlers.onControllerEvent);
            c.addEventListener('move', this.xrHandlers.onControllerEvent);
            c.addEventListener('connected', this.xrHandlers.onConnect)
        })

        const controllerModelFactory = new XRControllerModelFactory();

        let controllerGrip1 = this.renderer.xr.getControllerGrip(0);
        controllerGrip1.add(controllerModelFactory.createControllerModel(controllerGrip1));

        let controllerGrip2 = this.renderer.xr.getControllerGrip(1);
        controllerGrip2.add(controllerModelFactory.createControllerModel(controllerGrip2));

        this.xrController1 = c1
        this.xrController2 = c2

        this.xrControllerGrip1 = controllerGrip1;
        this.xrControllerGrip2 = controllerGrip2;

        this.xrCamera = this.renderer.xr.getCamera();
    }

    disposeXr(){
        if (this.renderer.xr.isPresenting){
            this.renderer.xr.getSession().end();
        }
        [this.xrController1, this.xrController2].forEach(c=>{
            c.removeEventListener('select', this.xrHandlers.onSelect);
            c.removeEventListener('selectstart', this.xrHandlers.onControllerEvent);
            c.removeEventListener('selectend', this.xrHandlers.onControllerEvent);
            c.removeEventListener('move', this.xrHandlers.onControllerEvent);
            c.removeEventListener('connected', this.xrHandlers.onConnect);
        })
    }

    initCameraPivot() {
        this.cameraWorld = new THREE.Group();
        this.cameraRig = new THREE.Group();
        this.cameraRig.add(this.perspectiveCamera);
        this.cameraRig.add(this.orthographicCamera);
        this.cameraRig.rotation.y = Math.PI;
        this.cameraWorld.add(this.cameraRig);
        this.sceneWrapper.add(this.cameraWorld);
    }

    async initPhysics() {
        this.physics = new Physics(this);
        await this.physics.init();
    }

    handleXrActionGameMode(gameEngine, delta) {

    }

    handleXrActionDesignMode(gameEngine, delta) {
        if (gameEngine.xrController2?.gamepad) {
            let gp2 = gameEngine.xrController2.gamepad;
            let gp1 = gameEngine.xrController1.gamepad;
            // if (gp.axes[3] != 0){
            //     let sc = gameEngine.scene.scale.x + gp.axes[3] * delta * 0.5;
            //     gameEngine.scene.scale.set(sc, sc, sc);
            // }
            if (gp2.axes[2] != 0 || gp2.axes[3] != 0) {
                if (gp1.buttons[4]?.pressed) {
                    gameEngine.activeObjects.position.y += gp2.axes[2] * delta;
                } else if (gp1.buttons[5]?.pressed) {
                    let sc = gameEngine.activeObjects.scale.x 
                        * (gp2.axes[2] * delta * 0.5 + 1)
                        * (gp2.axes[3] * delta * 0.05 + 1);
                    gameEngine.activeObjects.scale.setScalar(sc);
                } else {
                    gameEngine.activeObjects.rotation.y += gp2.axes[2] * delta * 0.5 + gp2.axes[3] * delta * 0.05;
                }
            }
            if (gp2.buttons[5]?.pressed && (gp1.axes[3] != 0 || gp1.axes[2] != 0)) {
                gameEngine.ambientLight.intensity += gp1.axes[3] * delta * 5 + gp1.axes[2] * delta;
                return;
            }
            if (gp2.buttons[4]?.pressed) {
                let x = performance.now() % 1000;
                if (x < 333) {
                    gameEngine.transformControls?.setMode('translate');
                }else if (x < 666) {
                    gameEngine.transformControls?.setMode('scale');
                }else{
                    gameEngine.transformControls?.setMode('rotate');
                }
            }
        }
        
        if (gameEngine.xrController1?.gamepad) {
            let gp1 = gameEngine.xrController1.gamepad;
            if (gp1.axes[3] != 0) {
                gameEngine.activeObjects.position.z += (Math.exp(1+Math.abs(gp1.axes[3]))-2) * delta * Math.sign(gp1.axes[3]) * 0.2;
            }
            if (gp1.axes[2] != 0) {
                gameEngine.activeObjects.position.x += (Math.exp(1+Math.abs(gp1.axes[2]))-2) * delta * Math.sign(gp1.axes[2]) * 0.2;
            }
        }
    }

    $ = THREE;

    async load(url, path = assetPath, progress) {
        return new Promise((resolve, reject) => {
            GameEngine.gltfLoader.load(`${path}${url}`, o => {
                o.scene.traverse(object => {
                    if (object.name == 'environment' || object.material){
                        //console.log('env recomputing')
                        object.material.shading = THREE.SmoothShading;
                        object.geometry.computeVertexNormals(true);
                        //object.material.metalness = 0;
                        //if (object.material.map) object.material.map.colorSpace = THREE.SRGBColorSpace;
                    }
                    if ( object instanceof THREE.Mesh ) {
                        //object.material.envMap = this.scene.environment;
                        if (object.material.map) {
                            object.material.map.colorSpace = THREE.SRGBColorSpace;
                        }
                        
                        if (object.userData){
                            if (object.userData['re.renderOrder'] !== undefined){
                                object.renderOrder = object.userData['re.renderOrder'];
                                //object.material.alphaTest = true;
                            }
                        }
                    }
                    object.frustumCulled = false;
                    object.castShadow = true;
                    object.receiveShadow = true;
                });
                resolve(o);
                this.loadedObjects.push(o);
            }, progress, reject)
        })
    }

    async loadPanorama(url, path = assetPath) {
        let t = await GameEngine.loadTexture(url, path);
        t.mapping = THREE.EquirectangularReflectionMapping;
        t.generateMipmaps = false;
        t.minFilter = THREE.LinearFilter;
        this.scene.background?.dispose?.();
        this.scene.environment?.dispose?.(); 
        this.scene.background = t;
        this.scene.environment = t;
        //this.scene.environment = this.pmremGenerator.fromEquirectangular(t).texture;
    }

    async captureScreenshot(type = 'image/webp', quality = 80) {
        return new Promise((resolve, reject) => {
            this.renderer.domElement.toBlob(resolve, type, quality)
        })
    }

    playAnimation(object, clip, play = true) {
        let action = this.mixers[0].clipAction(clip, object);
        action[play ? 'play' : 'stop']();
        return action;
    }

    stop() {
        this.renderer?.setAnimationLoop(null);
        this.stats?.dom?.remove();
    }

    getMouseVector(mouseEvent, domElement) {
        //console.log(mouseEvent, domElement)
        const mouse = new THREE.Vector2();
        if (this.renderType == 'VR') {
            let x;
            if (mouseEvent.offsetX > window.innerWidth / 2) {
                x = (mouseEvent.offsetX - window.innerWidth / 2) * 2
            } else {
                x = mouseEvent.offsetX * 2;
            }
            mouse.x = (x / window.innerWidth) * 2 - 1;
            mouse.y = - (mouseEvent.offsetY / window.innerHeight) * 2 + 1;
        } else {
            mouse.x = (mouseEvent.offsetX / domElement.clientWidth) * 2 - 1;
            mouse.y = - (mouseEvent.offsetY / domElement.clientHeight) * 2 + 1;
        }
        return mouse;
    }

    // intersect(mouseEvent, domElement, objects, recursive = false, returnInputObjects = true) {
    //     let mouse = this.getMouseVector(mouseEvent, domElement);
    //     this.raycaster.setFromCamera(mouse, this.camera);
    //     let intersects = this.raycaster.intersectObjects(objects.filter(o => o.visible), recursive);
    //     if (returnInputObjects && recursive) {
    //         intersects.forEach(o => {
    //             while (o.object && !objects.includes(o.object)) {
    //                 o.object = o.object.parent;
    //             }
    //         })
    //     }
    //     //console.log('intersects', intersects);
    //     return intersects;
    // }

    onClick(mouseEvent, domElement){
        let mouse;
        if (this.pointerControls.isLocked){
            mouse = new THREE.Vector3(0,0,0);
        }else{
            mouse = this.getMouseVector(mouseEvent, domElement);
        }
        //this.raycaster.setFromCamera(mouse, this.camera);
        this.clickable.handleMouse(mouse, mouseEvent);
        this.hero?.idleReset();
    }

    onPointer(mouseEvent, domElement, type){
        let mouse;
        if (this.pointerControls.isLocked){
            mouse = new THREE.Vector3(0,0,0);
        }else{
            mouse = this.getMouseVector(mouseEvent, domElement);
        }
        this.draggable?.handleMouse(mouse, type);
    }

    setCamera(camera) {
        //camera.updateProjectionMatrix();
        this.camera = camera;
        //this.transformControls.camera = camera;
        this.orbitControls.object = camera;
    }

    setCameraOrthographic() {
        let o = this.orthographicCamera;
        // o.position.copy(o.position);
        // const distance = o.position.distanceTo(this.orbitControls.target);
        // const halfWidth = this.frustumWidthAtDistance(o, distance) / 2;
        // const halfHeight = this.frustumHeightAtDistance(o, distance) / 2;
        // o.top = halfHeight;
        // o.bottom = -halfHeight;
        // o.left = -halfWidth;
        // o.right = halfWidth;
        // o.zoom = 1;
        // o.lookAt(this.orbitControls.target);
        // o.updateProjectionMatrix();
        this.camera = o;
        this.transformControls.camera = o;
        //this.orbitControls.object = o;
        if (this.gizmo) {
            this.gizmo.camera = o;
        }
    }

    setCameraPerspective() {
        let o = this.perspectiveCamera;
        // const oldY = o.position.y;
        // o.position.copy(o.position);
        // o.position.y = oldY / o.zoom;
        // o.updateProjectionMatrix();
        this.camera = o;
        this.transformControls.camera = o;
        //this.orbitControls.object = o;
        if (this.gizmo) {
            this.gizmo.camera = o;
        }
    }

    resize(w, h) {
        this.w = w;
        this.h = h;
        this.aspect = this.w / this.h
        this.perspectiveCamera.aspect = this.aspect;
        this.perspectiveCamera.updateProjectionMatrix();
        this.orthographicCamera.left = - this.frustumSize * this.aspect / 2;
        this.orthographicCamera.right = this.frustumSize * this.aspect / 2;
        this.orthographicCamera.top = this.frustumSize / 2;
        this.orthographicCamera.bottom = - this.frustumSize / 2;
        this.renderer.setSize(w, h);
        this.renderer.setViewport(0, 0, this.w, this.h);
        this.anaglyph.setSize(w, h);
        this.stereo.setSize(w, h);
        this.gizmo?.update();
    }

    render(scene, camera) {
        if (this.renderType == 'VR') {
            this.stereo?.render(scene, camera);
        } else if (this.renderType == 'AG') {
            this.anaglyph?.render(scene, camera);
        } else {
            this.renderer.render(scene, camera);
        }
        this.stats?.update()
    }

    farArray = [];

    hideIfFar(object, distance){
        object.visible = false;
        this.farArray.push({object, distance})
    }

    processHideIfFar(){
        let v = new THREE.Vector3(), cv = new THREE.Vector3();
        this.cameraWorld.getWorldPosition(cv);
        this.farArray.forEach(e=>{
            e.object.getWorldPosition(v);
            let dst = v.distanceTo(cv) / this.scale;
            if (dst <= e.distance && !e.object.visible) {
                e.object.visible = true;
            }else if (dst > e.distance && e.object.visible){
                e.object.visible = false;
            }
        })
    }

    async playAmbientSound(source, path){
        let buffer = await GameEngine.loadAudio(source, path);
        this.ambientSound.setBuffer(buffer);
        this.ambientSound.setLoop(true);
        this.ambientSound.play();
    }

    immersive(show, t = 1){
        this.motionQueue.add([
            {
                o: this.scene,
                a: { 
                    backgroundIntensity: show ? 0.1 : 1, environmentIntensity: show ? 0.1 : 1
                },
                t
            },{
                o: this.ambientLight,
                a: { intensity: show ? 0.1 : defaultLightIntensity},
                t
            }
        ])
    }

    clearScene(){
        this.hero?.dispose();
        this.transformControls?.dispose();
        this.pointerControls.dispose();
        //this.activeObjects.clear();
        this.physics.stop();
        this.physics.clear();
        this.clickable.removeAll();
        this.gizmo?.dispose();
        this.motionQueue.clearAll();
        this.ambientSound.stop();
        this.loadedObjects.forEach(o=>this.meshUtils.clearObject(o.scene))
        GameEngine.loadedTextures.forEach(t=>{
            t.dispose();
        });
        GameEngine.loadedTextures.splice(0, GameEngine.loadedTextures.length);
        this.scene.background?.dispose?.();
        this.scene.environment?.dispose?.(); 
        this.meshUtils.clearObject(this.scene);
        this.scene = null;
        this.tm?.setScene(null);
        this.removeAllListenersOfType('beforeRender');
    }

    async resetScene(){
        this.dashboard?.reset();//moved from clearscene to resetscene, because an updateText sync callback waits infinitely after gameengine destroy 
        this.clearScene();
        await this.initScene();
    }

    dispose(){
        this.stop();
        this.clearScene();
        this.disposeXr();
        this.renderer.dispose();
        this.pmremGenerator.dispose();
        this.arBtn?.remove();
        this.xrBtn?.remove();
        this.stats?.dom?.remove();
        this.renderer.renderLists.dispose();
        this.renderer.properties.dispose();
        this.renderer.domElement.removeEventListener('wheel', this._wheelEvent)
        this.renderer.domElement.remove();
        super.dispose();
        //console.log('Engine Disposed', this.renderer.info.memory.textures );
    }

    static textureLoader = new THREE.TextureLoader();
    static audioLoader = new THREE.AudioLoader();
    static draco = new DRACOLoader().setDecoderPath('/3rdparty/draco/');
    static ktxLoader = new KTX2Loader().setTranscoderPath( '/3rdparty/basis/' );
    static gltfLoader = new GLTFLoader().setDRACOLoader(this.draco).setKTX2Loader(this.ktxLoader);

    static loadedTextures = []

    static async loadTexture(url, path = assetPath, progress, assignTo) {
        let loader = url.toLowerCase().endsWith('.ktx2') ? GameEngine.ktxLoader : GameEngine.textureLoader;
        return new Promise((resolve, reject) => {
            loader.load(`${path}${url}`, texture => {
                texture.colorSpace = THREE.SRGBColorSpace;
                texture.name = url;
                if (assignTo){
                    assignTo[0][assignTo[1]] = texture;
                }
                resolve(texture)
                GameEngine.loadedTextures.push(texture);
            }, progress, reject)
        })
    }

    static async loadAudio(url, path = assetPath, progress){
        return new Promise((resolve, reject) => {
            GameEngine.audioLoader.load(`${path}${url}`, buffer => {
                resolve(buffer)
            }, progress, reject)
        })
    }

    static scale = 1.33;

    loadTexture = GameEngine.loadTexture
}

export { GameEngine }