<template>
	<div class="input">
		<input
			type="text"
			v-model="inputText"
			ref="input"
			@keydown.up="keyUp"
			@keydown.down="keyDown"
			@keydown.enter="toggleActive"
			@keydown.backspace="keyBackspace"
		>
		<button
			@click="manualToggle()"
			@keydown.up="keyUp"
			@keydown.down="keyDown"
			@keydown.enter.space="dropdownKey"
		>
			v
		</button>
		<div
			v-if="showDropdown"
			class="dropdown"
		>
			<ul class="options">
				<li
					v-for="entity of matchingEntities"
					:class="{ active: activeId === entity.id, selected: selectedIds.has(entity.id) }"
					@click="activeId = entity.id; toggleActive($event)"
					tabindex="-1"
				>
					{{ entity.name }}
				</li>
				<li v-if="matchingEntities.length === 0">
					no results
				</li>
			</ul>
		</div>
	</div>
</template>

<script lang="ts" setup>
import type { ApiEntity } from '~/shared/types/api';
import type { Id } from '~/shared/types/common';
import type { ClientEntity } from '~/utils/client-entity';

const selectedIds = defineModel<Set<Id>>({ required: true });
const activeId = ref<Id>();
const props = defineProps<{
	entities: ClientMap<ClientEntity<ApiEntity> & { name?: string }>,
	multi: boolean,
}>();

// Reset manualToggle if the focus leaves this component and doesn't come back.
const focusTimer = ref<ReturnType<typeof setTimeout>>();
function focusout() {
	focusTimer.value = setTimeout(() => {
		manualState.value = "closed";
	}, 0);
}
function focusin() {
	clearTimeout(focusTimer.value);
}
function esc() {
	manualState.value = undefined;
}
function windowBlur() {
	clearTimeout(focusTimer.value);
}
onMounted(() => {
	window.addEventListener("blur", windowBlur);
});
onUnmounted(() => {
	window.removeEventListener("blur", windowBlur);
});

defineExpose({
	activeId,
	focusin,
	focusout,
	esc,
});

const inputText = ref("");
const manualState = ref<"open" | "closed">();
const showDropdown = computed(() => {
	if (manualState.value) {
		return manualState.value === "open";
	}
	return Boolean(inputText.value)
});

function manualToggle() {
	if (showDropdown.value) {
		manualState.value = "closed";
		activeId.value = undefined;
	} else {
		manualState.value = "open";
	}
}

function dropdownKey(event: KeyboardEvent) {
	if (showDropdown.value) {
		toggleActive(event);
	}
}

function ensureActive(match: "first" | "last") {
	const active = props.entities.get(activeId.value!);
	if (!active || !active.name || !fuzzyMatch(inputText.value, active.name)) {
		// Set active id to the first match in the chosen direction
		const entities = match === "first" ? matchingEntities.value : [...matchingEntities.value].reverse();
		for (const entity of entities) {
			if (entity.name && fuzzyMatch(inputText.value, entity.name)) {
				activeId.value = entity.id;
				return;
			}
		}
		// No matches
		activeId.value = undefined;
	}
}
watch(inputText, () => {
	if (manualState.value === "closed") {
		manualState.value = undefined;
	}
	if (!inputText.value) {
		return;
	}
	ensureActive("first");
})
function keyUp(event: KeyboardEvent) {
	if (activeId.value === undefined) {
		ensureActive("last");
		event.preventDefault();
		return;
	}
	const index = matchingEntities.value.findIndex(entity => entity.id === activeId.value);
	if (index !== -1 && index !== 0) {
		activeId.value = matchingEntities.value[index - 1].id;
		event.preventDefault();
	}
}
function keyDown(event: KeyboardEvent) {
	if (activeId.value === undefined) {
		ensureActive("first");
		event.preventDefault();
		return;
	}
	const index = matchingEntities.value.findIndex(entity => entity.id === activeId.value);
	if (index !== -1 && index !== matchingEntities.value.length) {
		activeId.value = matchingEntities.value[index + 1].id;
		event.preventDefault();
	}
}
const inputElement = useTemplateRef("input");
function keyBackspace(event: KeyboardEvent) {
	if (
		inputElement.value?.selectionStart === 0
		&& inputElement.value?.selectionEnd === 0
	) {
		const lastId = [...selectedIds.value].slice(-1)[0];
		if (lastId === undefined) {
			return;
		}
		if (props.multi) {
			selectedIds.value.delete(lastId);
		} else {
			selectedIds.value = new Set();
		}
		const entity = props.entities.get(lastId);
		inputText.value = `${entity?.name ?? lastId}${inputText.value ? " " : ""}${inputText.value}`;
	}
}
function toggleActive(event: MouseEvent | KeyboardEvent) {
	if (activeId.value !== undefined) {
		if (props.multi) {
			if (selectedIds.value.has(activeId.value)) {
				selectedIds.value.delete(activeId.value);
			} else {
				selectedIds.value.add(activeId.value);
			}
		} else {
			selectedIds.value = new Set([activeId.value]);
		}
		if (!props.multi || !event.ctrlKey) {
			manualState.value = undefined;
			activeId.value = undefined;
			inputText.value = "";
		}
		event.preventDefault();
	}
}

const fuzzyMatch = useFuzzyMatch();
const stringSort = useStringSort();
const matchingEntities = computed(() => {
	let entities = [...props.entities.values()];
	if (inputText.value) {
		entities = entities.filter(entity => entity.name && fuzzyMatch(inputText.value, entity.name));
	}
	return entities.sort((a, b) => stringSort(a.name ?? "", b.name ?? ""));
});
</script>

<style scoped>
.input {
	display: flex;
	flex-grow: 1;
}

input {
	width: 2rem;
	flex-grow: 1;
}
.dropdown {
	position: absolute;
	z-index: 1;
	background: Canvas;
	color: CanvasText;
	left: 0;
	right: 0;
	top: 100%;
	max-height: 50lvh;
	overflow-y: auto;
}
.options {
	list-style: none;
	padding-inline-start: 0;
}
.options li:focus,
.options .active {
	color: Canvas;
	background-color: CanvasText;
}
.options .selected {
	color: color-mix(in oklab, CanvasText, orange 30%);
	background-color: color-mix(in oklab, Canvas, orange 30%);
}
.options li:focus.selected,
.options .active.selected {
	color: color-mix(in oklab, Canvas, orange 30%);
	background-color: color-mix(in oklab, CanvasText, orange 30%);
}
</style>
