/**
 * Graph position management
 *
 * Per-node position atoms derived from Graphology.
 * Structural equality caching prevents unnecessary re-renders.
 */

import { atom } from 'jotai';
import { atomFamily } from 'jotai-family';
import Graph from 'graphology';
import type { NodeId, NodePosition, GraphNodeAttributes, GraphEdgeAttributes } from './types';
import { graphAtom, graphUpdateVersionAtom, graphOptions } from './graph-store';
import { createDebug } from '../utils/debug';
import { clearAllPendingMutations } from '../utils/mutation-queue';
import { canvasMark } from './perf';

const debug = createDebug('graph:position');

// --- Position Cache ---

/**
 * Cache for the previous position values, scoped per Graph instance via WeakMap.
 * Enables structural equality — only notify dependents when x/y actually changed.
 */
const _positionCacheByGraph = new WeakMap<object, Map<string, NodePosition>>();
function getPositionCache(graph: object): Map<string, NodePosition> {
	let cache = _positionCacheByGraph.get(graph);
	if (!cache) {
		cache = new Map();
		_positionCacheByGraph.set(graph, cache);
	}
	return cache;
}

// --- Position Atoms ---

/**
 * Counter to trigger edge re-renders when positions change
 */
export const nodePositionUpdateCounterAtom = atom(0);

/**
 * Per-node position atoms - derived from Graphology (single source of truth)
 * These are read-only atoms that derive position from the graph.
 */
export const nodePositionAtomFamily = atomFamily((nodeId: NodeId) =>
	atom((get) => {
		get(nodePositionUpdateCounterAtom); // Invalidation trigger for fine-grained updates
		const graph = get(graphAtom);
		if (!graph.hasNode(nodeId)) {
			return { x: 0, y: 0 };
		}
		const x = graph.getNodeAttribute(nodeId, 'x') as number;
		const y = graph.getNodeAttribute(nodeId, 'y') as number;

		// Structural equality: return the same reference if position unchanged
		const cache = getPositionCache(graph);
		const prev = cache.get(nodeId);
		if (prev && prev.x === x && prev.y === y) {
			return prev;
		}
		const pos = { x, y };
		cache.set(nodeId, pos);
		return pos;
	}),
);

/**
 * Update a node's position
 */
export const updateNodePositionAtom = atom(null, (get, set, { nodeId, position }: { nodeId: string; position: NodePosition }) => {
	const end = canvasMark('drag-frame');
	const graph = get(graphAtom);
	if (graph.hasNode(nodeId)) {
		debug('Updating node %s position to %o', nodeId, position);
		graph.setNodeAttribute(nodeId, 'x', position.x);
		graph.setNodeAttribute(nodeId, 'y', position.y);
		// Position atom is derived - no need to set it, just trigger re-read
		set(nodePositionUpdateCounterAtom, (c) => c + 1);
	}
	end();
});

/**
 * Cleanup position atom for a deleted node
 * Note: Position atoms are now derived, so cleanup is optional but helps memory
 */
export const cleanupNodePositionAtom = atom(null, (get, _set, nodeId: NodeId) => {
	nodePositionAtomFamily.remove(nodeId);
	const graph = get(graphAtom);
	getPositionCache(graph).delete(nodeId);
	debug('Removed position atom for node: %s', nodeId);
});

/**
 * Cleanup all position atoms (used when switching graphs)
 * Note: Position atoms are now derived, so cleanup is optional but helps memory
 */
export const cleanupAllNodePositionsAtom = atom(null, (get, _set) => {
	const graph = get(graphAtom);
	const nodeIds = graph.nodes();
	nodeIds.forEach((nodeId) => {
		nodePositionAtomFamily.remove(nodeId);
	});
	_positionCacheByGraph.delete(graph);
	debug('Removed %d position atoms', nodeIds.length);
});

/**
 * Clear graph when switching to a new graph
 */
export const clearGraphOnSwitchAtom = atom(null, (get, set) => {
	debug('Clearing graph for switch');
	set(cleanupAllNodePositionsAtom);
	clearAllPendingMutations();
	const emptyGraph = new Graph<GraphNodeAttributes, GraphEdgeAttributes>(graphOptions);
	set(graphAtom, emptyGraph);
	set(graphUpdateVersionAtom, (v) => v + 1);
});
