<script setup>
import { ref, toRefs, computed, watch, onMounted, useAttrs } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'src/components/FetchData.vue';
import { useValidator } from 'src/composables/useValidator';
const emit = defineEmits(['update:modelValue', 'update:options', 'remove']);

const $props = defineProps({
    modelValue: {
        type: [String, Number, Object],
        default: null,
    },
    options: {
        type: Array,
        default: () => [],
    },
    optionLabel: {
        type: [String],
        default: 'name',
    },
    optionValue: {
        type: String,
        default: 'id',
    },
    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,
    },
});
const { validations } = useValidator();
const requiredFieldRule = (val) => validations().required($attrs.required, val);
const $attrs = useAttrs();
const { t } = useI18n();
const mixinRules = [requiredFieldRule, ...($attrs.rules ?? [])];
const { optionLabel, optionValue, optionFilter, optionFilterValue, options, modelValue } =
    toRefs($props);
const myOptions = ref([]);
const myOptionsOriginal = ref([]);
const vnSelectRef = ref();
const dataRef = ref();
const lastVal = ref();
const noOneText = t('globals.noOne');
const noOneOpt = ref({
    [optionValue.value]: false,
    [optionLabel.value]: noOneText,
});

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 ($props.url && $props.modelValue && !findKeyInOptions())
        fetchFilter($props.modelValue);
    if ($props.focusOnMount) setTimeout(() => vnSelectRef.value.showPopup(), 300);
});

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

function setOptions(data) {
    myOptions.value = JSON.parse(JSON.stringify(data));
    myOptionsOriginal.value = JSON.parse(JSON.stringify(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 || !dataRef.value) 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;

    return dataRef.value.fetch(fetchOptions);
}

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);

defineExpose({ opts: myOptions });
</script>

<template>
    <FetchData
        ref="dataRef"
        :url="$props.url"
        @on-fetch="(data) => setOptions(data)"
        :where="where || { [optionValue]: value }"
        :limit="limit"
        :sort-by="sortBy"
        :fields="fields"
        :params="params"
    />
    <QSelect
        v-model="value"
        :options="myOptions"
        :option-label="optionLabel"
        :option-value="optionValue"
        v-bind="$attrs"
        @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: $attrs.required }"
        :rules="mixinRules"
        virtual-scroll-slice-size="options.length"
        hide-bottom-space
    >
        <template v-if="isClearable" #append>
            <QIcon
                v-show="value"
                name="close"
                @click.stop="
                    () => {
                        value = null;
                        emit('remove');
                    }
                "
                class="cursor-pointer"
                size="xs"
            />
        </template>
        <template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName">
            <slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
        </template>
    </QSelect>
</template>

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