import { AnimationMixer, Vector3, Vector2 } from 'three';
import { QueryFilterFlags } from '@dimforge/rapier3d';

const zero2 = new Vector2(0,0);

const animationBoost = {
    walk: 1.25
}

class Hero{
    #walkDirection = new Vector3()
    #desiredMovement = new Vector2(0,0)
    #correctedMovement = new Vector2(0,0);
    #rotateAngle = new Vector3(0, 1, 0)
    cameraMode = 'rotate';
    #cameraZ = 4
    #timeScale = 1;

    get cameraZ(){
        return this.#cameraZ;
    }

    set cameraZ(v){
        this.#cameraZ = Math.min(Math.max(v, this.cameraZMin), this.cameraZMax);
        if (this.#cameraZ == this.cameraZMin && !this.fpv){
            if (this.engine.renderer.xr.isPresenting){
                this.#cameraZ = 0;
                this.fpv = true;
                this.cameraY = this.cameraYBase;
                this.camera.rotation.set(0,0,0);
            }else{
                this.engine.pointerControls.lock(true).then(()=>{
                    this.#cameraZ = 0;
                    this.fpv = true;
                    this.cameraY = this.cameraYBase;
                    this.engine.dashboard.pointer.visible = true;
                    this.engine.pointerControls.once('unlock', ()=>{
                        this.fpv = false;
                        this.cameraY = this.cameraYBase * 1.5;
                        this.engine.dashboard.pointer.visible = false;
                    })
                }).catch(err=>{
                    console.log(err);
                })
            }
        }else if(this.#cameraZ > this.cameraZMin && this.fpv){
            this.fpv = false;
            this.cameraY = this.cameraYBase * 1.5;
        }
        //this.engine.dashboard.updateText(this.#cameraZ);
    }
    
    fadeDuration = 0.2
    characterGapOffset = 0.01;
    tsBoost = 1;
    accelerate = 0;

    lerp = (x, y, a) => x * (1 - a) + y * a;

    constructor(engine, io){
        this.source = io.source;
        this.model = io.object
        //this.data = data;
        
        this.engine = engine;

        this.mixer = new AnimationMixer(this.model);
        engine.mixers.push( this.mixer );

        this.animationsMap = {};
        this.source.animations.forEach(a=>{
            this.animationsMap[a.name] = this.mixer.clipAction(a);
        })

        engine.hero = this;

        // let bb = this.model.userData.bbox;
        // let size = getBoundingBoxSize(bb);
        // let center = getBoundingBoxCenterPoint(bb, this.model.userData.object.position)
        let bb = engine.meshUtils.getBoundingBox(io.object);
        this.size = engine.meshUtils.getBoundingBoxSize(bb);
        //console.log('Hero size is', this.size);
        this.cameraYBase = this.size.y / this.engine.scale;
        this.cameraZMin = 0.25 / this.engine.scale;
        this.cameraZMax = 12 / this.engine.scale;
        this.#cameraZ = this.cameraZDefault = 3.77 / this.engine.scale;

        // let center = getBoundingBoxCenterPoint(bb, io.object.position)

        // console.log('hero', size, center, size.y - 2*this.characterGapOffset)

        this.runVelocity = this.size.y * 4
        this.walkVelocity = this.size.y * 1

        this.po = engine.physics.add(this.model, 'kinematicPositionBased', false, undefined, 'capsule', { radius: this.size.y/4, halfHeight: this.size.y/4})
        this.po.collider.setTranslationWrtParent({x: 0, y: this.size.y/2, z: 0});
        
        this.currentAction = 'idle';
        this.animationsMap[this.currentAction].play()
        
        this.characterController = this.engine.physics.world.createCharacterController(this.characterGapOffset);
        //this.characterController.setUp({x:0, y:1, z:0});
        this.po.rigidBody.setTranslation(this.model.position)
        this.po.characterController = this.characterController;
        //this.characterController.enableSnapToGround(0.5);
        // // Don’t allow climbing slopes larger than 45 degrees.
        //this.characterController.setMaxSlopeClimbAngle(90 * Math.PI / 180);
        // Automatically slide down on slopes smaller than 30 degrees.
        // this.characterController.setMinSlopeSlideAngle(30 * Math.PI / 180);
        //this.characterController.enableAutostep(0.5, 0.002, true);
        //this.characterController.setApplyImpulsesToDynamicBodies(true);
        // this.characterController.setCharacterMass(50);

        this.orbitControl = this.engine.orbitControls
        //this.camera = this.engine.camera;
        this.camera = this.engine.cameraWorld;
        this.cameraY = this.cameraYBase * 1.5;

        this.direction = this.model.rotation.y;
        //this.directionVelocity = 0;
        this.actionStart = 0;
        this.cameraDelta = 0;
        this.cameraIdleDelta = 0;
        this.fpv = false;
        this.ready = true;
        this.camera.rotation.reorder('YZX');
        this.engine.camera.rotation.reorder('YZX');
        this.model.rotation.reorder('YZX');
    }

    update(delta){
        if (this.ready && this.engine.physics.started && !this.disableInput && delta < 1) {
            this.updateCharacterControls(delta)
            if (this.engine.renderer.xr.isPresenting && !this.fpv){
                this.cameraMode = 'fixed'
                this.engine.cameraRig.position.y = -this.engine.camera.position.y;
                //this.engine.dashboard.object.position.y = this.engine.camera.position.y;
                //this.cameraY = this.size.y * 1;
                //this.camera = this.engine.cameraWorld;
                // this.engine.activeObjects.position.x = -this.camera.position.x;
                // this.engine.activeObjects.position.z = -this.camera.position.z;
            }else if (this.engine.renderer.xr.isPresenting){
                this.cameraMode = 'rotate'
                this.cameraDelta = 0;
                this.engine.cameraRig.position.y = - this.engine.camera.position.y;
                //this.engine.dashboard.object.position.y = this.engine.camera.position.y;
            }else{
                this.cameraMode = 'rotate'
                this.engine.cameraRig.position.y = 0;
                //this.cameraY = this.size.y * 1.5;
                // this.camera = this.engine.cameraWorld;
                // this.engine.camera.position.set(0,0,0);
                // this.engine.camera.rotation.set(0,0,0);
            }
        }
    }

    dispose(){
        delete this.engine.hero;
        this.engine.mixers.splice(this.engine.mixers.indexOf(this.mixer), 1);
    }

    updateCharacterControls(delta) {
        let pc = this.engine.pointerControls;
        if (this.fpv && this.model.visible){
            this.model.visible = false;
        }
        if (!this.fpv && !this.model.visible){
            this.model.visible = true;
            this.#cameraZ = this.cameraZDefault;
            this.engine.camera.fov = this.engine.defaultCameraFov;
            this.engine.camera.updateProjectionMatrix();
        }
        let input = pc.input;

        if (this.cameraMode == 'fixed'){
            let vec = new Vector2(input[0], input[1]);
            vec.rotateAround(zero2, this.model.rotation.y - this.cameraDelta);
            input = [vec.x, vec.y];
        }

        let play = this.currentAction || 'idle', velocity = this.walkVelocity;
        this.fadeDuration = 0.2;
        if (input[1] && pc.running && this.#timeScale > 0.5) {
            play = 'run';
            velocity = this.runVelocity;
        } else if (input[1] > 0 && this.#timeScale > 0.2) {
            play = 'walk'
        } else if (input[1] < 0) {
            play = 'backward'
            velocity = this.walkVelocity * 0.9
        } else if (input[0] < 0) {
            play = 'right'
        } else if (input[0] > 0) {
            play = 'left'
        } else if (!this.currentAction.startsWith('idle')){
            play = 'idle';
        } else if (this.actionStart == -1){
            this.actionStart = 0;
            play = 'idle';
        }

        if (this.currentAction.startsWith('idle') && play.startsWith('idle') && this.actionStart > 25){
            let idx = 1 + parseInt(this.currentAction.split('.')[1] || 0);
            if (this.animationsMap[`idle.${idx}`]) {
                play = `idle.${idx}`
            }else{
                play = 'idle'
            }
            this.fadeDuration = 1;
        }

        if (this.currentAction.startsWith('idle') && play != 'idle' && !this.fpv && !this.engine.renderer.xr.isPresenting){
            this.cameraIdleDelta += delta * 0.33;
        }else {
            this.cameraIdleDelta = 0;
        }

        if (this.currentAction != play) {
            const toPlay = this.animationsMap[play]
            const current = this.animationsMap[this.currentAction]

            current.fadeOut(this.fadeDuration)
            //toPlay.timeScale = 1;
            toPlay.reset().fadeIn(this.fadeDuration).play();

            this.currentAction = play
            this.actionStart = 0;
            this.tsBoost = animationBoost[play] || 1
            this.accelerate = 0.77;
        }

        this.accelerate = this.lerp(this.accelerate, 1, delta);

        velocity *= this.tsBoost * this.accelerate;

        this.animationsMap[this.currentAction].timeScale = this.#timeScale * this.tsBoost * this.accelerate;

        this.actionStart += delta;
        this.cameraDelta += delta * ( pc.cameraLeft * -1 +  pc.cameraRight * 1)
        this.#walkDirection.setScalar(0);

        if (pc.cameraUp && this.cameraY < 5){
            this.cameraY+=delta;
        }
        if (pc.cameraDown && this.cameraY > 1){
            this.cameraY-=delta;
        }

        if (pc.moveCloser){
            this.cameraZ += delta*7;
        }

        if (pc.moveAway){
            this.cameraZ -= delta*7;
        }

        if (pc.motion){
            if (!this.fpv){
                //this.directionVelocity = this.directionVelocity * 2.5 * Math.abs(input[0])
                this.direction += input[0] * delta * 2.5 //this.directionVelocity;
                this.#walkDirection.set(0, 0, input[1])
            }else{
                if (this.engine.renderer.xr.isPresenting){
                    this.direction = this.engine.camera.rotation.y;
                }else{
                    this.direction = this.camera.rotation.y;
                }
                this.#walkDirection.set(input[0], 0, input[1])
                //console.log(this.camera.rotation.y * 180/Math.PI);
                // this.camera.getWorldDirection(this.walkDirection);
                // this.walkDirection.applyAxisAngle(this.rotateAngle, input[0]*Math.PI/2 + (input[1]<0 ? Math.PI : 0))
                //this.walkDirection.multiplyScalar(input[1]);
            }
            this.#walkDirection.applyAxisAngle(this.#rotateAngle, this.direction)
            this.#walkDirection.normalize();
            this.model.rotation.y = this.direction;
        }else if (this.fpv){
            this.model.rotation.y = this.camera.rotation.y;
        }

        this.#walkDirection.x = this.#walkDirection.x * velocity * delta// + this.model.position.x
        this.#walkDirection.y = -delta * velocity * 0.1 * 9.8; //gravity!!!
        this.#walkDirection.z = this.#walkDirection.z * velocity * delta// + this.model.position.z

        //const translation = this.rigidBody.translation();

        this.characterController.computeColliderMovement(
            this.po.collider,    // The collider we would like to move.
            this.#walkDirection, // The movement we would like to apply if there wasn’t any obstacle.
            QueryFilterFlags['EXCLUDE_SENSORS']
        );

        // for (let i = 0; i < this.characterController.numComputedCollisions(); i++) {
        //     let collision = this.characterController.computedCollision(i);
        //     //console.log(collision)
        //     // Do something with that collision information.
        // }

        let correctedMovement = this.characterController.computedMovement();
        // this.walkDirection.sub(correctedMovement)
        // let diff = Math.sqrt(this.walkDirection.x * this.walkDirection.x + 
        //     this.walkDirection.z * this.walkDirection.z) / delta / velocity
        let l1 = this.#desiredMovement.set(this.#walkDirection.x, this.#walkDirection.z).lengthSq(), 
            l2 = this.#correctedMovement.set(correctedMovement.x, correctedMovement.z).lengthSq();
        let ts = l2 / l1
        if (ts>0 && ts<10){
            this.#timeScale = this.lerp(this.#timeScale, Math.sqrt(ts), delta*2);
        }

        let v = new Vector3();
        v.copy(this.po.rigidBody.translation());
        v.add(correctedMovement)

        this.po.rigidBody.setNextKinematicTranslation(v);

        this.model.position.copy(this.po.rigidBody.nextTranslation())

        let cameraPosition = new Vector3().copy(this.camera.position)
        let cameraDesiredPosition;
        if (this.cameraMode == 'rotate'){
            cameraDesiredPosition = new Vector3(
                this.model.position.x + this.#cameraZ* Math.sin(this.model.rotation.y + Math.PI + this.cameraDelta + this.cameraIdleDelta),
                this.model.position.y + this.cameraY,
                this.model.position.z + this.#cameraZ* Math.cos(this.model.rotation.y + Math.PI + this.cameraDelta + this.cameraIdleDelta)
            )
        } else {
            // cameraDesiredPosition = new Vector3(
            //     this.model.position.x,
            //     this.model.position.y + (pc.isLocked? this.size.y*0.9 : this.cameraY),
            //     this.model.position.z - this.#cameraZ
            // )
            cameraDesiredPosition = new Vector3(
                this.model.position.x + this.#cameraZ* Math.sin(0+ Math.PI + this.cameraDelta + this.cameraIdleDelta),
                this.model.position.y + this.cameraY,
                this.model.position.z + this.#cameraZ* Math.cos(0+ Math.PI + this.cameraDelta + this.cameraIdleDelta)
            )
        }
        cameraDesiredPosition.multiplyScalar(this.engine.scale)

        cameraPosition.lerp(cameraDesiredPosition, this.cameraMode == 'fixed' ? delta*2 : delta*2)
        //cameraPosition.lerp(cameraDesiredPosition, this.fpv ? 1 : delta*2)
        this.camera.position.copy(cameraPosition)
        if (!this.fpv){
            this.camera.lookAt(
                this.engine.scale * this.model.position.x, 
                this.camera.position.y,//this.engine.scale * (this.cameraY -this.size.y * 0.5 + this.model.position.y), 
                this.engine.scale * this.model.position.z
            )
        }
    }

    idleReset(){
        this.actionStart = -1;
        //this.currentAction = 'idle'
    }

    async teleport(v3){
        if (this.model.visible){
            await this.engine.motionQueue.add({
                o: this.model,
                a: { visible: k => Math.round(k * 20) % 2 == 0},
                t: 1.5, p: true
            })
            this.po.rigidBody.setNextKinematicTranslation(v3);
            this.model.position.copy(v3);
            await this.engine.motionQueue.add({
                o: this.model,
                a: { visible: k => Math.round(k * 20) % 2 == 0},
                t: 1.5, p: true
            })
        }else{
            this.po.rigidBody.setNextKinematicTranslation(v3);
            this.model.position.copy(v3);
        }
    }

}

export { Hero }