/**
 * Viewport state management
 *
 * Manages pan, zoom, and viewport rect for the canvas.
 */

import { atom } from 'jotai';
import type { PanOffset } from './types';

// --- Core Viewport Atoms ---

/**
 * Current zoom level (1 = 100%)
 */
export const zoomAtom = atom<number>(1);

/**
 * Current pan offset in pixels
 */
export const panAtom = atom<PanOffset>({ x: 0, y: 0 });

/**
 * Viewport DOM rect (set by the Viewport component)
 */
export const viewportRectAtom = atom<DOMRect | null>(null);

// --- Coordinate Conversion ---

/**
 * Convert screen coordinates to world coordinates
 */
export const screenToWorldAtom = atom((get) => {
	return (screenX: number, screenY: number): { x: number; y: number } => {
		const pan = get(panAtom);
		const zoom = get(zoomAtom);
		const rect = get(viewportRectAtom);

		if (!rect) {
			return { x: screenX, y: screenY };
		}

		const relativeX = screenX - rect.left;
		const relativeY = screenY - rect.top;

		return {
			x: (relativeX - pan.x) / zoom,
			y: (relativeY - pan.y) / zoom,
		};
	};
});

/**
 * Convert world coordinates to screen coordinates
 */
export const worldToScreenAtom = atom((get) => {
	return (worldX: number, worldY: number): { x: number; y: number } => {
		const pan = get(panAtom);
		const zoom = get(zoomAtom);
		const rect = get(viewportRectAtom);

		if (!rect) {
			return { x: worldX, y: worldY };
		}

		return {
			x: worldX * zoom + pan.x + rect.left,
			y: worldY * zoom + pan.y + rect.top,
		};
	};
});

// --- Viewport Actions ---

/**
 * Set zoom level with optional center point
 */
export const setZoomAtom = atom(
	null,
	(get, set, { zoom, centerX, centerY }: { zoom: number; centerX?: number; centerY?: number }) => {
		const currentZoom = get(zoomAtom);
		const pan = get(panAtom);
		const rect = get(viewportRectAtom);

		// Clamp zoom
		const newZoom = Math.max(0.1, Math.min(5, zoom));

		if (centerX !== undefined && centerY !== undefined && rect) {
			// Zoom towards the center point
			const relativeX = centerX - rect.left;
			const relativeY = centerY - rect.top;

			const worldX = (relativeX - pan.x) / currentZoom;
			const worldY = (relativeY - pan.y) / currentZoom;

			const newPanX = relativeX - worldX * newZoom;
			const newPanY = relativeY - worldY * newZoom;

			set(panAtom, { x: newPanX, y: newPanY });
		}

		set(zoomAtom, newZoom);
	},
);

/**
 * Reset viewport to default state
 */
export const resetViewportAtom = atom(null, (_get, set) => {
	set(zoomAtom, 1);
	set(panAtom, { x: 0, y: 0 });
});

// =============================================================================
// Headless Layout Atoms
// Fit-to-bounds and center-on-node without React hooks
// =============================================================================

import { graphAtom } from './graph-store';
import { nodePositionUpdateCounterAtom } from './graph-position';
import { uiNodesAtom } from './graph-derived';
import { selectedNodeIdsAtom } from './selection-store';
import { calculateBounds, FitToBoundsMode, type Rect } from '../utils/layout';

/**
 * Write atom: fit viewport to show all nodes or selection.
 * Pure Jotai — no React required.
 */
export const fitToBoundsAtom = atom(
	null,
	(get, set, { mode, padding = 20 }: { mode: FitToBoundsMode | 'graph' | 'selection'; padding?: number }) => {
		const normalizedMode = typeof mode === 'string'
			? (mode === 'graph' ? FitToBoundsMode.Graph : FitToBoundsMode.Selection)
			: mode;

		const viewportSize = get(viewportRectAtom);
		if (!viewportSize || viewportSize.width <= 0 || viewportSize.height <= 0) return;

		// Force position counter read so bounds are fresh
		get(nodePositionUpdateCounterAtom);

		let bounds: Rect;
		if (normalizedMode === FitToBoundsMode.Graph) {
			const graph = get(graphAtom);
			const nodes: Rect[] = graph.nodes().map((node) => {
				const attrs = graph.getNodeAttributes(node);
				return { x: attrs.x, y: attrs.y, width: attrs.width || 500, height: attrs.height || 500 };
			});
			bounds = calculateBounds(nodes);
		} else {
			const selectedIds = get(selectedNodeIdsAtom);
			const allNodes = get(uiNodesAtom);
			const selectedNodes = allNodes
				.filter((n) => selectedIds.has(n.id))
				.map((n) => ({ x: n.position.x, y: n.position.y, width: n.width ?? 500, height: n.height ?? 500 }));
			bounds = calculateBounds(selectedNodes);
		}

		if (bounds.width <= 0 || bounds.height <= 0) return;

		const maxHPad = Math.max(0, viewportSize.width / 2 - 1);
		const maxVPad = Math.max(0, viewportSize.height / 2 - 1);
		const safePadding = Math.max(0, Math.min(padding, maxHPad, maxVPad));

		const effW = Math.max(1, viewportSize.width - 2 * safePadding);
		const effH = Math.max(1, viewportSize.height - 2 * safePadding);

		const scale = Math.min(effW / bounds.width, effH / bounds.height);
		if (scale <= 0 || !isFinite(scale)) return;

		set(zoomAtom, scale);

		const scaledW = bounds.width * scale;
		const scaledH = bounds.height * scale;
		const startX = safePadding + (effW - scaledW) / 2;
		const startY = safePadding + (effH - scaledH) / 2;

		set(panAtom, { x: startX - bounds.x * scale, y: startY - bounds.y * scale });
	},
);

/**
 * Write atom: center viewport on a specific node.
 */
export const centerOnNodeAtom = atom(
	null,
	(get, set, nodeId: string) => {
		const nodes = get(uiNodesAtom);
		const node = nodes.find((n) => n.id === nodeId);
		if (!node) return;

		const { x, y, width = 200, height = 100 } = node;
		const zoom = get(zoomAtom);
		const centerX = x + width / 2;
		const centerY = y + height / 2;
		const rect = get(viewportRectAtom);
		const halfWidth = rect ? rect.width / 2 : 400;
		const halfHeight = rect ? rect.height / 2 : 300;
		set(panAtom, { x: halfWidth - centerX * zoom, y: halfHeight - centerY * zoom });
	},
);

// =============================================================================
// Zoom Transition State
// When zooming in on a node past a threshold, trigger a fade transition
// =============================================================================

/** The zoom level at which we start the transition (zoom in) */
export const ZOOM_TRANSITION_THRESHOLD = 3.5;

/** The zoom level at which we exit transition mode (zoom out) */
export const ZOOM_EXIT_THRESHOLD = 2.0;

/** The node ID that's currently being zoomed into (center of viewport) */
export const zoomFocusNodeIdAtom = atom<string | null>(null);

/** Transition progress: 0 = normal view, 1 = fully transitioned to locked view */
export const zoomTransitionProgressAtom = atom<number>(0);

/** Computed: whether we're in a zoom transition */
export const isZoomTransitioningAtom = atom((get) => {
	const progress = get(zoomTransitionProgressAtom);
	return progress > 0 && progress < 1;
});

// --- Zoom Animation Target ---

export interface ZoomAnimationTarget {
	/** Target zoom level */
	targetZoom: number;
	/** Target pan offset */
	targetPan: PanOffset;
	/** Starting zoom when animation began */
	startZoom: number;
	/** Starting pan when animation began */
	startPan: PanOffset;
	/** Animation duration in ms */
	duration: number;
	/** Timestamp when animation started */
	startTime: number;
}

/** Current zoom animation target, or null when idle */
export const zoomAnimationTargetAtom = atom<ZoomAnimationTarget | null>(null);

/**
 * Write atom: start an animated zoom-to-node transition.
 * Sets the animation target; the actual animation loop runs in useZoomTransition hook.
 */
export const animateZoomToNodeAtom = atom(
	null,
	(get, set, { nodeId, targetZoom, duration = 300 }: { nodeId: string; targetZoom?: number; duration?: number }) => {
		const nodes = get(uiNodesAtom);
		const node = nodes.find((n) => n.id === nodeId);
		if (!node) return;

		const { x, y, width = 200, height = 100 } = node;
		const centerX = x + width / 2;
		const centerY = y + height / 2;
		const rect = get(viewportRectAtom);
		const halfWidth = rect ? rect.width / 2 : 400;
		const halfHeight = rect ? rect.height / 2 : 300;

		const finalZoom = targetZoom ?? get(zoomAtom);
		const targetPan = {
			x: halfWidth - centerX * finalZoom,
			y: halfHeight - centerY * finalZoom,
		};

		set(zoomFocusNodeIdAtom, nodeId);
		set(zoomAnimationTargetAtom, {
			targetZoom: finalZoom,
			targetPan,
			startZoom: get(zoomAtom),
			startPan: { ...get(panAtom) },
			duration,
			startTime: performance.now(),
		});
	},
);

/**
 * Write atom: start an animated fit-to-bounds transition.
 * Computes the target viewport, then animates to it.
 */
export const animateFitToBoundsAtom = atom(
	null,
	(get, set, { mode, padding = 20, duration = 300 }: { mode: 'graph' | 'selection'; padding?: number; duration?: number }) => {
		const viewportSize = get(viewportRectAtom);
		if (!viewportSize || viewportSize.width <= 0 || viewportSize.height <= 0) return;

		get(nodePositionUpdateCounterAtom);

		let bounds: Rect;
		if (mode === 'graph') {
			const graph = get(graphAtom);
			const nodes: Rect[] = graph.nodes().map((node) => {
				const attrs = graph.getNodeAttributes(node);
				return { x: attrs.x, y: attrs.y, width: attrs.width || 500, height: attrs.height || 500 };
			});
			bounds = calculateBounds(nodes);
		} else {
			const selectedIds = get(selectedNodeIdsAtom);
			const allNodes = get(uiNodesAtom);
			const selectedNodes = allNodes
				.filter((n) => selectedIds.has(n.id))
				.map((n) => ({ x: n.position.x, y: n.position.y, width: n.width ?? 500, height: n.height ?? 500 }));
			bounds = calculateBounds(selectedNodes);
		}

		if (bounds.width <= 0 || bounds.height <= 0) return;

		const safePadding = Math.max(0, Math.min(padding, viewportSize.width / 2 - 1, viewportSize.height / 2 - 1));
		const effW = Math.max(1, viewportSize.width - 2 * safePadding);
		const effH = Math.max(1, viewportSize.height - 2 * safePadding);
		const scale = Math.min(effW / bounds.width, effH / bounds.height);
		if (scale <= 0 || !isFinite(scale)) return;

		const scaledW = bounds.width * scale;
		const scaledH = bounds.height * scale;
		const startX = safePadding + (effW - scaledW) / 2;
		const startY = safePadding + (effH - scaledH) / 2;
		const targetPan = { x: startX - bounds.x * scale, y: startY - bounds.y * scale };

		set(zoomAnimationTargetAtom, {
			targetZoom: scale,
			targetPan,
			startZoom: get(zoomAtom),
			startPan: { ...get(panAtom) },
			duration,
			startTime: performance.now(),
		});
	},
);
