<script setup lang="ts">
import { onKeyStroke, useDebounce } from '@vueuse/core';

import { normaliseQuery, validateQuery } from '~/src/search.ts';

const query = ref('');
const normalisedQuery = computed(() => {
    return normaliseQuery(query.value);
});
const debouncedQuery = useDebounce(normalisedQuery);

const queryValidation = computed(() => {
    return validateQuery(normalisedQuery.value);
});

const searchAsyncData = useAsyncData('search', async () => {
    selected.value = null;
    if (queryValidation.value !== undefined) {
        return [];
    }
    return await $fetch('/api/search', {
        query: {
            query: debouncedQuery.value,
        },
    });
}, {
    watch: [debouncedQuery],
});

const isLoading = computed(() => {
    return queryValidation.value === undefined &&
        (normalisedQuery.value !== debouncedQuery.value || searchAsyncData.status.value === 'pending');
});

const hasNoResults = computed(() => {
    return !isLoading.value && searchAsyncData.data.value?.length === 0;
});

const dialogue = useTemplateRef('dialogue');

const open = () => {
    query.value = '';
    searchAsyncData.clear();
    dialogue.value?.showModal();
    selected.value = null;
};

const close = () => {
    dialogue.value?.close();
};

onKeyStroke('k', (event) => {
    const isCtrlKeyPressed = isMac.value ? event.metaKey : event.ctrlKey;
    if (isCtrlKeyPressed) {
        event.preventDefault();
        open();
    }
});

const onMousedown = (event: MouseEvent) => {
    const rect = dialogue.value?.getBoundingClientRect();
    if (rect && (
        event.clientX < rect.left || event.clientX > rect.right ||
        event.clientY < rect.top || event.clientY > rect.bottom)
    ) {
        close();
    }
};

const selected = ref<number | null>(null);
const router = useRouter();
onKeyStroke('ArrowDown', () => {
    selected.value = Math.min(
        (selected.value ?? -1) + 1,
        (searchAsyncData.data?.value?.length ?? 0) - 1,
    );
});
onKeyStroke('ArrowUp', () => {
    selected.value = Math.max(
        (selected.value ?? -1) - 1,
        0,
    );
});
onKeyStroke('Enter', () => {
    if (selected.value === null) {
        return;
    }

    const document = searchAsyncData.data.value?.[selected.value];
    if (!document) {
        return;
    }

    router.push(document.url);
    close();
});

defineExpose({ open, close });
</script>

<template>
    <dialog ref="dialogue" class="container m-auto h-100 rounded border" @mousedown="onMousedown">
        <div class="input-group mb-4">
            <span class="input-group-text">
                <Spinner v-if="isLoading" size="1.25em" />
                <Icon v-else v="search" />
            </span>
            <input
                v-model="query"
                type="search"
                class="form-control border-primary"
                :placeholder="$t('crud.search')"
            >
            <button class="btn btn-outline-danger" @click="close()">
                <Icon v="times" />
            </button>
        </div>
        <div v-if="queryValidation !== undefined" class="alert alert-info">
            <T>search.{{ queryValidation }}</T>
        </div>
        <ul
            v-else-if="searchAsyncData.data.value?.length ?? 0 > 0"
            class="list-group"
        >
            <li
                v-for="(document, index) of searchAsyncData.data.value"
                :key="`${document.kind}-${document.id}`"
                :class="['list-group-item list-group-item-action p-0', selected === index ? 'list-group-item-active' : '']"
            >
                <SearchItem :document="document" @click="close()" />
            </li>
        </ul>
        <div v-else-if="hasNoResults" class="alert alert-info">
            <T :params="{ query }">search.noResults</T>
        </div>
    </dialog>
</template>

<style lang="scss">
@import "assets/variables";

.list-group-item-active {
    background-color: $list-group-hover-bg !important;
    border-left: 3px solid $primary !important;
    margin-inline-start: -2px; /** compensate for the border mark, minus 1px (regular border) */
}
</style>
