import Hammer from "hammerjs";
import { Signal } from "./signaling";
import { logger } from "./logging";
import { offset } from "./dom";
import { getDeltaY } from "./util/wheel";
import { reversed, is_empty } from "./util/array";
import { isString } from "./util/types";
import { is_mobile } from "./util/compat";
import * as events from "./bokeh_events";
import { ContextMenu } from "./util/menus";
function is_touch(event) {
    return typeof TouchEvent !== "undefined" && event instanceof TouchEvent;
}
export class UIEvents {
    constructor(plot_view, toolbar, hit_area) {
        this.plot_view = plot_view;
        this.toolbar = toolbar;
        this.hit_area = hit_area;
        this.pan_start = new Signal(this, 'pan:start');
        this.pan = new Signal(this, 'pan');
        this.pan_end = new Signal(this, 'pan:end');
        this.pinch_start = new Signal(this, 'pinch:start');
        this.pinch = new Signal(this, 'pinch');
        this.pinch_end = new Signal(this, 'pinch:end');
        this.rotate_start = new Signal(this, 'rotate:start');
        this.rotate = new Signal(this, 'rotate');
        this.rotate_end = new Signal(this, 'rotate:end');
        this.tap = new Signal(this, 'tap');
        this.doubletap = new Signal(this, 'doubletap');
        this.press = new Signal(this, 'press');
        this.pressup = new Signal(this, 'pressup');
        this.move_enter = new Signal(this, 'move:enter');
        this.move = new Signal(this, 'move');
        this.move_exit = new Signal(this, 'move:exit');
        this.scroll = new Signal(this, 'scroll');
        this.keydown = new Signal(this, 'keydown');
        this.keyup = new Signal(this, 'keyup');
        this.hammer = new Hammer(this.hit_area, {
            touchAction: 'auto',
            inputClass: Hammer.TouchMouseInput,
        });
        this._configure_hammerjs();
        // Mouse & keyboard events not handled through hammerjs
        // We can 'add and forget' these event listeners because this.hit_area is a DOM element
        // that will be thrown away when the view is removed
        this.hit_area.addEventListener("mousemove", (e) => this._mouse_move(e));
        this.hit_area.addEventListener("mouseenter", (e) => this._mouse_enter(e));
        this.hit_area.addEventListener("mouseleave", (e) => this._mouse_exit(e));
        this.hit_area.addEventListener("contextmenu", (e) => this._context_menu(e));
        this.hit_area.addEventListener("wheel", (e) => this._mouse_wheel(e));
        // But we MUST remove listeners registered on document or we'll leak memory: register
        // 'this' as the listener (it implements the event listener interface, i.e. handleEvent)
        // instead of an anonymous function so we can easily refer back to it for removing
        document.addEventListener("keydown", this);
        document.addEventListener("keyup", this);
        this.menu = new ContextMenu([], {
            prevent_hide: (event) => event.button == 2 && event.target == this.hit_area,
        });
        this.hit_area.appendChild(this.menu.el);
    }
    destroy() {
        this.menu.remove();
        this.hammer.destroy();
        document.removeEventListener("keydown", this);
        document.removeEventListener("keyup", this);
    }
    handleEvent(e) {
        if (e.type == "keydown")
            this._key_down(e);
        else if (e.type == "keyup")
            this._key_up(e);
    }
    _configure_hammerjs() {
        // This is to be able to distinguish double taps from single taps
        this.hammer.get('doubletap').recognizeWith('tap');
        this.hammer.get('tap').requireFailure('doubletap');
        this.hammer.get('doubletap').dropRequireFailure('tap');
        this.hammer.on('doubletap', (e) => this._doubletap(e));
        this.hammer.on('tap', (e) => this._tap(e));
        this.hammer.on('press', (e) => this._press(e));
        this.hammer.on('pressup', (e) => this._pressup(e));
        this.hammer.get('pan').set({ direction: Hammer.DIRECTION_ALL });
        this.hammer.on('panstart', (e) => this._pan_start(e));
        this.hammer.on('pan', (e) => this._pan(e));
        this.hammer.on('panend', (e) => this._pan_end(e));
        this.hammer.get('pinch').set({ enable: true });
        this.hammer.on('pinchstart', (e) => this._pinch_start(e));
        this.hammer.on('pinch', (e) => this._pinch(e));
        this.hammer.on('pinchend', (e) => this._pinch_end(e));
        this.hammer.get('rotate').set({ enable: true });
        this.hammer.on('rotatestart', (e) => this._rotate_start(e));
        this.hammer.on('rotate', (e) => this._rotate(e));
        this.hammer.on('rotateend', (e) => this._rotate_end(e));
    }
    register_tool(tool_view) {
        const et = tool_view.model.event_type;
        if (et != null) {
            if (isString(et))
                this._register_tool(tool_view, et);
            else {
                // Multi-tools should only registered shared events once
                et.forEach((e, index) => this._register_tool(tool_view, e, index < 1));
            }
        }
    }
    _register_tool(tool_view, et, shared = true) {
        const v = tool_view;
        const { id } = v.model;
        const conditionally = (fn) => (arg) => {
            if (arg.id == id)
                fn(arg.e);
        };
        const unconditionally = (fn) => (arg) => {
            fn(arg.e);
        };
        switch (et) {
            case "pan": {
                if (v._pan_start != null)
                    v.connect(this.pan_start, conditionally(v._pan_start.bind(v)));
                if (v._pan != null)
                    v.connect(this.pan, conditionally(v._pan.bind(v)));
                if (v._pan_end != null)
                    v.connect(this.pan_end, conditionally(v._pan_end.bind(v)));
                break;
            }
            case "pinch": {
                if (v._pinch_start != null)
                    v.connect(this.pinch_start, conditionally(v._pinch_start.bind(v)));
                if (v._pinch != null)
                    v.connect(this.pinch, conditionally(v._pinch.bind(v)));
                if (v._pinch_end != null)
                    v.connect(this.pinch_end, conditionally(v._pinch_end.bind(v)));
                break;
            }
            case "rotate": {
                if (v._rotate_start != null)
                    v.connect(this.rotate_start, conditionally(v._rotate_start.bind(v)));
                if (v._rotate != null)
                    v.connect(this.rotate, conditionally(v._rotate.bind(v)));
                if (v._rotate_end != null)
                    v.connect(this.rotate_end, conditionally(v._rotate_end.bind(v)));
                break;
            }
            case "move": {
                if (v._move_enter != null)
                    v.connect(this.move_enter, conditionally(v._move_enter.bind(v)));
                if (v._move != null)
                    v.connect(this.move, conditionally(v._move.bind(v)));
                if (v._move_exit != null)
                    v.connect(this.move_exit, conditionally(v._move_exit.bind(v)));
                break;
            }
            case "tap": {
                if (v._tap != null)
                    v.connect(this.tap, conditionally(v._tap.bind(v)));
                break;
            }
            case "press": {
                if (v._press != null)
                    v.connect(this.press, conditionally(v._press.bind(v)));
                if (v._pressup != null)
                    v.connect(this.pressup, conditionally(v._pressup.bind(v)));
                break;
            }
            case "scroll": {
                if (v._scroll != null)
                    v.connect(this.scroll, conditionally(v._scroll.bind(v)));
                break;
            }
            default:
                throw new Error(`unsupported event_type: ${et}`);
        }
        // Skip shared events if registering multi-tool
        if (!shared)
            return;
        if (v._doubletap != null)
            v.connect(this.doubletap, unconditionally(v._doubletap.bind(v)));
        if (v._keydown != null)
            v.connect(this.keydown, unconditionally(v._keydown.bind(v)));
        if (v._keyup != null)
            v.connect(this.keyup, unconditionally(v._keyup.bind(v)));
        // Dual touch hack part 1/2
        // This is a hack for laptops with touch screen who may be pinching or scrolling
        // in order to use the wheel zoom tool. If it's a touch screen the WheelZoomTool event
        // will be linked to pinch. But we also want to trigger in the case of a scroll.
        if (is_mobile && v._scroll != null && et == 'pinch') {
            logger.debug("Registering scroll on touch screen");
            v.connect(this.scroll, conditionally(v._scroll.bind(v)));
        }
    }
    _hit_test_renderers(sx, sy) {
        const views = this.plot_view.get_renderer_views();
        for (const view of reversed(views)) {
            const { level } = view.model;
            if ((level == 'annotation' || level == 'overlay') && view.interactive_hit != null) {
                if (view.interactive_hit(sx, sy))
                    return view;
            }
        }
        return null;
    }
    _hit_test_frame(sx, sy) {
        return this.plot_view.frame.bbox.contains(sx, sy);
    }
    _hit_test_canvas(sx, sy) {
        return this.plot_view.layout.bbox.contains(sx, sy);
    }
    _trigger(signal, e, srcEvent) {
        const gestures = this.toolbar.gestures;
        const event_type = signal.name;
        const base_type = event_type.split(":")[0];
        const view = this._hit_test_renderers(e.sx, e.sy);
        const on_canvas = this._hit_test_canvas(e.sx, e.sy);
        switch (base_type) {
            case "move": {
                const active_gesture = gestures[base_type].active;
                if (active_gesture != null)
                    this.trigger(signal, e, active_gesture.id);
                const active_inspectors = this.toolbar.inspectors.filter(t => t.active);
                let cursor = "default";
                // the event happened on a renderer
                if (view != null) {
                    cursor = view.cursor(e.sx, e.sy) || cursor;
                    if (!is_empty(active_inspectors)) {
                        // override event_type to cause inspectors to clear overlays
                        signal = this.move_exit; // XXX
                    }
                    // the event happened on the plot frame but off a renderer
                }
                else if (this._hit_test_frame(e.sx, e.sy)) {
                    if (!is_empty(active_inspectors)) {
                        cursor = "crosshair";
                    }
                }
                this.plot_view.set_cursor(cursor);
                this.plot_view.set_toolbar_visibility(on_canvas);
                active_inspectors.map((inspector) => this.trigger(signal, e, inspector.id));
                break;
            }
            case "tap": {
                const { target } = srcEvent;
                if (target != null && target != this.hit_area)
                    return; // don't trigger bokeh events
                if (view != null && view.on_hit != null)
                    view.on_hit(e.sx, e.sy);
                const active_gesture = gestures[base_type].active;
                if (active_gesture != null)
                    this.trigger(signal, e, active_gesture.id);
                break;
            }
            case "scroll": {
                // Dual touch hack part 2/2
                // This is a hack for laptops with touch screen who may be pinching or scrolling
                // in order to use the wheel zoom tool. If it's a touch screen the WheelZoomTool event
                // will be linked to pinch. But we also want to trigger in the case of a scroll.
                const base = is_mobile ? "pinch" : "scroll";
                const active_gesture = gestures[base].active;
                if (active_gesture != null) {
                    srcEvent.preventDefault();
                    srcEvent.stopPropagation();
                    this.trigger(signal, e, active_gesture.id);
                }
                break;
            }
            case "pan": {
                const active_gesture = gestures[base_type].active;
                if (active_gesture != null) {
                    srcEvent.preventDefault();
                    this.trigger(signal, e, active_gesture.id);
                }
                break;
            }
            default: {
                const active_gesture = gestures[base_type].active;
                if (active_gesture != null)
                    this.trigger(signal, e, active_gesture.id);
            }
        }
        this._trigger_bokeh_event(e);
    }
    trigger(signal, e, id = null) {
        signal.emit({ id, e });
    }
    _trigger_bokeh_event(e) {
        const ev = (() => {
            const xscale = this.plot_view.frame.xscales.default;
            const yscale = this.plot_view.frame.yscales.default;
            const { sx, sy } = e;
            const x = xscale.invert(sx);
            const y = yscale.invert(sy);
            switch (e.type) {
                case "wheel":
                    return new events.MouseWheel(sx, sy, x, y, e.delta);
                case "mousemove":
                    return new events.MouseMove(sx, sy, x, y);
                case "mouseenter":
                    return new events.MouseEnter(sx, sy, x, y);
                case "mouseleave":
                    return new events.MouseLeave(sx, sy, x, y);
                case "tap":
                    return new events.Tap(sx, sy, x, y);
                case "doubletap":
                    return new events.DoubleTap(sx, sy, x, y);
                case "press":
                    return new events.Press(sx, sy, x, y);
                case "pressup":
                    return new events.PressUp(sx, sy, x, y);
                case "pan":
                    return new events.Pan(sx, sy, x, y, e.deltaX, e.deltaY);
                case "panstart":
                    return new events.PanStart(sx, sy, x, y);
                case "panend":
                    return new events.PanEnd(sx, sy, x, y);
                case "pinch":
                    return new events.Pinch(sx, sy, x, y, e.scale);
                case "pinchstart":
                    return new events.PinchStart(sx, sy, x, y);
                case "pinchend":
                    return new events.PinchEnd(sx, sy, x, y);
                case "rotate":
                    return new events.Rotate(sx, sy, x, y, e.rotation);
                case "rotatestart":
                    return new events.RotateStart(sx, sy, x, y);
                case "rotateend":
                    return new events.RotateEnd(sx, sy, x, y);
                default:
                    return undefined;
            }
        })();
        if (ev != null)
            this.plot_view.model.trigger_event(ev);
    }
    _get_sxy(event) {
        const { pageX, pageY } = is_touch(event) ? (event.touches.length != 0 ? event.touches : event.changedTouches)[0] : event;
        const { left, top } = offset(this.hit_area);
        return {
            sx: pageX - left,
            sy: pageY - top,
        };
    }
    _pan_event(e) {
        return Object.assign(Object.assign({ type: e.type }, this._get_sxy(e.srcEvent)), { deltaX: e.deltaX, deltaY: e.deltaY, shiftKey: e.srcEvent.shiftKey, ctrlKey: e.srcEvent.ctrlKey });
    }
    _pinch_event(e) {
        return Object.assign(Object.assign({ type: e.type }, this._get_sxy(e.srcEvent)), { scale: e.scale, shiftKey: e.srcEvent.shiftKey, ctrlKey: e.srcEvent.ctrlKey });
    }
    _rotate_event(e) {
        return Object.assign(Object.assign({ type: e.type }, this._get_sxy(e.srcEvent)), { rotation: e.rotation, shiftKey: e.srcEvent.shiftKey, ctrlKey: e.srcEvent.ctrlKey });
    }
    _tap_event(e) {
        return Object.assign(Object.assign({ type: e.type }, this._get_sxy(e.srcEvent)), { shiftKey: e.srcEvent.shiftKey, ctrlKey: e.srcEvent.ctrlKey });
    }
    _move_event(e) {
        return Object.assign(Object.assign({ type: e.type }, this._get_sxy(e)), { shiftKey: e.shiftKey, ctrlKey: e.ctrlKey });
    }
    _scroll_event(e) {
        return Object.assign(Object.assign({ type: e.type }, this._get_sxy(e)), { delta: getDeltaY(e), shiftKey: e.shiftKey, ctrlKey: e.ctrlKey });
    }
    _key_event(e) {
        return {
            type: e.type,
            keyCode: e.keyCode,
        };
    }
    _pan_start(e) {
        const ev = this._pan_event(e);
        // back out delta to get original center point
        ev.sx -= e.deltaX;
        ev.sy -= e.deltaY;
        this._trigger(this.pan_start, ev, e.srcEvent);
    }
    _pan(e) {
        this._trigger(this.pan, this._pan_event(e), e.srcEvent);
    }
    _pan_end(e) {
        this._trigger(this.pan_end, this._pan_event(e), e.srcEvent);
    }
    _pinch_start(e) {
        this._trigger(this.pinch_start, this._pinch_event(e), e.srcEvent);
    }
    _pinch(e) {
        this._trigger(this.pinch, this._pinch_event(e), e.srcEvent);
    }
    _pinch_end(e) {
        this._trigger(this.pinch_end, this._pinch_event(e), e.srcEvent);
    }
    _rotate_start(e) {
        this._trigger(this.rotate_start, this._rotate_event(e), e.srcEvent);
    }
    _rotate(e) {
        this._trigger(this.rotate, this._rotate_event(e), e.srcEvent);
    }
    _rotate_end(e) {
        this._trigger(this.rotate_end, this._rotate_event(e), e.srcEvent);
    }
    _tap(e) {
        this._trigger(this.tap, this._tap_event(e), e.srcEvent);
    }
    _doubletap(e) {
        // NOTE: doubletap event triggered unconditionally
        const ev = this._tap_event(e);
        this._trigger_bokeh_event(ev);
        this.trigger(this.doubletap, ev);
    }
    _press(e) {
        this._trigger(this.press, this._tap_event(e), e.srcEvent);
    }
    _pressup(e) {
        this._trigger(this.pressup, this._tap_event(e), e.srcEvent);
    }
    _mouse_enter(e) {
        this._trigger(this.move_enter, this._move_event(e), e);
    }
    _mouse_move(e) {
        this._trigger(this.move, this._move_event(e), e);
    }
    _mouse_exit(e) {
        this._trigger(this.move_exit, this._move_event(e), e);
    }
    _mouse_wheel(e) {
        this._trigger(this.scroll, this._scroll_event(e), e);
    }
    _context_menu(e) {
        if (!this.menu.is_open && this.menu.can_open) {
            e.preventDefault();
        }
        const { sx, sy } = this._get_sxy(e);
        this.menu.toggle({ left: sx, top: sy });
    }
    _key_down(e) {
        // NOTE: keyup event triggered unconditionally
        this.trigger(this.keydown, this._key_event(e));
    }
    _key_up(e) {
        // NOTE: keyup event triggered unconditionally
        this.trigger(this.keyup, this._key_event(e));
    }
}
UIEvents.__name__ = "UIEvents";
//# sourceMappingURL=ui_events.js.map