<script lang="ts">
	import type { BufferedRange } from '$lib/types';
	import { onDestroy } from 'svelte';
	import { ChunkedMediaSource, type Chunk } from '$lib/components/video-player';
	import { cn } from '$lib/utils';
	import SkullIcon from 'phosphor-svelte/lib/SkullIcon';
	import SpeakerHighIcon from 'phosphor-svelte/lib/SpeakerHighIcon';
	import { playerColor } from '$lib/utils/player-colors';

	interface Props {
		chunks: Chunk[];
		masterTime: number;
		masterPaused: boolean;
		isLeader: boolean;
		isCurrent: boolean;
		isMuted: boolean;
		playbackRate?: number;
		available: boolean;
		duration: number;
		player: number;
		side: 'CT' | 'T' | string;
		onSelect: () => void;
		onLeaderTime?: (t: number) => void;
		onLeaderEnded?: () => void;
		onBufferedChange?: (player: number, ranges: BufferedRange[]) => void;
		onReady?: (player: number) => void;
	}

	let {
		chunks,
		masterTime,
		masterPaused,
		isLeader,
		isCurrent,
		isMuted,
		playbackRate = 1,
		available,
		duration,
		player,
		side,
		onSelect,
		onLeaderTime,
		onLeaderEnded,
		onBufferedChange,
		onReady
	}: Props = $props();

	let videoEl = $state<HTMLVideoElement | undefined>();
	let mse: ChunkedMediaSource | null = null;
	let videoSrc = $state<string | undefined>(undefined);

	function chunksSig(cs: Chunk[]): string {
		return cs.length ? `${cs.length}|${cs[0].src}|${cs[cs.length - 1].src}` : '';
	}
	let prevSig = '';

	$effect(() => {
		const sig = chunksSig(chunks);
		if (sig === prevSig) return;
		prevSig = sig;
		if (mse) {
			mse.destroy();
			mse = null;
		}
		if (videoSrc) {
			URL.revokeObjectURL(videoSrc);
			videoSrc = undefined;
		}
		if (!chunks.length) return;
		const m = new ChunkedMediaSource(chunks, {
			codecOverride: ChunkedMediaSource.getCachedCodec() ?? undefined,
			priorityTime: masterTime
		});
		mse = m;
		videoSrc = m.url;
		m.start().catch(() => {});
	});

	// Drift correction: pull our currentTime back toward the master if we
	// fall out of step (network jitter, decoder hiccups, etc.).
	$effect(() => {
		if (!videoEl) return;
		const t = masterTime;
		if (!Number.isFinite(t)) return;
		if (Math.abs(videoEl.currentTime - t) > 0.35) {
			try {
				videoEl.currentTime = t;
			} catch {
				/* not seekable yet */
			}
		}
	});

	// Mirror the master play/pause state.
	$effect(() => {
		if (!videoEl) return;
		if (masterPaused) videoEl.pause();
		else videoEl.play()?.catch(() => {});
	});

	$effect(() => {
		if (videoEl) videoEl.muted = isMuted;
	});

	// Re-apply on src swap (chunks change) — browsers reset playbackRate on load.
	$effect(() => {
		if (!videoEl) return;
		void videoSrc;
		videoEl.playbackRate = playbackRate;
	});

	// Drive leader-time emission off rAF instead of the <video>'s `timeupdate`
	// event. `timeupdate` fires at ~4 Hz best-case and drops to ~1 Hz under
	// load, which is what was making the minimap stutter. rAF is bound to the
	// display refresh, free when paused (t doesn't change → no emit), and
	// auto-throttled when the tab is backgrounded.
	$effect(() => {
		if (!isLeader || !videoEl) return;
		const el = videoEl;
		let cancelled = false;
		let raf = 0;
		let last = -1;
		const tick = () => {
			if (cancelled) return;
			const t = el.currentTime;
			if (t !== last) {
				last = t;
				onLeaderTime?.(t);
			}
			raf = requestAnimationFrame(tick);
		};
		raf = requestAnimationFrame(tick);
		return () => {
			cancelled = true;
			cancelAnimationFrame(raf);
		};
	});

	function onEnded() {
		if (isLeader) onLeaderEnded?.();
	}

	function onProgress() {
		if (!videoEl || !onBufferedChange) return;
		const ranges = videoEl.buffered;
		const out: BufferedRange[] = [];
		for (let i = 0; i < ranges.length; i++) {
			out.push({ start: ranges.start(i), end: ranges.end(i) });
		}
		onBufferedChange(player, out);
	}

	function onLoadedData() {
		// First decoded frame available — the tile is now safe to play.
		onReady?.(player);
	}

	function onKeyDown(e: KeyboardEvent) {
		if (!available) return;
		if (e.key === 'Enter' || e.key === ' ') {
			e.preventDefault();
			onSelect();
		}
	}

	onDestroy(() => {
		mse?.destroy();
		if (videoSrc) URL.revokeObjectURL(videoSrc);
	});

	const ct = $derived(side === 'CT');
	const color = $derived(playerColor(player));
</script>

<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
	data-current={isCurrent}
	data-side={side}
	aria-disabled={!available}
	class={cn(
		'group relative aspect-video cursor-pointer overflow-hidden rounded-md border bg-black transition',
		'aria-disabled:opacity-60 data-[current=false]:border-border data-[current=false]:hover:border-foreground/40',
		'data-[current=true]:ring-2 data-[side=CT]:data-[current=true]:border-sky-500 data-[side=CT]:data-[current=true]:ring-sky-500/40',
		'data-[side=T]:data-[current=true]:border-amber-500 data-[side=T]:data-[current=true]:ring-amber-500/40'
	)}
	onclick={() => available && onSelect()}
	onkeydown={onKeyDown}
	role="button"
	tabindex={available ? 0 : -1}
	aria-label="Switch to player {player}"
>
	<!-- svelte-ignore a11y_media_has_caption -->
	<video
		bind:this={videoEl}
		src={videoSrc}
		muted={isMuted}
		playsinline
		autoplay={!masterPaused}
		preload="auto"
		disablepictureinpicture
		disableremoteplayback
		controlslist="nodownload nofullscreen noplaybackrate noremoteplayback"
		oncontextmenu={(e) => e.preventDefault()}
		onended={onEnded}
		onprogress={onProgress}
		onloadeddata={onLoadedData}
		class="size-full object-cover"
	></video>

	<!-- top-left: player number, colored per slot -->
	<div
		class="absolute top-1.5 left-1.5 flex size-6 items-center justify-center rounded-sm font-mono text-[11px] font-bold text-white/95 shadow-sm ring-1 ring-black/20"
		style="background-color: {color};"
	>
		{player}
	</div>

	{#if isLeader}
		<div
			class="absolute top-1.5 right-1.5 grid size-5 place-items-center rounded-sm bg-emerald-500/85 text-white shadow-sm ring-1 ring-black/20"
			aria-label="Audio source"
			title="Audio source"
		>
			<SpeakerHighIcon size={11} weight="fill" />
		</div>
	{/if}

	{#if !available}
		<div
			class="absolute right-1.5 bottom-1.5 grid size-5 place-items-center rounded-sm bg-rose-500/85 text-white shadow-sm ring-1 ring-black/20"
			aria-label="Player is dead"
			title="Player is dead"
		>
			<SkullIcon size={11} weight="duotone" />
		</div>
	{/if}
</div>
