/**
 * Virtualization state management
 *
 * Provides viewport-based culling to only render visible nodes and edges.
 * Uses a SpatialGrid index for O(visible) node lookups instead of O(N) scans.
 * Improves performance for large graphs (100+ nodes).
 */

import { atom } from 'jotai';
import { graphAtom, graphUpdateVersionAtom, edgeCreationAtom } from './graph-store';
import { nodePositionUpdateCounterAtom } from './graph-position';
import { nodeKeysAtom, edgeKeysAtom } from './graph-derived';
import { panAtom, zoomAtom, viewportRectAtom } from './viewport-store';
import { virtualizationEnabledAtom } from './settings-store';
import { collapsedEdgeRemapAtom } from './group-store';
import type { GraphNodeAttributes } from './types';
import { SpatialGrid } from './spatial-index';
import { canvasMark } from './perf';

// --- Configuration ---

/**
 * Buffer in world-space pixels beyond viewport edges.
 * Nodes within this buffer are still rendered for smooth panning.
 */
export const VIRTUALIZATION_BUFFER = 200;

// Re-export for backwards compatibility
export { virtualizationEnabledAtom } from './settings-store';

// --- Spatial Index ---

/**
 * Spatial grid index rebuilt on graph structure changes.
 * Incrementally updated on position changes via the update path.
 */
export const spatialIndexAtom = atom((get) => {
	get(graphUpdateVersionAtom);
	get(nodePositionUpdateCounterAtom);
	const graph = get(graphAtom);
	const grid = new SpatialGrid(500);

	graph.forEachNode((nodeId, attrs) => {
		const a = attrs as GraphNodeAttributes;
		grid.insert(nodeId, a.x, a.y, a.width || 200, a.height || 100);
	});

	return grid;
});

// --- Visible Bounds ---

export interface VisibleBounds {
	minX: number;
	minY: number;
	maxX: number;
	maxY: number;
}

/**
 * Calculate visible bounds in world coordinates.
 * Includes buffer for smooth panning.
 */
export const visibleBoundsAtom = atom((get): VisibleBounds | null => {
	const viewport = get(viewportRectAtom);
	const pan = get(panAtom);
	const zoom = get(zoomAtom);

	if (!viewport || zoom === 0) {
		return null;
	}

	const buffer = VIRTUALIZATION_BUFFER;

	return {
		minX: (-buffer - pan.x) / zoom,
		minY: (-buffer - pan.y) / zoom,
		maxX: (viewport.width + buffer - pan.x) / zoom,
		maxY: (viewport.height + buffer - pan.y) / zoom,
	};
});

// --- Node Visibility ---

/**
 * Node keys filtered to only those visible in viewport.
 * Uses SpatialGrid.query() for O(visible) performance when enabled.
 * Falls back to all nodes when virtualization disabled or bounds unavailable.
 */
export const visibleNodeKeysAtom = atom((get) => {
	const end = canvasMark('virtualization-cull');
	const enabled = get(virtualizationEnabledAtom);
	const allKeys = get(nodeKeysAtom);

	if (!enabled) {
		end();
		return allKeys;
	}

	const bounds = get(visibleBoundsAtom);
	if (!bounds) {
		end();
		return allKeys;
	}

	const grid = get(spatialIndexAtom);
	const visibleSet = grid.query(bounds);

	// Return keys in original order (preserves rendering order)
	const result = allKeys.filter((k) => visibleSet.has(k));
	end();
	return result;
});

// --- Edge Visibility ---

/**
 * Edge keys filtered to only those where both endpoints are visible.
 * Temp edge during creation is always included.
 *
 * When groups are collapsed, edges to/from collapsed children are re-routed
 * to the group node. Internal edges (both endpoints in the same collapsed group)
 * are hidden.
 */
export const visibleEdgeKeysAtom = atom((get) => {
	const enabled = get(virtualizationEnabledAtom);
	const allEdgeKeys = get(edgeKeysAtom);
	const edgeCreation = get(edgeCreationAtom);
	const remap = get(collapsedEdgeRemapAtom);

	// Always include temp edge if creating
	const tempEdgeKey = edgeCreation.isCreating ? 'temp-creating-edge' : null;

	// Depend on graph changes
	get(graphUpdateVersionAtom);
	const graph = get(graphAtom);

	// Filter edges: resolve effective endpoints through remap
	const filteredEdges = allEdgeKeys.filter((edgeKey) => {
		const source = graph.source(edgeKey);
		const target = graph.target(edgeKey);
		const effectiveSource = remap.get(source) ?? source;
		const effectiveTarget = remap.get(target) ?? target;

		// Hide internal edges (both endpoints map to the same collapsed group)
		if (effectiveSource === effectiveTarget) return false;

		return true;
	});

	if (!enabled) {
		return tempEdgeKey ? [...filteredEdges, tempEdgeKey] : filteredEdges;
	}

	// Get visible node set for O(1) lookup
	const visibleNodeKeys = get(visibleNodeKeysAtom);
	const visibleNodeSet = new Set(visibleNodeKeys);

	const visibleEdges = filteredEdges.filter((edgeKey) => {
		const source = graph.source(edgeKey);
		const target = graph.target(edgeKey);
		const effectiveSource = remap.get(source) ?? source;
		const effectiveTarget = remap.get(target) ?? target;

		// Edge visible if both effective endpoints are visible
		return visibleNodeSet.has(effectiveSource) && visibleNodeSet.has(effectiveTarget);
	});

	return tempEdgeKey ? [...visibleEdges, tempEdgeKey] : visibleEdges;
});

// --- Metrics ---

/**
 * Virtualization metrics for debugging/monitoring.
 */
export const virtualizationMetricsAtom = atom((get) => {
	const enabled = get(virtualizationEnabledAtom);
	const totalNodes = get(nodeKeysAtom).length;
	const totalEdges = get(edgeKeysAtom).length;
	const visibleNodes = get(visibleNodeKeysAtom).length;
	const visibleEdges = get(visibleEdgeKeysAtom).length;
	const bounds = get(visibleBoundsAtom);

	return {
		enabled,
		totalNodes,
		totalEdges,
		visibleNodes,
		visibleEdges,
		culledNodes: totalNodes - visibleNodes,
		culledEdges: totalEdges - visibleEdges,
		bounds,
	};
});
