// Pull-to-refresh gesture. Only engages when the page is scrolled to the top.
// Calls `onrefresh` once `threshold` px of overscroll have been pulled. The
// element should set `overscroll-behavior-y: contain` so the browser doesn't
// hijack the gesture (mobile Chrome's built-in refresh).
//
// `onpull` fires during the drag with `delta` (px pulled, clamped ≥ 0) and a
// `progress` ratio (0–1+). Consumers can use it to render a growing indicator
// that closes the visual loop before the refresh actually starts.

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

export interface PullToRefreshOptions {
	onrefresh: () => void | Promise<void>;
	onpull?: (delta: number, progress: number) => void;
	threshold?: number;
	disabled?: boolean;
}

export function pullToRefresh(
	node: HTMLElement,
	options: PullToRefreshOptions
): ActionReturn<PullToRefreshOptions> {
	let opts = options;
	let startY = 0;
	let startX = 0;
	let pulling = false;
	let triggered = false;
	let intentLocked = false; // have we decided this gesture is a vertical pull?

	// Distance the finger must travel before we commit to "this is a vertical pull"
	// vs. a horizontal swipe or an incidental tap.
	const INTENT_SLOP = 8;
	// Rubber-band resistance: the sheet follows the finger at a fraction of 1:1 so
	// the pull feels elastic rather than rigid.
	const RESISTANCE = 0.5;

	function scroller(): HTMLElement | (Window & typeof globalThis) {
		return node.scrollHeight > node.clientHeight ? node : window;
	}

	function scrollTop(): number {
		const s = scroller();
		return s instanceof Window ? window.scrollY : s.scrollTop;
	}

	function reportPull(delta: number) {
		if (!opts.onpull) return;
		const threshold = opts.threshold ?? 60;
		opts.onpull(Math.max(0, delta), Math.max(0, delta) / threshold);
	}

	function reset() {
		pulling = false;
		intentLocked = false;
	}

	function onTouchStart(e: TouchEvent) {
		if (opts.disabled) return;
		// Ignore pinch / multi-finger gestures entirely.
		if (e.touches.length !== 1) {
			reset();
			return;
		}
		if (scrollTop() > 0) return;
		startY = e.touches[0].clientY;
		startX = e.touches[0].clientX;
		pulling = true;
		triggered = false;
		intentLocked = false;
	}

	function onTouchMove(e: TouchEvent) {
		if (!pulling || triggered) return;
		// A second finger landing mid-gesture cancels the pull.
		if (e.touches.length !== 1) {
			reset();
			return;
		}

		const rawDy = e.touches[0].clientY - startY;
		const dx = e.touches[0].clientX - startX;

		// Decide intent once, after the finger has moved past the slop. A
		// horizontal-dominant or upward move is not a pull — bail and let the
		// browser scroll normally.
		if (!intentLocked) {
			if (Math.abs(rawDy) < INTENT_SLOP && Math.abs(dx) < INTENT_SLOP) return;
			if (rawDy <= 0 || Math.abs(rawDy) <= Math.abs(dx)) {
				reset();
				return;
			}
			intentLocked = true;
		}

		// Committed vertical pull at the top edge: stop the browser's own
		// overscroll / pull-to-refresh from competing for the gesture.
		if (e.cancelable) e.preventDefault();

		const delta = rawDy * RESISTANCE;
		reportPull(delta);
		if (delta > (opts.threshold ?? 60)) {
			triggered = true;
			void opts.onrefresh();
		}
	}

	function onTouchEnd() {
		if (pulling && !triggered) reportPull(0);
		reset();
	}

	node.addEventListener('touchstart', onTouchStart, { passive: true });
	// Non-passive so we can preventDefault() once a top-edge pull is in progress.
	node.addEventListener('touchmove', onTouchMove, { passive: false });
	node.addEventListener('touchend', onTouchEnd);
	node.addEventListener('touchcancel', onTouchEnd);

	return {
		update(newOptions) {
			opts = newOptions;
		},
		destroy() {
			node.removeEventListener('touchstart', onTouchStart);
			node.removeEventListener('touchmove', onTouchMove);
			node.removeEventListener('touchend', onTouchEnd);
			node.removeEventListener('touchcancel', onTouchEnd);
		}
	};
}
