import { isAndroid } from "../utils/device.js";
import { matches } from "../utils/util.js";

const SWIPE_THRESHOLD = 40;

/**
 * Controls all touch interactions and navigations for
 * a presentation.
 */
export default class Touch {
    constructor(Reveal) {
        this.Reveal = Reveal;

        // Holds information about the currently ongoing touch interaction
        this.touchStartX = 0;
        this.touchStartY = 0;
        this.touchStartCount = 0;
        this.touchCaptured = false;

        this.onPointerDown = this.onPointerDown.bind(this);
        this.onPointerMove = this.onPointerMove.bind(this);
        this.onPointerUp = this.onPointerUp.bind(this);
        this.onTouchStart = this.onTouchStart.bind(this);
        this.onTouchMove = this.onTouchMove.bind(this);
        this.onTouchEnd = this.onTouchEnd.bind(this);
    }

    /**
     *
     */
    bind() {
        let revealElement = this.Reveal.getRevealElement();

        if ("onpointerdown" in window) {
            // Use W3C pointer events
            revealElement.addEventListener(
                "pointerdown",
                this.onPointerDown,
                false,
            );
            revealElement.addEventListener(
                "pointermove",
                this.onPointerMove,
                false,
            );
            revealElement.addEventListener(
                "pointerup",
                this.onPointerUp,
                false,
            );
        } else if (window.navigator.msPointerEnabled) {
            // IE 10 uses prefixed version of pointer events
            revealElement.addEventListener(
                "MSPointerDown",
                this.onPointerDown,
                false,
            );
            revealElement.addEventListener(
                "MSPointerMove",
                this.onPointerMove,
                false,
            );
            revealElement.addEventListener(
                "MSPointerUp",
                this.onPointerUp,
                false,
            );
        } else {
            // Fall back to touch events
            revealElement.addEventListener(
                "touchstart",
                this.onTouchStart,
                false,
            );
            revealElement.addEventListener(
                "touchmove",
                this.onTouchMove,
                false,
            );
            revealElement.addEventListener("touchend", this.onTouchEnd, false);
        }
    }

    /**
     *
     */
    unbind() {
        let revealElement = this.Reveal.getRevealElement();

        revealElement.removeEventListener(
            "pointerdown",
            this.onPointerDown,
            false,
        );
        revealElement.removeEventListener(
            "pointermove",
            this.onPointerMove,
            false,
        );
        revealElement.removeEventListener("pointerup", this.onPointerUp, false);

        revealElement.removeEventListener(
            "MSPointerDown",
            this.onPointerDown,
            false,
        );
        revealElement.removeEventListener(
            "MSPointerMove",
            this.onPointerMove,
            false,
        );
        revealElement.removeEventListener(
            "MSPointerUp",
            this.onPointerUp,
            false,
        );

        revealElement.removeEventListener(
            "touchstart",
            this.onTouchStart,
            false,
        );
        revealElement.removeEventListener("touchmove", this.onTouchMove, false);
        revealElement.removeEventListener("touchend", this.onTouchEnd, false);
    }

    /**
     * Checks if the target element prevents the triggering of
     * swipe navigation.
     */
    isSwipePrevented(target) {
        // Prevent accidental swipes when scrubbing timelines
        if (matches(target, "video[controls], audio[controls]")) return true;

        while (target && typeof target.hasAttribute === "function") {
            if (target.hasAttribute("data-prevent-swipe")) return true;
            target = target.parentNode;
        }

        return false;
    }

    /**
     * Handler for the 'touchstart' event, enables support for
     * swipe and pinch gestures.
     *
     * @param {object} event
     */
    onTouchStart(event) {
        this.touchCaptured = false;

        if (this.isSwipePrevented(event.target)) return true;

        this.touchStartX = event.touches[0].clientX;
        this.touchStartY = event.touches[0].clientY;
        this.touchStartCount = event.touches.length;
    }

    /**
     * Handler for the 'touchmove' event.
     *
     * @param {object} event
     */
    onTouchMove(event) {
        if (this.isSwipePrevented(event.target)) return true;

        let config = this.Reveal.getConfig();

        // Each touch should only trigger one action
        if (!this.touchCaptured) {
            this.Reveal.onUserInput(event);

            let currentX = event.touches[0].clientX;
            let currentY = event.touches[0].clientY;

            // There was only one touch point, look for a swipe
            if (event.touches.length === 1 && this.touchStartCount !== 2) {
                let availableRoutes = this.Reveal.availableRoutes({
                    includeFragments: true,
                });

                let deltaX = currentX - this.touchStartX,
                    deltaY = currentY - this.touchStartY;

                if (
                    deltaX > SWIPE_THRESHOLD &&
                    Math.abs(deltaX) > Math.abs(deltaY)
                ) {
                    this.touchCaptured = true;
                    if (config.navigationMode === "linear") {
                        if (config.rtl) {
                            this.Reveal.next();
                        } else {
                            this.Reveal.prev();
                        }
                    } else {
                        this.Reveal.left();
                    }
                } else if (
                    deltaX < -SWIPE_THRESHOLD &&
                    Math.abs(deltaX) > Math.abs(deltaY)
                ) {
                    this.touchCaptured = true;
                    if (config.navigationMode === "linear") {
                        if (config.rtl) {
                            this.Reveal.prev();
                        } else {
                            this.Reveal.next();
                        }
                    } else {
                        this.Reveal.right();
                    }
                } else if (deltaY > SWIPE_THRESHOLD && availableRoutes.up) {
                    this.touchCaptured = true;
                    if (config.navigationMode === "linear") {
                        this.Reveal.prev();
                    } else {
                        this.Reveal.up();
                    }
                } else if (deltaY < -SWIPE_THRESHOLD && availableRoutes.down) {
                    this.touchCaptured = true;
                    if (config.navigationMode === "linear") {
                        this.Reveal.next();
                    } else {
                        this.Reveal.down();
                    }
                }

                // If we're embedded, only block touch events if they have
                // triggered an action
                if (config.embedded) {
                    if (this.touchCaptured || this.Reveal.isVerticalSlide()) {
                        event.preventDefault();
                    }
                }
                // Not embedded? Block them all to avoid needless tossing
                // around of the viewport in iOS
                else {
                    event.preventDefault();
                }
            }
        }
        // There's a bug with swiping on some Android devices unless
        // the default action is always prevented
        else if (isAndroid) {
            event.preventDefault();
        }
    }

    /**
     * Handler for the 'touchend' event.
     *
     * @param {object} event
     */
    onTouchEnd(event) {
        this.touchCaptured = false;
    }

    /**
     * Convert pointer down to touch start.
     *
     * @param {object} event
     */
    onPointerDown(event) {
        if (
            event.pointerType === event.MSPOINTER_TYPE_TOUCH ||
            event.pointerType === "touch"
        ) {
            event.touches = [
                { clientX: event.clientX, clientY: event.clientY },
            ];
            this.onTouchStart(event);
        }
    }

    /**
     * Convert pointer move to touch move.
     *
     * @param {object} event
     */
    onPointerMove(event) {
        if (
            event.pointerType === event.MSPOINTER_TYPE_TOUCH ||
            event.pointerType === "touch"
        ) {
            event.touches = [
                { clientX: event.clientX, clientY: event.clientY },
            ];
            this.onTouchMove(event);
        }
    }

    /**
     * Convert pointer up to touch end.
     *
     * @param {object} event
     */
    onPointerUp(event) {
        if (
            event.pointerType === event.MSPOINTER_TYPE_TOUCH ||
            event.pointerType === "touch"
        ) {
            event.touches = [
                { clientX: event.clientX, clientY: event.clientY },
            ];
            this.onTouchEnd(event);
        }
    }
}
