<script lang="ts">
	import { Skeleton } from '$lib/components/ui/skeleton';
	import * as ToggleGroup from '$lib/components/ui/toggle-group';
	import { cn } from '$lib/utils';
	import {
		loadMapData,
		radarUrl,
		hasLowerFloor,
		floorForZ,
		worldToImage,
		yawToScreenDeg,
		RADAR_PX,
		type MapData,
		type Floor
	} from '$lib/utils/map';

	type Player = {
		steamid: string;
		name: string;
		team_num: number;
		is_alive: boolean;
		health: number;
		X: number;
		Y: number;
		Z: number;
		yaw?: number;
		color: string;
		slot?: number;
	};

	type Props = {
		mapName: string;
		players: Player[];
		activePlayer?: number | null;
		availablePlayers?: Set<number> | null;
		onSelect?: (player: number) => void;
		size?: number;
		floor?: 'upper' | 'lower' | 'auto';
		class?: string;
	};

	let {
		mapName,
		players,
		activePlayer = null,
		availablePlayers = null,
		onSelect,
		size,
		floor = 'auto',
		class: className = ''
	}: Props = $props();

	let allMaps = $state<Record<string, MapData> | null>(null);
	let userFloor = $state<Floor | null>(null);

	$effect(() => {
		let cancelled = false;
		loadMapData()
			.then((d) => {
				if (!cancelled) allMaps = d;
			})
			.catch(() => {});
		return () => {
			cancelled = true;
		};
	});

	const map = $derived(allMaps?.[mapName] ?? null);
	const lowerAvailable = $derived(map ? hasLowerFloor(map) : false);

	const autoFloor = $derived.by<Floor>(() => {
		if (!map || !lowerAvailable) return 'upper';
		let upper = 0;
		let lower = 0;
		for (const p of players) {
			if (!p.is_alive) continue;
			if (floorForZ(map, p.Z) === 'lower') lower++;
			else upper++;
		}
		return lower > upper ? 'lower' : 'upper';
	});

	const displayedFloor = $derived<Floor>(
		userFloor ?? (floor === 'auto' ? autoFloor : (floor as Floor))
	);

	const toggleValue = $derived<Floor>(displayedFloor);
</script>

<div
	class={cn('relative', size === undefined && 'aspect-square w-full', className)}
	style={size === undefined ? undefined : `width: ${size}px; height: ${size}px;`}
>
	{#if !map}
		<Skeleton class="size-full rounded-md" />
	{:else}
		<img
			src={radarUrl(mapName, displayedFloor)}
			alt="{mapName} radar"
			class="absolute inset-0 size-full rounded-md opacity-70 select-none"
			draggable="false"
		/>
		<svg
			viewBox="0 0 {RADAR_PX} {RADAR_PX}"
			class="absolute inset-0 size-full"
			aria-label="Player positions on {mapName}"
		>
			{#each players as p (p.steamid)}
				{@const { px, py } = worldToImage(map, p.X, p.Y)}
				{@const playerFloor = floorForZ(map, p.Z)}
				{@const onDisplayed = !lowerAvailable || playerFloor === displayedFloor}
				{@const baseOpacity = onDisplayed ? 1 : 0.35}
				{@const isActive = activePlayer !== null && p.slot === activePlayer}
				{@const r = isActive ? 42 : 32}
				{@const fontSize = isActive ? 38 : 30}
				{@const coneLen = isActive ? 110 : 90}
				{@const coneHalf = isActive ? 38 : 32}
				{@const slotKnown = p.slot !== undefined}
				{@const available =
					!availablePlayers || (slotKnown && availablePlayers.has(p.slot as number))}
				{@const clickable = !!onSelect && slotKnown && available}
				<!-- svelte-ignore a11y_click_events_have_key_events -->
				<!-- svelte-ignore a11y_no_static_element_interactions -->
				<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
				<g
					class="mp-marker"
					data-active={isActive || undefined}
					data-clickable={clickable || undefined}
					data-unavailable={!!onSelect && slotKnown && !available ? true : undefined}
					opacity={baseOpacity}
					role={clickable ? 'button' : undefined}
					tabindex={clickable ? 0 : undefined}
					aria-label={slotKnown ? `Switch to player ${p.slot}` : undefined}
					onclick={clickable ? () => onSelect?.(p.slot as number) : undefined}
					onkeydown={clickable
						? (e: KeyboardEvent) => {
								if (e.key === 'Enter' || e.key === ' ') {
									e.preventDefault();
									onSelect?.(p.slot as number);
								}
							}
						: undefined}
				>
					<title>{p.name}{p.slot !== undefined ? ` (#${p.slot})` : ''}</title>
					{#if clickable || isActive}
						<!-- Hit-target + hover ring. Transparent stroke by default; the
						     :hover style flips it on. Larger than the badge so the
						     click area is generous. -->
						<circle
							class="mp-marker__hit"
							cx={px}
							cy={py}
							r={r + 14}
							fill="transparent"
							stroke={p.color}
							stroke-opacity="0"
							stroke-width="3"
						/>
					{/if}
					{#if p.is_alive}
						{#if p.yaw !== undefined}
							<!-- Vision cone: wide + faint fill, soft edge. Reads at a
							     glance without overpowering the badge. -->
							<polygon
								points="0,-{coneHalf} {coneLen},0 0,{coneHalf}"
								fill={p.color}
								fill-opacity="0.18"
								stroke={p.color}
								stroke-opacity="0.55"
								stroke-width="2"
								transform="translate({px} {py}) rotate({yawToScreenDeg(p.yaw)})"
							/>
						{/if}
						{#if isActive}
							<!-- Active player halo: a white inner ring + colored outer ring. -->
							<circle
								cx={px}
								cy={py}
								r={r + 7}
								fill="none"
								stroke="#fff"
								stroke-width="2.5"
								stroke-opacity="0.9"
							/>
							<circle
								cx={px}
								cy={py}
								r={r + 12}
								fill="none"
								stroke={p.color}
								stroke-width="2.5"
								stroke-opacity="0.75"
							/>
						{/if}
						<circle
							cx={px}
							cy={py}
							{r}
							fill={p.color}
							stroke="#0b0f14"
							stroke-width={isActive ? 4 : 3}
						/>
						{#if p.slot !== undefined}
							<text
								x={px}
								y={py}
								text-anchor="middle"
								dominant-baseline="central"
								font-size={fontSize}
								font-weight="800"
								fill="#fff"
								stroke="#0b0f14"
								stroke-width="0.9"
								paint-order="stroke"
								style="pointer-events: none; user-select: none;"
							>
								{p.slot}
							</text>
						{/if}
					{:else}
						<g
							opacity={baseOpacity * 0.55}
							transform="translate({px} {py})"
							stroke={p.color}
							stroke-width="3.5"
							fill="none"
						>
							<circle r="18" />
							<line x1="-10" y1="-10" x2="10" y2="10" />
							<line x1="-10" y1="10" x2="10" y2="-10" />
						</g>
					{/if}
				</g>
			{/each}
		</svg>

		{#if lowerAvailable}
			<div class="absolute top-2 right-2">
				<ToggleGroup.Root
					type="single"
					value={toggleValue}
					onValueChange={(v) => {
						if (v === 'upper' || v === 'lower') userFloor = v;
					}}
					variant="outline"
					size="sm"
					spacing={1}
					aria-label="Floor"
					class="bg-background/80 backdrop-blur-sm"
				>
					<ToggleGroup.Item value="upper" aria-label="Upper floor" class="text-xs">
						Upper
					</ToggleGroup.Item>
					<ToggleGroup.Item value="lower" aria-label="Lower floor" class="text-xs">
						Lower
					</ToggleGroup.Item>
				</ToggleGroup.Root>
			</div>
		{/if}
	{/if}
</div>

<style>
	.mp-marker[data-clickable='true'] {
		cursor: pointer;
	}
	.mp-marker[data-unavailable='true'] {
		cursor: not-allowed;
	}
	/* Hover & focus on a clickable marker: light up the hit-ring and lift the
	   group with a subtle drop-shadow so it feels selectable. */
	.mp-marker[data-clickable='true']:hover .mp-marker__hit,
	.mp-marker[data-clickable='true']:focus-visible .mp-marker__hit {
		stroke-opacity: 0.9;
	}
	.mp-marker[data-clickable='true'] {
		transition: filter 120ms ease-out;
	}
	.mp-marker[data-clickable='true']:hover,
	.mp-marker[data-clickable='true']:focus-visible {
		filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.45));
	}
	.mp-marker:focus {
		outline: none;
	}
	.mp-marker:focus-visible {
		outline: none;
	}
	@media (prefers-reduced-motion: reduce) {
		.mp-marker[data-clickable='true'] {
			transition: none;
		}
	}
</style>
