// Map-side cargo-route arrows. Each `ReportRouteEntry` becomes a
// short arrow from the source planet to its destination, drawn as
// three `LinePrim` segments — one shaft and two arrowhead wings —
// styled per load type so the four cargo kinds are
// distinguishable at a glance. Phase 16 ships placeholder
// colours; Phase 35 polish picks final values.
//
// Geometry uses `torusShortestDelta` so an arrow that crosses the
// torus seam takes the wrap, not the long way round, matching the
// engine's reach test (`util.ShortDistance`,
// `pkg/util/map.go.deltas`).

import type { GameReport, ReportPlanet } from "../api/game-state";
import type { CargoLoadType } from "../sync/order-types";
import { torusShortestDelta } from "./math";
import type { LinePrim, PrimitiveID, Style } from "./world";

export const STYLE_ROUTE_COL: Style = {
	strokeColor: 0x4fc3f7,
	strokeAlpha: 0.95,
	strokeWidthPx: 2,
};
export const STYLE_ROUTE_CAP: Style = {
	strokeColor: 0xffb74d,
	strokeAlpha: 0.95,
	strokeWidthPx: 2,
};
export const STYLE_ROUTE_MAT: Style = {
	strokeColor: 0x81c784,
	strokeAlpha: 0.95,
	strokeWidthPx: 2,
};
export const STYLE_ROUTE_EMP: Style = {
	strokeColor: 0x90a4ae,
	strokeAlpha: 0.85,
	strokeWidthPx: 1,
};

const STYLE_BY_LOAD_TYPE: Record<CargoLoadType, Style> = {
	COL: STYLE_ROUTE_COL,
	CAP: STYLE_ROUTE_CAP,
	MAT: STYLE_ROUTE_MAT,
	EMP: STYLE_ROUTE_EMP,
};

/** Per-load-type priority. Higher wins hit-test ties; planets sit
 * at 1..4 (`state-binding.ts.priorityFor`), so route arrows always
 * lose to planet primitives. The internal ordering follows the
 * engine's COL > CAP > MAT > EMP preference so when two arrows
 * overlap exactly, the higher-priority cargo wins the click. */
const PRIORITY_BY_LOAD_TYPE: Record<CargoLoadType, number> = {
	COL: 8,
	CAP: 7,
	MAT: 6,
	EMP: 5,
};

const LOAD_TYPE_INDEX: Record<CargoLoadType, number> = {
	COL: 0,
	CAP: 1,
	MAT: 2,
	EMP: 3,
};

/** High-bit prefix on every cargo-route line id so it cannot
 * collide with a planet number (planets use uint64 numbers ≪
 * 2^31). The renderer's hit-test treats ids opaquely; the
 * inspector never resolves a planet by a line id, so the prefix
 * is internal-only. */
export const ROUTE_LINE_ID_PREFIX = 0x80000000;

const SHAFT_OFFSET = 0;
const WING_LEFT_OFFSET = 1;
const WING_RIGHT_OFFSET = 2;

/** Arrowhead size in world units. Picked so the head is visible
 * at default zoom but does not eat the destination planet glyph. */
const HEAD_LENGTH_WORLD = 6;
/** Half-angle of the arrowhead opening, in radians (~25°). */
const HEAD_HALF_ANGLE = (25 * Math.PI) / 180;

/**
 * buildCargoRouteLines emits one `LinePrim` per shaft + two per
 * arrowhead wing for every (source, loadType, destination) entry
 * in `report.routes`. Skips routes whose source or destination is
 * not present in the planet list (e.g. a destination newly
 * unidentified after a turn cutoff). Pure: relies only on the
 * report; no DOM access; no Pixi calls.
 */
export function buildCargoRouteLines(report: GameReport): LinePrim[] {
	if (report.routes.length === 0) return [];
	const planetById = new Map<number, ReportPlanet>();
	for (const planet of report.planets) {
		planetById.set(planet.number, planet);
	}
	const lines: LinePrim[] = [];
	for (const route of report.routes) {
		const source = planetById.get(route.sourcePlanetNumber);
		if (source === undefined) continue;
		for (const entry of route.entries) {
			const dest = planetById.get(entry.destinationPlanetNumber);
			if (dest === undefined) continue;
			const dx = torusShortestDelta(source.x, dest.x, report.mapWidth);
			const dy = torusShortestDelta(source.y, dest.y, report.mapHeight);
			const length = Math.hypot(dx, dy);
			if (length === 0) continue;
			const headX = source.x + dx;
			const headY = source.y + dy;
			const ux = dx / length;
			const uy = dy / length;
			const cosA = Math.cos(HEAD_HALF_ANGLE);
			const sinA = Math.sin(HEAD_HALF_ANGLE);
			const leftX = headX - HEAD_LENGTH_WORLD * (ux * cosA + uy * sinA);
			const leftY = headY - HEAD_LENGTH_WORLD * (uy * cosA - ux * sinA);
			const rightX = headX - HEAD_LENGTH_WORLD * (ux * cosA - uy * sinA);
			const rightY = headY - HEAD_LENGTH_WORLD * (uy * cosA + ux * sinA);
			const baseId = routeLineBaseId(
				route.sourcePlanetNumber,
				entry.loadType,
			);
			const style = STYLE_BY_LOAD_TYPE[entry.loadType];
			const priority = PRIORITY_BY_LOAD_TYPE[entry.loadType];
			lines.push({
				kind: "line",
				id: baseId + SHAFT_OFFSET,
				priority,
				style,
				hitSlopPx: 0,
				x1: source.x,
				y1: source.y,
				x2: headX,
				y2: headY,
			});
			lines.push({
				kind: "line",
				id: baseId + WING_LEFT_OFFSET,
				priority,
				style,
				hitSlopPx: 0,
				x1: headX,
				y1: headY,
				x2: leftX,
				y2: leftY,
			});
			lines.push({
				kind: "line",
				id: baseId + WING_RIGHT_OFFSET,
				priority,
				style,
				hitSlopPx: 0,
				x1: headX,
				y1: headY,
				x2: rightX,
				y2: rightY,
			});
		}
	}
	return lines;
}

/** Unique numeric id for a route's three line primitives. The
 * three segments occupy `baseId + 0..2`. Encoded as
 * `prefix | (source << 8) | (loadTypeIndex << 4)` so a planet
 * number up to 2^23 and the four load-type slots fit without
 * collision. */
function routeLineBaseId(
	sourcePlanetNumber: number,
	loadType: CargoLoadType,
): PrimitiveID {
	return (
		ROUTE_LINE_ID_PREFIX |
		((sourcePlanetNumber & 0x7fffff) << 8) |
		(LOAD_TYPE_INDEX[loadType] << 4)
	);
}
