<script>
	import { createEventDispatcher, onMount, tick } from "svelte";

	// Generate a default unique ID
	function generateDefaultID() {
		return "virtual-select-" + Math.random().toString(36).slice(2);
	}

	// Props
	export let options = [];
	export let selected = null;
	export let inputPlaceholder = "Search options...";
	export let noOptionsText = "No options found";
	export let inputAriaLabel = "Search and select an option";
	export let toggleAriaLabel = "Toggle dropdown";
	export let clearAriaLabel = "Clear selection";
	export let filterFn = null; // Custom filter function
	export let autofocus = false; // Autofocus input
	export let inputElementID = generateDefaultID(); // Input element ID

	// Internal state
	let searchTerm = "";
	let filteredOptions = options;
	let isOpen = false;
	let container;
	let visibleItems = [];
	let startIndex = 0;
	let itemHeight = 40; // Fixed height for each option (in pixels)
	let visibleCount = 10; // Default number of items to render
	let focusedIndex = -1; // Track the focused option index (-1 means no focus)
	let inputElement; // Reference to input element

	const dispatch = createEventDispatcher();

	// Filter options based on search term
	$: {
		filteredOptions = searchTerm
			? filterFn
				? options.filter((option) => filterFn(option, searchTerm))
				: options.filter((option) =>
						option.label.toLowerCase().includes(searchTerm.toLowerCase())
					)
			: options;
		// Reset scroll and focus when filtered options change
		startIndex = 0;
		focusedIndex = -1;
		updateVisibleItems();
	}

	// Update visible items based on scroll position
	function updateVisibleItems() {
		if (!container) return;
		const scrollTop = container.scrollTop;
		startIndex = Math.floor(scrollTop / itemHeight);
		const endIndex = Math.min(
			startIndex + visibleCount,
			filteredOptions.length
		);
		visibleItems = filteredOptions.slice(startIndex, endIndex);
	}

	// Handle scroll event
	function handleScroll() {
		updateVisibleItems();
	}

	// Calculate visible item count based on container height
	async function updateVisibleCount() {
		if (container) {
			await tick(); // Wait for DOM to render
			visibleCount = Math.ceil(container.clientHeight / itemHeight) + 2; // Buffer of 2 items
			updateVisibleItems();
		}
	}

	// Handle option selection
	function selectOption(option) {
		selected = option.value;
		isOpen = false;
		searchTerm = option.label; // Set searchTerm to the selected option's label
		focusedIndex = -1;
		dispatch("onSelected", option.value);
	}

	// Handle clear selection
	function clearSelection() {
		selected = null;
		searchTerm = "";
		focusedIndex = -1;
		dispatch("onSelected", null);
		dispatch("onClear");
	}

	// Reset component state
	export function reset() {
		selected = null;
		searchTerm = "";
		isOpen = false;
		focusedIndex = -1;
		startIndex = 0;
		updateVisibleItems();
		dispatch("onSelected", null);
		dispatch("onClear");
	}

	// Toggle dropdown
	async function toggleDropdown() {
		isOpen = !isOpen;
		if (isOpen) {
			forceVisibleItems();
		}
	}

	// Handle click outside to close dropdown
	function handleClickOutside(event) {
		if (!event.target.closest(".select-container")) {
			isOpen = false;
			focusedIndex = -1;
		}
	}

	// Handle input focus to open dropdown
	async function handleInputFocus() {
		// forceVisibleItems();
	}

	// Handle input typing to open dropdown
	async function handleInput() {
		forceVisibleItems();
	}

	async function forceVisibleItems() {
		isOpen = true;
		await updateVisibleCount(); // Ensure items render on focus
		// these 2 timeouts are a more or less tmp fix for rendering items when dropdown opens
		setTimeout(async () => {
			await updateVisibleCount(); // Ensure items render on focus
		}, 25);
		setTimeout(async () => {
			await updateVisibleCount(); // Ensure items render on focus
		}, 50);
	}

	// Handle keyboard navigation
	function handleKeydown(event, index) {
		if (!isOpen) return;

		if (event.key === "ArrowDown") {
			event.preventDefault();
			if (focusedIndex < filteredOptions.length - 1) {
				focusedIndex += 1;
				scrollToFocusedItem();
			}
		} else if (event.key === "ArrowUp") {
			event.preventDefault();
			if (focusedIndex > 0) {
				focusedIndex -= 1;
				scrollToFocusedItem();
			} else if (focusedIndex === -1 && filteredOptions.length > 0) {
				focusedIndex = 0;
				scrollToFocusedItem();
			}
		} else if (event.key === "Enter" && index >= 0) {
			event.preventDefault();
			selectOption(filteredOptions[index]);
		} else if (event.key === "Escape") {
			event.preventDefault();
			isOpen = false;
			focusedIndex = -1;
		}
	}

	// Scroll to the focused item
	function scrollToFocusedItem() {
		if (!container || focusedIndex < 0) return;

		const itemTop = focusedIndex * itemHeight;
		const itemBottom = itemTop + itemHeight;
		const containerTop = container.scrollTop;
		const containerBottom = containerTop + container.clientHeight;

		if (itemTop < containerTop) {
			container.scrollTop = itemTop;
		} else if (itemBottom > containerBottom) {
			container.scrollTop = itemBottom - container.clientHeight;
		}
		updateVisibleItems();
	}

	// Initialize container size observer and autofocus fallback
	onMount(async () => {
		if (container) {
			const resizeObserver = new ResizeObserver(updateVisibleCount);
			resizeObserver.observe(container);
			return () => resizeObserver.disconnect();
		}
		// Fallback autofocus with tick to ensure inputElement is bound
		if (autofocus && inputElement) {
			await tick();
			inputElement.focus();
		}
	});

	// Get display text for the input
	function getDisplayText() {
		if (!selected) return inputPlaceholder;
		const selectedOption = options.find((option) => option.value === selected);
		return selectedOption ? selectedOption.label : inputPlaceholder;
	}
</script>

<svelte:window on:click={handleClickOutside} />

<div class="select-container relative w-full">
	<!-- Select element with inline search -->
	<div
		class="border rounded-md px-3 py-2 bg-white shadow-sm flex items-center gap-2"
		role="combobox"
		aria-expanded={isOpen}
	>
		<input
			autocomplete="off"
			type="text"
			id={inputElementID}
			bind:value={searchTerm}
			bind:this={inputElement}
			placeholder={getDisplayText()}
			class="w-full bg-transparent focus:outline-none {selected
				? 'text-black'
				: 'text-gray-700'}"
			{autofocus}
			on:focus={handleInputFocus}
			on:input={handleInput}
			on:keydown={(e) => {
				if (e.key === "Enter" && !isOpen) {
					toggleDropdown();
				} else {
					handleKeydown(e, focusedIndex);
				}
			}}
			aria-label={inputAriaLabel}
		/>
		{#if selected}
			<button
				type="button"
				class="w-5 h-5 flex items-center justify-center text-gray-500 hover:text-gray-700"
				on:click={clearSelection}
				on:keydown={(e) => {
					if (e.key === "Enter" || e.key === " ") {
						e.preventDefault();
						clearSelection();
					} else if (e.key === "Escape") {
						e.preventDefault();
						isOpen = false;
						focusedIndex = -1;
					}
				}}
				role="button"
				tabindex="0"
				aria-label={clearAriaLabel}
			>
				<svg
					class="w-4 h-4"
					fill="none"
					stroke="currentColor"
					viewBox="0 0 24 24"
				>
					<path
						stroke-linecap="round"
						stroke-linejoin="round"
						stroke-width="2"
						d="M6 18L18 6M6 6l12 12"
					/>
				</svg>
			</button>
		{/if}
		<svg
			class="w-4 h-4 text-gray-500 transform {isOpen ? 'rotate-180' : ''}"
			fill="none"
			stroke="currentColor"
			viewBox="0 0 24 24"
			on:click={toggleDropdown}
			role="button"
			tabindex="0"
			on:keydown={(e) => {
				if (e.key === "Enter") toggleDropdown();
				else if (e.key === "Escape") {
					e.preventDefault();
					isOpen = false;
					focusedIndex = -1;
				}
			}}
			aria-label={toggleAriaLabel}
		>
			<path
				stroke-linecap="round"
				stroke-linejoin="round"
				stroke
				Politeness="round"
				stroke-width="2"
				d="M19 9l-7 7-7-7"
			/>
		</svg>
	</div>

	<!-- Dropdown -->
	{#if isOpen}
		<div
			class="absolute z-10 w-full mt-1 bg-white border rounded-md shadow-lg max-h-80 overflow-auto"
			bind:this={container}
			on:scroll={handleScroll}
			role="listbox"
		>
			{#if filteredOptions.length > 0}
				<!-- Virtualized list container -->
				<div style="height: {filteredOptions.length * itemHeight}px;">
					<div style="transform: translateY({startIndex * itemHeight}px);">
						{#each visibleItems as item, i (item.label + "-" + (startIndex + i))}
							<div
								class="px-3 py-2 hover:bg-blue-100 cursor-pointer {selected ===
								item.value
									? 'bg-blue-50'
									: ''} {focusedIndex === startIndex + i
									? 'bg-blue-200 outline outline-2 outline-blue-500'
									: ''}"
								on:click={() => selectOption(item)}
								on:keydown={(e) => handleKeydown(e, startIndex + i)}
								role="option"
								tabindex="0"
								aria-selected={selected === item.value}
							>
								{item.label}
							</div>
						{/each}
					</div>
				</div>
			{:else}
				<div class="px-3 py-2 text-gray-500">{noOptionsText}</div>
			{/if}
		</div>
	{/if}
</div>

<style>
	/* Ensure Tailwind classes handle additional styling */
	:global(.select-container input:focus) {
		border-color: #3b82f6; /* Tailwind's blue-500 */
	}
	:global([role="option"]:focus) {
		outline: 2px solid #3b82f6;
		outline-offset: -2px;
	}
	:global([role="button"]:focus) {
		outline: 2px solid #3b82f6;
		outline-offset: -2px;
	}
</style>
