import { Camera, DeepImmutable, Observable, PointerEventTypes, PointerInfo } from './loader/babylonjs-import';
import { ViewerCamera } from './camera/ViewerCamera';
import { PickOptionType, PredefinedCanvasPosition } from './loader/Selectables';

/**
 * Pointer button is pressed
 */
export interface PointerButtonInfoDown {
    /** Pointer button is pressed */
    event: 'down';
    /** The pointer button associated with the {@link PointerButtonInfo} instance. */
    button: number;
    /** Exact time in ms, relative ``performance.timeOrigin``, when button was pressed. */
    downAt: number;
    /** Duration in ms that button has been pressed. */
    duration: number;
}

/**
 * Pointer button is released
 */
export interface PointerButtonInfoUp {
    /** Pointer button is released */
    event: 'up';
    /** The pointer button associated with the {@link PointerButtonInfo} instance. */
    button: number;
    /** Exact time in ms, relative ``performance.timeOrigin``, when button was pressed. */
    downAt: number;
    /** Exact time in ms, relative ``performance.timeOrigin``, when button was relased. */
    upAt: number;
    /** Duration in ms that button was pressed. Same as {@link upAt} - {@link downAt}. */
    duration: number;
}

/**
 * Pointer button is not pressed
 */
export interface PointerButtonInfoNone {
    /** No pointer button is pressed */
    event: 'none';
}

/** Represents pointer button events and timings. */
export type PointerButtonInfo = PointerButtonInfoDown | PointerButtonInfoUp | PointerButtonInfoNone;

/** Predefined point button id's */
export enum PredefinedPointerButtonId {
    /** Main button , usually the left button */
    Main = 0,
    /** Auxiliary button pressed, usually the wheel button or the middle button (if present) */
    Auxiliary = 1,
    /** Secondary button pressed, usually the right button */
    Secondary = 2,
    /** Fourth button, typically the Browser Back button */
    Fourth = 3,
    /** Fifth button, typically the Browser Forward button */
    Fifth = 4
}

/** Extends BabylonJS `PointerInfo` with additional twinfinity data. */
export interface PointerInfoWithTimings extends PointerInfo {
    /** Twinfinity extensions for pointer info */
    twinfinity: {
        /** Get timing information for a specific button. */
        button(buttonId: number | PredefinedPointerButtonId): PointerButtonInfo;

        /**
         * Perform pick operation in 3D worldspace where pointer is located.
         * @param isGeometryIntersectionEnabled `true` uses geometry intersection testing which
         * gives accurate results (exactly where something intersected). This is more expensive than `false`
         * which gives the object that was intersected but not exactly where, on the object, intersection occured.
         * @returns Picked object (if any)
         */
        pick: (isGeometryIntersectionEnabled: boolean) => ReturnType<ViewerCamera['pick']>;
    };
}

/**
 * Parse the `MouseEvent.buttons` property into a collection of buttons ids corresponding to possible ids
 * that `MouseEvent.button` can assume. In essence a collection of currently pressed pointer event buttons.
 * @param mouseEvent `MouseEvent.buttons`
 * @returns Collection of buttons ids.
 */
export function getMouseEventPressedButtons(mouseEvent: Pick<MouseEvent, 'buttons'>): Set<number> {
    const pressedButtons = new Set<number>();
    for (let button = 0; button < 32; ++button) {
        const buttonsIdMask = 1 << button;
        if (buttonsIdMask > mouseEvent.buttons) {
            break;
        }
        if ((mouseEvent.buttons & buttonsIdMask) > 0) {
            // Flip Auxiliary and Secondary as the spec is like that and the values differ between buttons and button
            // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
            // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
            if (buttonsIdMask === 2) {
                pressedButtons.add(2);
            } else if (buttonsIdMask === 4) {
                pressedButtons.add(1);
            } else {
                pressedButtons.add(button);
            }
        }
    }

    return pressedButtons;
}

/**
 * Creates a
 * @hidden
 * @internal
 * @param scene Scene
 * @returns Observable
 */
export function createPointerInfoEnricherAndForwarder(
    camera: Camera
): (eventData: PointerInfo, dst: Pick<Observable<DeepImmutable<PointerInfoWithTimings>>, 'notifyObservers'>) => void {
    // Contains timestamps for when a button was pressed and released
    const pointerButtonTimings = new Map<PointerInfo['event']['button'], PointerButtonInfo>();
    const noPointerButtonInfo = Object.freeze({ event: 'none' });

    return (eventData, dstObservable): void => {
        try {
            // Enrich original PointerInfo instance with additional useful methods
            // which can be used to find out which buttons are pressed, how long they have been pressed
            // etc.
            const eD = eventData as PointerInfoWithTimings;
            eD.twinfinity = {
                button: (buttonId: number | PredefinedPointerButtonId): PointerButtonInfo => {
                    return pointerButtonTimings.get(buttonId) ?? noPointerButtonInfo;
                },
                pick: (isGeometryIntersectionEnabled: boolean) => {
                    return camera.twinfinity.pick({
                        type: PickOptionType.Canvas,
                        position: PredefinedCanvasPosition.Mouse,
                        isGeometryIntersectionEnabled
                    });
                }
            };

            const now = performance.now();
            if (eventData.type === PointerEventTypes.POINTERDOWN) {
                pointerButtonTimings.set(eventData.event.button, {
                    event: 'down',
                    button: eventData.event.button,
                    downAt: now,
                    duration: 0
                });
            } else if (eventData.type === PointerEventTypes.POINTERUP) {
                const existingButtonTiming = pointerButtonTimings.get(eventData.event.button);
                if (existingButtonTiming?.event === 'down') {
                    pointerButtonTimings.set(eventData.event.button, {
                        event: 'up',
                        button: eventData.event.button,
                        downAt: existingButtonTiming.downAt,
                        upAt: now,
                        duration: now - existingButtonTiming.downAt
                    });
                }
            } else {
                // Attempt to update timings for all currently pressed buttons
                // We end up here when other pointer events occur, wheel, tap, double tap and move etc.
                for (const pointerButtonTiming of pointerButtonTimings.values()) {
                    if (pointerButtonTiming.event !== 'none') {
                        pointerButtonTiming.duration = now - pointerButtonTiming.downAt;
                    }
                }
            }

            dstObservable.notifyObservers(eD);
        } finally {
            // Clear pointer timings for button when its released. We rely on the fact
            // that buttons will contain the pressed button during all pointer events up until its released
            // so when PointerEventTypes.POINTERUP occurs then eventData.event.buttons no longer contains
            // the button. Therefore we know that we can remove the button from pointerButtonTimings. We do it like
            // this because it is more robust in cases where canvas looses focus and we therefore loose a PointerEventTypes.POINTERUP
            // event.
            // const pressedButtons = getMouseEventPressedButtons(eventData.event);
            // const buttonTimingKeys = [...pointerButtonTimings.keys()];
            // for (const buttonTimingId of buttonTimingKeys) {
            //     if (!pressedButtons.has(buttonTimingId)) {
            //         pointerButtonTimings.delete(buttonTimingId);
            //     }
            // }

            if (eventData.type === PointerEventTypes.POINTERUP) {
                pointerButtonTimings.delete(eventData.event.button);
            }
        }
    };
}
