<script lang="ts">
	import { createEventDispatcher, tick } from "svelte";
	import { Download, Image as ImageIcon } from "@gradio/icons";
	import { DownloadLink } from "@gradio/wasm/svelte";
	import { uploadToHuggingFace } from "@gradio/utils";
	import {
		BlockLabel,
		IconButton,
		ShareButton,
		SelectSource,
	} from "@gradio/atoms";
	import { Upload } from "@gradio/upload";
	import { Webcam } from "@gradio/image";
	import type { FileData, Client } from "@gradio/client";
	import type { I18nFormatter, SelectData } from "@gradio/utils";
	import { Clear } from "@gradio/icons";
	import ImageCanvas from "./ImageCanvas.svelte";
	import AnnotatedImageData from "./AnnotatedImageData";

	type source_type = "upload" | "webcam" | "clipboard" | null;

	export let value: null | AnnotatedImageData;
	export let label: string | undefined = undefined;
	export let show_label: boolean;
	export let sources: source_type[] = ["upload", "webcam", "clipboard"];
	export let selectable = false;
	export let root: string;
	export let interactive: boolean;
	export let i18n: I18nFormatter;
	export let showShareButton: boolean;
	export let showDownloadButton: boolean;
	export let showClearButton: boolean;
	export let boxesAlpha;
	export let labelList: string[];
	export let labelColors: string[];
	export let boxMinSize: number;
	export let handleSize: number;
	export let height: number | string;
	export let width: number | string;
	export let boxThickness: number;
	export let disableEditBoxes: boolean;
	export let singleBox: boolean;
	export let showRemoveButton: boolean;
	export let handlesCursor: boolean;
	export let boxSelectedThickness: number;
	export let max_file_size: number | null = null;
	export let cli_upload: Client["upload"];
	export let stream_handler: Client["stream_factory"];
	export let useDefaultLabel: boolean;
	export let enableKeyboardShortcuts: boolean;

	type AnnotationShape = "box" | "point";
	type AnnotationItem = {
		id: string;
		label: string;
		shape: AnnotationShape;
		coords: string;
		color: string;
		isSelected: boolean;
	};
	let annotationItems: AnnotationItem[] = [];
	let annotationTick = 0;
	let selectedAnnotationId: string | null = null;
	let focusSelectedOnly = false;

	function formatBoxCoords(box: any) {
		const xmin = Math.round(box._xmin ?? box.xmin ?? 0);
		const ymin = Math.round(box._ymin ?? box.ymin ?? 0);
		const xmax = Math.round(box._xmax ?? box.xmax ?? 0);
		const ymax = Math.round(box._ymax ?? box.ymax ?? 0);
		return `xmin:${xmin}, ymin:${ymin}, xmax:${xmax}, ymax:${ymax}`;
	}

	function formatPointCoords(point: any) {
		const x = Math.round(point._x ?? point.x ?? 0);
		const y = Math.round(point._y ?? point.y ?? 0);
		return `x:${x}, y:${y}`;
	}

	$: {
		annotationTick;
		selectedAnnotationId;
		if (!value) {
			annotationItems = [];
		} else {
			const boxes = value.boxes.map((box, index) => ({
				id: `box-${index}`,
				label: box.label?.trim() ? box.label : "(No label)",
				shape: "box" as AnnotationShape,
				coords: formatBoxCoords(box),
				color: box.color ?? "transparent",
				isSelected: selectedAnnotationId === `box-${index}`,
			}));
			const points = value.points.map((point, index) => ({
				id: `point-${index}`,
				label: point.label?.trim() ? point.label : "(No label)",
				shape: "point" as AnnotationShape,
				coords: formatPointCoords(point),
				color: point.color ?? "transparent",
				isSelected: selectedAnnotationId === `point-${index}`,
			}));
			annotationItems = [...boxes, ...points];
		}
		if (
			selectedAnnotationId &&
			!annotationItems.some((item) => item.id === selectedAnnotationId)
		) {
			selectedAnnotationId = null;
			focusSelectedOnly = false;
		}
	}

	let upload: any;
	let uploading = false;
	export let active_source: source_type = null;

	function handle_upload({ detail }: CustomEvent<FileData>): void {
		value = new AnnotatedImageData();
		value.image = detail;
		dispatch("upload", undefined);
	}

	async function handle_save(img_blob: Blob | any): Promise<void> {
		const f = await upload.load_files([new File([img_blob], `webcam.png`)]);
		const image = f?.[0] || null;
		if (image) {
			value = new AnnotatedImageData();
			value.image = image;
		} else {
			value = null;
		}
		await tick();
		dispatch("change");
	}

	$: if (uploading) clear();

	const dispatch = createEventDispatcher<{
		change: undefined;
		clear: undefined;
		drag: boolean;
		upload?: undefined;
		select: SelectData;
	}>();

	let dragging = false;

	$: dispatch("drag", dragging);

	$: if (!active_source && sources) {
		active_source = sources[0];
	}

	async function handle_select_source(
		source: (typeof sources)[number],
	): Promise<void> {
		switch (source) {
			case "clipboard":
				upload.paste_clipboard();
				break;
			default:
				break;
		}
	}

	function handleCanvasChange() {
		annotationTick += 1;
		dispatch("change");
	}

	function toggleAnnotationSelection(item: AnnotationItem) {
		if (selectedAnnotationId === item.id) {
			selectedAnnotationId = null;
			focusSelectedOnly = false;
		} else {
			selectedAnnotationId = item.id;
			focusSelectedOnly = true;
		}
	}

	function handleAnnotationClick(event: MouseEvent, item: AnnotationItem) {
		// ダブルクリックだと click が2回発生し、選択トグルが「選択→解除」になってしまう。
		// ここでは単クリック（detail===1）のみを選択トグル対象にする。
		if (event.detail !== 1) return;
		toggleAnnotationSelection(item);
	}

	function clearAnnotationSelection() {
		selectedAnnotationId = null;
		focusSelectedOnly = false;
	}

	function handleAnnotationKeydown(
		event: KeyboardEvent,
		item: AnnotationItem,
	) {
		if (event.key === "Enter" || event.key === " ") {
			event.preventDefault();
			toggleAnnotationSelection(item);
		}
	}

	function clear() {
		value = null;
		dispatch("clear");
		dispatch("change");
	}
</script>

<BlockLabel {show_label} Icon={ImageIcon} label={label || "Image Annotator"} />

<div class="icon-buttons">
	{#if showDownloadButton && value !== null}
		<DownloadLink
			href={value.image.url}
			download={value.image.orig_name || "image"}
		>
			<IconButton Icon={Download} label={i18n("common.download")} />
		</DownloadLink>
	{/if}
	{#if showShareButton && value !== null}
		<ShareButton
			{i18n}
			on:share
			on:error
			formatter={async (value) => {
				if (value === null) return "";
				let url = await uploadToHuggingFace(value.image, "base64");
				return `<img src="${url}" />`;
			}}
			{value}
		/>
	{/if}
	{#if showClearButton && value !== null && interactive}
		<div>
			<IconButton Icon={Clear} label="Remove Image" on:click={clear} />
		</div>
	{/if}
</div>

<div data-testid="image" class="image-container">
	<div class="upload-container">
		<Upload
			hidden={value !== null || active_source === "webcam"}
			bind:this={upload}
			bind:uploading
			bind:dragging
			filetype={active_source === "clipboard" ? "clipboard" : "image/*"}
			on:load={handle_upload}
			on:error
			{root}
			{max_file_size}
			disable_click={!sources.includes("upload")}
			upload={cli_upload}
			{stream_handler}
		>
			{#if value === null}
				<slot />
			{/if}
		</Upload>
		{#if value === null && active_source === "webcam"}
			<Webcam
				{root}
				mirror_webcam={false}
				on:capture={(e) => handle_save(e.detail)}
				on:stream={(e) => handle_save(e.detail)}
				on:error
				on:drag
				on:upload={(e) => handle_save(e.detail)}
				mode="image"
				include_audio={false}
				{i18n}
				{upload}
			/>
		{/if}
		{#if value !== null}
			<div class="annotator-layout">
				<div class:selectable class="image-frame">
					<ImageCanvas
						bind:value
						on:change={handleCanvasChange}
						{height}
						{width}
						{boxesAlpha}
						{labelList}
						{labelColors}
						{boxMinSize}
						{interactive}
						{handleSize}
						{boxThickness}
						{singleBox}
						{disableEditBoxes}
						{showRemoveButton}
						{handlesCursor}
						{boxSelectedThickness}
						{useDefaultLabel}
						{enableKeyboardShortcuts}
						{selectedAnnotationId}
						{focusSelectedOnly}
						onClearSelection={clearAnnotationSelection}
						src={value.image.url}
					/>
				</div>
				<aside class="annotation-panel" aria-label="Annotations list">
					<div class="annotation-panel__header">Annotations</div>
					{#if annotationItems.length === 0}
						<div class="annotation-panel__empty">
							No annotations yet.
						</div>
					{:else}
						<ul class="annotation-list">
							{#each annotationItems as item}
								<li class="annotation-item">
									<button
										type="button"
										class="annotation-item__button"
										class:annotation-item__button--selected={item.isSelected}
										on:click={(event) =>
											handleAnnotationClick(event, item)}
										on:keydown={(event) =>
											handleAnnotationKeydown(
												event,
												item,
											)}
									>
										<div class="annotation-item__title">
											<span
												class="annotation-item__swatch"
												style={`background-color: ${item.color};`}
											></span>
											{item.label}
										</div>
										<div class="annotation-item__meta">
											Shape: {item.shape}
										</div>
										<div class="annotation-item__coords">
											Coords: {item.coords}
										</div>
									</button>
								</li>
							{/each}
						</ul>
					{/if}
				</aside>
			</div>
		{/if}
	</div>
	{#if (sources.length > 1 || sources.includes("clipboard")) && value === null && interactive}
		<SelectSource
			{sources}
			bind:active_source
			handle_clear={clear}
			handle_select={handle_select_source}
		/>
	{/if}
</div>

<style>
	.image-frame :global(img) {
		width: var(--size-full);
		height: var(--size-full);
		object-fit: cover;
	}

	.image-frame {
		object-fit: cover;
		width: 100%;
	}

	.annotator-layout {
		display: flex;
		gap: var(--spacing-lg);
		align-items: flex-start;
		width: 100%;
	}

	.annotation-panel {
		width: 240px;
		min-width: 200px;
		border: 1px solid var(--border-color-primary);
		border-radius: var(--radius-md);
		background: var(--background-fill-secondary);
		padding: var(--spacing-lg);
		color: var(--body-text-color);
		font-size: var(--text-md);
	}

	.annotation-panel__header {
		font-weight: 600;
		margin-bottom: var(--spacing-md);
	}

	.annotation-panel__empty {
		color: var(--neutral-500);
		font-size: var(--text-sm);
	}

	.annotation-list {
		list-style: none;
		padding: 0;
		margin: 0;
		display: flex;
		flex-direction: column;
		gap: var(--spacing-md);
	}

	.annotation-item {
		list-style: none;
	}

	.annotation-item__button {
		border-radius: var(--radius-md);
		padding: var(--spacing-md);
		border: 1px solid transparent;
		background: var(--background-fill-primary);
		display: flex;
		flex-direction: column;
		gap: var(--spacing-xs);
		font-size: var(--text-sm);
		cursor: pointer;
		width: 100%;
		text-align: left;
		transition:
			border-color 0.2s ease,
			box-shadow 0.2s ease;
	}

	.annotation-item__button--selected {
		border-color: var(--color-accent);
		box-shadow: 0 0 0 1px
			color-mix(in srgb, var(--color-accent) 55%, transparent);
	}

	.annotation-item__button:focus-visible {
		outline: 2px solid var(--color-accent);
		outline-offset: 2px;
	}
	.annotation-item__title {
		font-weight: 600;
		display: flex;
		align-items: center;
		gap: var(--spacing-xs);
		user-select: none;
	}

	.annotation-item__swatch {
		width: 10px;
		height: 10px;
		border-radius: 999px;
		border: 1px solid var(--border-color-primary);
		flex-shrink: 0;
	}

	.annotation-item__meta,
	.annotation-item__coords {
		color: var(--neutral-600);
		font-size: var(--text-xs);
	}

	.upload-container {
		height: 100%;
		width: 100%;
		flex-shrink: 1;
		max-height: 100%;
	}

	.image-container {
		display: flex;
		height: 100%;
		flex-direction: column;
		justify-content: center;
		align-items: center;
		max-height: 100%;
	}

	.selectable {
		cursor: crosshair;
	}

	.icon-buttons {
		display: flex;
		position: absolute;
		top: 6px;
		right: 6px;
		gap: var(--size-1);
	}
</style>
