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 { ARButton } from 'three/addons/webxr/ARButton.js';
import { XRButton } from 'three/addons/webxr/XRButton.js';
import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.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';

THREE.Cache.enabled = true

const assetPath = '/asset/default/';

class GameEngine extends THREE.EventDispatcher{
    async init(domNode, opts = {}) {
        this.w = domNode.clientWidth || 1200, this.h = domNode.clientHeight || 800;
        this.aspect = this.w / this.h
        this.opts = opts;
        const gameEngine = this;

        this.perspectiveCamera = new THREE.PerspectiveCamera(45, this.aspect, 0.01, 99);
        this.raycaster = new THREE.Raycaster();
        this.perspectiveCamera.position.set(0, 0, 10);

        this.camera = this.perspectiveCamera;

        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
        );
        this.orthographicCamera.position.set(0, 0, 100);
        const scene = new THREE.Scene();
        this.scene = scene;

        this.initCameraPivot()

        // let light = new THREE.AmbientLight( 0x404040, 24 ); // soft white light
        // scene.add( light );

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

        const dirLight = new THREE.DirectionalLight(0xffffff, 4);
        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 = 1000;
        dirLight.shadow.bias = - 0.001;

        const renderer = new THREE.WebGLRenderer({
            antialias: true,
            alpha: false,
            preserveDrawingBuffer: true, //this is important for screenshots capturing
            powerPreference: "high-performance",
        });
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.toneMapping = THREE.CineonToneMapping;
        renderer.toneMappingExposure = 1.0;
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 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.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;

        const dashboard = new DashBoard(this);
        this.dashboard = dashboard;

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

        this.activeObjects = new THREE.Group();
        scene.add(this.activeObjects);

        const controls = new OrbitControls(this.camera, renderer.domElement);
        if (opts.gizmo) {
            const gizmo = new ViewportGizmo(this.camera, renderer, {
                container: '.renderer-gizmo',
                //type:'cube'
            });
            gizmo.attachControls(controls);
            this.gizmo = gizmo;
        }

        this.orbitControls = controls;
        controls.enableZoom = false;
        //const controls = new MapControls( camera, renderer.domElement );
        this.transformControls = new TransformControls(this.camera, renderer.domElement);
        this.transformControls.addEventListener('dragging-changed', function (event) {
            controls.enabled = !event.value;
        });
        // controls.enableDamping = true;
        // controls.screenSpacePanning = true;

        this.renderType = 'ST';

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

            gameEngine.render(scene, gameEngine.camera);
            if (!renderer.xr.isPresenting) {
                gameEngine.gizmo?.render();
            }
            // renderer.autoClear = false;
            // dashboard.render();
            // renderer.autoClear = true;
        }
        renderer.setAnimationLoop(animate.bind(this));

        const mixer = new THREE.AnimationMixer(this.scene);
        const clock = new THREE.Clock();

        this.clock = clock;
        this.renderer = renderer;

        this.draco = new DRACOLoader().setDecoderPath('/3rdparty/draco/');
        this.loader = new GLTFLoader();
        this.loader.setDRACOLoader(this.draco);
        this.mixers = [mixer];

        domNode.appendChild(renderer.domElement);

        let texture = await GameEngine.loadTexture('/static/textures/bck.webp', '');
        // let bck = await this.loadTexture('/static/textures/bck.webp');
        // bck.premultiplyAlpha = true;
        texture.mapping = THREE.EquirectangularReflectionMapping;
        // scene.background = bck; //new THREE.Color(0.7,0.7,0.7);
        scene.environment = texture;
        scene.background = new THREE.Color(1, 1, 1);
        //console.log('GameEngine started')
        renderer.domElement.addEventListener('wheel', (event) => {
            if (gameEngine.hero){
                if (!gameEngine.hero.pointerControls.controls.isLocked){
                    gameEngine.hero.cameraZ += event.deltaY / 100;
                }else{
                    gameEngine.camera.fov += event.deltaY / 100;
                    gameEngine.camera.fov = Math.min(Math.max(gameEngine.camera.fov, 0.01), 90);
                    gameEngine.camera.updateProjectionMatrix();
                }
            }else{
                gameEngine.camera.zoom -= event.deltaY / 1000;
                gameEngine.camera.zoom = Math.max(gameEngine.camera.zoom, .4);
                controls.rotateSpeed = 1 / gameEngine.camera.zoom;
                gameEngine.camera.updateProjectionMatrix();
            }
        })

        await this.initPhysics();

        this.stats = new Stats();
        document.body.appendChild(this.stats.dom);

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

        this.clickable = new Clickable(20);
        this.draggable = new Draggable(20);
    }

    initXrControllers() {
        let c1 = this.renderer.xr.getController(0);
        c1.addEventListener('select', this.onSelect.bind(this));
        c1.addEventListener('selectstart', this.onControllerEvent.bind(this));
        c1.addEventListener('selectend', this.onControllerEvent.bind(this));
        c1.addEventListener('move', this.onControllerEvent.bind(this));
        c1.userData.active = false;
        c1.addEventListener('connected', e => {
            c1.gamepad = e.data.gamepad;
            // this.session = this.renderer.xr.getSession();
            // this.session.addEventListener('selectstart', this.onControllerEvent.bind(this));
        })
        this.scene.add(c1);

        let c2 = this.renderer.xr.getController(1);
        c2.addEventListener('select', this.onSelect.bind(this));
        c2.addEventListener('selectstart', this.onControllerEvent.bind(this));
        c2.addEventListener('selectend', this.onControllerEvent.bind(this));
        c2.addEventListener('move', this.onControllerEvent.bind(this));
        c2.userData.active = true;
        c2.addEventListener('connected', e => {
            c2.gamepad = e.data.gamepad;
        })
        this.scene.add(c2);

        const controllerModelFactory = new XRControllerModelFactory();

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

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

        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.name = 'line';
        line.scale.z = 5;

        this.controllerLine = line;

        this.xrController1 = c1
        this.xrController2 = c2
    }

    initCameraPivot() {
        const pivot = new THREE.Object3D()
        pivot.position.set(0, 0, 0)

        const yaw = new THREE.Object3D()
        const pitch = new THREE.Object3D()

        this.scene.add(pivot)
        pivot.add(yaw)
        yaw.add(pitch)
        this.scene.add(this.perspectiveCamera);
        this.scene.add(this.orthographicCamera);
        this.cameraPivot = pivot;
        this.cameraYaw = yaw;
    }

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

    handleXrAction(gameEngine, delta) {
        //console.log(event.type);
        if (gameEngine.xrController1?.gamepad) {
            let gp = gameEngine.xrController1.gamepad;
            if (gp.axes[3] != 0) {
                gameEngine.activeObjects.position.z += gp.axes[3] * delta;
            }
            if (gp.axes[2] != 0) {
                gameEngine.activeObjects.position.x += gp.axes[2] * delta;
            }
        }
        if (gameEngine.xrController2?.gamepad) {
            let gp = 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 (gp.axes[2] != 0) {
                if (gp1.buttons[4]?.pressed) {
                    gameEngine.activeObjects.position.y += gp.axes[2] * delta;
                } else if (gp1.buttons[5]?.pressed) {
                    let sc = gameEngine.activeObjects.scale.x * (gp.axes[2] * delta * 0.5 + 1);
                    gameEngine.activeObjects.scale.set(sc, sc, sc);
                } else {
                    gameEngine.activeObjects.rotation.y += gp.axes[2] * delta * 0.5;
                }
            }
            if (gp.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');
                }
                // let nextMode = {
                //     'translate': 'scale',
                //     'scale': 'rotate',
                //     'rotate': 'translate'
                // }
                // if(gameEngine.opts.designMode){;
                //     gameEngine.transformControls.setMode(nextMode[gameEngine.transformControls.getMode()]);
                // }
            }
            // if (gp.buttons[5]?.pressed) {
            //     // gameEngine.setCameraOrthographic();
            //     // gameEngine.renderer.xr.updateCamera(gameEngine.orthographicCamera);
            //     let session = gameEngine.renderer.xr.getFrame().session;
            //     console.log(session);
            //     session.resumeDepthSensing();
            // }
        }
    }

    onControllerEvent(event) {
        //this.handleXrAction(event, this);
        //event.type !== 'move' && console.log(event)
        const controller = event.target;
        //console.log(event)
        
        if (this.opts.designMode){
            if (!controller.userData) return
            if (controller.userData.active === false) return;
            this.transformControls.getRaycaster().setFromXRController(controller);
            switch (event.type) {
                case 'selectstart':
                    this.transformControls.pointerDown(null);
                    break;
                case 'selectend':
                    this.transformControls.pointerUp(null);
                    break;
                case 'move':
                    this.transformControls.pointerHover(null);
                    this.transformControls.pointerMove(null);
                    break;
            }
        }
    }

    onSelect(event) {
        const controller = event.target;

        if (this.opts.designMode){
            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.raycaster.setFromXRController(controller);
            const intersects = this.raycaster.intersectObjects(this.activeObjects.children, true);

            intersects.forEach(o => {
                while (o.object && !this.activeObjects.children.includes(o.object)) {
                    o.object = o.object.parent;
                }
            })

            if (intersects.length > 0) {
                setTimeout(() => {
                    this.transformControls.attach(intersects[0].object);
                }, 100);
            }
        }
    }

    $ = THREE;

    async load(url, path = assetPath, progress) {
        return new Promise((resolve, reject) => {
            this.loader.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;
                        }
                    }
                    object.frustumCulled = false;
                    object.castShadow = true;
                    object.receiveShadow = true;
                });
                resolve(o);
            }, progress, reject)
        })
    }

    async loadPanorama(url, path = assetPath) {
        let t = await GameEngine.loadTexture(url, path);
        t.mapping = THREE.EquirectangularReflectionMapping;
        this.scene.background = t;
        this.scene.environment = t;
    }

    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);
        if (play) action.play();
        else action.stop();
    }

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

    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 = this.getMouseVector(mouseEvent, domElement);
        this.raycaster.setFromCamera(mouse, this.camera);
        this.clickable.update(mouse, this.camera, mouseEvent);
        this.hero?.idleReset();
    }

    onPointer(mouseEvent, domElement, type){
        let mouse = this.getMouseVector(mouseEvent, domElement);
        this.draggable?.update(mouse, this.camera, 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()
    }

    clearScene(){
        this.hero?.destroy();
        this.dashboard?.reset();
        this.activeObjects.clear();
        this.physics.stop();
        this.physics.clear();
        this.clickable.removeAll();
        this.motionQueue.clearAll();
        this.ambientSound.stop();
    }

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

    static textureLoader = new THREE.TextureLoader();
    static audioLoader = new THREE.AudioLoader();

    static async loadTexture(url, path = assetPath, progress, assignTo) {
        return new Promise((resolve, reject) => {
            GameEngine.textureLoader.load(`${path}${url}`, texture => {
                texture.colorSpace = THREE.SRGBColorSpace;
                if (assignTo){
                    assignTo[0][assignTo[1]] = texture;
                }
                resolve(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)
        })
    }

    loadTexture = GameEngine.loadTexture
}

export { GameEngine }