<script lang="ts">
    import { onMount } from 'svelte';
    import { swipeDirection } from '$lib/swipe';

    let {
        onPrev,
        onNext,
        onToggle,
        onLongPress,
        testid = 'tap-zone'
    }: {
        onPrev: () => void;
        onNext: () => void;
        onToggle: () => void;
        /**
         * Fires when a touch lingers > 450ms on any zone without
         * moving. The viewport-coord `anchor` lets the reader open a
         * sheet anchored to the press location. Desktop pointers
         * (`pointerType !== 'touch'`) never trigger this — right-click
         * is the desktop entry point and lives in the reader.
         */
        onLongPress?: (anchor: { x: number; y: number }) => void;
        testid?: string;
    } = $props();

    const LONG_PRESS_MS = 450;
    const MOVE_TOLERANCE = 8;

    let pressTimer: ReturnType<typeof setTimeout> | null = null;
    let pressStart: { x: number; y: number } | null = null;
    // Swipe origin — tracked for every touch pointerdown (independent of the
    // long-press timer) so a horizontal drag can turn the page.
    let swipeStart: { x: number; y: number } | null = null;
    // Self-expiring suppression: cleared either by an actual click
    // reaching `withSuppress` or by the SUPPRESS_EXPIRY_MS fallback
    // timer below. The latter matters when the user lifts off the
    // tap zone after a long-press (finger slid onto the sheet) — no
    // click is synthesized, so without an expiry the flag would leak
    // and eat the next legitimate tap.
    const SUPPRESS_EXPIRY_MS = 500;
    let suppressNextClick = false;
    let suppressExpiryTimer: ReturnType<typeof setTimeout> | null = null;

    function clearPress() {
        if (pressTimer != null) {
            clearTimeout(pressTimer);
            pressTimer = null;
        }
        pressStart = null;
    }

    function clearSuppress() {
        suppressNextClick = false;
        if (suppressExpiryTimer != null) {
            clearTimeout(suppressExpiryTimer);
            suppressExpiryTimer = null;
        }
    }

    function onPointerDown(e: PointerEvent) {
        if (e.pointerType !== 'touch') return;
        // Record the swipe origin for every touch, whether or not long-press
        // is wired up.
        swipeStart = { x: e.clientX, y: e.clientY };
        if (!onLongPress) return;
        clearPress();
        pressStart = { x: e.clientX, y: e.clientY };
        const startX = e.clientX;
        const startY = e.clientY;
        pressTimer = setTimeout(() => {
            pressTimer = null;
            // A long-press consumed the gesture — cancel any swipe so a
            // subsequent drag-then-lift (finger held, then moved) doesn't
            // also turn the page on top of opening the action sheet.
            swipeStart = null;
            suppressNextClick = true;
            if (suppressExpiryTimer != null) clearTimeout(suppressExpiryTimer);
            suppressExpiryTimer = setTimeout(() => {
                suppressNextClick = false;
                suppressExpiryTimer = null;
            }, SUPPRESS_EXPIRY_MS);
            onLongPress?.({ x: startX, y: startY });
        }, LONG_PRESS_MS);
    }

    function onPointerMove(e: PointerEvent) {
        if (pressTimer == null || pressStart == null) return;
        const dx = e.clientX - pressStart.x;
        const dy = e.clientY - pressStart.y;
        if (Math.hypot(dx, dy) > MOVE_TOLERANCE) clearPress();
    }

    function onPointerUp(e: PointerEvent) {
        if (swipeStart && e.pointerType === 'touch') {
            const dir = swipeDirection(e.clientX - swipeStart.x, e.clientY - swipeStart.y);
            if (dir) {
                // A swipe consumed the gesture — suppress the click the
                // browser may synthesize on the zone so it doesn't also fire
                // that zone's tap action (reusing the long-press suppressor).
                suppressNextClick = true;
                if (suppressExpiryTimer != null) clearTimeout(suppressExpiryTimer);
                suppressExpiryTimer = setTimeout(() => {
                    suppressNextClick = false;
                    suppressExpiryTimer = null;
                }, SUPPRESS_EXPIRY_MS);
                (dir === 'next' ? onNext : onPrev)();
            }
        }
        swipeStart = null;
        clearPress();
    }

    function onPointerCancel() {
        swipeStart = null;
        clearPress();
    }

    function onScroll() {
        // Scrolling while pressed almost always means the user is
        // panning, not deliberately holding. Cancel.
        swipeStart = null;
        clearPress();
    }

    function withSuppress(handler: () => void) {
        return () => {
            if (suppressNextClick) {
                clearSuppress();
                return;
            }
            handler();
        };
    }

    // onMount only runs on the client, so the window-scroll listener
    // is automatically guarded — no SSR check needed. The teardown
    // also clears any in-flight long-press timer.
    onMount(() => {
        if (!onLongPress) return;
        window.addEventListener('scroll', onScroll, true);
        return () => {
            window.removeEventListener('scroll', onScroll, true);
            clearPress();
            clearSuppress();
        };
    });
</script>

<div class="tap-zones" data-testid={testid}>
    <button
        type="button"
        class="zone left"
        onclick={withSuppress(onPrev)}
        onpointerdown={onPointerDown}
        onpointermove={onPointerMove}
        onpointerup={onPointerUp}
        onpointercancel={onPointerCancel}
        aria-label="Previous page"
        data-testid="{testid}-left"
    ></button>
    <button
        type="button"
        class="zone center"
        onclick={withSuppress(onToggle)}
        onpointerdown={onPointerDown}
        onpointermove={onPointerMove}
        onpointerup={onPointerUp}
        onpointercancel={onPointerCancel}
        aria-label="Toggle controls"
        data-testid="{testid}-center"
    ></button>
    <button
        type="button"
        class="zone right"
        onclick={withSuppress(onNext)}
        onpointerdown={onPointerDown}
        onpointermove={onPointerMove}
        onpointerup={onPointerUp}
        onpointercancel={onPointerCancel}
        aria-label="Next page"
        data-testid="{testid}-right"
    ></button>
</div>

<style>
    /* Invisible viewport overlay split into thirds. Mirrors the Mihon
       / Tachiyomi convention: left-third = prev, center-third = toggle
       chrome, right-third = next. `pointer-events: none` on the wrap
       so unrelated clicks (chrome buttons, sheets) aren't trapped;
       each zone re-enables pointer events locally. */
    .tap-zones {
        position: fixed;
        inset: 0;
        z-index: 5;
        display: grid;
        grid-template-columns: 1fr 1fr 1fr;
        pointer-events: none;
    }

    .zone {
        pointer-events: auto;
        background: transparent;
        border: 0;
        padding: 0;
        margin: 0;
        cursor: pointer;
        /* Let vertical panning through to the page but keep horizontal
           gestures for our swipe handler rather than browser scroll. */
        touch-action: pan-y;
        /* No visible style — these are interaction surfaces, not
           controls. Focus-visible is honored so keyboard users get
           a ring if they tab into one. */
    }

    .zone:focus-visible {
        outline: 2px solid var(--focus-ring);
        outline-offset: -4px;
    }
</style>
