// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

import type { InteractionEvent } from "../transport/protocol";

/**
 * Captures wheel / pointer events on the GL canvas, normalizes coords
 * to canvas-relative CSS pixels, and emits semantic
 * {@link InteractionEvent}s to the Renderer over its transport. The
 * Renderer owns the `ZoomController`s and runs the actual hit-test +
 * apply logic — see `applyWheel` / `applyPan` in `zoom-router.ts`.
 *
 * Pointer capture is set on `pointerdown` and released on `pointerup`
 * so drags continue to deliver `pointermove` events when the cursor
 * leaves the canvas.
 */
export class RawEventForwarder {
    private _element: HTMLElement | null = null;
    private _emit: ((e: InteractionEvent) => void) | null = null;
    private _pointerId: number | null = null;

    private _onWheel: ((e: WheelEvent) => void) | null = null;
    private _onPointerDown: ((e: PointerEvent) => void) | null = null;
    private _onPointerMove: ((e: PointerEvent) => void) | null = null;
    private _onPointerUp: ((e: PointerEvent) => void) | null = null;
    private _onPointerLeave: (() => void) | null = null;
    private _onClick: ((e: MouseEvent) => void) | null = null;
    private _onDblClick: ((e: MouseEvent) => void) | null = null;

    attach(element: HTMLElement, emit: (e: InteractionEvent) => void): void {
        this.detach();
        this._element = element;
        this._emit = emit;

        this._onWheel = (e: WheelEvent) => {
            const rect = element.getBoundingClientRect();
            const mx = e.clientX - rect.left;
            const my = e.clientY - rect.top;

            // `preventDefault` is fired unconditionally so the page
            // does not scroll while the chart is hovered — the worker
            // may still no-op if the cursor is outside any facet's
            // plot rect.
            e.preventDefault();
            emit({ type: "wheel", mx, my, deltaY: e.deltaY });
        };

        this._onPointerDown = (e: PointerEvent) => {
            const rect = element.getBoundingClientRect();
            const mx = e.clientX - rect.left;
            const my = e.clientY - rect.top;
            element.setPointerCapture(e.pointerId);
            this._pointerId = e.pointerId;
            emit({ type: "pointerdown", mx, my, pointerId: e.pointerId });
        };

        this._onPointerMove = (e: PointerEvent) => {
            const rect = element.getBoundingClientRect();
            const mx = e.clientX - rect.left;
            const my = e.clientY - rect.top;
            emit({ type: "pointermove", mx, my });
        };

        this._onPointerUp = (e: PointerEvent) => {
            if (this._pointerId !== null) {
                try {
                    element.releasePointerCapture(this._pointerId);
                } catch {
                    // capture may have already been released
                }

                this._pointerId = null;
            }

            void e;
            emit({ type: "pointerup" });
        };

        // Pointerleave gives the renderer a single signal that the
        // cursor truly exited the canvas — used for tooltip dismissal.
        // Hover + drag both ride the `pointermove` stream above, so no
        // parallel `mousemove` channel exists.
        this._onPointerLeave = () => {
            emit({ type: "pointerleave" });
        };

        this._onClick = (e: MouseEvent) => {
            const rect = element.getBoundingClientRect();
            emit({
                type: "click",
                mx: e.clientX - rect.left,
                my: e.clientY - rect.top,
            });
        };

        this._onDblClick = (e: MouseEvent) => {
            const rect = element.getBoundingClientRect();
            emit({
                type: "dblclick",
                mx: e.clientX - rect.left,
                my: e.clientY - rect.top,
            });
        };

        element.addEventListener("wheel", this._onWheel, { passive: false });
        element.addEventListener("pointerdown", this._onPointerDown);
        element.addEventListener("pointermove", this._onPointerMove);
        element.addEventListener("pointerup", this._onPointerUp);
        element.addEventListener("pointerleave", this._onPointerLeave);
        element.addEventListener("click", this._onClick);
        element.addEventListener("dblclick", this._onDblClick);
    }

    detach(): void {
        if (this._element) {
            if (this._onWheel) {
                this._element.removeEventListener("wheel", this._onWheel);
            }

            if (this._onPointerDown) {
                this._element.removeEventListener(
                    "pointerdown",
                    this._onPointerDown,
                );
            }

            if (this._onPointerMove) {
                this._element.removeEventListener(
                    "pointermove",
                    this._onPointerMove,
                );
            }

            if (this._onPointerUp) {
                this._element.removeEventListener(
                    "pointerup",
                    this._onPointerUp,
                );
            }

            if (this._onPointerLeave) {
                this._element.removeEventListener(
                    "pointerleave",
                    this._onPointerLeave,
                );
            }

            if (this._onClick) {
                this._element.removeEventListener("click", this._onClick);
            }

            if (this._onDblClick) {
                this._element.removeEventListener("dblclick", this._onDblClick);
            }
        }

        this._element = null;
        this._emit = null;
        this._pointerId = null;
        this._onPointerLeave = null;
        this._onClick = null;
        this._onDblClick = null;
    }
}
