// Svelte action — fires a `longpress` CustomEvent when the user holds the pointer
// down for `duration` ms without moving too far.
//
// Usage:
//   <div use:longpress={{ duration: 500 }} onlongpress={() => openMenu()} />
//
// Cancels on:
// - pointerup before duration elapses
// - pointermove > MOVE_THRESHOLD pixels (so a scroll attempt doesn't accidentally
//   trigger the menu)
// - pointercancel / pointerleave / contextmenu
//
// When the long-press fires we also swallow the *next* click that comes from the same
// pointer-release. Without this, holding on a post card would (a) open the context
// sheet and then (b) trigger the post's onclick → lightbox at pointer-up. Same goes
// for the native contextmenu event on long-press desktop right-click.

import type { ActionReturn } from 'svelte/action';

const MOVE_THRESHOLD = 10; // px

export interface LongpressOptions {
	duration?: number;
}

interface LongpressAttributes {
	onlongpress?: (event: CustomEvent<void>) => void;
}

export function longpress(
	node: HTMLElement,
	options: LongpressOptions = {}
): ActionReturn<LongpressOptions, LongpressAttributes> {
	let duration = options.duration ?? 500;
	let timer: ReturnType<typeof setTimeout> | null = null;
	let startX = 0;
	let startY = 0;
	/** Set true when the long-press fires; the very next click is then swallowed. */
	let suppressNextClick = false;

	const cancel = () => {
		if (timer !== null) {
			clearTimeout(timer);
			timer = null;
		}
	};

	const fireLongpress = () => {
		suppressNextClick = true;
		// Reset the flag on the next event loop if no click followed — protects against
		// the case where the user lifts their finger by lifting the device, no click.
		setTimeout(() => {
			suppressNextClick = false;
		}, 400);
		node.dispatchEvent(new CustomEvent('longpress'));
		timer = null;
	};

	const onPointerDown = (e: PointerEvent) => {
		startX = e.clientX;
		startY = e.clientY;
		suppressNextClick = false;
		cancel();
		timer = setTimeout(fireLongpress, duration);
	};

	const onPointerMove = (e: PointerEvent) => {
		if (timer === null) return;
		const dx = Math.abs(e.clientX - startX);
		const dy = Math.abs(e.clientY - startY);
		if (dx > MOVE_THRESHOLD || dy > MOVE_THRESHOLD) cancel();
	};

	const onClickCapture = (e: Event) => {
		if (suppressNextClick) {
			suppressNextClick = false;
			e.stopPropagation();
			e.preventDefault();
		}
	};

	const onContextMenu = (e: Event) => {
		// Block the desktop right-click menu when we've already fired our own.
		if (suppressNextClick) e.preventDefault();
	};

	node.addEventListener('pointerdown', onPointerDown);
	node.addEventListener('pointermove', onPointerMove);
	node.addEventListener('pointerup', cancel);
	node.addEventListener('pointerleave', cancel);
	node.addEventListener('pointercancel', cancel);
	node.addEventListener('contextmenu', onContextMenu);
	// Capture phase so we beat the inner button's bubbling click handler.
	node.addEventListener('click', onClickCapture, true);

	return {
		update(newOptions) {
			duration = newOptions.duration ?? 500;
		},
		destroy() {
			cancel();
			node.removeEventListener('pointerdown', onPointerDown);
			node.removeEventListener('pointermove', onPointerMove);
			node.removeEventListener('pointerup', cancel);
			node.removeEventListener('pointerleave', cancel);
			node.removeEventListener('pointercancel', cancel);
			node.removeEventListener('contextmenu', onContextMenu);
			node.removeEventListener('click', onClickCapture, true);
		}
	};
}
