// Map pick-mode contract: a generic "pick a destination on the map"
// interaction the inspector triggers and the renderer drives. Phase
// 16 adds the cargo-route picker on top of this; later phases
// (19/20) drive ship-group dispatch through the same surface.
//
// The renderer-facing API lives on `RendererHandle.setPickMode`
// (see `render.ts`); this module owns the option / handle types and
// the pure overlay-draw helper that translates the pick state into a
// drawing spec the renderer can lift straight onto a Pixi `Graphics`.
// Keeping the math here means the lifecycle (dim / cursor line /
// hover outline / click+Escape resolution) can be tested without
// booting a Pixi `Application`.

import { torusShortestDelta } from "./math";
import {
	displayPointRadiusWorld,
	type PointPrim,
	type PrimitiveID,
} from "./world";

/**
 * PickModeOptions configures a pick-mode session. The caller is
 * responsible for computing `reachableIds` from the current report
 * (e.g. cargo routes apply the `40 * driveTech` rule before opening
 * the picker). The renderer never validates reach itself — it only
 * dims primitives whose id is missing from this set.
 */
export interface PickModeOptions {
	/** Numeric id of the source planet primitive. Stays full-alpha
	 * during the session and anchors the cursor line. */
	readonly sourcePrimitiveId: PrimitiveID;
	/** World coordinates of the source. Pre-computed so the renderer
	 * can draw the anchor ring and the line endpoint without
	 * crawling the primitive list. */
	readonly sourceX: number;
	readonly sourceY: number;
	/** Ids whose primitives stay full-alpha and accept clicks. */
	readonly reachableIds: ReadonlySet<PrimitiveID>;
	/** Resolution callback. Fires with the chosen primitive id on a
	 * successful pick, or `null` when the player cancels via Escape
	 * or the imperative `cancel()` handle. */
	readonly onPick: (id: PrimitiveID | null) => void;
}

export interface PickModeHandle {
	/**
	 * cancel terminates the session immediately and resolves
	 * `onPick(null)`. Idempotent — repeated calls are no-ops.
	 */
	cancel(): void;
}

/**
 * PickOverlaySpec is the pure description the renderer paints onto
 * its overlay graphic each frame. Keeps the lifecycle logic
 * Pixi-free so it can be exercised by Vitest.
 */
export interface PickOverlaySpec {
	/** Highlight ring around the source planet (slightly outside the
	 * visible disc). */
	readonly anchor: {
		readonly x: number;
		readonly y: number;
		readonly radius: number;
	};
	/** Line from source to current cursor; `null` while the cursor
	 * is off-canvas. */
	readonly line: {
		readonly x1: number;
		readonly y1: number;
		readonly x2: number;
		readonly y2: number;
	} | null;
	/** Outline circle around the hovered reachable planet; `null`
	 * when the hover is empty or aimed at a non-reachable primitive. */
	readonly hoverOutline: {
		readonly x: number;
		readonly y: number;
		readonly radius: number;
	} | null;
	/** Ids to dim (alpha 0.3). Everything not in `reachableIds` and
	 * not the source. */
	readonly dimmedIds: ReadonlySet<PrimitiveID>;
}

/** Anchor / hover outline padding. F8-12 / #5 retired the anchor
 * ring from the picker overlay, so `ANCHOR_PADDING_WORLD` is now
 * dead — kept exported for legacy test coverage that asserts the
 * spec stays shaped the same way. `HOVER_PADDING_PX` is the
 * screen-pixel gap the picker hover-ring leaves between the
 * destination disc edge and the stroke; it matches the regular
 * planet outline (`OUTLINE_RADIUS_PADDING_PX` in `render.ts`) so
 * "selection" and "pick hover" outlines feel identical at every
 * zoom. */
export const ANCHOR_PADDING_WORLD = 6;
export const HOVER_PADDING_PX = 1;

/**
 * computePickOverlay produces a `PickOverlaySpec` for the current
 * pick state. Pure: no DOM access, no Pixi calls. Callers prepare
 * `pointPrimitivesById` from the active world before invoking.
 *
 * `world` enables torus-wrap geometry for the cursor line: in torus
 * mode the renderer paints the overlay graphic into every torus copy
 * (`render.ts.openPickMode`), so the line endpoint is computed as
 * `(sourceX + torusShortestDelta(...), sourceY + ...)` — extending
 * beyond `[0, W) × [0, H)` when the short path crosses the seam. With
 * 9-copy replication the user sees a short line in whatever copy is
 * under the cursor. Pass `null` (the default) for no-wrap mode, where
 * the line goes straight from source to canonical cursor.
 *
 * `anchor` and `hoverOutline` stay at the canonical source / target
 * coordinates regardless of mode: the copy with the offset that puts
 * them under the user's view gets rendered by the renderer's
 * per-copy graphics, the others fall off-screen and consume nothing.
 */
export function computePickOverlay(
	options: PickModeOptions,
	cursorWorld: { x: number; y: number } | null,
	hoveredId: PrimitiveID | null,
	pointPrimitivesById: ReadonlyMap<PrimitiveID, PointPrim>,
	allPrimitiveIds: Iterable<PrimitiveID>,
	world: { width: number; height: number } | null = null,
	cameraScale: number = 1,
	scaleRef: number = 1,
): PickOverlaySpec {
	const sourcePrim = pointPrimitivesById.get(options.sourcePrimitiveId);
	const sourceVisibleRadius =
		sourcePrim === undefined
			? 0
			: displayPointRadiusWorld(sourcePrim.style, cameraScale, scaleRef);
	const sourceRadius = sourceVisibleRadius + ANCHOR_PADDING_WORLD;

	const dimmed = new Set<PrimitiveID>();
	for (const id of allPrimitiveIds) {
		if (id === options.sourcePrimitiveId) continue;
		if (options.reachableIds.has(id)) continue;
		dimmed.add(id);
	}

	let line: PickOverlaySpec["line"] = null;
	if (cursorWorld !== null) {
		if (world !== null) {
			const dx = torusShortestDelta(
				options.sourceX,
				cursorWorld.x,
				world.width,
			);
			const dy = torusShortestDelta(
				options.sourceY,
				cursorWorld.y,
				world.height,
			);
			line = {
				x1: options.sourceX,
				y1: options.sourceY,
				x2: options.sourceX + dx,
				y2: options.sourceY + dy,
			};
		} else {
			line = {
				x1: options.sourceX,
				y1: options.sourceY,
				x2: cursorWorld.x,
				y2: cursorWorld.y,
			};
		}
	}

	let hoverOutline: PickOverlaySpec["hoverOutline"] = null;
	if (
		hoveredId !== null &&
		hoveredId !== options.sourcePrimitiveId &&
		options.reachableIds.has(hoveredId)
	) {
		const target = pointPrimitivesById.get(hoveredId);
		if (target !== undefined) {
			const targetRadius = displayPointRadiusWorld(
				target.style,
				cameraScale,
				scaleRef,
			);
			const paddingWorld = cameraScale > 0 ? HOVER_PADDING_PX / cameraScale : 0;
			hoverOutline = {
				x: target.x,
				y: target.y,
				radius: targetRadius + paddingWorld,
			};
		}
	}

	return {
		anchor: {
			x: options.sourceX,
			y: options.sourceY,
			radius: sourceRadius,
		},
		line,
		hoverOutline,
		dimmedIds: dimmed,
	};
}

/**
 * PICK_OVERLAY_STYLE captures the per-channel alphas and widths the
 * renderer applies to the pick overlay, plus the dim alpha for
 * non-reachable primitives. The colours themselves — the highlight
 * colour shared by the anchor / line / hover channels and the dim
 * multiply tint — come from the active `Theme` (`pickHighlight`,
 * `pickDimTint`) so the overlay tracks the light / dark palette.
 *
 * `dimAlpha` and `Theme.pickDimTint` are applied together to
 * non-reachable primitives during a pick session: the alpha drops
 * their brightness while the tint collapses the colour identity
 * (planet kind) into a single muted shade, so the disabled set reads
 * as obviously inert against the map background.
 */
export const PICK_OVERLAY_STYLE = {
	anchor: { alpha: 0.9, widthPx: 2 },
	// F8-12 / #5: cursor line uses the same screen-pixel thickness as
	// a regular cargo-route shaft (0.6 px), and the hover ring around
	// the destination matches the planet-outline stroke (1.5 px). The
	// renderer divides by `cameraScale` before drawing so the values
	// stay constant on screen at any zoom.
	line: { alpha: 0.95, widthPx: 0.6 },
	hover: { alpha: 0.95, widthPx: 1.5 },
	dimAlpha: 0.35,
} as const;
