<template>
  <div class="select-wrapper">
    <field v-bind="$props">
      <template #default>
        <div
          class="select-input"
          @click="onToggleDatalist"
          :class="classes"
          ref="selectElement"
        >
          <div v-if="multiple" class="select-input__tags">
            <slot
              name="selected-items"
              :selected-items="selectedItems"
              :dismiss="(value: string) => onRemoveOption(value)"
            >
              <BmsTag
                v-for="tag in selectedItems"
                :small="small"
                active
                @dismiss="onRemoveOption(tag.value)"
              >
                <UiSelectIcon :icon="tag.icon" />
                {{ tag.label }}
              </BmsTag>
            </slot>
          </div>
          <slot
            v-if="singleSelectedItem"
            name="selected-item"
            :selectedItem="singleSelectedItem"
          ></slot>
          <slot
            name="input-element"
            :searching="searching"
            :onOpenDatalist="openDatalist"
          >
            <span
              class="field__input-icon field__input-icon--start"
              v-if="hasStartIcon"
            >
              <slot name="icon-start"></slot>
            </span>
            <UiSelectIcon
              v-if="!multiple && singleSelectedItem?.icon"
              :icon="singleSelectedItem?.icon"
            />
            <input
              v-if="!$slots['selected-item']"
              ref="inputElement"
              type="text"
              role="input"
              v-model="searching"
              :readonly="!autocomplete"
              :placeholder="placeholder"
              :required="required"
              :disabled="disabled"
              @keyup.down="openDatalist"
              @input="filteringActive = true"
            />
            <span
              class="field__input-icon field__input-icon--end"
              v-if="hasEndIcon"
            >
              <slot name="icon-end"></slot>
            </span>
          </slot>
          <span class="icon-container">
            <template v-if="canClear">
              <X
                class="combobox-clear-icon icon-button"
                @click.stop="onClearInput"
              />
            </template>
            <template v-else>
              <ChevronUp v-if="isDatalistOpen" class="icon-button" />
              <ChevronDown v-else class="icon-button" />
            </template>
          </span>
        </div>
      </template>
      <template #datalist>
        <FieldDatalist
          v-show="isDatalistOpen"
          :options="showSearchHint ? [] : filteredLocalOptions"
          :is-input-focused="isDatalistOpen"
          :can-add-new-option="canAddNewOption"
          :new-option="searching"
          :small="small"
          :selected-value="modelValue"
          @add-new-option="(option: string) => emits('addNewOption', option)"
          @select="onSelect"
        >
          <template #option="{ option }: { option: InputOption }">
            <slot
              name="option"
              :option="option"
              :is-selected="isOptionSelected(option)"
            >
              <div
                class="combobox-option"
                :class="{
                  'combobox-option--select-all':
                    option.value === SELECT_ALL_VALUE,
                }"
              >
                <div class="combobox-option__check">
                  <Check v-if="isOptionSelected(option)" :size="12" />
                  <Minus
                    v-else-if="
                      option.value === SELECT_ALL_VALUE && someOptionsSelected
                    "
                    :size="12"
                  />
                </div>
                <template v-if="option.icon">
                  <UiSelectIcon :icon="option?.icon" />
                </template>
                {{ option.label }}
              </div>
            </slot>
          </template>
          <template #search-hint v-if="request && autocomplete">
            <slot name="search-hint">Veuillez saisir un libellé</slot>
          </template>
          <template #new-option="{ newOption }: { newOption: string }">
            <slot name="new-option" :newOption="newOption"></slot>
          </template>
        </FieldDatalist>
      </template>
    </field>
  </div>
</template>

<script lang="ts">
export const SELECT_ALL_VALUE = '__bms_select_all__';
</script>

<script setup lang="ts">
import { InputOption } from '@/models';
import { FieldComponentProps } from '@/plugins/field/field-component.model';
import FieldDatalist from '@/plugins/field/FieldDatalist.vue';
import { Check, ChevronDown, ChevronUp, Minus, X } from 'lucide-vue-next';
import {
  computed,
  onMounted,
  watchEffect,
  Ref,
  ref,
  useSlots,
  useTemplateRef,
  watch,
} from 'vue';
import BmsTag from './BmsTag.vue';
import {
  onClickOutside,
  onKeyDown,
  onKeyUp,
  useDebounceFn,
} from '@vueuse/core';
import UiSelectIcon from './UiSelectIcon.vue';

export interface SelectProps extends FieldComponentProps {
  multiple?: boolean;
  autocomplete?: boolean;
  options?: string[] | InputOption[];
  placeholder?: string;
  open?: boolean;
  canAddNewOption?: boolean; //Only if autocomplete
  selectAll?: boolean; //Only if multiple
  selectAllLabel?: string;
  request?: (
    abortController: AbortController,
    inputValue?: string,
  ) => Promise<{ data: InputOption[] }>;
}

const props = withDefaults(defineProps<SelectProps>(), {
  multiple: false,
  autocomplete: false,
  disabled: false,
  small: false,
  placeholder: '',
  options: () => [],
  open: false,
  canAddNewOption: false,
  selectAll: false,
  selectAllLabel: 'Tout sélectionner',
});

const emits = defineEmits<{
  addNewOption: [newOption: any];
  select: [option: InputOption];
}>();

const modelValue = defineModel<any | any[] | null>();
const searching = defineModel<string>('searching', { default: '' });

const slots = useSlots();
const hasStartIcon = computed(() => !!slots['icon-start']?.());
const hasEndIcon = computed(() => !!slots['icon-end']?.());

const inputElement: Ref<HTMLElement | null> = useTemplateRef('inputElement');
const selectElement: Ref<HTMLElement | null> = useTemplateRef('selectElement');
const allServerLoadedOptions = ref<InputOption[]>([]);
const currentServerLoadedOptions = ref<InputOption[]>([]);
const controller = ref<AbortController>();
const loading = ref(true);
const isDatalistOpen = ref(props.open);
const filteringActive = ref(false);

const canClear = computed(() => {
  const hasValueToClear =
    (searching.value && searching.value.length > 0) ||
    (modelValue.value && modelValue.value.length > 0);
  const isMultipleOrAutocomplete = props.multiple || props.autocomplete;
  return hasValueToClear && isMultipleOrAutocomplete;
});

const classes = computed(() => ({
  'is-error': props.errors?.length,
  'is-disabled': props.disabled,
}));

watch(searching, (newSearch) => {
  if (props.request) {
    debouncedLoadData(newSearch);
  } else if (!props.multiple && props.canAddNewOption) {
    const foundOption = props.options.find(
      (o) => (o as InputOption)?.label === newSearch,
    ) as InputOption;
    if (foundOption) {
      modelValue.value = foundOption.value;
    } else {
      modelValue.value = newSearch;
    }
  }
});

const loadData = async (search: string) => {
  loading.value = true;
  try {
    if (controller.value) {
      controller.value.abort();
    }
    if (props.request) {
      controller.value = new AbortController();
      loading.value = true;
      const { data } = await props.request(
        controller.value,
        props.autocomplete ? search : '',
      );

      currentServerLoadedOptions.value = data;
      data.forEach((element) => {
        if (
          !allServerLoadedOptions.value.find((o) => o.value === element.value)
        ) {
          allServerLoadedOptions.value.push(element);
        }
      });
      if (!props.autocomplete) {
        synchronizeSearchWithValue(modelValue.value);
      }
    }
  } catch (e) {
    console.error(e);
  } finally {
    loading.value = false;
  }
};

const debouncedLoadData = useDebounceFn(
  (search?: string) => loadData(search ?? ''),
  500,
);

if (import.meta.env.DEV) {
  watchEffect(() => {
    if (props.selectAll && !props.multiple) {
      console.warn(
        '[BmsCombobox] "selectAll" requires "multiple" to be enabled.',
      );
    }
  });
}

onMounted(() => {
  if (props.request) {
    loadData('');
  }
  synchronizeSearchWithValue(modelValue.value);
});

watch(
  () => [props.options, modelValue.value],
  () => {
    synchronizeSearchWithValue(modelValue.value);
  },
);

const synchronizeSearchWithValue = (newValue?: any | any[] | null) => {
  if (Array.isArray(newValue)) {
    searching.value = '';
  } else {
    const foundOption = localOptions.value.find((o) => o.value === newValue);
    if (!newValue) {
      searching.value = '';
    }
    if (foundOption) {
      searching.value = foundOption.label;
    } else if (props.canAddNewOption) {
      searching.value = newValue;
    }
  }
};

const singleSelectedItem = computed(() => {
  if (props.multiple) return null;

  if (props.request)
    return allServerLoadedOptions.value.find(
      (o) => o.value === modelValue.value,
    );

  return localOptions.value.find((o) => o.value === modelValue.value);
});

const allOptionsSelected = computed(() => {
  if (!Array.isArray(modelValue.value) || !localOptions.value.length)
    return false;
  return localOptions.value.every((o) =>
    (modelValue.value as any[]).includes(o.value),
  );
});

const someOptionsSelected = computed(() => {
  if (!Array.isArray(modelValue.value) || !modelValue.value.length)
    return false;
  return (
    !allOptionsSelected.value &&
    localOptions.value.some((o) =>
      (modelValue.value as any[]).includes(o.value),
    )
  );
});

const filteredLocalOptions = computed<InputOption[]>(() => {
  if (props.request) return currentServerLoadedOptions.value;

  let options = localOptions.value;

  if (props.autocomplete && filteringActive.value) {
    options = options.filter((o) =>
      o.label
        ?.toLocaleLowerCase()
        .includes((searching.value ?? '')?.toLocaleLowerCase()),
    );
  }

  if (props.selectAll && props.multiple) {
    return [
      { label: props.selectAllLabel, value: SELECT_ALL_VALUE },
      ...options,
    ];
  }

  return options;
});

const localOptions = computed<InputOption[]>(() => {
  const options = props.request ? allServerLoadedOptions.value : props.options;
  return Array.isArray(options) &&
    !!options.length &&
    typeof options[0] === 'string'
    ? options.map((o) => ({ label: o, value: o }) as InputOption)
    : (options as InputOption[]);
});

const selectedItems = computed<InputOption[]>(() => {
  if (!modelValue.value || !Array.isArray(modelValue.value)) return [];

  const items: InputOption[] = [];
  localOptions.value.forEach((option) => {
    if (modelValue.value === option.value) {
      items.push(option);
    } else if (modelValue.value?.includes(option.value)) {
      items.push(option);
    }
  });
  return items;
});

const showSearchHint = computed(
  () => props.request && props.autocomplete && !searching.value,
);

const isOptionSelected = (option: InputOption) => {
  if (option.value === SELECT_ALL_VALUE) return allOptionsSelected.value;

  if (Array.isArray(modelValue.value)) {
    return (
      (modelValue.value as any[])?.findIndex(
        (o) => option.value === (o as InputOption)?.value || option.value === o,
      ) >= 0
    );
  }

  return option.value === modelValue.value;
};

const setFocus = () => {
  if (inputElement.value) (inputElement.value as HTMLElement).focus();
};

const onClearInput = () => {
  modelValue.value = props.multiple ? [] : '';
  searching.value = '';
  openDatalist();
};

const onBackspace = () => {
  if (
    searching.value.length === 0 &&
    props.multiple &&
    modelValue.value &&
    modelValue.value.length > 0
  )
    modelValue.value = (modelValue.value as string[]).toSpliced(-1);
};

const closeDatalist = () => {
  isDatalistOpen.value = false;
};

const openDatalist = () => {
  if (!props.disabled) {
    isDatalistOpen.value = true;
    filteringActive.value = false;
    setFocus();
  }
};

onKeyUp('Escape', () => {
  if (isDatalistOpen.value) closeDatalist();
});
onKeyUp('Tab', () => {
  if (isDatalistOpen.value) closeDatalist();
});
onKeyDown('Backspace', () => {
  if (isDatalistOpen.value) onBackspace();
});

onClickOutside(selectElement, closeDatalist, {
  ignore: ['.datalist-option', '.options-list'],
});

const onToggleDatalist = () => {
  if (!props.disabled) {
    if (isDatalistOpen.value && !props.autocomplete) {
      closeDatalist();
    } else {
      openDatalist();
    }
    if (isDatalistOpen.value) {
      setFocus();
    }
  }
};

const onRemoveOption = (value: any) => {
  if (Array.isArray(modelValue.value))
    modelValue.value = (modelValue.value ?? []).filter((o) => o !== value);
  openDatalist();
};

const onSelect = (selectedOption: InputOption) => {
  if (selectedOption.value === SELECT_ALL_VALUE) {
    modelValue.value = allOptionsSelected.value
      ? []
      : localOptions.value.map((o) => o.value);
    return;
  }

  emits('select', selectedOption);
  if (props.multiple) {
    const currentValue = Array.isArray(modelValue.value)
      ? modelValue.value
      : [];
    if (currentValue.includes(selectedOption.value)) {
      modelValue.value = currentValue.filter((o) => o !== selectedOption.value);
    } else {
      modelValue.value = [...currentValue, selectedOption.value];
    }
    searching.value = '';
  } else {
    modelValue.value = selectedOption.value;
    if (props.autocomplete && props.request) {
      searching.value = '';
      openDatalist();
    } else {
      synchronizeSearchWithValue(selectedOption.value);
      closeDatalist();
    }
  }
};

defineExpose({
  setFocus,
});
</script>

<style scoped lang="scss">
.select-wrapper {
  --field-border-color: light-dark(var(--bms-grey-50), var(--bms-grey-140));
  --field-border-color-active: var(--bms-main-100);
  --input-background-color: var(--bms-white);

  .field__input-label {
    display: block;
    color: var(--bms-grey-50);
    font-size: 12px;
    line-height: 0.2em;
    margin-top: 6px;
    margin-bottom: 4px;
  }

  .select-input {
    width: 100%;
    padding: 0 0 0 var(--field-padding);
    border-radius: var(--bms-border-radius);
    border: 1px solid var(--field-border-color);
    background-color: var(--input-background-color);
    min-height: var(--field-height);

    display: flex;
    align-items: center;
    justify-content: space-between;

    .field__input-icon {
      display: flex;
      &--start {
        margin-right: 0.5em;
      }
      &--end {
        margin-left: 0.5em;
      }
    }

    input {
      border: none;
      background-color: transparent;
      flex-grow: 1;
      flex-basis: 40px;
      cursor: pointer;
      outline: none;
      appearance: none;
      margin: 0;
      padding: 0;
      font: inherit;
      color: inherit;
      line-height: 1;
      width: 100%;
      height: 100%;
    }
    &__tags {
      display: flex;
      gap: 0.5em;
      padding: var(--field-margin);
      flex-wrap: wrap;
      &:not(:empty) {
        margin-right: 0.5em;
      }
    }

    &:hover {
      --field-border-color: var(--bms-grey-100);
    }

    &:has(input:focus) {
      --field-border-color: var(--field-border-color-active);
    }

    &.is-error {
      --field-border-color: var(--bms-red-100);
      --input-background-color: var(--bms-red-25);
    }

    &.is-disabled {
      --field-border-color: var(--bms-field-disabled-border-color);
      --input-background-color: var(--bms-field-disabled-background);
      color: var(--bms-field-disabled-text-color);
      pointer-events: none;

      input:disabled {
        opacity: 1;
        -webkit-text-fill-color: currentColor;
      }
    }
  }

  .icon {
    &-container {
      height: 100%;
      display: flex;
      align-items: center;

      &:hover {
        cursor: pointer;
      }
    }
    &-button {
      width: 1em;
      margin: 0 var(--field-padding);
      display: block;
    }
  }
}

.combobox-option {
  display: flex;
  align-items: center;
  gap: 0.5em;
  &__check {
    width: 1em;
    display: flex;
    justify-content: center;
  }
  &--select-all {
    font-weight: 600;
    width: 100%;
  }
}
</style>
