/**
 * useCanvasGestures — Main viewport gesture hook
 *
 * Wires @use-gesture/react → recognize → resolve → dispatch.
 *
 * Responsibilities:
 * - Pointer tracking (down/up/cancel) via input-store
 * - Tap/double-tap/long-press via TimedStateRunner
 * - Drag intent resolution via mapper
 * - Pan with inertia (delegated to useInertia)
 * - Pinch zoom with inertia + snap-to-100%
 * - Wheel zoom
 * - Lasso / rect-select routing
 * - pointercancel + visibilitychange cleanup
 *
 * Guard context is built by useGuardContext.
 * Inertia is managed by useInertia.
 */

import { useRef, useEffect, useMemo } from 'react';
import { useAtom, useSetAtom, useAtomValue } from 'jotai';
import { useGesture } from '@use-gesture/react';
import { zoomAtom, panAtom, viewportRectAtom, screenToWorldAtom } from '../core/viewport-store';
import { clearSelectionAtom, selectedNodeIdsAtom } from '../core/selection-store';
import { classifyPointer } from '../core/input-classifier';
import type { InputSource } from '../core/input-classifier';
import {
	pointerDownAtom,
	pointerUpAtom,
	primaryInputSourceAtom,
	clearPointersAtom,
} from '../core/input-store';
import { prefersReducedMotionAtom } from '../core/reduced-motion-store';
import { startSelectionAtom, updateSelectionAtom, endSelectionAtom } from '../core/selection-path-store';

import { extractModifiers } from './normalize';
import { TimedStateRunner } from './timed-state-runner';
import { buildMappingIndex, resolve, type MappingIndex } from './mapper';
import { dispatch } from './dispatcher';
import { PALM_REJECTION_CONTEXT, DEFAULT_CONTEXT } from './contexts';
import { useGuardContext } from './useGuardContext';
import { useInertia, snapZoom, INERTIA_MIN_SPEED } from './useInertia';
import { createWheelHandler } from './useWheelZoom';
import { createPinchHandlers } from './usePinchZoom';
import type {
	BivariantCallback,
	HeldKeysState,
	GestureSubject,
	MappingContext,
	PointerGestureEvent,
	ResolvedAction,
	Button,
	InputEvent,
} from './types';
import { NO_HELD_KEYS, NO_MODIFIERS } from './types';

// =============================================================================
// Config
// =============================================================================

export interface UseCanvasGesturesConfig {
	/** Ref to the viewport DOM element */
	ref: React.RefObject<HTMLDivElement | null>;
	minZoom?: number;
	maxZoom?: number;
	zoomSensitivity?: number;
	enablePan?: boolean;
	enableZoom?: boolean;
	/** Pre-built mapping index (shared from GestureProvider). Overrides contexts/palmRejection. */
	mappingIndex?: MappingIndex;
	/** Additional mapping contexts to push (ignored if mappingIndex is provided) */
	contexts?: MappingContext[];
	/** Whether palm rejection is enabled (ignored if mappingIndex is provided) */
	palmRejection?: boolean;
	/** Called for every resolved gesture */
	onAction?: BivariantCallback<[InputEvent, ResolvedAction]>;
	/** Snapshot of currently held keyboard keys */
	heldKeys?: HeldKeysState;
}

// =============================================================================
// Hook
// =============================================================================

export function useCanvasGestures({
	ref,
	minZoom = 0.1,
	maxZoom = 5,
	zoomSensitivity = 0.0015,
	enablePan = true,
	enableZoom = true,
	mappingIndex: externalIndex,
	contexts: extraContexts,
	palmRejection = true,
	onAction,
	heldKeys = NO_HELD_KEYS,
}: UseCanvasGesturesConfig) {
	// --- Jotai atoms ---
	const [panVals, setPan] = useAtom(panAtom);
	const [zoomVal, setZoom] = useAtom(zoomAtom);
	const setViewportRect = useSetAtom(viewportRectAtom);
	const selectedNodeIds = useAtomValue(selectedNodeIdsAtom);
	const clearSelection = useSetAtom(clearSelectionAtom);
	const screenToWorld = useAtomValue(screenToWorldAtom);

	// Input tracking
	const setPointerDown = useSetAtom(pointerDownAtom);
	const setPointerUp = useSetAtom(pointerUpAtom);
	const clearPointers = useSetAtom(clearPointersAtom);
	const primaryInputSource = useAtomValue(primaryInputSourceAtom);
	const reducedMotion = useAtomValue(prefersReducedMotionAtom);

	// Selection path (lasso / rect-select)
	const startSelection = useSetAtom(startSelectionAtom);
	const updateSelection = useSetAtom(updateSelectionAtom);
	const endSelection = useSetAtom(endSelectionAtom);

	// --- Extracted hooks ---
	const guardRef = useGuardContext(heldKeys);
	const inertia = useInertia();

	// --- Refs (mutable, no re-renders) ---
	const panStartRef = useRef({ x: 0, y: 0 });
	const dragOriginatedOnBg = useRef(false);
	const dragSourceRef = useRef<InputSource>('mouse');
	const buttonRef = useRef<Button>(0);
	const dragIntentRef = useRef<string>('none');

	// Timed state (tap counting / long-press)
	const timedRunner = useRef(new TimedStateRunner());

	// --- Build mapping index (use external if provided) ---
	const internalIndex = useMemo(() => {
		const ctxList: MappingContext[] = [];
		if (palmRejection) ctxList.push(PALM_REJECTION_CONTEXT);
		if (extraContexts) ctxList.push(...extraContexts);
		ctxList.push(DEFAULT_CONTEXT);
		return buildMappingIndex(ctxList);
	}, [palmRejection, extraContexts]);
	const mappingIndex = externalIndex ?? internalIndex;

	// --- Helpers ---

	const isBackgroundTarget = (target: EventTarget | null): boolean => {
		if (!ref.current || !target) return false;
		const child = ref.current.firstChild as HTMLElement | null;
		return target === ref.current || target === child;
	};

	const makeGestureEvent = (
		type: PointerGestureEvent['type'],
		phase: PointerGestureEvent['phase'],
		subject: GestureSubject,
		screenX: number,
		screenY: number,
		extra?: Partial<PointerGestureEvent>,
	): PointerGestureEvent => {
		const worldPos = screenToWorld(screenX, screenY);
		return {
			kind: 'pointer',
			type,
			phase,
			subject,
			source: dragSourceRef.current,
			button: buttonRef.current,
			modifiers: NO_MODIFIERS,
			heldKeys,
			screenPosition: { x: screenX, y: screenY },
			worldPosition: worldPos,
			...extra,
		};
	};

	const resolveAndDispatch = (event: PointerGestureEvent): ResolvedAction | null => {
		const resolution = resolve(event, mappingIndex, guardRef.current);
		if (resolution) {
			dispatch(event, resolution);
			onAction?.(event, resolution);
		}
		return resolution;
	};

	// --- Timed state emit handler (for async timer-based gestures like long-press) ---
	useEffect(() => {
		const runner = timedRunner.current;
		runner.onEmit = (_gestureType) => {
			// Long-press fires asynchronously from the timer.
			// Position is captured at pointer-down time via the onPointerDown handler.
		};
		return () => {
			runner.destroy();
		};
	}, []);

	// --- Viewport resize observer ---
	useEffect(() => {
		const el = ref.current;
		if (!el) {
			setViewportRect(null);
			return;
		}
		setViewportRect(el.getBoundingClientRect());
		const observer = new ResizeObserver((entries) => {
			for (const entry of entries) {
				setViewportRect(entry.contentRect as DOMRect);
			}
		});
		observer.observe(el);
		return () => {
			observer.unobserve(el);
			observer.disconnect();
		};
	}, [setViewportRect]);

	// --- Cleanup on unmount ---
	useEffect(() => {
		return () => {
			inertia.cancelAll();
			timedRunner.current.destroy();
		};
	}, []);

	// --- Visibility change: clear pointers + cancel inertia ---
	useEffect(() => {
		const handleVisChange = () => {
			if (document.hidden) {
				inertia.cancelAll();
				clearPointers();
				timedRunner.current.feed('cancel');
			}
		};
		document.addEventListener('visibilitychange', handleVisChange);
		return () => document.removeEventListener('visibilitychange', handleVisChange);
	}, [clearPointers]);

	// ==========================================================================
	// @use-gesture binding
	// ==========================================================================

	useGesture(
		{
			onPointerDown: (state) => {
				if (!ref.current) {
					dragOriginatedOnBg.current = false;
					return;
				}

				const pe = state.event as PointerEvent;
				const classified = classifyPointer(pe);
				setPointerDown(classified);
				dragSourceRef.current = classified.source;
				buttonRef.current = (pe.button ?? 0) as Button;

				// Cancel inertia on new touch
				inertia.cancelAll();

				if (isBackgroundTarget(state.event.target)) {
					dragOriginatedOnBg.current = true;

					// Feed timed state machine for tap/long-press tracking
					timedRunner.current.feed('down');

					// Wire the long-press timer emit
					const startX = pe.clientX;
					const startY = pe.clientY;
					timedRunner.current.onEmit = (gestureType) => {
						if (gestureType === 'long-press') {
							const worldPos = screenToWorld(startX, startY);
							const event = makeGestureEvent(
								'long-press',
								'instant',
								{ kind: 'background' },
								startX,
								startY,
								{ modifiers: extractModifiers(pe), worldPosition: worldPos },
							);
							resolveAndDispatch(event);
						}
					};
				} else {
					dragOriginatedOnBg.current = false;
				}
			},

			onPointerUp: (state) => {
				const pe = state.event as PointerEvent;
				setPointerUp(pe.pointerId);
			},

			onDragStart: ({ event }) => {
				if (!dragOriginatedOnBg.current) return;

				const pe = event as PointerEvent | undefined;
				const modifiers = pe ? extractModifiers(pe) : NO_MODIFIERS;

				// Resolve drag intent once at start
				const gestureEvent = makeGestureEvent(
					'drag',
					'start',
					{ kind: 'background' },
					pe?.clientX ?? 0,
					pe?.clientY ?? 0,
					{ modifiers },
				);
				const resolution = resolve(gestureEvent, mappingIndex, guardRef.current);
				dragIntentRef.current = resolution?.actionId ?? 'none';

				if (dragIntentRef.current === 'pan' && enablePan) {
					panStartRef.current = { ...panVals };
					inertia.panSampler.reset();
				} else if (
					dragIntentRef.current === 'lasso-select' ||
					dragIntentRef.current === 'rect-select'
				) {
					const worldPos = screenToWorld(pe?.clientX ?? 0, pe?.clientY ?? 0);
					startSelection({
						type: dragIntentRef.current === 'lasso-select' ? 'lasso' : 'rect',
						point: worldPos,
					});
				}

				// Dispatch the start phase
				if (resolution) {
					dispatch(gestureEvent, resolution);
					onAction?.(gestureEvent, resolution);
				}
			},

			onDrag: ({ movement: [mx, my], tap, active, pinching, event, velocity: [vx, vy], direction: [dx, dy] }) => {
				if (tap && dragOriginatedOnBg.current) {
					// Tap on background — the timed runner tracks tap counting.
					const emitted = timedRunner.current.feed('up');
					const pe = event as PointerEvent;
					const modifiers = extractModifiers(pe);
					const tapType = emitted ?? 'tap';

					const gestureEvent = makeGestureEvent(
						tapType,
						'instant',
						{ kind: 'background' },
						pe.clientX,
						pe.clientY,
						{ modifiers },
					);
					const resolution = resolve(gestureEvent, mappingIndex, guardRef.current);
					if (resolution) {
						dispatch(gestureEvent, resolution);
						onAction?.(gestureEvent, resolution);
					} else {
						clearSelection();
					}
					return;
				}

				if (!tap && active && !pinching && dragOriginatedOnBg.current) {
					// Cancel long-press on drag movement
					timedRunner.current.feed('move-beyond-threshold');

					const intent = dragIntentRef.current;

					if (intent === 'pan' && enablePan) {
						setPan({
							x: panStartRef.current.x + mx,
							y: panStartRef.current.y + my,
						});
						const now = performance.now();
						inertia.panSampler.sample(vx * dx, vy * dy, now);
					} else if (intent === 'lasso-select' || intent === 'rect-select') {
						const pe = event as PointerEvent;
						const worldPos = screenToWorld(pe.clientX, pe.clientY);
						updateSelection(worldPos);
					}
				}
			},

			onDragEnd: () => {
				timedRunner.current.feed('up');

				const intent = dragIntentRef.current;

				if (intent === 'lasso-select' || intent === 'rect-select') {
					endSelection();
				}

				// Pan inertia for finger drags (skip if reduced motion)
				if (!reducedMotion && dragOriginatedOnBg.current && dragSourceRef.current === 'finger' && intent === 'pan') {
					const avg = inertia.panSampler.average();
					const speed = Math.sqrt(avg.x ** 2 + avg.y ** 2);
					if (speed > INERTIA_MIN_SPEED) {
						inertia.startPanInertia(avg, setPan);
					}
				}

				dragOriginatedOnBg.current = false;
				dragIntentRef.current = 'none';
				inertia.panSampler.reset();
			},

			onWheel: createWheelHandler({
				ref,
				minZoom,
				maxZoom,
				zoomSensitivity,
				enableZoom,
				zoom: zoomVal,
				pan: panVals,
				selectedNodeIds,
				setZoom,
				setPan,
			}),

			...createPinchHandlers({
				ref,
				minZoom,
				maxZoom,
				enableZoom,
				reducedMotion,
				zoom: zoomVal,
				pan: panVals,
				setZoom,
				setPan,
				inertia,
			}),
		},
		{
			target: ref,
			eventOptions: { passive: false, capture: true },
			drag: {
				filterTaps: true,
				tapsThreshold: primaryInputSource === 'finger' ? 10 : 5,
				pointer: { touch: true, keys: false, capture: false, buttons: -1 },
			},
			wheel: {},
			pinch: {
				scaleBounds: () => ({ min: minZoom, max: maxZoom }),
				from: () => [zoomVal, 0],
			},
		},
	);
}
