<script lang="ts">
	import { onMount } from "svelte";
	import type { I18nFormatter } from "@gradio/utils";
	import WaveSurfer from "wavesurfer.js";
	import { skipAudio, process_audio } from "../shared/utils";
	import Record from "wavesurfer.js/dist/plugins/record.js";
	import WaveformControls from "../shared/WaveformControls.svelte";
	import WaveformRecordControls from "../shared/WaveformRecordControls.svelte";
	import RecordPlugin from "wavesurfer.js/dist/plugins/record.js";

	export let mode: string;
	export let i18n: I18nFormatter;
	export let dispatch: (event: any, detail?: any) => void;
	export let dispatch_blob: (
		blobs: Uint8Array[] | Blob[],
		event: "stream" | "change" | "stop_recording"
	) => Promise<void> | undefined;
	export let waveform_settings = {};
	export let handle_reset_value: () => void;

	let micWaveform: WaveSurfer;
	let recordingWaveform: WaveSurfer;
	let playing = false;
	let container: HTMLDivElement;

	let record: Record;
	let recordedAudio: string | null = null;

	// timestamps
	let timeRef: HTMLTimeElement;
	let durationRef: HTMLTimeElement;
	let audioDuration: number;
	let seconds = 0;
	let interval: NodeJS.Timeout;
	let timing = false;
	// trimming
	let trimDuration = 0;

	const start_interval = (): void => {
		clearInterval(interval);
		interval = setInterval(() => {
			seconds++;
		}, 1000);
	};

	const format_time = (seconds: number): string => {
		const minutes = Math.floor(seconds / 60);
		const secondsRemainder = Math.round(seconds) % 60;
		const paddedSeconds = `0${secondsRemainder}`.slice(-2);
		return `${minutes}:${paddedSeconds}`;
	};

	$: record?.on("record-start", () => {
		start_interval();
		timing = true;
		dispatch("start_recording");
	});

	$: record?.on("record-end", async (blob) => {
		seconds = 0;
		timing = false;
		clearInterval(interval);
		const array_buffer = await blob.arrayBuffer();
		const context = new AudioContext();
		const audio_buffer = await context.decodeAudioData(array_buffer);

		if (audio_buffer)
			await process_audio(audio_buffer).then(
				async (trimmedBlob: Uint8Array) => {
					await dispatch_blob([trimmedBlob], "change");
					await dispatch_blob([trimmedBlob], "stop_recording");
				}
			);
	});

	$: record?.on("record-pause", () => {
		dispatch("pause_recording");
		clearInterval(interval);
	});

	$: record?.on("record-resume", () => {
		start_interval();
	});

	$: recordingWaveform?.on("decode", (duration: any) => {
		audioDuration = duration;
		durationRef && (durationRef.textContent = format_time(duration));
	});

	$: recordingWaveform?.on(
		"timeupdate",
		(currentTime: any) =>
			timeRef && (timeRef.textContent = format_time(currentTime))
	);

	$: recordingWaveform?.on("pause", () => {
		dispatch("pause");
		playing = false;
	});

	$: recordingWaveform?.on("play", () => {
		dispatch("play");
		playing = true;
	});

	$: recordingWaveform?.on("finish", () => {
		dispatch("stop");
		dispatch("end");
		playing = false;
	});

	const create_mic_waveform = (): void => {
		const recorder = document.getElementById("microphone");
		if (recorder) recorder.innerHTML = "";
		if (micWaveform !== undefined) micWaveform.destroy();
		if (!recorder) return;
		micWaveform = WaveSurfer.create({
			...waveform_settings,
			container: recorder
		});

		record = micWaveform.registerPlugin(RecordPlugin.create());
		record.startMic();
	};

	const create_recording_waveform = (): void => {
		let recording = document.getElementById("recording");
		if (!recordedAudio || !recording) return;
		recordingWaveform = WaveSurfer.create({
			container: recording,
			url: recordedAudio,
			...waveform_settings
		});
	};

	$: record?.on("record-end", (blob) => {
		recordedAudio = URL.createObjectURL(blob);

		const microphone = document.getElementById("microphone");
		const recording = document.getElementById("recording");

		if (microphone) microphone.style.display = "none";
		if (recording && recordedAudio) {
			recording.innerHTML = "";
			create_recording_waveform();
		}
	});

	const handle_trim_audio = async (
		start: number,
		end: number
	): Promise<void> => {
		mode = "edit";
		const decodedData = recordingWaveform.getDecodedData();
		if (decodedData)
			await process_audio(decodedData, start, end).then(
				async (trimmedAudio: Uint8Array) => {
					await dispatch_blob([trimmedAudio], "change");
					recordingWaveform.destroy();
					create_recording_waveform();
				}
			);
		dispatch("edit");
	};

	onMount(() => {
		create_mic_waveform();

		window.addEventListener("keydown", (e) => {
			if (e.key === "ArrowRight") {
				skipAudio(recordingWaveform, 0.1);
			} else if (e.key === "ArrowLeft") {
				skipAudio(recordingWaveform, -0.1);
			}
		});
	});
</script>

<div class="component-wrapper">
	<div id="microphone" data-testid="microphone-waveform" />

	{#if micWaveform && !recordedAudio}
		<WaveformRecordControls bind:record {dispatch} />
	{/if}

	{#if recordingWaveform && recordedAudio}
		<WaveformControls
			bind:waveform={recordingWaveform}
			{container}
			{playing}
			{audioDuration}
			{i18n}
			interactive={true}
			{handle_trim_audio}
			bind:trimDuration
			bind:mode
			showRedo
			{handle_reset_value}
			{waveform_settings}
		/>
	{/if}
</div>

<style>
	#microphone {
		width: 100%;
		display: none;
	}

	.component-wrapper {
		padding: var(--size-3);
	}
</style>
