<template>
    <div class="select flex-grow-1">
        <input
            ref="filter"
            v-model="filter"
            type="text"
            class="form-control"
            v-bind="$attrs"
            @focus="show"
            @blur="hide"
            @keydown="filterKeydown"
        >
        <div v-show="shown" class="list-group shadow">
            <a
                v-for="(display, option) in visibleOptions()"
                :class="['list-group-item', 'list-group-item-action', highlightedOption === option ? 'active' : '']"
                href="#"
                @click.prevent="select(option)"
            >
                {{ display }}
            </a>
        </div>
    </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import type { PropType } from 'vue';

export default defineComponent({
    inheritAttrs: false,
    props: {
        modelValue: { required: true, type: String as PropType<string | null> },
        options: { required: true, type: Object as PropType<Record<string, string>> },
        freeform: { type: Boolean },
    },
    emits: ['update:modelValue'],
    data() {
        return {
            filter: this.options[this.modelValue!] || this.modelValue || null,
            shown: false,
            highlighted: -1,
        };
    },
    computed: {
        highlightedOption() {
            return Object.keys(this.visibleOptions())[this.highlighted];
        },
    },
    watch: {
        modelValue() {
            this.filter = this.options[this.modelValue!] || this.modelValue;
            this.highlighted = -1;
        },
        filter() {
            if (this.freeform) {
                this.$emit('update:modelValue', this.filter);
            }
        },
    },
    methods: {
        select(option: string) {
            this.$emit('update:modelValue', option);
            this.hide();
            this.highlighted = -1;
        },
        show() {
            this.shown = true;
        },
        hide() {
            setTimeout(() => {
                this.shown = false;
            }, 100);
        },
        filterKeydown(e: KeyboardEvent) {
            if (!this.shown) {
                this.show();
            }

            switch (e.key) {
                case 'ArrowUp':
                    this.highlighted--;
                    e.preventDefault();
                    e.stopPropagation();
                    break;
                case 'ArrowDown':
                    this.highlighted++;
                    e.preventDefault();
                    e.stopPropagation();
                    break;
                case 'Enter':
                    if (this.highlightedOption) {
                        this.select(this.highlightedOption);
                        e.preventDefault();
                        e.stopPropagation();
                    }
                    break;
                default:
                    break;
            }
        },
        visibleOptions(): Record<string, string> {
            return Object.fromEntries(Object.entries(this.options).filter(([option, display]) => {
                return !this.filter ||
                    option.toLowerCase().includes(this.filter.toLowerCase()) ||
                    display.toLowerCase().includes(this.filter.toLowerCase());
            }));
        },
    },
});
</script>

<style lang="scss" scoped>
.select {
    position: relative;
    .list-group {
        position: absolute;
        top: 100%;
        max-height: 300px;
        width: min(300px, 100%);
        overflow-y: auto;
        z-index: 999;
    }
}
</style>
