/**
 * Spatial Grid Index
 *
 * Fixed-cell grid for O(visible + bucket) viewport culling.
 * Replaces the O(N) linear scan in visibleNodeKeysAtom.
 *
 * Default cell size is 500 world-space pixels — chosen to balance
 * bucket count vs. false positives for typical node sizes (200–500px).
 */

export interface SpatialRect {
	x: number;
	y: number;
	width: number;
	height: number;
}

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

interface Entry {
	id: string;
	x: number;
	y: number;
	width: number;
	height: number;
}

export class SpatialGrid {
	private readonly cellSize: number;
	/** cell key → set of node IDs in that cell */
	private readonly cells = new Map<string, Set<string>>();
	/** node ID → entry data (for update/remove) */
	private readonly entries = new Map<string, Entry>();

	constructor(cellSize = 500) {
		this.cellSize = cellSize;
	}

	/** Number of tracked entries */
	get size(): number {
		return this.entries.size;
	}

	private cellKey(cx: number, cy: number): string {
		return `${cx},${cy}`;
	}

	private getCellRange(x: number, y: number, w: number, h: number) {
		const cs = this.cellSize;
		return {
			minCX: Math.floor(x / cs),
			minCY: Math.floor(y / cs),
			maxCX: Math.floor((x + w) / cs),
			maxCY: Math.floor((y + h) / cs),
		};
	}

	/**
	 * Insert a node into the index.
	 * If the node already exists, it is updated.
	 */
	insert(id: string, x: number, y: number, width: number, height: number): void {
		if (this.entries.has(id)) {
			this.update(id, x, y, width, height);
			return;
		}

		const entry: Entry = { id, x, y, width, height };
		this.entries.set(id, entry);

		const { minCX, minCY, maxCX, maxCY } = this.getCellRange(x, y, width, height);
		for (let cx = minCX; cx <= maxCX; cx++) {
			for (let cy = minCY; cy <= maxCY; cy++) {
				const key = this.cellKey(cx, cy);
				let cell = this.cells.get(key);
				if (!cell) {
					cell = new Set();
					this.cells.set(key, cell);
				}
				cell.add(id);
			}
		}
	}

	/**
	 * Update a node's position/dimensions.
	 */
	update(id: string, x: number, y: number, width: number, height: number): void {
		const prev = this.entries.get(id);
		if (!prev) {
			this.insert(id, x, y, width, height);
			return;
		}

		// Quick check: if cell range didn't change, just update entry
		const prevRange = this.getCellRange(prev.x, prev.y, prev.width, prev.height);
		const newRange = this.getCellRange(x, y, width, height);

		prev.x = x;
		prev.y = y;
		prev.width = width;
		prev.height = height;

		if (
			prevRange.minCX === newRange.minCX &&
			prevRange.minCY === newRange.minCY &&
			prevRange.maxCX === newRange.maxCX &&
			prevRange.maxCY === newRange.maxCY
		) {
			return; // Same cells, no grid update needed
		}

		// Remove from old cells
		for (let cx = prevRange.minCX; cx <= prevRange.maxCX; cx++) {
			for (let cy = prevRange.minCY; cy <= prevRange.maxCY; cy++) {
				const key = this.cellKey(cx, cy);
				const cell = this.cells.get(key);
				if (cell) {
					cell.delete(id);
					if (cell.size === 0) this.cells.delete(key);
				}
			}
		}

		// Add to new cells
		for (let cx = newRange.minCX; cx <= newRange.maxCX; cx++) {
			for (let cy = newRange.minCY; cy <= newRange.maxCY; cy++) {
				const key = this.cellKey(cx, cy);
				let cell = this.cells.get(key);
				if (!cell) {
					cell = new Set();
					this.cells.set(key, cell);
				}
				cell.add(id);
			}
		}
	}

	/**
	 * Remove a node from the index.
	 */
	remove(id: string): void {
		const entry = this.entries.get(id);
		if (!entry) return;

		const { minCX, minCY, maxCX, maxCY } = this.getCellRange(entry.x, entry.y, entry.width, entry.height);
		for (let cx = minCX; cx <= maxCX; cx++) {
			for (let cy = minCY; cy <= maxCY; cy++) {
				const key = this.cellKey(cx, cy);
				const cell = this.cells.get(key);
				if (cell) {
					cell.delete(id);
					if (cell.size === 0) this.cells.delete(key);
				}
			}
		}

		this.entries.delete(id);
	}

	/**
	 * Query all node IDs whose bounding box overlaps the given bounds.
	 * Returns a Set for O(1) membership checks.
	 */
	query(bounds: QueryBounds): Set<string> {
		const result = new Set<string>();
		const { minCX, minCY, maxCX, maxCY } = this.getCellRange(
			bounds.minX,
			bounds.minY,
			bounds.maxX - bounds.minX,
			bounds.maxY - bounds.minY,
		);

		for (let cx = minCX; cx <= maxCX; cx++) {
			for (let cy = minCY; cy <= maxCY; cy++) {
				const cell = this.cells.get(this.cellKey(cx, cy));
				if (!cell) continue;

				for (const id of cell) {
					if (result.has(id)) continue;

					// AABB intersection test against actual entry bounds
					const entry = this.entries.get(id)!;
					const entryRight = entry.x + entry.width;
					const entryBottom = entry.y + entry.height;

					if (
						entry.x <= bounds.maxX &&
						entryRight >= bounds.minX &&
						entry.y <= bounds.maxY &&
						entryBottom >= bounds.minY
					) {
						result.add(id);
					}
				}
			}
		}

		return result;
	}

	/**
	 * Clear all entries.
	 */
	clear(): void {
		this.cells.clear();
		this.entries.clear();
	}

	/**
	 * Check if a node is tracked.
	 */
	has(id: string): boolean {
		return this.entries.has(id);
	}
}
