<template>
  <div
    ref="rootEl"
    :class="[cn.b(), cn.is('active', active), cn.is('filled', isFilled)]"
    @keydown.esc="onEscape"
  >
    <div
      ref="wrapperEl"
      :class="cn.e('wrapper')"
      tabindex="0"
      @click="toggleDropdown"
      @keydown.enter.space.prevent="toggleDropdown"
    >
      <p :class="cn.e('label')">
        {{ label }}
      </p>

      <div :class="cn.e('content')">
        <!--        <UiCoin -->
        <!--          v-if="selectedOptionCoin" -->
        <!--          :name="selectedOptionCoin" -->
        <!--          :class="cn.e('coin')" -->
        <!--          circle -->
        <!--        /> -->

        <input
          ref="controlEl"
          :value="selectedOptionLabel"
          type="text"
          :class="cn.e('control')"
          autocomplete="off"
          tabindex="-1"
          :placeholder="placeholder || $t('ui.select.select_option')"
          @keydown.enter.stop.prevent="toggleOption(options[0])"
        >

        <UiIconSCrossCompact
          v-if="clearable && isFilled"
          :class="cn.e('clear')"
          @click.stop="clear"
        />
      </div>

      <UiIconChevronDown :class="cn.e('chevron')" />
    </div>

    <Transition
      name="dropdown"
      @enter="$emit('opened')"
      @after-leave="onAfterLeave"
    >
      <div
        v-if="active"
        ref="popperEl"
        :class="cn.e('popper')"
        :style="floatingStyles"
      >
        <div
          ref="dropdownEl"
          role="listbox"
          :class="cn.e('dropdown')"
        >
          <UiSearch
            v-if="searchable"
            ref="searchInput"
            v-model="searchTerm"
            :class="cn.e('search')"
            :label="$t('ui.search')"
            size="small"
          />

          <div
            ref="itemsEl"
            :class="cn.e('items')"
          >
            <div
              ref="scrollerEl"
              :class="cn.e('items-scroller')"
            >
              <div
                :style="{
                  position: 'relative',
                  height: `${virtualizer?.getTotalSize()}px`,
                }"
              >
                <UiSelectOption
                  v-for="item in virtualItems"
                  :key="item.index"
                  :ref="(el) => virtualizer.measureElement(el?.$el)"
                  :data-index="item.index"
                  :label="getOptionLabel(item.option)"
                  :coin="getOptionCoin(item.option)"
                  :selected="isOptionSelected(item.option)"
                  :disabled="isOptionDisabled(item.option)"
                  :multiple="multiple"
                  :style="item.styles"
                  @click="toggleOption(item.option)"
                />
              </div>
            </div>
          </div>
        </div>
      </div>
    </Transition>

    <div
      v-if="invalid || caption"
      :class="[cn.e('bottom')]"
    >
      <span
        v-if="invalid"
        :class="[cn.e('validation-message')]"
      >
        {{ errorMessage }}
      </span>

      <span
        v-else-if="caption"
        :class="[cn.e('caption')]"
      >{{ caption }}</span>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, onMounted, ref, toRef, watch } from 'vue'
import { isEqual } from 'lodash-es'

// import UiCoin from 'alfabit-ui/components/coin/coin.vue';
import { useField } from 'vee-validate'
import { useI18n } from 'vue-i18n'
import { autoUpdate, useFloating } from '@floating-ui/vue'
import { useVirtualizer } from '@tanstack/vue-virtual'
import { observeElementRect } from '@tanstack/virtual-core'
import { onClickOutside } from '@vueuse/core'
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
import { useOverlayScrollbars } from 'overlayscrollbars-vue'
import UiSearch from '../search/index.vue'
import useFuseSearch from '../../composables/use-fuse-search.js'
import useClassname from '../../composables/use-classname'
import UiSelectOption from './option.vue'
import 'overlayscrollbars/overlayscrollbars.css'
import type { Props } from './types'

defineOptions({
  name: 'UiSelect',
})

const props = withDefaults(defineProps<Props>(), {
  disabled: false,
  required: false,
  clearable: false,
  searchable: false,
  multiple: false,
  emitValue: false,
  mapOptions: false,
})

const emit = defineEmits([
  'update:modelValue',
  'show',
  'closed',
  'opened',
  'blur',
  'focus',
])

const rootEl = ref()
const wrapperEl = ref()
const popperEl = ref()
const dropdownEl = ref()
const controlEl = ref()
const virtualScrollEl = ref()
const scrollbarEl = ref()

const itemsEl = ref()
const scrollerEl = ref()

const active = ref(false)
const searchTerm = ref('')

const cn = useClassname('ui-select')
const { t } = useI18n()
// TODO: Так как позиция всегда фиксированная, может быть убрать useFloating?
const { floatingStyles } = useFloating(wrapperEl, popperEl, {
  placement: 'bottom',
  whileElementsMounted: autoUpdate,
  open: active,
})

const { activate: activateFocusTrap, deactivate: deactivateFocusTrap }
  = useFocusTrap(rootEl)
const [initScrollbar, scrollbarInstance] = useOverlayScrollbars({
  options: {
    scrollbars: {
      theme: 'os-small-theme',
    },
  },
})

onClickOutside(rootEl, () => {
  toggleDropdown(false)
})

const isEmptyValue = value => [null, undefined, ''].includes(value)

const {
  value: modelValue,
  errorMessage,
  handleChange,
  handleBlur,
  validate,
} = useField(
  toRef(props, 'id'),
  computed(() => ({ required: props.required })),
  {
    initialValue: !isEmptyValue(props.modelValue)
      ? props.modelValue
      : undefined,
    validateOnValueUpdate: false,
    syncVModel: true,
  },
)

const invalid = computed(() => !props.disabled && !!errorMessage.value)

const localValue = computed(() => {
  const isEmpty = isEmptyValue(modelValue.value)
  const mapNull = props.mapOptions && !props.multiple
  let val = []

  if (!isEmpty || mapNull) {
    val
      = props.multiple && Array.isArray(modelValue.value)
        ? modelValue.value
        : [modelValue.value]
  }

  if (props.mapOptions) {
    const values = val.map((v) => {
      return props.options.find(opt => isEqual(getOptionValue(opt), v))
    })

    return isEmpty && mapNull ? values.filter(v => !!v) : values
  }

  return val
})

const isFilled = computed(() => localValue.value.length > 0)

const getOptionValue = getPropValueFn(props.optionValue, 'value')
const getOptionLabel = getPropValueFn(props.optionLabel, 'label')
const getOptionCoin = getPropValueFn(
  props.optionCoin,
  'coin',
  !props.forceCoin,
  true,
)

function isOptionDisabled(opt) {
  return getPropValueFn(props.optionDisabled, 'disabled')(opt) === true
}

const localOptionsValue = computed(() => {
  return localValue.value.map(opt => getOptionValue(opt))
})

const optionsLabelMap = computed(() => {
  return props.options.map((opt, idx) => ({
    index: idx,
    value: getOptionLabel(opt),
  }))
})

const selectedOptionLabel = computed(() => {
  if (props.multiple && localValue.value.length > 1)
    return t('ui.select.selected', [localValue.value.length])

  return getOptionLabel(localValue.value?.[0])
})

const selectedOptionCoin = computed(() => {
  if (props.multiple && localValue.value.length > 1)
    return null

  return getOptionCoin(localValue.value?.[0])
})

const { result: searchResult } = useFuseSearch(
  optionsLabelMap,
  {
    keys: ['value'],
  },
  searchTerm,
)

const options = computed(() => {
  return searchResult.value.map(opt => props.options[opt.index])
})

const virtualizer = useVirtualizer({
  get count() {
    return options.value.length
  },
  getScrollElement: () => scrollerEl.value,
  estimateSize: () => 44,
  overscan: 5,
  initialRect: {
    width: 1,
    height: 44,
  },
  observeElementRect: process.server ? () => {} : observeElementRect,
})

const virtualItems = computed(() => {
  return virtualizer.value.getVirtualItems().map((virtualItem) => {
    return {
      ...virtualItem,
      option: options.value[virtualItem.index],
      styles: {
        position: 'absolute',
        top: 0,
        left: 0,
        width: '100%',
        transform: `translateY(${virtualItem.start}px)`,
      },
    }
  })
})

watch(options, () => {
  virtualizer.value.scrollToOffset(0)
})

watch([itemsEl, scrollerEl], ([itemsEl, scrollerEl]) => {
  if (!itemsEl || !scrollerEl)
    return

  initScrollbar({
    target: itemsEl,
    elements: {
      viewport: scrollerEl,
    },
  })
})

watch(popperEl, (value) => {
  if (value)
    activateFocusTrap()
  else
    deactivateFocusTrap()
})

onMounted(async () => {
  if (!isEmptyValue(modelValue.value) && !props.disabled)
    await validate()
})

function clear() {
  handleChange(props.multiple ? [] : null)

  if (active.value)
    active.value = false
}

function toggleDropdown(state) {
  if (props.disabled)
    return

  if (typeof state === 'boolean') {
    active.value = state
  }
  else {
    if (!popperEl.value || !popperEl.value.contains(document.activeElement))
      active.value = !active.value
  }
}

function getPropValueFn(
  propValue,
  defaultVal,
  noPrimitives = false,
  omitDefault = false,
) {
  const val = propValue !== undefined ? propValue : defaultVal

  if (typeof val === 'function')
    return val

  return (opt) => {
    if (['string', 'number'].includes(typeof opt))
      return noPrimitives ? null : opt

    if (omitDefault)
      return opt?.[val] ?? null
    else
      return opt?.[val] ?? opt?.[defaultVal] ?? null
  }
}

function isOptionSelected(opt) {
  const val = getOptionValue(opt)

  return localOptionsValue.value.findIndex(v => isEqual(v, val)) > -1
}

function toggleOption(opt, keepOpen = false) {
  if (isOptionDisabled(opt))
    return

  const optValue = getOptionValue(opt)

  if (!props.multiple) {
    if (
      !isFilled.value
      || !isEqual(getOptionValue(localValue.value[0]), optValue)
    )
      handleChange(props.emitValue ? optValue : opt)

    !keepOpen && toggleDropdown(false)

    return
  }

  if (!isFilled.value) {
    handleChange([props.emitValue ? optValue : opt])

    return
  }

  const model = modelValue.value.slice()
  const index = localOptionsValue.value.findIndex(v => isEqual(v, optValue))

  if (index > -1)
    model.splice(index, 1)
  else
    model.push(props.emitValue ? optValue : opt)

  handleChange(model)
}

function onAfterLeave() {
  scrollbarInstance()?.destroy()

  if (props.searchable)
    searchTerm.value = ''
}

function onEscape(event) {
  if (active.value) {
    event.stopPropagation()
    active.value = false
  }
}
</script>

<style lang="scss">
@use 'styles';
</style>
