<script lang="ts">
	import FrameSlider from './FrameSlider.svelte';
	import { type Video, type VideoConstructor } from '$/lib/Player/Video';
	import Keybinds from './Keybinds.svelte';
	import { onMount } from 'svelte';

	let frame = $state(0);
	let canvas = $state(null as HTMLCanvasElement | null);
	let audio = $state(null as HTMLAudioElement | null);
	let lastCanvas = null as typeof canvas;
	let video = $state(undefined as Video | undefined);
	let frameCount = $state(0);
	let playing = $state(false);
	let playbackRelativeTo = $state(0);
	let playbackFrameOffset = 0;
	let renderPromise: Promise<void> | void = void 0;
	let renderId = 0;
	let audioSource = $state(null as null | string);
	let {
		Video: VideoImplementation
	}: {
		Video: VideoConstructor;
	} = $props();
	const newCanvas = async (canvas: HTMLCanvasElement, videoImplementation: VideoConstructor) => {
		if (video) video.cleanup();
		lastCanvas = canvas;
		video = new VideoImplementation(canvas);
		const audioSourcePromise = (async () => {
			if (video?.audioUrl?.[1]) {
				audioSource = await fetch(video.audioUrl[1])
					.then(async (v) => {
						if (v.status.toString().startsWith('2')) return URL.createObjectURL(await v.blob());
						else throw new Error('non-2xx audio');
					})
					.catch((e) => {
						console.warn('Failed to get audio', e);
						return video?.audioUrl?.[1] ?? null;
					});
			}
		})();
		video['_isInit'] = true;
		renderPromise = video.init({
			isPreview: true
		});
		await renderPromise;
		video['_isInit'] = false;
		frameCount = video.length;
		await audioSourcePromise;
	};
	const renderPreviewFrame = async (video: Video, frame: number) => {
		const ourId = ++renderId;
		if (renderPromise) await renderPromise;
		if (renderId !== ourId) return;
		if (frame > video.length) frame = video.length;
		renderPromise =
			video.renderFrame({
				frames: frame,
				milliseconds: (frame / video.fps) * 1000,
				seconds: frame / video.fps
			}) ?? Promise.resolve();
		return renderPromise;
	};
	$effect(() => {
		if (canvas && canvas !== lastCanvas) newCanvas(canvas, VideoImplementation);
	});
	$effect(() => {
		if (video && !playing) renderPreviewFrame(video, frame);
	});
	let playbackLoopId = 0;
	const startPlaybackLoop = (id = ++playbackLoopId) => {
		if (video && id === playbackLoopId) {
			const ms = performance.now() - playbackRelativeTo;
			let f = Math.floor((ms / 1000) * video.fps) + playbackFrameOffset;

			if (audio) {
				const desiredTime = frame / video.fps;
				const timeDelta = Math.abs(desiredTime - audio.currentTime);
				// If we get too out of lock-step with audio, we re-sync - we assume audio source is accurate*, as to avoid too much jumping
				// *it may not be
				if (desiredTime < audio.duration - 0.5 && timeDelta > 0.1) {
					if (timeDelta > 0.3) {
						// too out of sync, we trust ourselves more
						audio.currentTime = desiredTime;
					} else {
						f = audio.currentTime * video.fps;
					}
				}
			}

			if (f > frameCount) {
				f = frameCount;
				playbackRelativeTo = 0;
				playbackFrameOffset = 0;
				playing = false;
			}
			frame = f;
			renderPreviewFrame(video, f).then(() =>
				requestAnimationFrame(() => (playing ? startPlaybackLoop(id) : void 0))
			);
		}
	};
	const startPlaying = () => {
		if (frame === frameCount && playbackRelativeTo === 0) {
			frame = 0;
		}
		playbackRelativeTo = performance.now();
		playbackFrameOffset = frame;
		startPlaybackLoop();
	};
	// playback starter
	$effect(() => {
		if (playing) {
			startPlaying();
		} else {
			playbackRelativeTo = 0;
			playbackFrameOffset = 0;
		}
	});
	let loadedFrameTimestamp = false;
	onMount(() => {
		const t = sessionStorage.getItem('timestamp');
		const tI = t ? parseInt(t, 36) : null;
		if (tI && !isNaN(tI)) {
			frame = tI;
			requestAnimationFrame(() => (frame = tI));
		}
		loadedFrameTimestamp = true;
	});
	let nextSaveFrameNumber = 0;
	let lastSave = 0;
	const saveFramePosition = async (frame: number) => {
		nextSaveFrameNumber = frame;
		await new Promise((rs) => setTimeout(rs, 100));
		if (frame === nextSaveFrameNumber || performance.now() - lastSave > 500)
			try {
				lastSave = performance.now();
				sessionStorage.setItem('timestamp', frame.toString(36));
			} catch (_) {}
	};
	$effect(() => {
		if (loadedFrameTimestamp) saveFramePosition(frame);
	});
	let seeking = false;
	let properlyFinishedMounting = false;
	onMount(() => requestAnimationFrame(() => (properlyFinishedMounting = true)));
	$effect(() => {
		if (audio && video && !playing && !playbackFrameOffset) {
			if (properlyFinishedMounting)
				try {
					const f = frame;
					audio.currentTime = frame / video.fps;
					audio.play();
					seeking = true;
					(async () => {
						const targetTime = (frame + 1) / video.fps;
						while (audio.currentTime <= targetTime) {
							await new Promise((rs) => requestAnimationFrame(rs));
							if (playing) {
								seeking = false;
								return;
							}
							if (frame !== f) return;
						}
						seeking = false;
						audio.pause();
						audio.currentTime = frame / video.fps;
					})();
				} catch (error) {
					console.warn('Seek Error:', error);
				}
		}
	});
	const updateAudioPlayingState = (playing: boolean) => {
		if (audio) {
			if (playing) audio.play();
			else if (!seeking) audio.pause();
		}
	};
	$effect(() => updateAudioPlayingState(playing));
</script>

<svelte:window
	onresize={() => {
		if (canvas && video) {
			(async () => {
				video['_isInit'] = true;
				renderPromise = await video.init({
					isPreview: true
				});
				video['_isInit'] = false;
				renderPreviewFrame(video, frame);
			})();
		}
	}}
/>

<Keybinds
	bind:frame
	{frameCount}
	fps={video?.fps}
	bind:playing
	bind:playbackStarted={playbackRelativeTo}
/>

<div class="p-2 w-screen h-screen relative flex flex-col">
	<div class="flex-1 relative">
		<div class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
			<canvas class="bg-black" bind:this={canvas}>
				Your browser doesn't support the canvas API.
			</canvas>
			{#if audioSource}
				<audio src={audioSource} bind:this={audio} preload="auto"></audio>
			{/if}
		</div>
	</div>
	<FrameSlider bind:frame {frameCount} bind:playing bind:playbackStarted={playbackRelativeTo} />
</div>
