<script lang="ts" context="module">
	export interface EditorData {
		background: FileData | null;
		layers: FileData[] | null;
		composite: FileData | null;
	}

	export interface ImageBlobs {
		background: FileData | null;
		layers: FileData[];
		composite: FileData | null;
	}
</script>

<script lang="ts">
	import { createEventDispatcher } from "svelte";
	import { type I18nFormatter } from "@gradio/utils";
	import { prepare_files, type FileData, type Client } from "@gradio/client";

	import ImageEditor from "./ImageEditor.svelte";
	import Layers from "./layers/Layers.svelte";
	import { type Brush as IBrush } from "./tools/Brush.svelte";
	import { type Eraser } from "./tools/Brush.svelte";

	import { Tools, Crop, Brush, Sources } from "./tools";
	import { BlockLabel } from "@gradio/atoms";
	import { Image as ImageIcon } from "@gradio/icons";
	import { inject } from "./utils/parse_placeholder";

	export let brush: IBrush | null;
	export let eraser: Eraser | null;
	export let sources: ("clipboard" | "webcam" | "upload")[];
	export let crop_size: [number, number] | `${string}:${string}` | null = null;
	export let i18n: I18nFormatter;
	export let root: string;
	export let label: string | undefined = undefined;
	export let show_label: boolean;
	export let changeable = false;
	export let value: EditorData | null = {
		background: null,
		layers: [],
		composite: null
	};
	export let transforms: "crop"[] = ["crop"];
	export let layers: boolean;
	export let accept_blobs: (a: any) => void;
	export let status: "pending" | "complete" | "error" = "complete";
	export let canvas_size: [number, number] | undefined;
	export let realtime: boolean;
	export let upload: Client["upload"];
	export let stream_handler: Client["stream"];
	export let dragging: boolean;
	export let placeholder: string | undefined = undefined;
	export let height = 400;

	const dispatch = createEventDispatcher<{
		clear?: never;
		upload?: never;
		change?: never;
	}>();

	let editor: ImageEditor;

	function is_not_null(o: Blob | null): o is Blob {
		return !!o;
	}

	function is_file_data(o: null | FileData): o is FileData {
		return !!o;
	}

	$: if (bg) dispatch("upload");

	export async function get_data(): Promise<ImageBlobs> {
		const blobs = await editor.get_blobs();

		const bg = blobs.background
			? upload(
					await prepare_files([new File([blobs.background], "background.png")]),
					root
				)
			: Promise.resolve(null);

		const layers = blobs.layers
			.filter(is_not_null)
			.map(async (blob, i) =>
				upload(await prepare_files([new File([blob], `layer_${i}.png`)]), root)
			);

		const composite = blobs.composite
			? upload(
					await prepare_files([new File([blobs.composite], "composite.png")]),
					root
				)
			: Promise.resolve(null);

		const [background, composite_, ...layers_] = await Promise.all([
			bg,
			composite,
			...layers
		]);

		return {
			background: Array.isArray(background) ? background[0] : background,
			layers: layers_
				.flatMap((layer) => (Array.isArray(layer) ? layer : [layer]))
				.filter(is_file_data),
			composite: Array.isArray(composite_) ? composite_[0] : composite_
		};
	}

	function handle_value(value: EditorData | null): void {
		if (!editor) return;
		if (value == null) {
			editor.handle_remove();
		}
	}

	$: handle_value(value);

	$: crop_constraint = crop_size;
	let bg = false;
	let history = false;

	export let image_id: null | string = null;

	$: editor &&
		editor.set_tool &&
		(sources && sources.length
			? editor.set_tool("bg")
			: editor.set_tool("draw"));

	type BinaryImages = [string, string, File, number | null][];

	function nextframe(): Promise<void> {
		return new Promise((resolve) => setTimeout(() => resolve(), 30));
	}

	let uploading = false;
	let pending = false;
	async function handle_change(e: CustomEvent<Blob | any>): Promise<void> {
		if (!realtime) return;
		if (uploading) {
			pending = true;
			return;
		}

		uploading = true;

		await nextframe();
		const blobs = await editor.get_blobs();

		const images: BinaryImages = [];

		let id = Math.random().toString(36).substring(2);

		if (blobs.background)
			images.push([
				id,
				"background",
				new File([blobs.background], "background.png"),
				null
			]);
		if (blobs.composite)
			images.push([
				id,
				"composite",
				new File([blobs.composite], "composite.png"),
				null
			]);
		blobs.layers.forEach((layer, i) => {
			if (layer)
				images.push([
					id as string,
					`layer`,
					new File([layer], `layer_${i}.png`),
					i
				]);
		});

		await Promise.all(
			images.map(async ([image_id, type, data, index]) => {
				return accept_blobs({
					binary: true,
					data: { file: data, id: image_id, type, index }
				});
			})
		);
		image_id = id;
		dispatch("change");

		await nextframe();
		uploading = false;
		if (pending) {
			pending = false;
			uploading = false;
			handle_change(e);
		}
	}

	let active_mode: "webcam" | "color" | null = null;
	let editor_height = height - 100;

	$: [heading, paragraph] = placeholder ? inject(placeholder) : [false, false];
</script>

<BlockLabel
	{show_label}
	Icon={ImageIcon}
	label={label || i18n("image.image")}
/>
<ImageEditor
	{canvas_size}
	crop_size={Array.isArray(crop_size) ? crop_size : undefined}
	bind:this={editor}
	bind:height={editor_height}
	parent_height={height}
	{changeable}
	on:save
	on:change={handle_change}
	on:clear={() => dispatch("clear")}
	bind:history
	bind:bg
	{sources}
	crop_constraint={!!crop_constraint}
>
	<Tools {i18n}>
		<Layers layer_files={value?.layers || null} enable_layers={layers} />

		<Sources
			bind:dragging
			{i18n}
			{root}
			{sources}
			{upload}
			{stream_handler}
			bind:bg
			bind:active_mode
			background_file={value?.background || value?.composite || null}
		></Sources>

		{#if transforms.includes("crop")}
			<Crop {crop_constraint} />
		{/if}
		{#if brush}
			<Brush
				color_mode={brush.color_mode}
				default_color={brush.default_color}
				default_size={brush.default_size}
				colors={brush.colors}
				mode="draw"
			/>
		{/if}

		{#if brush && eraser}
			<Brush default_size={eraser.default_size} mode="erase" />
		{/if}
	</Tools>

	{#if !bg && !history && active_mode !== "webcam" && status !== "error"}
		<div class="empty wrap" style:height={`${editor_height}px`}>
			{#if sources && sources.length}
				{#if heading || paragraph}
					{#if heading}
						<h2>{heading}</h2>
					{/if}
					{#if paragraph}
						<p>{paragraph}</p>
					{/if}
				{:else}
					<div>Upload an image</div>
				{/if}
			{/if}

			{#if sources && sources.length && brush && !placeholder}
				<div class="or">or</div>
			{/if}
			{#if brush && !placeholder}
				<div>select the draw tool to start</div>
			{/if}
		</div>
	{/if}
</ImageEditor>

<style>
	h2 {
		font-size: var(--text-xl);
	}

	p,
	h2 {
		white-space: pre-line;
	}

	.empty {
		display: flex;
		flex-direction: column;
		justify-content: center;
		align-items: center;
		position: absolute;
		height: 100%;
		width: 100%;
		left: 0;
		right: 0;
		margin: auto;
		z-index: var(--layer-top);
		text-align: center;
		color: var(--body-text-color);
		top: var(--size-8);
	}

	.wrap {
		display: flex;
		flex-direction: column;
		justify-content: center;
		align-items: center;
		color: var(--block-label-text-color);
		line-height: var(--line-md);
		font-size: var(--text-md);
		pointer-events: none;
	}

	.or {
		color: var(--body-text-color-subdued);
	}
</style>
