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

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

/**
 * Manage the mouse inputs to control the movement of a PivotTargetCamera.
 * @see http://doc.babylonjs.com/how_to/customizing_camera_inputs
 */
export class TwinfinityTargetCameraMouseZoomInput<TCamera extends TargetCamera> implements ICameraInput<TCamera> {
    private _delta = 0;
    private _lastWheelSpinTimeInMs?: number;
    private _currentDirection: ZoomDirection = 0;
    private _lastScreenCoordinate = Vector2.Zero();
    private _screenCoordinate = Vector2.Zero();

    /**
     * This function is run every time onPointerObservable fires
     */
    private _wheelHandler!: Nullable<(p: PointerInfo, s: EventState) => void>;
    private _clickHandler!: Nullable<(p: PointerInfo, s: EventState) => void>;
    /**
     * This variable contains the Observable
     */
    private _wheelObserver!: Nullable<Observer<PointerInfo>>;
    private _clickObserver!: Nullable<Observer<PointerInfo>>;

    private _mouseWheelDown = false;

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

    /**
     * Gets or Set the mouse wheel precision or how fast is the camera zooming.
     */
    @serialize()
    public wheelPrecision = 180.0;

    public constructor(private readonly _cameraOptions: CameraOptions) {}

    private _counter = 0;
    /**
     * 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 {
        this._wheelHandler = (p, s) => {
            //sanity check - this should be a PointerWheel event.
            if (p.type !== PointerEventTypes.POINTERWHEEL || !this._cameraOptions.isZoomEnabled) {
                return;
            }

            if (this._mouseWheelDown) {
                return;
            }
            const now = performance.now();
            const deltaTime = Math.max(1, now - (this._lastWheelSpinTimeInMs ?? 0));
            this._screenCoordinate.copyFromFloats(scene.pointerX, scene.pointerY);

            const evt = p.event as WheelEvent;
            const mouseWheelLegacyEvent = evt as any;
            let delta = 0;
            let wheelDelta = 0;

            if (mouseWheelLegacyEvent.wheelDelta) {
                wheelDelta = mouseWheelLegacyEvent.wheelDelta;
            } else {
                wheelDelta = -(evt.deltaY || evt.detail) * 60;
            }

            delta = wheelDelta / (this.wheelPrecision * this.camera.getEngine().getHardwareScalingLevel());

            // If wheel has never been turned, if wheel has not been turned in N ms or if
            // mouse pointer differs more than M pixels since last time. Then we say that
            // we start a zoom operation. Otherwise we count it as a ongoing zoom operation.
            const currentDirection = Math.sign(delta) as ZoomDirection;
            const switchedDirection = currentDirection !== this._currentDirection;
            const isZoomOperationStart =
                this._lastWheelSpinTimeInMs === undefined ||
                deltaTime > 500 ||
                Vector2.Distance(this._screenCoordinate, this._lastScreenCoordinate) > 5 ||
                switchedDirection;
            this._lastScreenCoordinate.copyFrom(this._screenCoordinate);
            this._currentDirection = currentDirection;
            this._lastWheelSpinTimeInMs = now;
            if (isZoomOperationStart || !this.camera.twinfinity.pivot.isInFront) {
                this.camera.twinfinity.pivot.update({ canvasCoordinate: this._screenCoordinate });
                this._delta = 0;
            }

            this._delta += Math.abs(delta);

            if (!noPreventDefault) {
                evt.preventDefault();
            }
        };

        this._clickHandler = (p, s) => {
            if (p.event.button === 1) {
                this._mouseWheelDown = p.type === PointerEventTypes.POINTERDOWN;
            }
        };

        // register the pointerInput event to on pointer observable
        const scene = this.camera.getScene();
        this._wheelObserver = scene.onPointerObservable.add(this._wheelHandler, PointerEventTypes.POINTERWHEEL);
        this._clickObserver = scene.onPointerObservable.add(
            this._clickHandler,
            PointerEventTypes.POINTERDOWN | PointerEventTypes.POINTERUP
        );
    }

    /**
     * Checks if user is zooming.
     */
    public get isZooming(): boolean {
        return !Scalar.WithinEpsilon(this._delta, 0);
    }

    public checkInputs(): void {
        // This function is executed on every frame. Use accumulated delta and count it down to zero using inertia
        if (this._clickObserver && this._cameraOptions.isZoomEnabled) {
            if (!this.isZooming) {
                return;
            }

            const step = Math.max(0.01, this._delta * 0.5);
            let deltaToUse = step;
            //this._delta = Math.max(0, this._delta - deltaToUse);
            if (this._delta <= step) {
                deltaToUse = this._delta;
                this._delta = 0;
            } else {
                this._delta -= step;
            }

            this.camera.twinfinity.pivot.zoom(this._currentDirection * deltaToUse, this._screenCoordinate);
        }
    }

    /**
     * Detaches the current controls from the specified DOM element.
     */
    public detachControl(): void {
        if (this._wheelObserver) {
            this.camera.getScene().onPointerObservable.remove(this._wheelObserver);
            this._wheelObserver = null;
            this._wheelHandler = null;
            this._clickObserver = null;
            this._clickHandler = null;
        }
    }

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

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

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