// Pure helpers for the Phase 29 visibility layer. The map view
// (`lib/active-view/map.svelte`) reads `GameStateStore.mapToggles`
// every effect run and feeds the result through these functions to
// produce the renderer inputs:
//
//   1. `computeHiddenPlanetNumbers` resolves the per-kind toggles and
//      the optional `unreachablePlanets` filter into a set of planet
//      numbers to hide. LOCAL planets are always exempt.
//   2. `computeHiddenIds` cascades that set onto every primitive id
//      tracked in `planetDependents` (planet, marker, in-space and
//      incoming group, trajectory line), then unions in the
//      category-toggled-off primitives walked from `categories`.
//   3. `computeFogCircles` produces the visibility-fog input —
//      empty when the toggle is off, otherwise one circle per LOCAL
//      planet at `VisibilityDistance(localPlayerDrive)`.
//
// The constants `FLIGHT_DISTANCE_PER_DRIVE` and
// `VISIBILITY_DISTANCE_PER_DRIVE` mirror `pkg/calc/race.go`:
//
//   FlightDistance(driveTech)     = driveTech * 40
//   VisibilityDistance(driveTech) = driveTech * 30
//
// A WASM bridge for the race-level calc helpers does not exist yet
// (Phase 18 wired ship-level math only); the constants are
// duplicated in TS following the same precedent as
// `lib/inspectors/ship-group/actions.svelte` (`40 * localPlayerDrive`)
// and `sync/order-types.ts:298`.

import type { GameReport } from "../api/game-state";
import type { MapToggles } from "../lib/game-state.svelte";
import { torusShortestDistance } from "./math";
import type { MapCategory } from "./state-binding";
import type { PrimitiveID } from "./world";

export const FLIGHT_DISTANCE_PER_DRIVE = 40;
export const VISIBILITY_DISTANCE_PER_DRIVE = 30;

/**
 * isCategoryVisible reports whether the supplied `MapCategory` is
 * currently visible per the toggle state. LOCAL planets are not
 * controlled by a toggle; the function returns `true` for them
 * unconditionally. The map view combines this with the planet
 * cascade so a kind toggle (e.g. `foreignPlanets = false`) hides
 * the planet itself AND every dependent primitive (markers, in-
 * space groups flying to it).
 */
export function isCategoryVisible(
	category: MapCategory,
	toggles: MapToggles,
): boolean {
	switch (category) {
		case "planet-local":
			return true;
		case "planet-foreign":
			return toggles.foreignPlanets;
		case "planet-uninhabited":
			return toggles.uninhabitedPlanets;
		case "planet-unidentified":
			return toggles.unidentifiedPlanets;
		case "hyperspaceGroup":
			return toggles.hyperspaceGroups;
		case "incomingGroup":
			return toggles.incomingGroups;
		case "unidentifiedGroup":
			return toggles.unidentifiedGroups;
		case "battleMarker":
			return toggles.battleMarkers;
		case "bombingMarker":
			return toggles.bombingMarkers;
	}
}

/**
 * computeHiddenPlanetNumbers returns every non-LOCAL planet whose
 * kind toggle is off or — when `unreachablePlanets` is off — which
 * sits beyond `FlightDistance(localPlayerDrive)` of every LOCAL
 * planet. LOCAL planets themselves are never returned.
 *
 * `localPlayerDrive === 0` (zero drive tech) collapses the reach
 * threshold to zero, so when `unreachablePlanets` is off the
 * function returns every non-LOCAL planet — matching the engine's
 * "no fleet can move" baseline.
 */
export function computeHiddenPlanetNumbers(
	report: GameReport,
	toggles: MapToggles,
): Set<number> {
	const hidden = new Set<number>();
	if (report.planets.length === 0) return hidden;
	const localPlanets: { x: number; y: number }[] = [];
	for (const p of report.planets) {
		if (p.kind === "local") localPlanets.push({ x: p.x, y: p.y });
	}
	const reachThreshold =
		toggles.unreachablePlanets || localPlanets.length === 0
			? Infinity
			: report.localPlayerDrive * FLIGHT_DISTANCE_PER_DRIVE;
	for (const p of report.planets) {
		if (p.kind === "local") continue;
		let kindVisible: boolean;
		switch (p.kind) {
			case "other":
				kindVisible = toggles.foreignPlanets;
				break;
			case "uninhabited":
				kindVisible = toggles.uninhabitedPlanets;
				break;
			case "unidentified":
				kindVisible = toggles.unidentifiedPlanets;
				break;
		}
		if (!kindVisible) {
			hidden.add(p.number);
			continue;
		}
		if (reachThreshold === Infinity) continue;
		let reachable = false;
		for (const lp of localPlanets) {
			const d = torusShortestDistance(
				p.x,
				p.y,
				lp.x,
				lp.y,
				report.mapWidth > 0 ? report.mapWidth : 1,
				report.mapHeight > 0 ? report.mapHeight : 1,
			);
			if (d <= reachThreshold) {
				reachable = true;
				break;
			}
		}
		if (!reachable) hidden.add(p.number);
	}
	return hidden;
}

/**
 * computeHiddenIds resolves the toggle state into the final hide-by-
 * id set fed to `RendererHandle.setHiddenPrimitiveIds`. Inputs:
 *
 *   - `categories`: every primitive's toggleable surface, as
 *     produced by `reportToWorld`.
 *   - `planetDependents`: for each planet number, the primitive ids
 *     whose visibility cascades on that planet (planet itself, the
 *     markers anchored on it, in-space / incoming groups flying to
 *     it, their lines). Produced by `reportToWorld`.
 *   - `hiddenPlanetNumbers`: the kind / reach-filtered set from
 *     `computeHiddenPlanetNumbers`.
 *   - `toggles`: the per-category toggle state.
 *
 * Returns the union of (a) every primitive id whose category toggle
 * is off and (b) every dependent of a hidden planet number.
 */
export function computeHiddenIds(
	categories: ReadonlyMap<PrimitiveID, MapCategory>,
	planetDependents: ReadonlyMap<number, ReadonlySet<PrimitiveID>>,
	hiddenPlanetNumbers: ReadonlySet<number>,
	toggles: MapToggles,
): Set<PrimitiveID> {
	const hidden = new Set<PrimitiveID>();
	for (const [id, category] of categories) {
		if (!isCategoryVisible(category, toggles)) hidden.add(id);
	}
	for (const planetNumber of hiddenPlanetNumbers) {
		const deps = planetDependents.get(planetNumber);
		if (deps === undefined) continue;
		for (const id of deps) hidden.add(id);
	}
	return hidden;
}

/**
 * computeFogCircles produces the visibility-fog input — empty when
 * the `visibleHyperspace` toggle is off, otherwise one circle per
 * LOCAL planet at `VisibilityDistance(localPlayerDrive)`. When the
 * drive tech is zero the function returns an empty list as well:
 * a zero-radius fog cutout would leave the entire world fogged,
 * which is more confusing than helpful in tutorial / debug
 * scenarios. The renderer-side fog Graphics is destroyed on an
 * empty list.
 */
export function computeFogCircles(
	report: GameReport,
	toggles: MapToggles,
): { x: number; y: number; radius: number }[] {
	if (!toggles.visibleHyperspace) return [];
	const radius = report.localPlayerDrive * VISIBILITY_DISTANCE_PER_DRIVE;
	if (radius <= 0) return [];
	const circles: { x: number; y: number; radius: number }[] = [];
	for (const p of report.planets) {
		if (p.kind !== "local") continue;
		circles.push({ x: p.x, y: p.y, radius });
	}
	return circles;
}

/**
 * fingerprintHiddenPlanets returns a stable string identifying the
 * supplied hidden-planet set. The map view threads it into the
 * extras fingerprint so a toggle flip that changes the planet set
 * — and therefore changes which routes / pending-Send lines must be
 * filtered out — reliably triggers an `setExtraPrimitives` push.
 */
export function fingerprintHiddenPlanets(
	hiddenPlanetNumbers: ReadonlySet<number>,
): string {
	if (hiddenPlanetNumbers.size === 0) return "";
	return Array.from(hiddenPlanetNumbers)
		.sort((a, b) => a - b)
		.join(",");
}
