import {
    AbstractMesh,
    DualShockPad,
    Engine,
    FreeCamera,
    FreeCameraGamepadInput,
    Gamepad as b_Gamepad,
    KeyboardEventTypes,
    Mesh,
    Quaternion,
    Scene,
    Vector3,
    WebXRCamera,
    WebXRDefaultExperience,
    WebXRSessionManager,
    WebXRState,
} from '@babylonjs/core/Legacy/legacy';

import { log } from './log.js';

if ((navigator as any).oscpu?.startsWith('Linux')) {
    // ^ oscpu is undefined on chrome on Android, at least...

    DualShockPad.prototype.update = function () {
        b_Gamepad.prototype.update.call(this);
        (window as any).G = this;

        (this as any)._rightStickAxisX = 3;
        (this as any)._rightStickAxisY = 4;

        this.buttonCross = this.browserGamepad.buttons[0].value;
        this.buttonCircle = this.browserGamepad.buttons[1].value;
        this.buttonTriangle = this.browserGamepad.buttons[2].value;
        this.buttonSquare = this.browserGamepad.buttons[3].value;

        this.buttonL1 = this.browserGamepad.buttons[4].value;
        this.buttonR1 = this.browserGamepad.buttons[5].value;
        this.leftTrigger = this.browserGamepad.buttons[6].value;
        this.rightTrigger = this.browserGamepad.buttons[7].value;
        this.buttonShare = this.browserGamepad.buttons[8].value;
        this.buttonOptions = this.browserGamepad.buttons[9].value;

        this.buttonLeftStick = this.browserGamepad.buttons[11].value;
        this.buttonRightStick = this.browserGamepad.buttons[12].value;

        this.dPadUp = this.browserGamepad.axes[7].value < 0 ? 1 : 0;
        this.dPadDown = this.browserGamepad.axes[7].value > 0 ? 1 : 0;
        this.dPadLeft = this.browserGamepad.axes[6].value < 0 ? 1 : 0;
        this.dPadRight = this.browserGamepad.axes[6].value > 0 ? 1 : 0;
    };
}

export type Interactivity = {
    floorMeshes: () => Mesh[],
    touchableMeshes: () => Mesh[],
};

export type EngineOptions = {
    initialPos: Vector3,
    initialRotation: Vector3,
    canvas: HTMLCanvasElement | null,
};

export type ButtonState = { [button: number]: boolean };

export class GamepadState {
    buttons: ButtonState = {};

    constructor (
        public gp: Gamepad, // NB. browser's Gamepad class, not Babylon's Gamepad class.
    ) {}

    latch(button: number): boolean {
        let result = false;
        const b = this.gp.buttons[button];
        if (b) {
            if (b.pressed) {
                if (!this.isDown(button)) result = true;
                this.buttons[button] = true;
            } else {
                this.buttons[button] = false;
            }
        }
        return result;
    }

    isDown(button: number): boolean {
        return this.buttons[button] ?? false;
    }
}

export class RunningEngine {
    // The active camera - plainCamera or xrCamera, depending.
    camera: FreeCamera;

    plainCamera: FreeCamera;
    gamepadInput: FreeCameraGamepadInput;

    xrCamera: WebXRCamera | null = null;

    leanBase: { position: Vector3 } | null = null;
    recenterBase: { rotation: Quaternion } | null = null;

    padStates = new Map<number, GamepadState>();
    keysDown = new Set<number>();
    keysChanged = new Map<number, boolean>();

    static async start(
        interactivity: Interactivity,
        options0: Partial<EngineOptions> = {},
    ): Promise<RunningEngine> {
        const options = Object.assign({
            initialPos: new Vector3(0, 1.6, 0),
            initialRotation: new Vector3(0, 0, 0).scaleInPlace(2 * Math.PI),
            canvas: document.getElementById("renderCanvas") as HTMLCanvasElement,
        }, options0);
        const engine = new Engine(options.canvas, true);
        const scene = new Scene(engine);
        return new RunningEngine(
            options,
            engine,
            interactivity,
            scene,
            (await WebXRSessionManager.IsSessionSupportedAsync('immersive-vr')
                ? await scene.createDefaultXRExperienceAsync({})
                : null));
    }

    private constructor (
        public options: EngineOptions,
        public engine: Engine,
        public interactivity: Interactivity,
        public scene: Scene,
        public xr: WebXRDefaultExperience | null,
    ) {
        this.plainCamera = new FreeCamera("camera", this.options.initialPos.clone(), this.scene);
        this.plainCamera.rotation = this.options.initialRotation;
        this.plainCamera.minZ = 0.1;
        this.plainCamera.inertia = 0.75;
        this.plainCamera.speed = 0.5;
        this.plainCamera.keysUp.push(87 /* W */);
        this.plainCamera.keysLeft.push(65 /* A */);
        this.plainCamera.keysDown.push(83 /* S */);
        this.plainCamera.keysRight.push(68 /* D */);
        this.plainCamera.keysUpward.push(69 /* E */);
        this.plainCamera.keysDownward.push(81 /* Q */);
        this.plainCamera.attachControl(true);

        scene.onKeyboardObservable.add(info => {
            if (info.event.metaKey) return;
            this.keysChanged.set(info.event.keyCode, info.type === KeyboardEventTypes.KEYDOWN);
        });

        this._setupCamera(this.plainCamera);

        if (this.xr) {
            this.xr.baseExperience.onStateChangedObservable.add(state => this.xrStateChanged(state));
            this.xrCamera = this.xr.baseExperience.camera;
            this._setupCamera(this.xrCamera);
        }

        this.camera = this.plainCamera;

        this.gamepadInput = new FreeCameraGamepadInput();
        this.gamepadInput.gamepadMoveSensibility = 320;
        this.gamepadInput.gamepadAngularSensibility = 100;
        this.plainCamera.inputs.add(this.gamepadInput);
        this.gamepadInput.attachControl();

        this.scene.gravity = new Vector3(0, -9.81 / 90, 0);
        this.scene.collisionsEnabled = true;

        if (this.options.canvas) {
            const canvas = this.options.canvas;
            if (this.xrAvailable) {
                canvas.onclick = () => this.xrToggle();
            } else {
                canvas.onclick = () => canvas.requestPointerLock?.();
            }
        }

        this.engine.runRenderLoop(() => {
            try {
                Array.from(navigator.getGamepads()).forEach(gp => {
                    if (gp !== null) this.checkGamepadInput(gp);
                });
                this.checkKeys();
                this.scene.render();
            } catch (e) {
                console.error('Error in render loop', e);
                throw e;
            }
        });

        window.addEventListener("resize", () => this.engine.resize());
    }

    _setupCamera(c: FreeCamera) {
        c.checkCollisions = true;
        c.applyGravity = false;
        (c as any)._needMoveForGravity = true;
        c.ellipsoid = new Vector3(0.25, this.options.initialPos.y / 2, 0.25);
    }

    get inXR(): boolean {
        return (this.xr !== null) && (this.xr.baseExperience.state === WebXRState.IN_XR);
    }

    get xrAvailable(): boolean {
        return this.xr !== null;
    }

    set onCollide(c: (m: AbstractMesh) => void) {
        this.plainCamera.onCollide = c;
        if (this.xrCamera) this.xrCamera.onCollide = c;
    }

    get position(): Vector3 {
        return this.camera.position;
    }

    get rotation(): Vector3 {
        return this.inXR
            ? this.camera.rotationQuaternion.toEulerAngles()
            : this.camera.rotation;
    }

    padStateFor(gp: Gamepad): GamepadState {
        const state = this.padStates.get(gp.index);
        if (state) {
            // Apparently reusing Gamepad instances across frames doesn't work,
            // so we update (!) the stored instance here:
            state.gp = gp;
            return state;
        }
        const newState = new GamepadState(gp);
        this.padStates.set(gp.index, newState);
        return newState;
    }

    checkGamepadInput(gp: Gamepad) {
        const state = this.padStateFor(gp);

        if (state.latch(9 /* options */)) location.reload();
        if (state.latch(3 /* triangle */)) this.turn180();

        if (this.xrAvailable) {
            if (state.latch(16 /* ps */)) this.xrToggle();
        }

        if (this.inXR) {
            this.xrUpdateLean(gp);
            if (state.latch(0 /* cross */)) this.xrTeleport();
            if (state.latch(1 /* circle */)) this.xrTouch();

            if (state.latch(8 /* share */)) {
                this.recenterBase = {
                    rotation: this.xrCamera!.rotationQuaternion.clone(),
                };
            }
            if (state.isDown(8 /* share */) && this.recenterBase) {
                this.xrCamera!.rotationQuaternion.copyFrom(
                    this.recenterBase.rotation);
            }
        }
    }

    checkKeys() {
        for (const [keyCode, state] of this.keysChanged.entries()) {
            if (state) {
                this.keysDown.add(keyCode);
                switch (keyCode) {
                    case 32 /* space */: this.jump(); break;
                    default: break;
                }
            } else {
                this.keysDown.delete(keyCode);
            }
        }
        this.keysChanged.clear();
    }

    jump() {
        if (Math.abs(this.camera.cameraDirection.y) < 0.1) {
            this.camera.cameraDirection.y += 1;
        }
    }

    xrUpdateLean(gp: Gamepad) {
        const pos = new Vector3((gp.axes[0]), (-gp.axes[1]), (-gp.axes[3]));
        if (pos.length() > 0.0625) {
            if (this.leanBase === null) {
                this.leanBase = { position: this.xrCamera!.position };
            }
            this.xrCamera!.position =
                pos.applyRotationQuaternion(this.xrCamera!.absoluteRotation)
                    .scale(0.25)
                    .add(this.leanBase.position);
        } else {
            if (this.leanBase !== null) {
                this.xrCamera!.position = this.leanBase.position;
            }
            this.leanBase = null;
        }
    }

    turn180() {
        const r = this.inXR
            ? this.xrCamera!.rotationQuaternion.toEulerAngles()
            : this.camera.rotation;
        r.y += Math.PI;
        r.y %= 2 * Math.PI;
        if (this.inXR) {
            this.xrCamera!.rotationQuaternion.copyFrom(r.toQuaternion());
        }
    }

    xrTouch() {
        const ray = this.xrCamera!.getForwardRay();
        const meshes = this.interactivity.touchableMeshes();
        const hit = this.scene.pickWithRay(ray, m => meshes.indexOf(m as any) !== -1);

        if (hit === null) return;
        if (meshes.indexOf(hit.pickedMesh as any) === -1) return;

        this.camera.onCollide?.(hit.pickedMesh!);
    }

    xrTeleport() {
        if (!this.inXR) return;

        const ray = this.xrCamera!.getForwardRay();
        const meshes = this.interactivity.floorMeshes();
        const hit = this.scene.pickWithRay(ray, m => meshes.indexOf(m as any) !== -1);

        if (hit === null) return;
        if (meshes.indexOf(hit.pickedMesh as any) === -1) return;
        if (hit.pickedPoint === null) return;

        const pos = hit.pickedPoint.add(new Vector3(0, 1.6, 0));
        this.xrCamera!.position = pos;
        this.xrCamera!.rotation = Vector3.Zero();
        if (this.leanBase !== null) this.leanBase.position = pos;
    }

    xrStateChanged(state: WebXRState) {
        switch (state) {
            case WebXRState.IN_XR:
                break;
            case WebXRState.ENTERING_XR:
                this.camera = this.xrCamera!;
                this.camera.position = this.plainCamera.position.clone();
                this.camera.rotationQuaternion = this.options.initialRotation.toQuaternion();
                break;
            case WebXRState.EXITING_XR:
                this.camera = this.plainCamera;
                this.camera.position = this.xrCamera!.position.clone();
                this.camera.rotation = this.xrCamera!.rotationQuaternion.toEulerAngles();
                break;
            case WebXRState.NOT_IN_XR:
                break;
        }
    }

    xrToggle() {
        if (this.inXR) {
            this.xr!.baseExperience.exitXRAsync();
        } else {
            this.xr!.baseExperience.enterXRAsync('immersive-vr', 'local').then(() => {
                this.xr!.baseExperience.sessionManager.session.onselect = () => this.xrTeleport();
            });
        }
    }
}
