<script setup>
import { ref, toRefs, computed, watch, onMounted, useAttrs } from 'vue';
import { useI18n } from 'vue-i18n';
import { useArrayData } from 'src/composables/useArrayData';
import { useRequired } from 'src/composables/useRequired';
import dataByOrder from 'src/utils/dataByOrder';
import { QItemLabel } from 'quasar';

const emit = defineEmits(['update:modelValue', 'update:options', 'remove']);
const $attrs = useAttrs();
const { t } = useI18n();

const isRequired = computed(() => {
    return useRequired($attrs).isRequired;
});
const requiredFieldRule = computed(() => {
    return useRequired($attrs).requiredFieldRule;
});

const $props = defineProps({
    modelValue: {
        type: [String, Number, Object],
        default: null,
    },
    options: {
        type: Array,
        default: () => [],
    },
    optionLabel: {
        type: [String],
        default: 'name',
    },
    optionValue: {
        type: String,
        default: 'id',
    },
    optionCaption: {
        type: String,
        default: null,
    },
    optionFilter: {
        type: String,
        default: null,
    },
    optionFilterValue: {
        type: String,
        default: null,
    },
    url: {
        type: String,
        default: null,
    },
    filterOptions: {
        type: [Array],
        default: () => [],
    },
    exprBuilder: {
        type: Function,
        default: null,
    },
    isClearable: {
        type: Boolean,
        default: true,
    },
    defaultFilter: {
        type: Boolean,
        default: true,
    },
    fields: {
        type: Array,
        default: null,
    },
    include: {
        type: [Object, Array],
        default: null,
    },
    where: {
        type: Object,
        default: null,
    },
    sortBy: {
        type: String,
        default: null,
    },
    limit: {
        type: [Number, String],
        default: '30',
    },
    focusOnMount: {
        type: Boolean,
        default: false,
    },
    useLike: {
        type: Boolean,
        default: true,
    },
    params: {
        type: Object,
        default: null,
    },
    noOne: {
        type: Boolean,
        default: false,
    },
    dataKey: {
        type: String,
        default: null,
    },
    isOutlined: {
        type: Boolean,
        default: false,
    },
});

const mixinRules = [requiredFieldRule, ...($attrs.rules ?? [])];
const {
    optionLabel,
    optionValue,
    optionCaption,
    optionFilter,
    optionFilterValue,
    options,
    modelValue,
} = toRefs($props);
const myOptions = ref([]);
const myOptionsOriginal = ref([]);
const vnSelectRef = ref();
const lastVal = ref();
const noOneText = t('globals.noOne');
const noOneOpt = ref({
    [optionValue.value]: false,
    [optionLabel.value]: noOneText,
});
const styleAttrs = computed(() => {
    return $props.isOutlined
        ? {
              dense: true,
              outlined: true,
              rounded: true,
          }
        : {};
});
const isLoading = ref(false);
const useURL = computed(() => $props.url);
const value = computed({
    get() {
        return $props.modelValue;
    },
    set(value) {
        setOptions(myOptionsOriginal.value);
        emit('update:modelValue', value);
    },
});

watch(options, (newValue) => {
    setOptions(newValue);
});

watch(modelValue, async (newValue) => {
    if (!myOptions?.value?.some((option) => option[optionValue.value] == newValue))
        await fetchFilter(newValue);

    if ($props.noOne) myOptions.value.unshift(noOneOpt.value);
});

onMounted(() => {
    setOptions(options.value);
    if (useURL.value && $props.modelValue && !findKeyInOptions())
        fetchFilter($props.modelValue);
    if ($props.focusOnMount) setTimeout(() => vnSelectRef.value.showPopup(), 300);
});

const arrayDataKey =
    $props.dataKey ?? ($props.url?.length > 0 ? $props.url : $attrs.name ?? $attrs.label);

const arrayData = useArrayData(arrayDataKey, {
    url: $props.url,
    searchUrl: false,
    mapKey: $attrs['map-key'],
});

function findKeyInOptions() {
    if (!$props.options) return;
    return filter($props.modelValue, $props.options)?.length;
}

function setOptions(data) {
    data = dataByOrder(data, $props.sortBy);
    myOptions.value = JSON.parse(JSON.stringify(data));
    myOptionsOriginal.value = JSON.parse(JSON.stringify(data));
    emit('update:options', data);
}

function filter(val, options) {
    const search = val?.toString()?.toLowerCase();

    if (!search) return options;

    return options.filter((row) => {
        if ($props.filterOptions.length) {
            return $props.filterOptions.some((prop) => {
                const propValue = String(row[prop]).toLowerCase();
                return propValue.includes(search);
            });
        }

        if (!row) return;
        const id = row[$props.optionValue];
        const optionLabel = String(row[$props.optionLabel]).toLowerCase();

        return id == search || optionLabel.includes(search);
    });
}

async function fetchFilter(val) {
    if (!$props.url) return;

    const { fields, include, sortBy, limit } = $props;
    const key =
        optionFilterValue.value ??
        (new RegExp(/\d/g).test(val)
            ? optionValue.value
            : optionFilter.value ?? optionLabel.value);

    let defaultWhere = {};
    if ($props.filterOptions.length) {
        defaultWhere = $props.filterOptions.reduce((obj, prop) => {
            if (!obj.or) obj.or = [];
            obj.or.push({ [prop]: getVal(val) });
            return obj;
        }, {});
    } else defaultWhere = { [key]: getVal(val) };
    const where = { ...(val ? defaultWhere : {}), ...$props.where };
    $props.exprBuilder && Object.assign(where, $props.exprBuilder(key, val));
    const fetchOptions = { where, include, limit };
    if (fields) fetchOptions.fields = fields;
    if (sortBy) fetchOptions.order = sortBy;
    arrayData.resetPagination();

    const { data } = await arrayData.applyFilter({ filter: fetchOptions });
    setOptions(data);
    return data;
}

async function filterHandler(val, update) {
    if (!val && lastVal.value === val) {
        lastVal.value = val;
        return update();
    }
    lastVal.value = val;
    let newOptions;

    if (!$props.defaultFilter) return update();
    if (
        $props.url &&
        ($props.limit || (!$props.limit && Object.keys(myOptions.value).length === 0))
    ) {
        newOptions = await fetchFilter(val);
    } else newOptions = filter(val, myOptionsOriginal.value);
    update(
        () => {
            if ($props.noOne && noOneText.toLowerCase().includes(val.toLowerCase()))
                newOptions.unshift(noOneOpt.value);

            myOptions.value = newOptions;
        },
        (ref) => {
            if (val !== '' && ref.options.length > 0) {
                ref.setOptionIndex(-1);
                ref.moveOptionSelection(1, true);
            }
        }
    );
}

function nullishToTrue(value) {
    return value ?? true;
}

const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val);

async function onScroll({ to, direction, from, index }) {
    const lastIndex = myOptions.value.length - 1;

    if (from === 0 && index === 0) return;
    if (!useURL.value && !$props.fetchRef) return;
    if (direction === 'decrease') return;
    if (to === lastIndex && arrayData.store.hasMoreData && !isLoading.value) {
        isLoading.value = true;
        await arrayData.loadMore();
        setOptions(arrayData.store.data);
        vnSelectRef.value.scrollTo(lastIndex);
        isLoading.value = false;
    }
}

defineExpose({ opts: myOptions, vnSelectRef });

function handleKeyDown(event) {
    if (event.key === 'Tab' && !event.shiftKey) {
        event.preventDefault();

        const inputValue = vnSelectRef.value?.inputValue;

        if (inputValue) {
            const matchingOption = myOptions.value.find(
                (option) =>
                    option[optionLabel.value].toLowerCase() === inputValue.toLowerCase()
            );

            if (matchingOption) {
                emit('update:modelValue', matchingOption[optionValue.value]);
            } else {
                emit('update:modelValue', inputValue);
            }
            vnSelectRef.value?.hidePopup();
        }

        const focusableElements = document.querySelectorAll(
            'a:not([disabled]), button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), details:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])'
        );
        const currentIndex = Array.prototype.indexOf.call(
            focusableElements,
            event.target
        );
        if (currentIndex >= 0 && currentIndex < focusableElements.length - 1) {
            focusableElements[currentIndex + 1].focus();
        }
    }
}

function getCaption(opt) {
    if (optionCaption.value === false) return;
    return opt[optionCaption.value] || opt[optionValue.value];
}
</script>

<template>
    <QSelect
        v-model="value"
        :options="myOptions"
        :option-label="optionLabel"
        :option-value="optionValue"
        v-bind="{ ...$attrs, ...styleAttrs }"
        @filter="filterHandler"
        :emit-value="nullishToTrue($attrs['emit-value'])"
        :map-options="nullishToTrue($attrs['map-options'])"
        :use-input="nullishToTrue($attrs['use-input'])"
        :hide-selected="nullishToTrue($attrs['hide-selected'])"
        :fill-input="nullishToTrue($attrs['fill-input'])"
        ref="vnSelectRef"
        lazy-rules
        :class="{ required: isRequired }"
        :rules="mixinRules"
        virtual-scroll-slice-size="options.length"
        hide-bottom-space
        :input-debounce="useURL ? '300' : '0'"
        :loading="isLoading"
        @virtual-scroll="onScroll"
        @keydown="handleKeyDown"
        :data-cy="$attrs.dataCy ?? $attrs.label + '_select'"
        :data-url="url"
    >
        <template #append>
            <QIcon
                v-show="isClearable && value"
                name="close"
                @click="
                    () => {
                        value = null;
                        emit('remove');
                    }
                "
                class="cursor-pointer"
                size="xs"
            />
        </template>
        <template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName">
            <div v-if="slotName == 'append'">
                <QIcon
                    v-show="isClearable && value"
                    name="close"
                    @click.stop="
                        () => {
                            value = null;
                            emit('remove');
                        }
                    "
                    class="cursor-pointer"
                    size="xs"
                />
                <slot name="append" v-if="$slots.append" v-bind="slotData ?? {}" />
            </div>
            <slot v-else :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
        </template>
        <template #option="{ opt, itemProps }">
            <QItem v-bind="itemProps">
                <QItemSection v-if="typeof opt !== 'object'"> {{ opt }}</QItemSection>
                <QItemSection v-else-if="opt[optionValue] == opt[optionLabel]">
                    <QItemLabel>{{ opt[optionLabel] }}</QItemLabel>
                </QItemSection>
                <QItemSection v-else>
                    <QItemLabel>
                        {{ opt[optionLabel] }}
                    </QItemLabel>
                    <QItemLabel caption v-if="getCaption(opt)">
                        {{ `#${getCaption(opt)}` }}
                    </QItemLabel>
                </QItemSection>
            </QItem>
        </template>
    </QSelect>
</template>

<style scoped lang="scss">
.q-field--outlined {
    max-width: 100%;
}
</style>
