// 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 { DEFAULT_POINT_RADIUS_PX, 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 in world units (the rings sit
 * outside the visible disc so the planet stays clearly visible). */
export const ANCHOR_PADDING_WORLD = 6;
export const HOVER_PADDING_WORLD = 4;

/**
 * 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.
 */
export function computePickOverlay(
	options: PickModeOptions,
	cursorWorld: { x: number; y: number } | null,
	hoveredId: PrimitiveID | null,
	pointPrimitivesById: ReadonlyMap<PrimitiveID, PointPrim>,
	allPrimitiveIds: Iterable<PrimitiveID>,
): PickOverlaySpec {
	const sourcePrim = pointPrimitivesById.get(options.sourcePrimitiveId);
	const sourceRadius =
		(sourcePrim?.style.pointRadiusPx ?? DEFAULT_POINT_RADIUS_PX) +
		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);
	}

	const line =
		cursorWorld === null
			? null
			: {
					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) {
			hoverOutline = {
				x: target.x,
				y: target.y,
				radius:
					(target.style.pointRadiusPx ?? DEFAULT_POINT_RADIUS_PX) +
					HOVER_PADDING_WORLD,
			};
		}
	}

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

/**
 * PICK_OVERLAY_STYLE captures the colours / widths the renderer
 * applies to each spec channel. Exported so tests and future themes
 * can read the same values.
 *
 * `dimAlpha` and `dimTint` are applied together to non-reachable
 * primitives during a pick session: the alpha drops their
 * brightness, and the tint multiplies their fill colour toward dark
 * gray so the colour identity (planet kind) collapses into a
 * single muted shade. The combination has to read as "obviously
 * disabled" against the dark theme — bright planets such as
 * `STYLE_LOCAL` (`0x6dd2ff`) survive a 0.3 alpha alone too
 * comfortably, so the tint pulls them down too.
 */
export const PICK_OVERLAY_STYLE = {
	anchor: { color: 0xffe082, alpha: 0.9, width: 2 },
	line: { color: 0xffe082, alpha: 0.5, width: 1 },
	hover: { color: 0xffe082, alpha: 1, width: 2 },
	dimAlpha: 0.35,
	dimTint: 0x303841,
} as const;
