<script lang="ts">
	import {
		beforeUpdate,
		afterUpdate,
		createEventDispatcher,
		tick,
	} from "svelte";
	import { BlockTitle } from "@gradio/atoms";
	import { Copy, Check } from "@gradio/icons";
	import { fade } from "svelte/transition";
	import type { SelectData } from "@gradio/utils";
	import { get_next_color } from "@gradio/utils";
	import { correct_color_map, getParentCursorPosition, getNodeAndOffset } from "./utils";

	const browser = typeof document !== "undefined";
	export let value: [string, string | null][] = [];
    export let value_is_output: boolean = false;
	export let label: string;
	export let legend_label: string;
	export let info: string | undefined = undefined;
	export let show_label = true;
	export let show_legend = false;
	export let show_legend_label = false;
	export let container = true;
	export let color_map: Record<string, string> = {};
	export let show_copy_button = false;
    export let disabled: boolean;
	
	let el: HTMLDivElement;
	let el_text: string = "";
	let marked_el_text: string = "";
	let ctx: CanvasRenderingContext2D;
	let current_color_map: Record<string, string> = {};
	let _color_map: Record<string, { primary: string; secondary: string }> = {};
	let copied = false;
	let timer: ReturnType<typeof setTimeout>;
    let can_scroll: boolean;

	function set_color_map(): void {
		if (!color_map || Object.keys(color_map).length === 0) {
			current_color_map = {};
		}
		else {
			current_color_map = color_map;
		}
		if (value.length > 0) {
			for (let [_, label] of value) {
				if (label !== null && !(label in current_color_map)) {
					let color = get_next_color(Object.keys(current_color_map).length);
					current_color_map[label] = color;
				}
			}
		}
		_color_map = correct_color_map(current_color_map, browser, ctx);
	}

	function set_text_from_value(): void {
		if (value.length > 0 && value_is_output) {
			el_text = value.map(([text, _]) => text).join(" ");
			marked_el_text = value.map(([text, category]) => {
				if (category !== null) {
					return `<mark class="hl ${category}" style="background-color:${_color_map[category].secondary}">${text}</mark>`;
				} else {
					return text;
				}
			}).join(" ") + " ";
		}
	}

	$: set_text_from_value();
	$: set_color_map();

	const dispatch = createEventDispatcher<{
		change: string;
		submit: undefined;
		blur: undefined;
		select: SelectData;
		input: undefined;
		focus: undefined;
	}>();

    beforeUpdate(() => {
		can_scroll = el && el.offsetHeight + el.scrollTop > el.scrollHeight - 100;
	});

	function handle_change(): void {
		dispatch("change", marked_el_text);
		if (!value_is_output) {
			dispatch("input");
		}
	}
	afterUpdate(() => {
		set_color_map();
		set_text_from_value();
		value_is_output = false;
	});
	$: marked_el_text, handle_change();

	// Given a string like Hello <mark class="hl red" style="background-color:#fee2e2">world!</mark>  This is cool.
	// for marked_el_text and its previous parsed version (value) like [["Hello ", null], ["world!", "red"], [" This is ", null], ["nice", "blue"]],
	// update value such that it matches marked_el_text (i.e. [["Hello ", null], ["world!", "red"], [" This is cool.", null]])
	function handle_blur(): void {
		let new_value: [string, string | null][] = [];
		let text = "";
		let category = null;
		let in_tag = false;
		let tag = "";
		for (let i = 0; i < marked_el_text.length; i++) {
			let char = marked_el_text[i];
			if (char === "<") {
				in_tag = true;
				if (text) {
					new_value.push([text, category]);
				}
				text = "";
				category = null;
			} else if (char === ">") {
				in_tag = false;
				if (tag.startsWith("mark")) {
					category = tag.match(/class="hl ([^"]+)"/)?.[1] || null;
				}
				tag = "";
			} else if (in_tag) {
				tag += char;
			} else {
				text += char;
			}
		}
		if (text) {
			new_value.push([text, category]);
		}
		value = new_value;
	}

	async function handle_copy(): Promise<void> {
		if ("clipboard" in navigator) {
			await navigator.clipboard.writeText(el_text);
			copy_feedback();
		}
	}

	function copy_feedback(): void {
		copied = true;
		if (timer) clearTimeout(timer);
		timer = setTimeout(() => {
			copied = false;
		}, 1000);
	}

	// Method to remove highlight if cursor is inside
	function checkAndRemoveHighlight() {
		const selection = window.getSelection();
		const cursorPosition = selection.anchorOffset;
		if (selection.rangeCount > 0) {
			var currParent = selection.getRangeAt(0).commonAncestorContainer.parentElement;
			if (currParent && currParent.tagName.toLowerCase() === 'mark') {
				const text = currParent.textContent;
				// replace the mark tag with its text content
				var textContainer = currParent.parentElement;
				var newTextNode = document.createTextNode(text);
				textContainer.replaceChild(newTextNode, currParent);
				marked_el_text = textContainer.innerHTML;
				// set the cursor position to the same position as before
				var range = document.createRange()
				var newSelection = window.getSelection()
				const newCursorPosition = cursorPosition + getParentCursorPosition(textContainer)
				var nodeAndOffset = getNodeAndOffset(textContainer, newCursorPosition);
				range.setStart(nodeAndOffset.node, nodeAndOffset.offset);
				range.setEnd(nodeAndOffset.node, nodeAndOffset.offset);
				newSelection.removeAllRanges();
				newSelection.addRange(range);
				handle_blur();
			}
		}
	}
</script>

<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events-->
<label class:container>
	{#if show_legend}
		<div
		class="category-legend"
		data-testid="highlighted-text:category-legend"
		>
			{#if show_legend_label}
				<div class="legend-description">{legend_label}</div>
			{/if}
			{#each Object.entries(_color_map) as [category, color], i}
				<!-- svelte-ignore a11y-no-static-element-interactions -->
				<div class="category-label" style={"background-color:" + color.secondary}>
					{category}
				</div>
			{/each}
		</div>
	{/if}
	<BlockTitle {show_label} {info}>{label}</BlockTitle>
	{#if show_copy_button}
		{#if copied}
			<button
				in:fade={{ duration: 300 }}
				aria-label="Copied"
				aria-roledescription="Text copied"><Check /></button
			>
		{:else}
			<button
				on:click={handle_copy}
				aria-label="Copy"
				aria-roledescription="Copy text"><Copy /></button
			>
		{/if}
	{/if}

    {#if disabled}
        <div 
            class="textfield"
            data-testid="highlighted-textbox"
            contenteditable="false"
            bind:this={el}
            bind:textContent={el_text}
            bind:innerHTML={marked_el_text}
        />
    {:else}
        <div
            class="textfield"
            data-testid="highlighted-textbox"
            contenteditable="true"
            bind:this={el}
            bind:textContent={el_text}
            bind:innerHTML={marked_el_text}
            on:blur={handle_blur}
            on:keypress
            on:select
            on:scroll
            on:input
            on:focus
            on:change
			on:mousedown={checkAndRemoveHighlight}
			on:keydown={checkAndRemoveHighlight}
			on:mouseup={checkAndRemoveHighlight}
			on:keyup={checkAndRemoveHighlight}
        />
    {/if}
</label>

<style>
	label {
		display: block;
		width: 100%;
	}

	button {
		display: flex;
		position: absolute;
		top: var(--block-label-margin);
		right: var(--block-label-margin);
		align-items: center;
		box-shadow: var(--shadow-drop);
		border: 1px solid var(--color-border-primary);
		border-top: none;
		border-right: none;
		border-radius: var(--block-label-right-radius);
		background: var(--block-label-background-fill);
		padding: 5px;
		width: 22px;
		height: 22px;
		overflow: hidden;
		color: var(--block-label-color);
		font: var(--font-sans);
		font-size: var(--button-small-text-size);
	}
	.container {
		display: flex;
		flex-direction: column;
		gap: var(--spacing-sm);
		padding: var(--block-padding);
	}

	.category-legend {
		display: flex;
		flex-wrap: wrap;
		gap: var(--spacing-sm);
		color: black;
		margin-bottom: var(--spacing-sm)
	}

	.category-label {
		border-radius: var(--radius-xs);
		padding-right: var(--size-2);
		padding-left: var(--size-2);
		font-weight: var(--weight-semibold);
	}

	.legend-description {
		background-color: transparent;
		color: var(--block-title-text-color);
		padding-left: 0px;
		border-radius: var(--radius-xs);
		font-weight: var(--input-text-weight);
	}

	.textfield {
		box-sizing: border-box;
		outline: none !important;
		box-shadow: var(--input-shadow);
		padding: var(--input-padding);
		border-radius: var(--radius-md);
		background: var(--input-background-fill);
		background-color: transparent;
		font-weight: var(--input-text-weight);
		font-size: var(--input-text-size);
		width: 100%;
		line-height: var(--line-sm);
		word-break: break-word;
		border: var(--input-border-width) solid var(--input-border-color);
		cursor: text;
	}

	.textfield:focus {
		box-shadow: var(--input-shadow-focus);
		border-color: var(--input-border-color-focus);
	}

	:global(mark) {
		border-radius: 3px;
	}
</style>