// Phase 19 ship-group → World primitive translation. Sits next to
// `state-binding.ts` so the latter can stay focused on planets while
// the more involved group geometry (in-hyperspace interpolation,
// incoming-trajectory lines) lives here.
//
// Position rules:
//   - On-planet local / other groups (origin === null, range === null)
//     are NOT rendered on the map. Stationed groups would otherwise
//     pile up next to every populated planet and turn the canvas
//     into noise; the planet inspector lists them instead
//     (see `lib/inspectors/planet/ship-groups.svelte`).
//   - In-hyperspace local / other groups (origin / range set) —
//     interpolated along the origin → destination line at `range`
//     world units from the destination. The line is the wrap-aware
//     shortest path on a torus.
//   - Incoming groups — origin and destination are always present;
//     emit a dashed red trajectory line from origin to a wrap-aware
//     destination plus a clickable point at the interpolated
//     position (range = the `distance` field).
//   - Unidentified groups — drawn at the absolute (x, y) the radar
//     reports.
//
// Torus-shortest deltas come from `map/math.torusShortestDelta`. The
// canonical Go-side equivalent is `pkg/calc.ShortestDelta`; the TS
// helper duplicates the formula because the renderer's hot path
// avoids the WASM boundary cost. Both implementations agree on the
// half-circumference tie-break.
//
// PrimitiveIDs are partitioned via large per-variant offsets so they
// never collide with planet ids (which run in `[0, planetCount)`).

import type {
	GameReport,
	ReportIncomingShipGroup,
	ReportLocalShipGroup,
	ReportOtherShipGroup,
	ReportPlanet,
	ReportUnidentifiedShipGroup,
} from "../api/game-state";
import type { ShipGroupRef } from "../lib/selection.svelte";
import { torusShortestDelta } from "./math";
import type { LinePrim, PointPrim, PrimitiveID, Style } from "./world";

/**
 * SHIP_GROUP_ID_OFFSETS partitions the primitive-id namespace so a
 * hit on a ship-group primitive is unambiguous: the offset alone
 * disambiguates the variant and `id - offset` recovers the index
 * (or, for `local`, lookup happens via the parallel hitLookup map
 * since UUID strings cannot fit in a numeric primitive id).
 */
export const SHIP_GROUP_ID_OFFSETS = {
	local: 100_000_000,
	localLine: 150_000_000,
	other: 200_000_000,
	incoming: 300_000_000,
	incomingLine: 350_000_000,
	unidentified: 400_000_000,
} as const;

const STYLE_LOCAL_GROUP: Style = {
	fillColor: 0xfff176,
	fillAlpha: 0.95,
	pointRadiusPx: 3,
};

const STYLE_LOCAL_INSPACE_LINE: Style = {
	strokeColor: 0xfff176,
	strokeAlpha: 0.7,
	strokeWidthPx: 1,
	strokeDashPx: 4,
};

const STYLE_OTHER_GROUP: Style = {
	fillColor: 0xff6f40,
	fillAlpha: 0.9,
	pointRadiusPx: 3,
};

const STYLE_INCOMING_GROUP: Style = {
	fillColor: 0xff5252,
	fillAlpha: 1,
	pointRadiusPx: 4,
};

const STYLE_INCOMING_LINE: Style = {
	strokeColor: 0xff5252,
	strokeAlpha: 0.85,
	strokeWidthPx: 1,
	strokeDashPx: 4,
};

const STYLE_UNIDENTIFIED_GROUP: Style = {
	fillColor: 0x9aa3a8,
	fillAlpha: 0.65,
	pointRadiusPx: 3,
};

// Priority order inside `hit-test`: ship groups outrank planets so a
// hyperspace group landing on top of an unidentified planet is
// selectable. The trajectory line itself is given the lowest priority
// so a click on the dashed segment never "wins" over the clickable
// point at the interpolated position.
const PRIORITY_LOCAL = 5;
const PRIORITY_LOCAL_LINE = 0;
const PRIORITY_OTHER = 5;
const PRIORITY_INCOMING_POINT = 6;
const PRIORITY_INCOMING_LINE = 0;
const PRIORITY_UNIDENTIFIED = 4;

export interface ShipGroupPrimitives {
	primitives: (PointPrim | LinePrim)[];
	lookup: Map<PrimitiveID, ShipGroupRef>;
}

export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives {
	const primitives: (PointPrim | LinePrim)[] = [];
	const lookup = new Map<PrimitiveID, ShipGroupRef>();
	const planetIndex = new Map<number, ReportPlanet>();
	for (const planet of report.planets) {
		planetIndex.set(planet.number, planet);
	}
	const w = report.mapWidth;
	const h = report.mapHeight;

	for (let i = 0; i < report.localShipGroups.length; i++) {
		const group = report.localShipGroups[i]!;
		const pos = computeInSpacePosition(group, planetIndex, w, h);
		if (pos === null) continue;
		const id = SHIP_GROUP_ID_OFFSETS.local + i;
		primitives.push(makePoint(id, pos.x, pos.y, PRIORITY_LOCAL, STYLE_LOCAL_GROUP));
		lookup.set(id, { variant: "local", id: group.id });
		// Yellow dashed track from the origin planet to the destination
		// planet. The colour matches the in-space group point so the
		// player can read both as one entity at a glance. Wrap-aware
		// like the incoming-line: we unwrap `destination` relative to
		// `origin`, drawing the segment in a single tile, and PixiJS
		// repeats the world in torus mode.
		const origin = planetIndex.get(group.origin!);
		const destination = planetIndex.get(group.destination);
		if (origin !== undefined && destination !== undefined) {
			const dx = torusShortestDelta(origin.x, destination.x, w);
			const dy = torusShortestDelta(origin.y, destination.y, h);
			primitives.push({
				kind: "line",
				id: SHIP_GROUP_ID_OFFSETS.localLine + i,
				priority: PRIORITY_LOCAL_LINE,
				style: STYLE_LOCAL_INSPACE_LINE,
				hitSlopPx: 0,
				x1: origin.x,
				y1: origin.y,
				x2: origin.x + dx,
				y2: origin.y + dy,
			});
		}
	}

	for (let i = 0; i < report.otherShipGroups.length; i++) {
		const group = report.otherShipGroups[i]!;
		const pos = computeInSpacePosition(group, planetIndex, w, h);
		if (pos === null) continue;
		const id = SHIP_GROUP_ID_OFFSETS.other + i;
		primitives.push(makePoint(id, pos.x, pos.y, PRIORITY_OTHER, STYLE_OTHER_GROUP));
		lookup.set(id, { variant: "other", index: i });
	}

	for (let i = 0; i < report.incomingShipGroups.length; i++) {
		const group = report.incomingShipGroups[i]!;
		const origin = planetIndex.get(group.origin);
		const destination = planetIndex.get(group.destination);
		if (origin === undefined || destination === undefined) continue;
		// Unwrap the destination relative to origin so the line crosses
		// the torus seam when that is the shorter path. Renderer-side
		// we draw the segment in a single tile; in torus mode PixiJS
		// repeats the world so the line still appears continuous on
		// the visible side of the seam.
		const dx = torusShortestDelta(origin.x, destination.x, w);
		const dy = torusShortestDelta(origin.y, destination.y, h);
		const destX = origin.x + dx;
		const destY = origin.y + dy;
		const lineId = SHIP_GROUP_ID_OFFSETS.incomingLine + i;
		primitives.push({
			kind: "line",
			id: lineId,
			priority: PRIORITY_INCOMING_LINE,
			style: STYLE_INCOMING_LINE,
			hitSlopPx: 0,
			x1: origin.x,
			y1: origin.y,
			x2: destX,
			y2: destY,
		});
		const pos = interpolateAlongLine(destX, destY, origin.x, origin.y, group.distance);
		const pointId = SHIP_GROUP_ID_OFFSETS.incoming + i;
		primitives.push(
			makePoint(
				pointId,
				pos.x,
				pos.y,
				PRIORITY_INCOMING_POINT,
				STYLE_INCOMING_GROUP,
				/*hitSlopPx*/ 4,
			),
		);
		lookup.set(pointId, { variant: "incoming", index: i });
	}

	for (let i = 0; i < report.unidentifiedShipGroups.length; i++) {
		const group: ReportUnidentifiedShipGroup =
			report.unidentifiedShipGroups[i]!;
		const id = SHIP_GROUP_ID_OFFSETS.unidentified + i;
		primitives.push(
			makePoint(
				id,
				group.x,
				group.y,
				PRIORITY_UNIDENTIFIED,
				STYLE_UNIDENTIFIED_GROUP,
			),
		);
		lookup.set(id, { variant: "unidentified", index: i });
	}

	return { primitives, lookup };
}

/**
 * computeInSpacePosition returns the renderer-side (x, y) of a local
 * or foreign group that is currently in hyperspace. On-planet groups
 * (origin === null || range === null) are intentionally skipped so the
 * map does not pile dozens of primitives onto every populated planet
 * — the planet inspector lists them instead. Returns null when either
 * the group is on-planet, or the origin / destination planets are
 * not visible to the local player.
 */
function computeInSpacePosition(
	group: ReportLocalShipGroup | ReportOtherShipGroup,
	planetIndex: Map<number, ReportPlanet>,
	mapWidth: number,
	mapHeight: number,
): { x: number; y: number } | null {
	if (group.origin === null || group.range === null) return null;
	const destination = planetIndex.get(group.destination);
	if (destination === undefined) return null;
	const origin = planetIndex.get(group.origin);
	if (origin === undefined) return null;
	const dx = torusShortestDelta(destination.x, origin.x, mapWidth);
	const dy = torusShortestDelta(destination.y, origin.y, mapHeight);
	const total = Math.hypot(dx, dy);
	if (total === 0 || group.range <= 0) {
		return { x: destination.x, y: destination.y };
	}
	const t = Math.min(1, group.range / total);
	return { x: destination.x + t * dx, y: destination.y + t * dy };
}

/**
 * interpolateAlongLine returns the point that sits `range` world
 * units away from `(dx, dy)` toward `(ox, oy)`. The total path length
 * is the Euclidean distance between the two anchors; the position is
 * `dest + (range / total) × (origin - dest)`. When the anchors are
 * coincident or `range` is zero the result is the destination, which
 * is fine for the ship-group rendering — a degenerate group still
 * gets a click target on the destination planet.
 */
function interpolateAlongLine(
	dx: number,
	dy: number,
	ox: number,
	oy: number,
	range: number,
): { x: number; y: number } {
	const ddx = ox - dx;
	const ddy = oy - dy;
	const total = Math.hypot(ddx, ddy);
	if (total === 0 || range <= 0) return { x: dx, y: dy };
	const t = Math.min(1, range / total);
	return { x: dx + t * ddx, y: dy + t * ddy };
}

function makePoint(
	id: PrimitiveID,
	x: number,
	y: number,
	priority: number,
	style: Style,
	hitSlopPx = 0,
): PointPrim {
	return { kind: "point", id, priority, style, hitSlopPx, x, y };
}
