<script lang="ts">
	import { onDestroy, createEventDispatcher, tick } from "svelte";
	import { Upload, ModifyUpload } from "@gradio/upload";
	import { prepare_files, type FileData, type Client } from "@gradio/client";
	import { BlockLabel, ShareButton, CustomButton } from "@gradio/atoms";
	import { Music } from "@gradio/icons";
	import { uploadToHuggingFace } from "@gradio/utils";
	import { StreamingBar } from "@gradio/statustracker";
	import AudioPlayer from "../player/AudioPlayer.svelte";

	import type { IBlobEvent, IMediaRecorder } from "extendable-media-recorder";
	import type { I18nFormatter } from "js/core/src/gradio_helper";
	import AudioRecorder from "../recorder/AudioRecorder.svelte";
	import StreamAudio from "../streaming/StreamAudio.svelte";
	import { init_media_recorder } from "../streaming/media_recorder";
	import type { IMediaRecorderConstructor } from "extendable-media-recorder";
	import { SelectSource } from "@gradio/atoms";
	import type { WaveformOptions, SubtitleData } from "../shared/types";

	export let value: null | FileData = null;
	export let subtitles: null | FileData | SubtitleData[] = null;
	export let label: string;
	export let root: string;
	export let loop: boolean;
	export let show_label = true;
	import type { CustomButton as CustomButtonType } from "@gradio/utils";
	export let buttons: (string | CustomButtonType)[] = ["download", "share"];
	export let on_custom_button_click: ((id: number) => void) | null = null;
	export let sources:
		| ["microphone"]
		| ["upload"]
		| ["microphone", "upload"]
		| ["upload", "microphone"] = ["microphone", "upload"];
	export let pending = false;
	export let streaming = false;
	export let i18n: I18nFormatter;
	export let waveform_settings: Record<string, any>;
	export let trim_region_settings = {};
	export let waveform_options: WaveformOptions = {};
	export let dragging: boolean;
	export let active_source: "microphone" | "upload";
	export let handle_reset_value: () => void = () => {};
	export let editable = true;
	export let max_file_size: number | null = null;
	export let upload: Client["upload"];
	export let stream_handler: Client["stream"];
	export let stream_every: number = 0.1;
	export let uploading = false;
	export let recording = false;
	export let class_name = "";
	export let upload_promise: Promise<any> | null = null;
	export let initial_value: FileData | null = null;
	export let playback_position = 0;

	export let time_limit: number | null = null;
	export let stream_state: "open" | "waiting" | "closed" = "closed";

	$: dispatch("drag", dragging);

	// TODO: make use of this
	// export let type: "normal" | "numpy" = "normal";
	let recorder: IMediaRecorder;
	let mode = "";
	let header: Uint8Array | undefined = undefined;
	let pending_stream: Uint8Array[] = [];
	let submit_pending_stream_on_pending_end = false;
	let inited = false;
	let streaming_media_recorder: IMediaRecorderConstructor;

	const NUM_HEADER_BYTES = 44;
	let audio_chunks: Blob[] = [];
	const is_browser = typeof window !== "undefined";
	if (is_browser && streaming) {
		init_media_recorder().then((a) => {
			streaming_media_recorder = a;
		});
	}

	const dispatch = createEventDispatcher<{
		change: FileData | null;
		stream: FileData;
		edit: never;
		play: never;
		pause: never;
		stop: never;
		end: never;
		drag: boolean;
		error: string;
		upload: FileData;
		clear: undefined;
		start_recording: undefined;
		pause_recording: undefined;
		stop_recording: undefined;
		close_stream: undefined;
	}>();

	const to_blob_parts = (parts: Uint8Array[] | Blob[]): BlobPart[] =>
		parts.map((part) => {
			if (part instanceof Blob) return part;
			return part.slice();
		});

	const dispatch_blob = async (
		blobs: Uint8Array[] | Blob[],
		event: "stream" | "change" | "stop_recording"
	): Promise<void> => {
		let _audio_blob = new File(to_blob_parts(blobs), "audio.wav", {
			type: "audio/wav"
		});
		if (_audio_blob.size === 0) {
			return;
		}
		const val = await prepare_files([_audio_blob], event === "stream");
		initial_value = value;
		value = (
			(await upload(val, root, undefined, max_file_size || undefined))?.filter(
				Boolean
			) as FileData[]
		)[0];
		dispatch(event, value);
	};

	onDestroy(() => {
		if (streaming && recorder && recorder.state !== "inactive") {
			recorder.stop();
		}
	});

	async function prepare_audio(): Promise<void> {
		let stream: MediaStream | null;

		try {
			stream = await navigator.mediaDevices.getUserMedia({ audio: true });
		} catch (err) {
			if (!navigator.mediaDevices) {
				dispatch("error", i18n("audio.no_device_support"));
				return;
			}
			if (err instanceof DOMException && err.name == "NotAllowedError") {
				dispatch("error", i18n("audio.allow_recording_access"));
				return;
			}
			throw err;
		}
		if (stream == null) return;
		if (streaming) {
			recorder = new streaming_media_recorder(stream, {
				mimeType: "audio/wav"
			});

			recorder.addEventListener("dataavailable", handle_chunk);
		} else {
			recorder = new MediaRecorder(stream);
			recorder.addEventListener("dataavailable", (event) => {
				audio_chunks.push(event.data);
			});
		}
		recorder.addEventListener("stop", async () => {
			recording = false;
			recorder.stop();
			await dispatch_blob(audio_chunks, "change");
			await dispatch_blob(audio_chunks, "stop_recording");
			audio_chunks = [];
		});
		inited = true;
	}

	async function handle_chunk(event: IBlobEvent): Promise<void> {
		let buffer = await event.data.arrayBuffer();
		let payload = new Uint8Array(buffer);
		if (!header) {
			header = new Uint8Array(buffer.slice(0, NUM_HEADER_BYTES));
			payload = new Uint8Array(buffer.slice(NUM_HEADER_BYTES));
		}
		if (pending) {
			pending_stream.push(payload);
		} else {
			let blobParts = [header].concat(pending_stream, [payload]);
			if (!recording || stream_state === "waiting") return;
			dispatch_blob(blobParts, "stream");
			pending_stream = [];
		}
	}

	$: if (submit_pending_stream_on_pending_end && pending === false) {
		submit_pending_stream_on_pending_end = false;
		if (header && pending_stream) {
			let blobParts: Uint8Array[] = [header].concat(pending_stream);
			pending_stream = [];
			dispatch_blob(blobParts, "stream");
		}
	}

	async function record(): Promise<void> {
		recording = true;
		dispatch("start_recording");
		if (!inited) await prepare_audio();

		header = undefined;
		if (streaming && recorder.state != "recording") {
			recorder.start(stream_every * 1000);
		}
	}

	function clear(): void {
		dispatch("change", null);
		dispatch("clear");
		mode = "";
		value = null;
	}

	function handle_load({ detail }: { detail: FileData }): void {
		value = detail;
		dispatch("change", detail);
		dispatch("upload", detail);
	}

	async function stop(): Promise<void> {
		recording = false;

		if (streaming) {
			dispatch("close_stream");
			dispatch("stop_recording");
			recorder.stop();

			if (pending) {
				submit_pending_stream_on_pending_end = true;
			}
			dispatch_blob(audio_chunks, "stop_recording");
			dispatch("clear");
			mode = "";
		}
	}

	$: if (!recording && recorder) stop();
	$: if (recording && recorder) record();
</script>

<BlockLabel
	{show_label}
	Icon={Music}
	float={active_source === "upload" && value === null}
	label={label || i18n("audio.audio")}
/>
<div
	class="audio-container {class_name}"
	data-testid={label ? "waveform-" + label : "unlabelled-audio"}
>
	<StreamingBar {time_limit} />
	{#if value === null || streaming}
		{#if active_source === "microphone"}
			<ModifyUpload {i18n} on:clear={clear} />
			{#if streaming}
				<StreamAudio
					{record}
					{recording}
					{stop}
					{i18n}
					{waveform_settings}
					{waveform_options}
					waiting={stream_state === "waiting"}
				/>
			{:else}
				<AudioRecorder
					bind:mode
					{i18n}
					{editable}
					{recording}
					{dispatch_blob}
					{waveform_settings}
					{waveform_options}
					{handle_reset_value}
					on:start_recording
					on:pause_recording
					on:stop_recording
				/>
			{/if}
		{:else if active_source === "upload"}
			<!-- explicitly listed out audio mimetypes due to iOS bug not recognizing audio/* -->
			<Upload
				bind:upload_promise
				filetype="audio/aac,audio/midi,audio/mpeg,audio/ogg,audio/wav,audio/x-wav,audio/opus,audio/webm,audio/flac,audio/vnd.rn-realaudio,audio/x-ms-wma,audio/x-aiff,audio/amr,audio/*"
				on:load={handle_load}
				bind:dragging
				bind:uploading
				on:error={({ detail }) => dispatch("error", detail)}
				{root}
				{max_file_size}
				{upload}
				{stream_handler}
				aria_label={i18n("audio.drop_to_upload")}
			>
				<slot />
			</Upload>
		{/if}
	{:else}
		<ModifyUpload
			{i18n}
			on:clear={clear}
			on:edit={() => (mode = "edit")}
			download={buttons === null
				? value.url
				: buttons.some((btn) => typeof btn === "string" && btn === "download")
					? value.url
					: null}
		>
			{#if value !== null && buttons}
				{#each buttons as btn}
					{#if typeof btn === "string"}
						{#if btn === "share"}
							<ShareButton
								{i18n}
								on:error
								on:share
								formatter={async (value) => {
									if (!value) return "";
									let url = await uploadToHuggingFace(value.url, "url");
									return `<audio controls src="${url}"></audio>`;
								}}
								{value}
							/>
						{/if}
					{:else}
						<CustomButton
							button={btn}
							on_click={(id) => {
								if (on_custom_button_click) {
									on_custom_button_click(id);
								}
							}}
						/>
					{/if}
				{/each}
			{/if}
		</ModifyUpload>

		<AudioPlayer
			bind:mode
			{value}
			subtitles={Array.isArray(subtitles) ? subtitles : subtitles?.url}
			{label}
			{i18n}
			{dispatch_blob}
			{waveform_settings}
			{waveform_options}
			{trim_region_settings}
			{handle_reset_value}
			{editable}
			{loop}
			bind:playback_position
			interactive
			on:stop
			on:play
			on:pause
			on:edit
		/>
	{/if}
	<SelectSource {sources} bind:active_source handle_clear={clear} />
</div>

<style>
	.audio-container {
		height: calc(var(--size-full) - var(--size-6));
		display: flex;
		flex-direction: column;
		justify-content: space-between;
	}
</style>
