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

import { Observer, EventState, Observable, TargetCamera, AbstractEngine } from '../../loader/babylonjs-import';

// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { serialize } from '../../loader/babylonjs-import';
import { Nullable } from '../../loader/babylonjs-import';
import { ICameraInput } from '../../loader/babylonjs-import';
import { PointerInfo, PointerEventTypes } from '../../loader/babylonjs-import';
import { Vertex2 } from '../../math';
import { CameraOptions } from '../ViewerCamera';
import { boundingClientRectCache } from '../../BoundingClientRectCache';

/**
 * Manage the mouse inputs to control the movement of a PivotTargetCamera.
 * @see http://doc.babylonjs.com/how_to/customizing_camera_inputs
 */
export class TwinfinityTargetCameraMouseRotationInput implements ICameraInput<TargetCamera> {
    private _isRotating = false;

    private _pointerEvent?: PointerEvent;

    private _srcElement?: HTMLElement;

    private _onBeginFrameObserver: Nullable<Observer<AbstractEngine>>;

    /**
     * Holds the last observed mouse position in screen space pixels.
     * This position is used to calculate the delta movement of mouse pointer.
     */
    private _previousScreenCoordinate?: Vertex2;

    /**
     * This variable contains the Observable
     */
    private _onPointerObserver!: Nullable<Observer<PointerInfo>>;

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

    /**
     * Defines the buttons associated with the input to handle camera move.
     * Left mouse button is used as default for Rotation.
     *  0: No button or un-initialized
     *  1: Primary button (usually the left button)
     *  2: Secondary button (usually the right button)
     *  4: Auxiliary button (usually the mouse wheel button or middle button)
     *  8: 4th button (typically the "Browser Back" button)
     * 16 : 5th button (typically the "Browser Forward" button)
     * A button number can represent multiple buttons. For example button left and right is represented by 1 + 2 = 3.
     */
    @serialize()
    public buttons = [1, 2];

    /**
     * Observable for when a pointer move event occurs containing the move offset
     */
    public onPointerMovedObservable = new Observable<{ offsetX: number; offsetY: number }>();
    private _buttonThatStartedRotation = -1;

    /**
     * Manages the mouse inputs to control the movement of a PivotTargetCamera.
     * @see http://doc.babylonjs.com/how_to/customizing_camera_inputs
     */
    public constructor(private readonly _cameraOptions: CameraOptions) {}

    /**
     * Checks if user is rotating the camera.
     */
    public get isRotating(): boolean {
        return this._isRotating;
    }

    /**
     * Attaches the input controls to a specific dom element to get the input from.
     * @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 {
        const engine = this.camera.getEngine();
        const scene = this.camera.getScene();
        const element = engine.getInputElement();
        const _noPreventDefault = noPreventDefault ?? false;

        // register the pointerInput event to on pointer observable
        this._onPointerObserver = scene.onPointerObservable.add(
            (p, s) => this.onPointerInput(p, s, _noPreventDefault),
            PointerEventTypes.POINTERDOWN | PointerEventTypes.POINTERUP | PointerEventTypes.POINTERMOVE
        );

        this._onBeginFrameObserver = scene.getEngine().onBeginFrameObservable.add(() => {
            // Stop rotating if our document no longer has focus. This happens when another
            // application (like for example dev tools, Alt-tab) unexpectedly takes focus.
            if (!document.hasFocus() && this._isRotating) {
                this.onEndRotate(this._srcElement, this._pointerEvent);
            }
        });

        element?.addEventListener('contextmenu', this.onContextMenu.bind(this) as EventListener, false);
    }

    /**
     * Is called by JS contextmenu event.
     * Override this method to provide functionality.
     */
    protected onContextMenu(evt: PointerEvent): void {
        evt.preventDefault();
    }

    /**
     * Detaches the current controls from the specified DOM element.
     */
    public detachControl(): void {
        this.onEndRotate(this._srcElement, this._pointerEvent);

        if (this._onBeginFrameObserver !== null) {
            this.camera.getEngine().onBeginFrameObservable.remove(this._onBeginFrameObserver);
            this._onBeginFrameObserver = null;
        }

        if (this._onPointerObserver) {
            this.camera.getScene().onPointerObservable.remove(this._onPointerObserver);
            this._onPointerObserver = null;
        }

        this.onPointerMovedObservable.clear();
        const element = this.camera.getEngine().getInputElement();
        element?.removeEventListener('contextmenu', this.onContextMenu as EventListener);
    }

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

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

    /**
     * This function is run every time onPointerObservable fires
     */
    private onPointerInput(p: PointerInfo, s: EventState, noPreventDefault: boolean): void {
        const evt = p.event as PointerEvent;
        if (evt.pointerType === 'touch' || !this._cameraOptions.isRotationEnabled) {
            return;
        }

        const engine = this.camera.getEngine();
        const scene = this.camera.getScene();
        const canvas = engine.getInputElement();
        const screenCoordinate = { x: scene.pointerX, y: scene.pointerY };

        this._pointerEvent = evt;
        this._srcElement = (evt.srcElement || evt.target) as HTMLElement;

        const rotateButtonClicked = this.buttons.indexOf(evt.buttons) > -1;
        if (!this._isRotating && rotateButtonClicked) {
            this.onRotateStart(this._srcElement, evt, screenCoordinate);
            if (!noPreventDefault) {
                evt.preventDefault();
                canvas?.focus();
            }
        } else if (this._isRotating && this._buttonThatStartedRotation === evt.buttons) {
            this.onRotate(this._srcElement, this._pointerEvent, screenCoordinate);
            // update previous position to current cursor position for next time
            if (!noPreventDefault) {
                evt.preventDefault();
            }
        } else if (this._isRotating) {
            this.onEndRotate(this._srcElement, this._pointerEvent);
            if (!noPreventDefault) {
                evt.preventDefault();
            }
        }
    }

    private onRotateStart(srcElement: HTMLElement, evt: PointerEvent, screenCoordinate: Vertex2): void {
        try {
            srcElement.setPointerCapture(evt.pointerId);
        } catch (e) {
            //Nothing to do with the error. Execution will continue.
        }
        this.camera.twinfinity.pivot.isVisible = true;
        this.camera.twinfinity.pivot.update({ canvasCoordinate: screenCoordinate });
        this._previousScreenCoordinate = screenCoordinate;
        this._isRotating = true;
        this._buttonThatStartedRotation = evt.buttons;
        this.camera.twinfinity.isFreeLook =
            (this._cameraOptions.mode === 'all' || this._cameraOptions.mode === 'fps') && evt.buttons === 2;
    }

    private onRotate(_srcElement: HTMLElement, _pointerEvent: PointerEvent, screenCoordinate: Vertex2): void {
        const scene = this.camera.getScene();
        const engine = this.camera.getEngine();

        const canvas = engine.getInputElement()!;
        const canvasRect = boundingClientRectCache.getOrAdd(canvas);
        if (!this._isRotating || engine.isPointerLock || !this._previousScreenCoordinate) {
            return;
        }

        // deltaX and deltaY will be in range [-1, 1]. Ie normalized screen coordinates
        let deltaX = (screenCoordinate.x - this._previousScreenCoordinate.x) / canvasRect.width; // * Math.PI * 4;
        const deltaY = (screenCoordinate.y - this._previousScreenCoordinate.y) / canvasRect.height; // * Math.PI * 4;
        if (scene.useRightHandedSystem) {
            deltaX *= -1;
        }
        if (this.camera.parent && this.camera.parent._getWorldMatrixDeterminant() < 0) {
            deltaX *= -1;
        }

        // set current delta to camera rotation, this value can camera use to rotate the camera accordingly
        const fullRotation = this.camera.twinfinity.isFreeLook ? Math.PI * 2 : Math.PI * 4;
        this.camera.twinfinity.pivot.rotate(deltaY * fullRotation, deltaX * fullRotation);
        // this.camera.cameraRotation.y += deltaX * fullRotation /*/ frameRateScale*/;
        // this.camera.cameraRotation.x += deltaY * fullRotation /*/ frameRateScale*/;

        this.onPointerMovedObservable.notifyObservers({ offsetX: deltaX, offsetY: deltaY });

        // update previous position to current cursor position for next time
        this._previousScreenCoordinate = screenCoordinate;
        this.camera.twinfinity.pivot.isVisible = true;
    }

    private onEndRotate(srcElement: HTMLElement | undefined, pointerEvent: PointerEvent | undefined): void {
        try {
            // release canvas from input
            if (srcElement !== undefined && pointerEvent !== undefined) {
                srcElement.releasePointerCapture(pointerEvent.pointerId);
            }
        } catch (e) {
            //Nothing to do with the error.
        }

        // reset previous position
        this.camera.twinfinity.pivot.isVisible = false;
        this._previousScreenCoordinate = undefined;
        this.camera.twinfinity.isFreeLook = false;
        this.camera.cameraRotation.x = 0;
        this.camera.cameraRotation.y = 0;
        this._buttonThatStartedRotation = -1;
        this._isRotating = false;
        this._srcElement = undefined;
        this._pointerEvent = undefined;
    }
}

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