/** @internal NOTE: Internal APIs. Subject to change. Use of these APIs in production applications is not supported. */

import {
    Observer,
    TargetCamera as Camera,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    serialize,
    Nullable,
    ICameraInput,
    Vector3,
    KeyboardInfo,
    KeyboardEventTypes,
    AbstractEngine
} from '../../loader/babylonjs-import';
import { CameraOptions } from '../ViewerCamera';

/**
 * Manage the keyboard inputs to control the movement of a TargetCamera.
 * @see http://doc.babylonjs.com/how_to/customizing_camera_inputs
 */
export class TwinfinityTargetCameraKeyboardInput<TCamera extends Camera> implements ICameraInput<TCamera> {
    public constructor(private readonly _cameraOptions: CameraOptions) {}

    /**
     * Defines the camera the input is attached to.
     */
    public camera!: TCamera;

    /**
     * Gets or Set the list of keyboard keys used to control the forward move of the camera.
     */
    @serialize()
    public keysUp = [38, 87];

    /**
     * Gets or Set the list of keyboard keys used to control the backward move of the camera.
     */
    @serialize()
    public keysDown = [40, 83];

    /**
     * Gets or Set the list of keyboard keys used to control the left strafe move of the camera.
     */
    @serialize()
    public keysLeft = [37, 65];

    /**
     * Gets or Set the list of keyboard keys used to control the right strafe move of the camera.
     */
    @serialize()
    public keysRight = [39, 68];

    private _keys = new Array<number>();
    private _onCanvasBlurObserver!: Nullable<Observer<AbstractEngine>>;
    private _onKeyboardObserver!: Nullable<Observer<KeyboardInfo>>;
    private _transformedDirection = Vector3.Zero();
    private _localDirection = Vector3.Zero();

    /**
     * Attach the input controls.
     * @param noPreventDefault Defines whether event caught by the controls should call preventdefault() (https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault)
     */
    public attachControl(noPreventDefault?: boolean): void {
        if (this._onCanvasBlurObserver) {
            return;
        }

        const scene = this.camera.getScene();
        const engine = this.camera.getEngine();

        this._onCanvasBlurObserver = engine.onCanvasBlurObservable.add(() => {
            this._keys = [];
        });

        this._onKeyboardObserver = scene.onKeyboardObservable.add((info) => {
            const evt = info.event;
            if (!evt.metaKey) {
                if (info.type === KeyboardEventTypes.KEYDOWN) {
                    if (
                        this.keysUp.indexOf(evt.keyCode) !== -1 ||
                        this.keysDown.indexOf(evt.keyCode) !== -1 ||
                        this.keysLeft.indexOf(evt.keyCode) !== -1 ||
                        this.keysRight.indexOf(evt.keyCode) !== -1
                    ) {
                        const index = this._keys.indexOf(evt.keyCode);

                        if (index === -1) {
                            this._keys.push(evt.keyCode);
                        }
                        if (!noPreventDefault) {
                            evt.preventDefault();
                        }
                    }
                } else {
                    if (
                        this.keysUp.indexOf(evt.keyCode) !== -1 ||
                        this.keysDown.indexOf(evt.keyCode) !== -1 ||
                        this.keysLeft.indexOf(evt.keyCode) !== -1 ||
                        this.keysRight.indexOf(evt.keyCode) !== -1
                    ) {
                        const index = this._keys.indexOf(evt.keyCode);

                        if (index >= 0) {
                            this._keys.splice(index, 1);
                        }
                        if (!noPreventDefault) {
                            evt.preventDefault();
                        }
                    }
                }
            }
        });
    }

    /**
     * Detach the current controls from the specified dom element.
     */
    public detachControl(): void {
        if (!this.camera) {
            return;
        }
        const scene = this.camera.getScene();
        const engine = this.camera.getEngine();

        if (this._onKeyboardObserver) {
            scene.onKeyboardObservable.remove(this._onKeyboardObserver);
        }

        if (this._onCanvasBlurObserver) {
            engine.onCanvasBlurObservable.remove(this._onCanvasBlurObserver);
        }
        this._onKeyboardObserver = null;
        this._onCanvasBlurObserver = null;

        this._keys = [];
    }

    /**
     * Update the current camera state depending on the inputs that have been used this frame.
     * This is a dynamically created lambda to avoid the performance penalty of looping for inputs in the render loop.
     */
    public checkInputs(): void {
        if (this._onKeyboardObserver && this._cameraOptions.isKeyboardMoveEnabled) {
            const camera = this.camera;

            // Keyboard
            for (let index = 0; index < this._keys.length; index++) {
                const keyCode = this._keys[index];
                const speed = camera._computeLocalCameraSpeed();

                if (this.keysLeft.indexOf(keyCode) !== -1) {
                    this._localDirection.copyFromFloats(-speed, 0, 0);
                } else if (this.keysUp.indexOf(keyCode) !== -1) {
                    this._localDirection.copyFromFloats(0, 0, speed);
                } else if (this.keysRight.indexOf(keyCode) !== -1) {
                    this._localDirection.copyFromFloats(speed, 0, 0);
                } else if (this.keysDown.indexOf(keyCode) !== -1) {
                    this._localDirection.copyFromFloats(0, 0, -speed);
                }

                if (camera.getScene().useRightHandedSystem) {
                    this._localDirection.z *= -1;
                }

                camera.getViewMatrix().invertToRef(camera._cameraTransformMatrix);
                Vector3.TransformNormalToRef(
                    this._localDirection,
                    camera._cameraTransformMatrix,
                    this._transformedDirection
                );
                camera.position.addInPlace(this._transformedDirection);
            }
        }
    }

    /**
     * Gets the class name of the current intput.
     * @returns the class name
     */
    public getClassName(): string {
        return 'TargetCameraKeyboardInput';
    }

    /** @hidden */
    public _onLostFocus(): void {
        this._keys = [];
    }

    /**
     * Get the friendly name associated with the input class.
     * @returns the input friendly name
     */
    public getSimpleName(): string {
        return 'keyboard';
    }
}

// (CameraInputTypes as any)['TargetCameraMousePanInput'] = TargetCameraMousePanInput;
