<template>
  <div
    ref="dropdownRef"
    class="mc-dropdown"
    :class="classObject"
    :style="setStyles"
  >
    <div class="mc-dropdown__main">
      <MTag
        v-if="multiple && listboxValue.length > 0"
        :id="tagId ? tagId : `dropdownTag-${uuid}`"
        ref="tagRef"
        :label="setTagLabel"
        :disabled="disabled"
        type="removable"
        class="mc-dropdown__tag"
        size="s"
        @remove-tag="clearListbox()"
      />
      <button
        ref="triggerRef"
        type="button"
        class="mc-select mc-dropdown__trigger"
        :class="classObjectTrigger"
        :disabled="disabled"
        :aria-labelledby="ariaLabelledBy"
        @click="openState = !openState"
      >
        {{ getButtonLabel }}
      </button>
      <div class="mc-dropdown__tools">
        <button
          v-if="isClearable"
          class="mc-dropdown__clear"
          type="button"
          @click="clearListbox"
        >
          <MIcon name="ControlTagCross24" class="mc-dropdown__clear-icon" />
          <span class="mc-dropdown__clear-text">{{ labelClearButton }}</span>
        </button>
      </div>
    </div>
    <Teleport :to="appendTo" :disabled="!appendTo">
      <Transition @before-enter="appendTo && onBeforeEnter()">
        <MListBox
          v-if="openState"
          ref="listboxRef"
          v-model="listboxValue"
          :class="classObjectListbox"
          :open="openState"
          :items="localItems"
          :multiple="multiple"
          :empty-search-label="emptySearchLabel"
          :data-text-expr="dataTextExpr"
          :data-value-expr="dataValueExpr"
          :select-all="selectAll"
          :label-select-all="labelSelectAll"
          :style="styleObjectListbox"
          :overlay="true"
          :max-width="maxWidth"
          width="100%"
          @update:model-value="onChangeListbox"
          @select-all="$emit('select-all')"
          @unselect-all="$emit('unselect-all')"
          @scrollend="$emit('scrollend')"
        >
          <template #item="{ item }">
            <!-- @slot Use this slot if you want to customize the label display of the listbox items -->
            <slot name="item" :item="item"></slot>
          </template>
        </MListBox>
      </Transition>
    </Teleport>
  </div>
</template>

<script setup>
import { computed, nextTick, ref, reactive, useId, watch } from 'vue';
import {
  onClickOutside,
  useElementBounding,
  useWindowSize,
} from '@vueuse/core';
import MIcon from '../icon/MIcon.vue';
import MTag from '../tags/MTag.vue';
import MListBox from '../listbox/MListBox.vue';

/**
 * The `MDropdown` component is the **Vue.js** implementation of the **Dropdown** component of Mozaic Design System.<br/>
 * The full specification of this component is available in [the associated documentation](https://mozaic.adeo.cloud/components/form/dropdown/).
 */

/* props */
const props = defineProps({
  /**
   * Id of the MTag component. _(Only available for the multi-selection version of the component)_
   */
  tagId: {
    type: String,
    default: null,
  },
  /**
   * MTag component label. _(Only available for the multi-selection version of the component, it is displayed inside the tag, after the number of selected elements)_
   */
  tagLabel: {
    type: String,
    default: '',
  },
  /**
   * Component placeholder
   */
  placeholder: {
    type: String,
    default: '-- Placeholder --',
  },
  /**
   * Component size
   * @values s, m
   */
  size: {
    type: String,
    default: 'm',
    validator: (value) => ['s', 'm'].includes(value),
  },
  /**
   * Allows to disabled the component
   */
  disabled: {
    type: Boolean,
    default: false,
  },
  /**
   * An array of objects that allows you to provide all the data needed to generate the items of the Listbox. _(can be used with a v-model)_
   * @type {[{ label: 'First Item', value: 1 }, { label: 'Second Item', value: 2 }]}
   */
  items: {
    type: Array,
    required: true,
  },
  /**
   * Value of the selected item(s). _(Can be used with a v-model)_
   */
  modelValue: {
    type: [Array, String, Number, Object],
    default: undefined,
  },
  /**
   * Allows to make the Listbox visible or hide it
   */
  open: {
    type: Boolean,
    default: false,
  },
  /**
   * Allows to activate the "multi-selection" version of the component
   */
  multiple: {
    type: Boolean,
    default: false,
  },
  /**
   * Text to be displayed in case of error or invalid search
   */
  emptySearchLabel: {
    type: String,
    default: 'No results found',
  },
  /**
   * Allows to specify another key than `label` to define the label of the items in the Listbox
   */
  dataTextExpr: {
    type: String,
    default: 'label',
  },
  /**
   * Allows to specify another key than `value` to define the value of the items in the Listbox
   */
  dataValueExpr: {
    type: String,
    default: 'value',
  },
  /**
   * Enable (or not) the automatic sorting of the listbox items
   */
  sort: {
    type: Boolean,
    default: true,
  },
  /**
   * Show or hide the clear button
   */
  clearable: {
    type: Boolean,
    default: true,
  },
  /**
   * The clear button label. _(required for accessibility)_
   */
  labelClearButton: {
    type: String,
    default: 'Clear',
  },
  /**
   * Allows you to define a maximum size for the component
   */
  maxWidth: {
    type: String,
    default: '100%',
  },
  /**
   * Allows to define the component as valid
   */
  isValid: {
    type: Boolean,
    default: false,
  },
  /**
   * Allows to define the component as invalid
   */
  isInvalid: {
    type: Boolean,
    default: false,
  },
  /**
   * A query selector that defines the target element to which to append the content of the Teleport
   */
  appendTo: {
    type: String,
    default: undefined,
  },
  /**
   * Activate or not the "Select all" feature
   */
  selectAll: {
    type: Boolean,
    default: false,
  },
  /**
   * Label of the "Select all" item
   */
  labelSelectAll: {
    type: String,
    default: 'Select all',
  },
  /**
   * Value of the `aria-labelledby` attribute used to link the MDropdown component to a text element acting as a label via its `id`
   */
  ariaLabelledBy: {
    type: String,
    default: null,
  },
});

/* emits */
const emit = defineEmits([
  'update:modelValue',
  'update:open',
  'clear',
  'open',
  'close',
  'select-all',
  'unselect-all',
  'scrollend',
]);

/* refs */
const dropdownRef = ref([]);
const tagRef = ref(null);
const triggerRef = ref(null);
const listboxRef = ref(null);
const listboxHeight = ref(221);
const shadowGap = ref(4);

/* data & state */
const state = reactive({
  visible: false,
});
const uuid = useId();
const openState = ref(props.open);
const tagWidth = ref('0px');
const tagValue = ref(null);
const localItems = ref(null);
const sortedListItems = ref(null);
const listboxValue = ref(null);
let triggerSize = reactive(useElementBounding(triggerRef));
const { height: windowInnerHeight } = reactive(useWindowSize());

function onBeforeEnter() {
  triggerSize = reactive(useElementBounding(triggerRef));
  state.visible = true;
}

/* watch */
watch(
  () => props.items,
  (value) => {
    localItems.value = value;
  },
  { immediate: true },
);

watch(
  () => props.modelValue,
  (value) => {
    if (!value && props.multiple) {
      listboxValue.value = [];
    } else {
      listboxValue.value = value;
    }
  },
  { immediate: true },
);

watch(
  () => listboxValue.value,
  (value) => {
    if (props.multiple) {
      tagValue.value = value;
    }
  },
  { deep: true, immediate: true },
);

watch(
  tagValue,
  async () => {
    await nextTick();
    setTagWidth();
  },
  { immediate: true },
);

watch(
  openState,
  (value) => {
    if (value) {
      /**
       * Triggered when the Listbox is opened
       */
      emit('open');
    } else {
      /**
       * Triggered when the Listbox is closed
       */
      emit('close');
    }

    /**
     * Triggered when the Listbox opening status is updated. _(can be used with a v-model)_
     */
    if (props.multiple && props.sort) {
      sortItems();
    }
    emit('update:open', value);
  },
  { immediate: true },
);

/* computed */
const classObject = computed(() => {
  return {
    'mc-dropdown--multi': props.multiple,
    'mc-dropdown--clearable': isClearable.value,
  };
});

const setStyles = computed(() => {
  return {
    '--dropdown-tag-width': tagWidth.value,
    '--dropdown-width': props.maxWidth,
  };
});

const setTagLabel = computed(() => {
  return listboxValue.value.length.toString() + ' ' + props.tagLabel;
});

const getListboxValue = computed(() => {
  const val = props.multiple
    ? listboxValue.value.length
    : listboxValue.value || !!(listboxValue.value === 0);

  return !!val;
});

const isClearable = computed(() => {
  return props.clearable && !props.disabled && getListboxValue.value;
});

const classObjectTrigger = computed(() => {
  return {
    'is-open': openState.value,
    'mc-select--s': props.size === 's',
    'is-valid': props.isValid,
    'is-invalid': props.isInvalid,
  };
});

const classObjectListbox = computed(() => {
  const spaceAbove = triggerSize.top;
  const spaceBelow = windowInnerHeight - triggerSize.bottom;
  const hasEnoughSpaceBelow = spaceBelow > listboxHeight.value;
  const hasEnoughSpaceAbove = spaceAbove > listboxHeight.value;
  return {
    'mc-dropdown--above': hasEnoughSpaceBelow
      ? !hasEnoughSpaceBelow
      : hasEnoughSpaceAbove,
  };
});

const getButtonLabel = computed(() => {
  let label = props.placeholder;

  if (props.modelValue && !props.items.length) {
    return label;
  }

  const value = listboxValue.value;
  const selectedItems = getSelectedItems(value);
  const selectedLabels = selectedItems.map((item) => item[props.dataTextExpr]);

  label =
    ((!value || value.length === 0) && props.placeholder) ||
    selectedLabels.join(', ');

  return label;
});

const styleObjectListbox = computed(() => {
  const triggerHeight = {
    '--trigger-height': `${triggerSize.height + shadowGap.value * 2}px`,
  };

  if (props.appendTo && state.visible) {
    return {
      top: `${triggerSize.bottom + window.scrollY + shadowGap.value}px`,
      left: `${triggerSize.left}px`,
      width: `${triggerSize.width}px`,
      zIndex: 1999999999,
      ...triggerHeight,
    };
  }

  return {
    top: `${triggerSize.height + shadowGap.value}px`,
    ...triggerHeight,
  };
});

/* methods */
function setTagWidth() {
  tagWidth.value =
    tagRef.value && tagRef.value.$el
      ? `${tagRef.value.$el.clientWidth + 8}px`
      : '0px';
}

function clearListbox() {
  listboxValue.value = props.multiple ? [] : undefined;
  onChangeListbox();
  /**
   * Triggered when the selected value is reset
   */
  emit('clear');
}

function closeListBox() {
  openState.value = false;
  state.visible = false;

  if (props.multiple && props.sort) {
    sortItems();
  } else {
    localItems.value = props.items;
  }
}

function onChangeListbox() {
  /**
   * Triggered each time a value is selected. _(can be used with a v-model)_
   */
  emit(
    'update:modelValue',
    listboxValue.value,
    getSelectedItems(listboxValue.value),
  );

  if (!props.multiple) {
    closeListBox();
  }
}

function getSelectedItems(val) {
  const value = val ? val : listboxValue.value;
  const selectedItems = props.items.filter((item) => {
    return props.multiple
      ? value.includes(item[props.dataValueExpr])
      : value === item[props.dataValueExpr];
  });

  return selectedItems;
}

function sortItems() {
  sortedListItems.value = [...props.items];
  const selectedItems = ref(getSelectedItems());

  sortedListItems.value.sort((a, b) => {
    const hasItemA = selectedItems.value.includes(a);
    const hasItemB = selectedItems.value.includes(b);

    if (hasItemA === hasItemB) {
      return a[props.dataValueExpr] - b[props.dataValueExpr];
    } else if (hasItemA < hasItemB) {
      return 1;
    } else {
      return -1;
    }
  });

  localItems.value = sortedListItems.value;
}

/* rendering  */
onClickOutside(dropdownRef, () => closeListBox(), { ignore: [listboxRef] });
</script>

<style lang="scss" scoped>
@import 'settings-tools/all-settings';
@import 'components/c.select';
@import 'components/c.dropdown';

.mc-dropdown {
  &--above {
    transform: translateY(calc(-100% - var(--trigger-height)));
  }
}
</style>
