// Hit-test pass over the world primitives.
//
// Algorithm: convert the cursor to world coordinates, then walk every
// primitive computing its squared distance to the cursor in world
// units. The threshold for a hit is (visualRadius + slopWorld)²
// where slopWorld = slopPx / camera.scale, so the on-screen click
// margin stays constant regardless of zoom. Candidates are sorted by
// (-priority, distSq, kindOrder, id) and the best is returned.
//
// In torus mode, distance is measured along the toroidal shortest
// path on each axis. In no-wrap mode, distance is plain Euclidean
// and a primitive does not get matched through wrap copies.

import { distSqPointToSegment, screenToWorld, torusShortestDelta } from "./math";
import {
	DEFAULT_HIT_SLOP_PX,
	DEFAULT_POINT_RADIUS_PX,
	KIND_ORDER,
	type Camera,
	type CirclePrim,
	type LinePrim,
	type PointPrim,
	type Primitive,
	type Viewport,
	type World,
	type WrapMode,
} from "./world";

export interface Hit {
	primitive: Primitive;
	distSq: number; // in world units squared
}

// hitTest returns the best-matching primitive under the cursor, or
// null if no primitive matches within its hit slop.
export function hitTest(
	world: World,
	camera: Camera,
	viewport: Viewport,
	cursorPx: { x: number; y: number },
	mode: WrapMode,
): Hit | null {
	const cursor = screenToWorld(cursorPx, camera, viewport);
	const candidates: Hit[] = [];

	for (const p of world.primitives) {
		const slopPx = p.hitSlopPx > 0 ? p.hitSlopPx : DEFAULT_HIT_SLOP_PX[p.kind];
		const slopWorld = slopPx / camera.scale;
		let result: number | null;
		if (p.kind === "point") {
			result = matchPoint(p, cursor, slopWorld, mode === "torus" ? world : null);
		} else if (p.kind === "circle") {
			result = matchCircle(p, cursor, slopWorld, mode === "torus" ? world : null);
		} else {
			result = matchLine(p, cursor, slopWorld, mode === "torus" ? world : null);
		}
		if (result !== null) {
			candidates.push({ primitive: p, distSq: result });
		}
	}

	if (candidates.length === 0) return null;
	candidates.sort(compareHits);
	return candidates[0];
}

function compareHits(a: Hit, b: Hit): number {
	if (a.primitive.priority !== b.primitive.priority) {
		return b.primitive.priority - a.primitive.priority;
	}
	if (a.distSq !== b.distSq) return a.distSq - b.distSq;
	const ka = KIND_ORDER[a.primitive.kind];
	const kb = KIND_ORDER[b.primitive.kind];
	if (ka !== kb) return ka - kb;
	return a.primitive.id - b.primitive.id;
}

// torusDelta returns (cursor - origin) measured along the toroidal
// shortest path when world is non-null, otherwise plain Euclidean.
function torusDelta(
	originX: number,
	originY: number,
	cursorX: number,
	cursorY: number,
	world: World | null,
): { dx: number; dy: number } {
	if (world === null) {
		return { dx: cursorX - originX, dy: cursorY - originY };
	}
	return {
		dx: torusShortestDelta(originX, cursorX, world.width),
		dy: torusShortestDelta(originY, cursorY, world.height),
	};
}

function matchPoint(
	p: PointPrim,
	cursor: { x: number; y: number },
	slopWorld: number,
	world: World | null,
): number | null {
	const { dx, dy } = torusDelta(p.x, p.y, cursor.x, cursor.y, world);
	const distSq = dx * dx + dy * dy;
	// The visible disc is `pointRadiusPx` world units; the hit zone
	// is the disc plus a small ergonomic slop on top. A click on any
	// painted pixel of the planet must register as a hit.
	const visibleRadius = p.style.pointRadiusPx ?? DEFAULT_POINT_RADIUS_PX;
	const r = visibleRadius + slopWorld;
	if (distSq <= r * r) return distSq;
	return null;
}

function matchCircle(
	p: CirclePrim,
	cursor: { x: number; y: number },
	slopWorld: number,
	world: World | null,
): number | null {
	const { dx, dy } = torusDelta(p.x, p.y, cursor.x, cursor.y, world);
	const distSq = dx * dx + dy * dy;
	const isFilled = p.style.fillColor !== undefined && (p.style.fillAlpha ?? 1) > 0;
	if (isFilled) {
		const r = p.radius + slopWorld;
		if (distSq <= r * r) return distSq;
		return null;
	}
	// Stroke-only ring: cursor must be within slop of the ring.
	const dist = Math.sqrt(distSq);
	if (Math.abs(dist - p.radius) <= slopWorld) {
		const ringGap = dist - p.radius;
		return ringGap * ringGap;
	}
	return null;
}

function matchLine(
	p: LinePrim,
	cursor: { x: number; y: number },
	slopWorld: number,
	world: World | null,
): number | null {
	// In torus mode the canonical line representation goes from
	// (x1,y1) to (x1 + dx, y1 + dy) where (dx,dy) is the torus-
	// shortest delta from end1 to end2. The cursor's distance is
	// then the perpendicular distance to this canonical segment,
	// using the torus-shortest cursor-to-end1 delta as the basis.
	if (world === null) {
		const distSq = distSqPointToSegment(cursor.x, cursor.y, p.x1, p.y1, p.x2, p.y2);
		if (distSq <= slopWorld * slopWorld) return distSq;
		return null;
	}
	const segDx = torusShortestDelta(p.x1, p.x2, world.width);
	const segDy = torusShortestDelta(p.y1, p.y2, world.height);
	const cur = torusDelta(p.x1, p.y1, cursor.x, cursor.y, world);
	const distSq = distSqPointToSegment(cur.dx, cur.dy, 0, 0, segDx, segDy);
	if (distSq <= slopWorld * slopWorld) return distSq;
	return null;
}
