// `MapPickService` is the Svelte-side adapter the inspector uses to
// drive a map-driven destination pick. The service owns the
// promise-shaped contract (`pick()` returns the picked planet
// number or `null` on cancel) and a reactive `active` flag for any
// surface that wants to disable other UI while a session is open.
//
// The actual renderer plumbing — dim outside `reachableIds`, anchor
// ring, cursor line, hover outline, click + Escape resolution —
// lives in `ui/frontend/src/map/render.ts.setPickMode`. The map
// active view (`lib/active-view/map.svelte`) is the only producer:
// it constructs the service, sets it on the layout context with
// `MAP_PICK_CONTEXT_KEY`, and binds a resolver that translates the
// service-level request into a `PickModeOptions` payload for the
// current renderer handle.

export const MAP_PICK_CONTEXT_KEY = Symbol("map-pick");

/** High-level pick request the inspector composes. The renderer
 * resolver (registered by the map view) is responsible for turning
 * `sourcePlanetNumber` into the underlying `PickModeOptions`. */
export interface MapPickRequest {
	readonly sourcePlanetNumber: number;
	readonly reachableIds: ReadonlySet<number>;
}

/** A renderer-side resolver registered by the map view. Returns an
 * imperative cancel hook the service uses for `cancel()`, or `null`
 * when the renderer cannot open a session right now (e.g. the
 * source planet is missing from the world). When `null` is
 * returned, the service resolves the pending promise with `null`
 * straight away. */
export type MapPickResolver = (input: {
	sourcePlanetNumber: number;
	reachableIds: ReadonlySet<number>;
	onResolve: (id: number | null) => void;
}) => { cancel(): void } | null;

/**
 * MapPickService coordinates pick-mode sessions between the Svelte
 * inspector and the renderer. Lives for the lifetime of the
 * in-game shell layout; renderer handles come and go through
 * `bindResolver` as the map remounts.
 */
export class MapPickService {
	/** Reactive flag — true while a pick session is open. The
	 * inspector reads this to render its "pick prompt" status line
	 * and to keep the slot button disabled until resolution. */
	active = $state(false);

	private resolver: MapPickResolver | null = null;
	private currentHandle: { cancel(): void } | null = null;
	private currentResolve: ((id: number | null) => void) | null = null;

	/**
	 * bindResolver attaches a renderer-side handler that opens
	 * pick-mode sessions. Pass `null` to detach (the map view does
	 * this on dispose); a detach with a session in progress
	 * resolves the pending promise with `null` so callers do not
	 * deadlock waiting for a renderer that no longer exists.
	 */
	bindResolver(resolver: MapPickResolver | null): void {
		if (resolver === null && this.currentResolve !== null) {
			const r = this.currentResolve;
			this.currentResolve = null;
			this.currentHandle = null;
			this.active = false;
			r(null);
		}
		this.resolver = resolver;
	}

	/**
	 * pick opens a pick session. Resolves to the picked planet
	 * number on a successful pick, or `null` when the player
	 * cancels via Escape, the inspector calls `cancel()`, or the
	 * renderer detaches mid-session.
	 *
	 * Calling `pick` while a session is already active cancels the
	 * old one first (its promise resolves to `null`). The
	 * inspector should normally guard against this via the
	 * reactive `active` flag, but the service stays defensive.
	 */
	pick(request: MapPickRequest): Promise<number | null> {
		return new Promise((resolve) => {
			if (this.resolver === null) {
				resolve(null);
				return;
			}
			if (this.currentHandle !== null) {
				const previousHandle = this.currentHandle;
				this.currentHandle = null;
				previousHandle.cancel();
			}
			this.currentResolve = resolve;
			this.active = true;
			const handle = this.resolver({
				sourcePlanetNumber: request.sourcePlanetNumber,
				reachableIds: request.reachableIds,
				onResolve: (id) => {
					// Guard against late notifications from a stale
					// session (e.g. resolver swapped while a pick was
					// in flight).
					if (this.currentResolve !== resolve) return;
					this.currentResolve = null;
					this.currentHandle = null;
					this.active = false;
					resolve(id);
				},
			});
			if (handle === null) {
				if (this.currentResolve === resolve) {
					this.currentResolve = null;
					this.active = false;
					resolve(null);
				}
				return;
			}
			this.currentHandle = handle;
		});
	}

	/**
	 * cancel terminates the active session, if any. Safe to call
	 * when no session is open — it is a no-op then. The pending
	 * promise resolves with `null`.
	 */
	cancel(): void {
		if (this.currentHandle === null) return;
		const handle = this.currentHandle;
		this.currentHandle = null;
		handle.cancel();
	}
}
