<script context="module" lang="ts">
	import { tick, untrack } from "svelte";
	import { pretty_si } from "./utils";

	let items: HTMLDivElement[] = [];

	let called = false;

	const is_browser = typeof window !== "undefined";
	const raf = is_browser
		? window.requestAnimationFrame
		: (cb: (...args: any[]) => void) => {};

	async function scroll_into_view(
		el: HTMLDivElement,
		enable: boolean | null = true
	): Promise<void> {
		if (
			window.__gradio_mode__ === "website" ||
			(window.__gradio_mode__ !== "app" && enable !== true)
		) {
			return;
		}

		items.push(el);
		if (!called) called = true;
		else return;

		await tick();

		raf(() => {
			let min = [0, 0];

			for (let i = 0; i < items.length; i++) {
				const element = items[i];

				const box = element.getBoundingClientRect();
				if (i === 0 || box.top + window.scrollY <= min[0]) {
					min[0] = box.top + window.scrollY;
					min[1] = i;
				}
			}

			window.scrollTo({ top: min[0] - 20, behavior: "smooth" });

			called = false;
			items = [];
		});
	}
</script>

<script lang="ts">
	import Loader from "./Loader.svelte";
	import type { LoadingStatus } from "./types";
	import type { I18nFormatter } from "@gradio/utils";

	import { IconButton } from "@gradio/atoms";
	import { Clear } from "@gradio/icons";

	interface Props {
		i18n: I18nFormatter;
		eta?: number | null;
		queue_position: number | null;
		queue_size: number | null;
		status:
			| "complete"
			| "pending"
			| "error"
			| "generating"
			| "streaming"
			| null;
		scroll_to_output?: boolean;
		timer?: boolean;
		show_progress?: "full" | "minimal" | "hidden";
		message?: string | null;
		progress?: LoadingStatus["progress"] | null | undefined;
		variant?: "default" | "center";
		loading_text?: string;
		absolute?: boolean;
		translucent?: boolean;
		border?: boolean;
		autoscroll: boolean;
		validation_error?: string | null;
		show_validation_error?: boolean;
		type?: "input" | "output" | null;
		on_clear_status?: () => void;
	}

	interface ProgressLevel {
		progress_level: (number | undefined)[] | null;
		last_progress_level: number | undefined;
		progress_bar_transition: string;
	}

	let {
		i18n,
		eta = null,
		queue_position,
		queue_size,
		status,
		scroll_to_output = false,
		timer = true,
		show_progress = "full",
		message = null,
		progress = null,
		variant = "default",
		loading_text = "Loading...",
		absolute = true,
		translucent = false,
		border = false,
		autoscroll,
		validation_error = null,
		show_validation_error = true,
		type = null,
		on_clear_status
	}: Props = $props();

	let el: HTMLDivElement;

	let _timer = false;
	let timer_start = $state(0);
	let old_eta = $state<number | null>(null);
	let eta_from_start = $state<number | null>(null);
	let message_visible = $state(false);
	let formatted_eta = $state<string | null>(null);
	let show_message_timeout = $state<NodeJS.Timeout | null>(null);

	const should_hide = $derived(
		type === "input" ||
			!status ||
			status === "complete" ||
			show_progress === "hidden" ||
			status == "streaming" ||
			!!(show_validation_error && validation_error)
	);

	let timer_diff = $state(0);

	const eta_level = $derived(
		eta_from_start === null || eta_from_start <= 0 || !timer_diff
			? 0
			: Math.min(timer_diff / eta_from_start, 1)
	);

	const formatted_timer = $derived(timer_diff.toFixed(1));

	let show_eta_bar = $derived(progress != null ? false : true);

	function run(): void {
		raf(() => {
			timer_diff = (performance.now() - timer_start) / 1000;
			if (_timer) run();
		});
	}

	let progress_level = $derived.by<ProgressLevel>(() => {
		let _progress_level: (number | undefined)[] | null = null;
		if (progress != null) {
			_progress_level = progress.map((p) => {
				if (p.index != null && p.length != null) {
					return p.index / p.length;
				} else if (p.progress != null) {
					return p.progress;
				}
				return undefined;
			});
		} else {
			_progress_level = null;
		}

		let _last_progress_level: number | undefined;
		let transition = "";
		if (_progress_level) {
			_last_progress_level = _progress_level[_progress_level.length - 1];

			if (_last_progress_level === 0) {
				transition = "0";
			} else {
				transition = "150ms";
			}
		} else {
			_last_progress_level = undefined;
		}

		return {
			progress_level: _progress_level,
			last_progress_level: _last_progress_level,
			progress_bar_transition: transition
		};
	});

	function start_timer(): void {
		if (_timer) return;

		old_eta = formatted_eta = null;
		timer_start = performance.now();

		_timer = true;
		run();
	}

	function stop_timer(): void {
		old_eta = formatted_eta = null;
		if (!_timer) return;
		_timer = false;
	}

	$effect(() => {
		if (status === "pending") {
			start_timer();
		} else {
			untrack(() => {
				stop_timer();
			});
		}
	});

	$effect(() => {
		if (
			el &&
			scroll_to_output &&
			(status === "pending" || status === "complete")
		) {
			scroll_into_view(el, autoscroll);
		}
	});

	$effect(() => {
		if (eta === null) {
			eta = old_eta;
		}
		if (eta != null && old_eta !== eta) {
			eta_from_start = (performance.now() - timer_start) / 1000 + eta;
			formatted_eta = eta_from_start.toFixed(1);
			old_eta = eta;
		}
	});

	function close_message(): void {
		message_visible = false;
		if (show_message_timeout !== null) {
			clearTimeout(show_message_timeout);
		}
	}

	$effect(() => {
		untrack(() => {
			close_message();
		});

		if (status === "error" && message) {
			message_visible = true;
		}
	});
</script>

<div
	class="wrap {variant} {show_progress}"
	class:no-click={validation_error && show_validation_error}
	class:hide={should_hide}
	class:translucent={(variant === "center" &&
		(status === "pending" || status === "error")) ||
		translucent ||
		show_progress === "minimal" ||
		validation_error}
	class:generating={status === "generating" && show_progress === "full"}
	class:border
	style:position={absolute ? "absolute" : "static"}
	style:padding={absolute ? "0" : "var(--size-8) 0"}
	data-testid="status-tracker"
	bind:this={el}
>
	{#if validation_error && show_validation_error}
		<div class="validation-error">
			{validation_error}
			<button
				><IconButton
					Icon={Clear}
					label={i18n ? i18n("common.clear") : "Clear"}
					disabled={false}
					size="x-small"
					background="var(--background-fill-primary)"
					color="var(--error-background-text)"
					border="var(--border-color-primary)"
					onclick={() => (validation_error = null)}
				/></button
			>
		</div>
	{/if}
	{#if status === "pending"}
		{#if variant === "default" && show_eta_bar && show_progress === "full"}
			<div
				class="eta-bar"
				style:transform="translateX({(eta_level || 0) * 100 - 100}%)"
			/>
		{/if}
		<div
			class:meta-text-center={variant === "center"}
			class:meta-text={variant === "default"}
			class="progress-text"
		>
			{#if progress}
				{#each progress as p}
					{#if p.index != null}
						{#if p.length != null}
							{pretty_si(p.index || 0)}/{pretty_si(p.length)}
						{:else}
							{pretty_si(p.index || 0)}
						{/if}
						{p.unit} | {" "}
					{/if}
				{/each}
			{:else if queue_position !== null && queue_size !== undefined && queue_position >= 0}
				queue: {queue_position + 1}/{queue_size} |
			{:else if queue_position === 0}
				processing |
			{/if}

			{#if timer}
				{formatted_timer}{eta ? `/${formatted_eta}` : ""}s
			{/if}
		</div>

		{#if progress_level.last_progress_level != null}
			<div class="progress-level">
				<div class="progress-level-inner">
					{#if progress != null}
						{#each progress as p, i}
							{#if p.desc != null || (progress_level.progress_level && progress_level.progress_level[i] != null)}
								{#if i !== 0}
									&nbsp;/
								{/if}
								{#if p.desc != null}
									{p.desc}
								{/if}
								{#if p.desc != null && progress_level.progress_level && progress_level.progress_level[i] != null}
									-
								{/if}
								{#if progress_level.progress_level != null}
									{(100 * (progress_level.progress_level[i] || 0)).toFixed(1)}%
								{/if}
							{/if}
						{/each}
					{/if}
				</div>

				<div class="progress-bar-wrap">
					<div
						class="progress-bar"
						style:width="{progress_level.last_progress_level * 100}%"
						style:transition={progress_level.progress_bar_transition}
					/>
				</div>
			</div>
		{:else if show_progress === "full"}
			<Loader margin={variant === "default"} />
		{/if}

		{#if !timer}
			<p class="loading">{loading_text}</p>
			<slot name="additional-loading-text" />
		{/if}
	{:else if status === "error"}
		<div class="clear-status">
			<IconButton
				Icon={Clear}
				label={i18n("common.clear")}
				disabled={false}
				on:click={() => {
					on_clear_status?.();
				}}
			/>
		</div>
		<span class="error">{i18n("common.error")}</span>
		<slot name="error" />
	{/if}
</div>

<style>
	.wrap {
		display: flex;
		flex-direction: column;
		justify-content: center;
		align-items: center;
		z-index: var(--layer-3);
		transition: opacity 0.1s ease-in-out;
		border-radius: var(--block-radius);
		background: var(--block-background-fill);
		padding: 0 var(--size-6);
		overflow: hidden;
	}
	.no-click {
		pointer-events: none;
	}

	.wrap.center {
		top: 0;
		right: 0px;
		left: 0px;
	}

	.wrap.default {
		top: 0px;
		right: 0px;
		bottom: 0px;
		left: 0px;
	}

	.hide {
		opacity: 0;
		pointer-events: none;
	}

	.generating {
		animation:
			pulseStart 1s cubic-bezier(0.4, 0, 0.6, 1),
			pulse 2s cubic-bezier(0.4, 0, 0.6, 1) 1s infinite;
		border: 2px solid var(--color-accent);
		background: transparent;
		z-index: var(--layer-1);
		pointer-events: none;
	}

	.translucent {
		background: none;
	}

	@keyframes pulseStart {
		0% {
			opacity: 0;
		}
		100% {
			opacity: 1;
		}
	}

	@keyframes pulse {
		0%,
		100% {
			opacity: 1;
		}
		50% {
			opacity: 0.5;
		}
	}

	.loading {
		z-index: var(--layer-2);
		color: var(--body-text-color);
	}
	.eta-bar {
		position: absolute;
		top: 0;
		right: 0;
		bottom: 0;
		left: 0;
		transform-origin: left;
		opacity: 0.8;
		z-index: var(--layer-1);
		transition: 10ms;
		background: var(--background-fill-secondary);
	}
	.progress-bar-wrap {
		border: 1px solid var(--border-color-primary);
		background: var(--background-fill-primary);
		width: 55.5%;
		height: var(--size-4);
	}
	.progress-bar {
		transform-origin: left;
		background-color: var(--loader-color);
		width: var(--size-full);
		height: var(--size-full);
	}

	.progress-level {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 1;
		z-index: var(--layer-2);
		width: var(--size-full);
	}

	.progress-level-inner {
		margin: var(--size-2) auto;
		color: var(--body-text-color);
		font-size: var(--text-sm);
		font-family: var(--font-mono);
	}

	.meta-text {
		position: absolute;
		bottom: 0;
		right: 0;
		z-index: var(--layer-2);
		padding: var(--size-1) var(--size-2);
		font-size: var(--text-sm);
		font-family: var(--font-mono);
	}

	.meta-text-center {
		display: flex;
		position: absolute;
		top: 0;
		right: 0;
		justify-content: center;
		align-items: center;
		transform: translateY(var(--size-6));
		z-index: var(--layer-2);
		padding: var(--size-1) var(--size-2);
		font-size: var(--text-sm);
		font-family: var(--font-mono);
		text-align: center;
	}

	.error {
		box-shadow: var(--shadow-drop);
		border: solid 1px var(--error-border-color);
		border-radius: var(--radius-full);
		background: var(--error-background-fill);
		padding-right: var(--size-4);
		padding-left: var(--size-4);
		color: var(--error-text-color);
		font-weight: var(--weight-semibold);
		font-size: var(--text-lg);
		line-height: var(--line-lg);
		font-family: var(--font);
	}

	.validation-error {
		pointer-events: auto;
		color: var(--error-text-color);
		font-weight: var(--weight-semibold);
		font-size: var(--text-lg);
		line-height: var(--line-lg);
		font-family: var(--font);
		position: absolute;
		background: var(--error-background-fill);
		top: 0;
		right: 0;
		z-index: var(--layer-3);
		padding: var(--size-1) var(--size-2);
		font-size: var(--text-md);
		text-align: center;
		border-bottom-left-radius: var(--radius-sm);
		border-bottom: 1px solid var(--error-border-color);
		border-left: 1px solid var(--error-border-color);
		display: flex;
		justify-content: space-between;
		align-items: center;
		gap: var(--spacing-xl);
	}

	.minimal {
		pointer-events: none;
	}

	.minimal .progress-text {
		background: var(--block-background-fill);
	}

	.border {
		border: 1px solid var(--border-color-primary);
	}

	.clear-status {
		position: absolute;
		display: flex;
		top: var(--size-2);
		right: var(--size-2);
		justify-content: flex-end;
		gap: var(--spacing-sm);
		z-index: var(--layer-1);
	}
</style>
