<script lang="ts">
	import { onMount } from 'svelte';
	import { env } from '$env/dynamic/public';
	import { t } from '$lib/i18n/store.svelte';
	import { MAX_BOUNDS, MIN_ZOOM, REGION_BOUNDS, REGIONS } from '$lib/map/regions';
	import styleOki from '$lib/map/style-oki.json';
	import type { Coordonnees, Region } from '$lib/types';
	import type { Map as MLMap, Marker, StyleSpecification } from 'maplibre-gl';

	interface Paire {
		from: Coordonnees;
		to: Coordonnees;
		label?: string;
	}

	interface MarqueurInfo {
		id: string;
		lat: number;
		lon: number;
		label: string;
	}

	interface Props {
		guess?: Coordonnees | null;
		real?: Coordonnees | null;
		interactive?: boolean;
		pairs?: Paire[];
		markers?: MarqueurInfo[];
		onguess?: (c: Coordonnees) => void;
		onvalidate?: () => void;
		onmarkerclick?: (id: string) => void;
	}

	let {
		guess = $bindable(null),
		real = null,
		interactive = true,
		pairs = [],
		markers = [],
		onguess,
		onvalidate,
		onmarkerclick
	}: Props = $props();

	let conteneur: HTMLDivElement;
	let map: MLMap | null = null;
	let ml: typeof import('maplibre-gl') | null = null;
	let guessMarker: Marker | null = null;
	let realMarker: Marker | null = null;
	let explorerMarkers: Marker[] = [];
	let raf = 0;

	const SRC_LIGNE = 'jwe-ligne';
	const SRC_PAIRES = 'jwe-paires';

	const reducedMotion =
		typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;

	function styleUrl(): string | StyleSpecification {
		// Bascule vers un tileserver auto-hébergé sans changement de code.
		if (env.PUBLIC_TILES_URL) return env.PUBLIC_TILES_URL;
		return styleOki as unknown as StyleSpecification;
	}

	function ensureSource(id: string): void {
		if (map && !map.getSource(id)) {
			map.addSource(id, {
				type: 'geojson',
				data: { type: 'FeatureCollection', features: [] }
			});
		}
	}

	function setGeoJSON(id: string, data: GeoJSON.FeatureCollection): void {
		const src = map?.getSource(id) as import('maplibre-gl').GeoJSONSource | undefined;
		src?.setData(data);
	}

	function ligneFeature(from: Coordonnees, to: Coordonnees, props: GeoJSON.GeoJsonProperties): GeoJSON.Feature {
		return {
			type: 'Feature',
			properties: props,
			geometry: { type: 'LineString', coordinates: [[from.lon, from.lat], [to.lon, to.lat]] }
		};
	}

	function pointFeature(c: Coordonnees, props: GeoJSON.GeoJsonProperties): GeoJSON.Feature {
		return {
			type: 'Feature',
			properties: props,
			geometry: { type: 'Point', coordinates: [c.lon, c.lat] }
		};
	}

	function dessinerRevelation(): void {
		if (!map || !ml || !real) return;
		ensureSource(SRC_LIGNE);
		if (!map.getLayer('jwe-ligne-line')) {
			map.addLayer({
				id: 'jwe-ligne-line',
				type: 'line',
				source: SRC_LIGNE,
				paint: { 'line-color': '#e4572e', 'line-width': 3, 'line-dasharray': [2, 1.5] }
			});
		}
		const features: GeoJSON.Feature[] = guess ? [ligneFeature(guess, real, null)] : [];
		setGeoJSON(SRC_LIGNE, { type: 'FeatureCollection', features });

		// Marqueur vert = position réelle
		realMarker?.remove();
		realMarker = new ml.Marker({ color: '#0b3d2e' }).setLngLat([real.lon, real.lat]).addTo(map);

		// Le marqueur du joueur « vole » vers la position réelle (sauf reduced-motion)
		if (guessMarker && guess) {
			const from = { ...guess };
			if (reducedMotion) {
				guessMarker.setLngLat([real.lon, real.lat]);
			} else {
				const debut = performance.now();
				const duree = 1400;
				const step = (now: number) => {
					if (!guessMarker) return;
					const k = Math.min(1, (now - debut) / duree);
					const e = 1 - Math.pow(1 - k, 3);
					guessMarker.setLngLat([
						from.lon + (real.lon - from.lon) * e,
						from.lat + (real.lat - from.lat) * e
					]);
					if (k < 1) raf = requestAnimationFrame(step);
				};
				raf = requestAnimationFrame(step);
			}
			const bounds = new ml.LngLatBounds(
				[Math.min(from.lon, real.lon), Math.min(from.lat, real.lat)],
				[Math.max(from.lon, real.lon), Math.max(from.lat, real.lat)]
			);
			map.fitBounds(bounds, { padding: 80, duration: reducedMotion ? 0 : 900, maxZoom: 12 });
		} else {
			map.easeTo({ center: [real.lon, real.lat], zoom: 11, duration: reducedMotion ? 0 : 900 });
		}
	}

	function nettoyerRevelation(): void {
		cancelAnimationFrame(raf);
		realMarker?.remove();
		realMarker = null;
		setGeoJSON(SRC_LIGNE, { type: 'FeatureCollection', features: [] });
	}

	function syncGuessMarker(): void {
		if (!map || !ml) return;
		if (guess) {
			if (!guessMarker) {
				guessMarker = new ml.Marker({ color: '#e4572e', draggable: true })
					.setLngLat([guess.lon, guess.lat])
					.addTo(map);
				guessMarker.on('dragend', () => {
					const ll = guessMarker?.getLngLat();
					if (ll) {
						guess = { lat: ll.lat, lon: ll.lng };
						onguess?.(guess);
					}
				});
			} else {
				guessMarker.setLngLat([guess.lon, guess.lat]);
			}
			guessMarker.setDraggable(interactive && !real);
		} else {
			guessMarker?.remove();
			guessMarker = null;
		}
	}

	function syncPaires(): void {
		if (!map || !ml) return;
		ensureSource(SRC_PAIRES);
		if (!map.getLayer('jwe-paires-line')) {
			map.addLayer({
				id: 'jwe-paires-line',
				type: 'line',
				source: SRC_PAIRES,
				filter: ['==', ['geometry-type'], 'LineString'],
				paint: { 'line-color': '#5a6b62', 'line-width': 2, 'line-dasharray': [2, 1.5] }
			});
			map.addLayer({
				id: 'jwe-paires-points',
				type: 'circle',
				source: SRC_PAIRES,
				filter: ['==', ['geometry-type'], 'Point'],
				paint: {
					'circle-radius': 7,
					'circle-color': ['get', 'couleur'],
					'circle-stroke-color': '#fffdf8',
					'circle-stroke-width': 2
				}
			});
		}
		const features: GeoJSON.Feature[] = [];
		for (const p of pairs) {
			features.push(ligneFeature(p.from, p.to, null));
			features.push(pointFeature(p.from, { couleur: '#e4572e' })); // joueur
			features.push(pointFeature(p.to, { couleur: '#0b3d2e' })); // réel
		}
		setGeoJSON(SRC_PAIRES, { type: 'FeatureCollection', features });
		if (pairs.length > 0) {
			const b = new ml.LngLatBounds();
			for (const p of pairs) {
				b.extend([p.from.lon, p.from.lat]);
				b.extend([p.to.lon, p.to.lat]);
			}
			map.fitBounds(b, { padding: 60, duration: reducedMotion ? 0 : 800 });
		}
	}

	function syncExplorerMarkers(): void {
		if (!map || !ml) return;
		for (const m of explorerMarkers) m.remove();
		explorerMarkers = [];
		for (const info of markers) {
			const el = document.createElement('button');
			el.type = 'button';
			el.className = 'marqueur-explorer';
			el.setAttribute('aria-label', info.label);
			const mk = new ml.Marker({ element: el, color: '#175e70' })
				.setLngLat([info.lon, info.lat])
				.addTo(map!);
			el.addEventListener('click', (e) => {
				e.stopPropagation();
				onmarkerclick?.(info.id);
			});
			explorerMarkers.push(mk);
		}
	}

	function sauterVers(region: Region): void {
		const b = REGION_BOUNDS[region];
		map?.fitBounds(
			[
				[b[0], b[1]],
				[b[2], b[3]]
			],
			{ duration: reducedMotion ? 0 : 800 }
		);
	}

	function onKeydown(e: KeyboardEvent): void {
		if (!map) return;
		const pas = 140; // pixels de déplacement
		if (e.key === 'Enter') {
			e.preventDefault();
			onvalidate?.();
			return;
		}
		const dirs: Record<string, [number, number]> = {
			ArrowUp: [0, -pas],
			ArrowDown: [0, pas],
			ArrowLeft: [-pas, 0],
			ArrowRight: [pas, 0]
		};
		const d = dirs[e.key];
		if (!d) return;
		e.preventDefault();
		if (guessMarker && interactive && !real) {
			// Déplacer le marqueur : conversion écran → géo pour garder un pas constant
			const ll = guessMarker.getLngLat();
			const pt = map.project([ll.lng, ll.lat]);
			const nv = map.unproject([pt.x + d[0], pt.y + d[1]]);
			guess = { lat: nv.lat, lon: nv.lng };
			onguess?.(guess);
		} else {
			map.panBy(d, { duration: 100 });
		}
	}

	onMount(() => {
		let detruit = false;
		// Lazy load : MapLibre (JS + CSS) n'est chargé que sur les écrans avec carte.
		void (async () => {
			const [mod] = await Promise.all([import('maplibre-gl'), import('maplibre-gl/dist/maplibre-gl.css')]);
			if (detruit) return;
			ml = mod;
			map = new mod.Map({
				container: conteneur,
				style: styleUrl(),
				center: [-58, 0],
				zoom: 3.6,
				minZoom: MIN_ZOOM,
				maxBounds: [
					[MAX_BOUNDS[0], MAX_BOUNDS[1]],
					[MAX_BOUNDS[2], MAX_BOUNDS[3]]
				],
				attributionControl: { compact: true },
				// Libellés FR/créole des contrôles natifs (sinon « Zoom in/out » en anglais)
				locale: {
					'NavigationControl.ZoomIn': t().carte.zoomIn,
					'NavigationControl.ZoomOut': t().carte.zoomOut
				}
			});
			map.addControl(new mod.NavigationControl({ showCompass: false }), 'top-left');
			map.on('click', (e) => {
				if (!interactive || real) return;
				guess = { lat: e.lngLat.lat, lon: e.lngLat.lng };
				onguess?.(guess);
			});
			map.on('load', () => {
				syncGuessMarker();
				syncPaires();
				syncExplorerMarkers();
				if (real) dessinerRevelation();
			});
		})();
		return () => {
			detruit = true;
			cancelAnimationFrame(raf);
			map?.remove();
			map = null;
		};
	});

	let ancienReal: Coordonnees | null = null;
	$effect(() => {
		if (!map || !map.loaded()) return;
		if (real && !ancienReal) dessinerRevelation();
		if (!real && ancienReal) nettoyerRevelation();
		ancienReal = real;
	});

	$effect(() => {
		if (map?.loaded()) syncGuessMarker();
		void guess;
		void interactive;
	});

	$effect(() => {
		if (map?.loaded()) syncPaires();
		void pairs;
	});

	$effect(() => {
		if (map?.loaded()) syncExplorerMarkers();
		void markers;
	});
</script>

<div class="carte-wrap">
	<!-- La zone carte est volontairement focusable : flèches = déplacer le marqueur, Entrée = valider -->
	<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
	<div
		class="carte"
		bind:this={conteneur}
		role="application"
		aria-label={t().carte.ariaLabel}
		tabindex="0"
		onkeydown={onKeydown}
	></div>
	<div class="sauts" role="group" aria-label={t().carte.sautRegion}>
		{#each REGIONS as region (region)}
			<button type="button" class="saut" onclick={() => sauterVers(region)}>
				{t().regions[region]}
			</button>
		{/each}
	</div>
</div>

<style>
	.carte-wrap {
		position: relative;
		width: 100%;
		height: 100%;
	}

	.carte {
		width: 100%;
		height: 100%;
		border-radius: var(--oki-radius);
		overflow: hidden;
	}

	.sauts {
		position: absolute;
		bottom: 0.75rem;
		left: 50%;
		transform: translateX(-50%);
		display: flex;
		gap: 0.4rem;
		flex-wrap: wrap;
		justify-content: center;
		padding: 0 0.5rem;
	}

	.saut {
		background: rgba(255, 253, 248, 0.94);
		color: var(--oki-vert);
		border: 1px solid var(--oki-sable-fonce);
		border-radius: 999px;
		padding: 0.35rem 0.8rem;
		font-size: 0.8rem;
		font-weight: 650;
		cursor: pointer;
		box-shadow: var(--oki-ombre);
	}

	.saut:hover {
		background: var(--oki-vert-doux);
	}

	:global(.marqueur-explorer) {
		width: 26px;
		height: 26px;
		border-radius: 50%;
		border: 2.5px solid var(--oki-blanc);
		background: var(--oki-bleu);
		cursor: pointer;
		box-shadow: var(--oki-ombre);
	}

	:global(.marqueur-explorer:hover),
	:global(.marqueur-explorer:focus-visible) {
		background: var(--oki-vert-clair);
	}
</style>
