/**
 * Animated Layout Transition Hook
 *
 * Shared hook that smoothly interpolates nodes from current positions
 * to target positions using requestAnimationFrame with cubic easing.
 * Used by tree/grid layout hooks.
 */

import { useAtomValue, useSetAtom } from 'jotai';
import { useRef } from 'react';
import { graphAtom } from '../core/graph-store';
import { updateNodePositionAtom, nodePositionUpdateCounterAtom } from '../core/graph-position';
import { pushHistoryAtom } from '../core/history-store';
import type { NodePosition, GraphNodeAttributes } from '../core/types';
import type { NodePositionUpdate } from './useForceLayout';
import { createDebug } from '../utils/debug';
import { prefersReducedMotionAtom } from '../core/reduced-motion-store';

const debug = createDebug('animated-layout');

export interface UseAnimatedLayoutOptions {
	/** Callback to persist positions after animation completes */
	onPositionsChanged?: (updates: NodePositionUpdate[]) => void | Promise<void>;
	/** Animation duration in ms. Default: 400 */
	duration?: number;
}

export interface UseAnimatedLayoutReturn {
	/** Animate nodes to target positions */
	animate: (targets: Map<string, NodePosition>, label?: string) => Promise<void>;
	/** Whether an animation is currently running */
	isAnimating: boolean;
}

export function easeInOutCubic(t: number): number {
	return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}

export function useAnimatedLayout(options: UseAnimatedLayoutOptions = {}): UseAnimatedLayoutReturn {
	const { onPositionsChanged, duration = 400 } = options;

	const graph = useAtomValue(graphAtom);
	const updateNodePosition = useSetAtom(updateNodePositionAtom);
	const pushHistory = useSetAtom(pushHistoryAtom);
	const setPositionCounter = useSetAtom(nodePositionUpdateCounterAtom);
	const reducedMotion = useAtomValue(prefersReducedMotionAtom);
	const isAnimatingRef = useRef(false);

	const animate = async (targets: Map<string, NodePosition>, label?: string): Promise<void> => {
		if (isAnimatingRef.current) return;
		if (targets.size === 0) return;

		// Snapshot for undo before animating
		if (label) pushHistory(label);

		isAnimatingRef.current = true;

		// Reduced motion: apply positions instantly (no animation)
		if (reducedMotion) {
			for (const [nodeId, target] of targets) {
				updateNodePosition({ nodeId, position: target });
			}
			isAnimatingRef.current = false;
			setPositionCounter((c) => c + 1);
			if (onPositionsChanged) {
				const updates: NodePositionUpdate[] = [];
				for (const [nodeId, target] of targets) {
					updates.push({ nodeId, position: target });
				}
				Promise.resolve(onPositionsChanged(updates)).catch((err) => debug.error('Position change callback failed: %O', err));
			}
			return;
		}

		// Record start positions
		const starts = new Map<string, NodePosition>();
		for (const [nodeId] of targets) {
			if (graph.hasNode(nodeId)) {
				const attrs = graph.getNodeAttributes(nodeId) as GraphNodeAttributes;
				starts.set(nodeId, { x: attrs.x, y: attrs.y });
			}
		}

		return new Promise<void>((resolve) => {
			const startTime = performance.now();

			function tick() {
				const elapsed = performance.now() - startTime;
				const t = Math.min(elapsed / duration, 1);
				const eased = easeInOutCubic(t);

				for (const [nodeId, target] of targets) {
					const start = starts.get(nodeId);
					if (!start) continue;

					const x = Math.round(start.x + (target.x - start.x) * eased);
					const y = Math.round(start.y + (target.y - start.y) * eased);
					updateNodePosition({ nodeId, position: { x, y } });
				}

				if (t < 1) {
					requestAnimationFrame(tick);
				} else {
					isAnimatingRef.current = false;
					setPositionCounter((c) => c + 1);

					// Persistence callback
					if (onPositionsChanged) {
						const updates: NodePositionUpdate[] = [];
						for (const [nodeId, target] of targets) {
							updates.push({ nodeId, position: target });
						}
						Promise.resolve(onPositionsChanged(updates)).catch((err) => debug.error('Position change callback failed: %O', err));
					}

					resolve();
				}
			}

			requestAnimationFrame(tick);
		});
	};

	return {
		animate,
		isAnimating: isAnimatingRef.current,
	};
}
