<script lang="ts">
	import { onMount } from "svelte";
	import { Button } from "$lib/components/ui/button";
	import { Input } from "$lib/components/ui/input";
	import {
		LIVE_FRAME_ERROR,
		LIVE_FRAME_INIT,
		LIVE_FRAME_MEDIA,
		SourceBufferAppender,
		buildLiveWsUrl,
		decodeLiveFrame,
		pickLiveMimeType
	} from "$lib/live-mse";

	const DEFAULT_STREAM_ID = "w9oheo19vhjmqgs";
	type Status = "idle" | "connecting" | "streaming" | "closed" | "error";

	let streamId = $state(DEFAULT_STREAM_ID);
	let wsUrl = $state("");
	let status = $state<Status>("idle");
	let wsState = $state("disconnected");
	let sourceMime = $state("pending");
	let bufferedRanges = $state("n/a");
	let errorText = $state("");
	let playbackHint = $state("");
	let initFrames = $state(0);
	let mediaFrames = $state(0);
	let pendingSegments = $state(0);

	let videoEl: HTMLVideoElement | null = null;
	let ws: WebSocket | null = null;
	let mediaSource: MediaSource | null = null;
	let objectUrl: string | null = null;
	let appender: SourceBufferAppender | null = null;
	let pendingInit: Uint8Array | null = null;
	let earlyMedia: Uint8Array[] = [];
	let monitorTimer: number | null = null;
	let didInitialSeek = $state(false);

	$effect(() => {
		if (typeof window === "undefined") {
			return;
		}
		wsUrl = buildLiveWsUrl(streamId);
	});

	onMount(() => {
		const current = new URL(window.location.href);
		const streamParam = current.searchParams.get("streamId");
		if (streamParam && streamParam.length > 0) {
			streamId = streamParam;
		}
		start();
		monitorTimer = window.setInterval(() => {
			bufferedRanges = formatBufferedRanges(videoEl);
			ensurePlaybackPosition();
		}, 500);
		return () => {
			if (monitorTimer !== null) {
				window.clearInterval(monitorTimer);
				monitorTimer = null;
			}
			stop("idle");
		};
	});

	function start() {
		if (streamId.trim().length === 0) {
			errorText = "Stream ID is required";
			status = "error";
			return;
		}
		stop("idle");
		status = "connecting";
		wsState = "connecting";
		errorText = "";
		playbackHint = "";
		sourceMime = "pending";
		initFrames = 0;
		mediaFrames = 0;
		pendingSegments = 0;
		didInitialSeek = false;
		earlyMedia = [];
		pendingInit = null;

		mediaSource = new MediaSource();
		mediaSource.addEventListener("sourceopen", onSourceOpen, { once: true });
		objectUrl = URL.createObjectURL(mediaSource);
		if (videoEl) {
			videoEl.src = objectUrl;
		}

		ws = new WebSocket(buildLiveWsUrl(streamId));
		ws.binaryType = "arraybuffer";
		ws.onopen = () => {
			wsState = "open";
			status = "streaming";
			void tryStartPlayback();
		};
		ws.onerror = () => {
			errorText = "WebSocket error";
			wsState = "error";
			status = "error";
		};
		ws.onclose = () => {
			if (status !== "error") {
				status = "closed";
			}
			wsState = "closed";
		};
		ws.onmessage = async (event) => {
			const payload = await toArrayBuffer(event.data);
			if (!payload) {
				return;
			}
			const frame = decodeLiveFrame(payload);
			if (!frame) {
				return;
			}
			handleFrame(frame.type, frame.payload);
		};
	}

	function stop(nextStatus: Status) {
		if (ws) {
			ws.onopen = null;
			ws.onclose = null;
			ws.onerror = null;
			ws.onmessage = null;
			try {
				ws.close();
			} catch {
				// ignore close errors
			}
			ws = null;
		}
		if (appender) {
			appender.dispose();
			appender = null;
		}
		pendingInit = null;
		earlyMedia = [];
		pendingSegments = 0;
		didInitialSeek = false;
		if (mediaSource?.readyState === "open") {
			try {
				mediaSource.endOfStream();
			} catch {
				// ignore end-of-stream errors
			}
		}
		mediaSource = null;
		if (objectUrl) {
			URL.revokeObjectURL(objectUrl);
			objectUrl = null;
		}
		if (videoEl) {
			videoEl.pause();
			videoEl.muted = false;
			videoEl.currentTime = 0;
			videoEl.removeAttribute("src");
			videoEl.load();
		}
		status = nextStatus;
		wsState = nextStatus === "idle" ? "disconnected" : wsState;
	}

	function onSourceOpen() {
		if (pendingInit) {
			initSourceBuffer(pendingInit);
			pendingInit = null;
		}
	}

	function handleFrame(frameType: number, payload: Uint8Array) {
		if (frameType === LIVE_FRAME_ERROR) {
			errorText = new TextDecoder().decode(payload) || "Live stream error";
			status = "error";
			return;
		}
		if (frameType === LIVE_FRAME_INIT) {
			initFrames += 1;
			if (!mediaSource || mediaSource.readyState !== "open") {
				pendingInit = payload;
				return;
			}
			initSourceBuffer(payload);
			return;
		}
		if (frameType === LIVE_FRAME_MEDIA) {
			mediaFrames += 1;
			if (appender) {
				appender.append(payload);
				pendingSegments = appender.pendingSegments();
				if (mediaFrames <= 3) {
					void tryStartPlayback();
				}
				return;
			}
			earlyMedia.push(payload);
			pendingSegments = earlyMedia.length;
		}
	}

	function initSourceBuffer(initSegment: Uint8Array) {
		if (!mediaSource || mediaSource.readyState !== "open") {
			pendingInit = initSegment;
			return;
		}
		if (appender) {
			return;
		}
		const mime = pickLiveMimeType(initSegment);
		if (!mime) {
			errorText = "No supported MP4 MIME type found for this stream";
			status = "error";
			return;
		}
		sourceMime = mime;
		let sourceBuffer: SourceBuffer;
		try {
			sourceBuffer = mediaSource.addSourceBuffer(mime);
		} catch (err) {
			errorText = `Failed to create SourceBuffer (${String(err)})`;
			status = "error";
			return;
		}
		appender = new SourceBufferAppender(sourceBuffer, (err) => {
			errorText = `appendBuffer failed: ${String(err)}`;
			status = "error";
		});
		appender.append(initSegment);
		for (const segment of earlyMedia) {
			appender.append(segment);
		}
		earlyMedia = [];
		pendingSegments = appender.pendingSegments();
		void tryStartPlayback();
	}

	async function tryStartPlayback() {
		if (!videoEl || status === "error" || !videoEl.paused) {
			return;
		}
		playbackHint = "";
		try {
			videoEl.muted = false;
			await videoEl.play();
			return;
		} catch {
			// Some browsers block autoplay with audio; muted autoplay is usually allowed.
		}

		try {
			videoEl.muted = true;
			await videoEl.play();
			playbackHint = "Playback auto-started muted by browser policy. Click Unmute for audio.";
			ensurePlaybackPosition();
		} catch {
			playbackHint = "Playback is waiting for user interaction. Click the video Play button.";
		}
	}

	async function unmutePlayback() {
		if (!videoEl) {
			return;
		}
		videoEl.muted = false;
		try {
			await videoEl.play();
			playbackHint = "";
		} catch {
			playbackHint = "Browser blocked unmute without interaction. Use the video controls to continue.";
		}
	}

	function ensurePlaybackPosition() {
		if (!videoEl || videoEl.buffered.length === 0) {
			return;
		}

		const firstStart = videoEl.buffered.start(0);
		const lastEnd = videoEl.buffered.end(videoEl.buffered.length - 1);
		const liveTarget = Math.max(firstStart, lastEnd - 0.35);
		const current = videoEl.currentTime;

		try {
			if (!didInitialSeek) {
				videoEl.currentTime = liveTarget;
				didInitialSeek = true;
				return;
			}

			if (current < firstStart-0.2 || current > lastEnd+0.2) {
				videoEl.currentTime = liveTarget;
			}
		} catch {
			// Ignore seek failures while metadata is still stabilizing.
		}
	}

	function formatBufferedRanges(video: HTMLVideoElement | null): string {
		if (!video || video.buffered.length === 0) {
			return "empty";
		}
		const chunks: string[] = [];
		for (let i = 0; i < video.buffered.length; i++) {
			const start = video.buffered.start(i).toFixed(2);
			const end = video.buffered.end(i).toFixed(2);
			chunks.push(`${start}-${end}s`);
		}
		return chunks.join(" | ");
	}

	async function toArrayBuffer(data: unknown): Promise<ArrayBuffer | null> {
		if (data instanceof ArrayBuffer) {
			return data;
		}
		if (data instanceof Blob) {
			return await data.arrayBuffer();
		}
		return null;
	}
</script>

<div class="min-h-screen bg-linear-to-br from-stone-950 via-slate-900 to-zinc-800 px-4 py-6 text-zinc-100 md:px-8">
	<div class="mx-auto grid w-full max-w-6xl gap-4">
		<div class="rounded-2xl border border-zinc-100/15 bg-black/20 p-5 backdrop-blur">
			<h1 class="text-2xl font-semibold tracking-tight md:text-3xl">Live Camera PoC</h1>
			<p class="mt-1 text-sm text-zinc-300">
				WebSocket binary stream to MSE using muxed fMP4 init/media segments.
			</p>
			<div class="mt-4 grid gap-3 md:grid-cols-[1fr_auto_auto]">
				<Input
					placeholder="Stream ID"
					bind:value={streamId}
					disabled={status === "connecting"}
				/>
				<Button onclick={start} disabled={status === "connecting"}>Connect</Button>
				<Button variant="secondary" onclick={() => stop("idle")}>Stop</Button>
			</div>
			<p class="mt-3 break-all text-xs text-zinc-400">{wsUrl}</p>
		</div>

		<div class="grid gap-4 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
			<div class="overflow-hidden rounded-2xl border border-zinc-100/15 bg-black/30">
				<video bind:this={videoEl} class="h-full min-h-85 w-full bg-black" controls autoplay playsinline></video>
			</div>
			<div class="rounded-2xl border border-zinc-100/15 bg-black/20 p-4 text-sm backdrop-blur">
				<div class="flex items-center justify-between border-b border-zinc-100/10 py-2">
					<span>Status</span>
					<span class="font-medium">{status}</span>
				</div>
				<div class="flex items-center justify-between border-b border-zinc-100/10 py-2">
					<span>WebSocket</span>
					<span class="font-medium">{wsState}</span>
				</div>
				<div class="flex items-center justify-between border-b border-zinc-100/10 py-2">
					<span>SourceBuffer MIME</span>
					<span class="font-medium">{sourceMime}</span>
				</div>
				<div class="flex items-center justify-between border-b border-zinc-100/10 py-2">
					<span>Init frames</span>
					<span class="font-medium">{initFrames}</span>
				</div>
				<div class="flex items-center justify-between border-b border-zinc-100/10 py-2">
					<span>Media frames</span>
					<span class="font-medium">{mediaFrames}</span>
				</div>
				<div class="flex items-center justify-between border-b border-zinc-100/10 py-2">
					<span>Pending append queue</span>
					<span class="font-medium">{pendingSegments}</span>
				</div>
				<div class="py-2">
					<div>Buffered ranges</div>
					<div class="mt-1 break-all text-xs text-zinc-300">{bufferedRanges}</div>
				</div>
				{#if errorText.length > 0}
					<div class="mt-3 rounded-lg border border-rose-400/40 bg-rose-900/25 px-3 py-2 text-xs text-rose-100">
						{errorText}
					</div>
				{/if}
				{#if playbackHint.length > 0}
					<div class="mt-3 rounded-lg border border-amber-300/40 bg-amber-900/20 px-3 py-2 text-xs text-amber-100">
						<p>{playbackHint}</p>
						<Button class="mt-2" size="sm" variant="secondary" onclick={unmutePlayback}>Unmute</Button>
					</div>
				{/if}
			</div>
		</div>
	</div>
</div>
