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

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

/**
 * Manage the mouse inputs to control the movement of a PivotTargetCamera.
 * @see http://doc.babylonjs.com/how_to/customizing_camera_inputs
 */
export class TwinfinityTargetCameraMousePanInput<TCamera extends TargetCamera> implements ICameraInput<TCamera> {
    private _isPanning = false;
    private _srcElement?: HTMLElement;
    private _pointerEvent?: PointerEvent;
    private _onBeginFrameObservable: Nullable<Observer<AbstractEngine>>;
    private _buttonThatCausedPan = -1;

    public constructor(private readonly _cameraOptions: CameraOptions) {}

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

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

    /**
     * Defines the buttons associated with the input to handle camera pan.
     * Middle mouse button is used as default (often invoked by pressing scroll wheel).
     *  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 = [3, 4];

    /**
     * Attach 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 {
        // register the pointerInput event to on pointer observable

        const scene = this.camera.getScene();
        const _noPreventDefault = noPreventDefault ?? false;
        this._onPointerInputObserver = scene.onPointerObservable.add(
            (p, e) => this.onPointerInput(p, e, _noPreventDefault),
            PointerEventTypes.POINTERDOWN | PointerEventTypes.POINTERUP | PointerEventTypes.POINTERMOVE
        );

        this._onBeginFrameObservable = scene.getEngine().onBeginFrameObservable.add(() => {
            // Stop panning 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._isPanning) {
                this.onPanEnd(this._srcElement, this._pointerEvent);
            }
        });
    }

    /**
     * Detach the current controls from the specified dom element.
     */
    public detachControl(): void {
        this.onPanEnd(this._srcElement, this._pointerEvent);
        if (this._onPointerInputObserver) {
            this.camera.getScene().onPointerObservable.remove(this._onPointerInputObserver);
            this._onPointerInputObserver = null;
        }

        if (this._onBeginFrameObservable) {
            this.camera.getScene().getEngine().onBeginFrameObservable.remove(this._onBeginFrameObservable);
            this._onBeginFrameObservable = null;
        }
    }

    /**
     * Checks if user is panning.
     */
    public get isPanning(): boolean {
        return this._isPanning;
    }

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

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

    /**
     * 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') {
            return;
        }
        if (!this._cameraOptions.isPanEnabled) {
            return;
        }

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

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

        const screenCoordinate = { x: scene.pointerX, y: scene.pointerY };
        // if (this._pointerEvent.buttons)
        const panButtonClicked = this.buttons.indexOf(evt.buttons) > -1;
        if (!this._isPanning && panButtonClicked && this._srcElement) {
            this.onPanStart(this._srcElement, this._pointerEvent, screenCoordinate);

            if (!noPreventDefault) {
                this._pointerEvent.preventDefault();
                engine.getInputElement()?.focus();
            }
        } else if (this._isPanning && this._buttonThatCausedPan === evt.buttons) {
            this.onPan(engine, screenCoordinate);

            if (!noPreventDefault) {
                this._pointerEvent.preventDefault();
            }
        } else if (this._isPanning && this._srcElement) {
            if (!noPreventDefault) {
                this._pointerEvent.preventDefault();
            }
            this.onPanEnd(this._srcElement, this._pointerEvent);
        }
    }

    private onPan(engine: AbstractEngine, screenCoordinate: Vertex2): void {
        if (this._isPanning && !engine.isPointerLock) {
            this.camera.twinfinity.pivot.isVisible = true;
            this.camera.twinfinity.pivot.pan(screenCoordinate);
        }
    }

    private onPanStart(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.update({ canvasCoordinate: screenCoordinate });
        this.camera.twinfinity.pivot.isVisible = true;
        this._isPanning = true;
        this._buttonThatCausedPan = evt.buttons;
    }

    private onPanEnd(srcElement: HTMLElement | undefined, pointerEvent: PointerEvent | undefined): void {
        try {
            if (pointerEvent && srcElement) {
                srcElement.releasePointerCapture(pointerEvent.pointerId);
            }
        } catch (e) {
            //Nothing to do with the error.
        }

        this.camera.twinfinity.pivot.isVisible = false;
        this._isPanning = false;
        this._pointerEvent = undefined;
        this._srcElement = undefined;
        this._buttonThatCausedPan = -1;
    }
}

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