<template>
  <div
    ref="autocompleteRef"
    class="mc-autocomplete"
    :class="classObject"
    :style="styleObject"
    @keydown.esc="closeListBox"
  >
    <div class="mc-autocomplete__main">
      <MTag
        v-if="multiple && listboxValue.length > 0 && !hasTagList"
        :id="tagId ? tagId : `autocompleteTag-${uuid}`"
        ref="tagRef"
        :label="getTagLabel"
        :disabled="disabled"
        type="removable"
        class="mc-autocomplete__tag"
        size="s"
        @remove-tag="clearListbox()"
      />
      <MTextInput
        :id="id"
        ref="triggerRef"
        v-model="inputValue"
        :placeholder="getPlaceholder"
        :is-invalid="invalid"
        :required="required"
        :is-valid="valid"
        :disabled="disabled"
        :size="size"
        :root-class="getRootClass"
        text-input-field-class="mc-autocomplete__trigger"
        icon-position="left"
        icon="DisplaySearch48"
        autocomplete="off"
        role="combobox"
        aria-autocomplete="list"
        :aria-expanded="state.open"
        :aria-controls="listBoxId"
        @click="onInputFocusGained"
        @focus="onInputFocusGained"
        @keydown.down="setFocusOnListBox"
      />
      <div
        v-if="!disabled && (loading || isClearable)"
        class="mc-autocomplete__tools"
      >
        <MLoader v-if="loading" class="mc-autocomplete__loader" size="s" />
        <button
          v-if="isClearable"
          class="mc-autocomplete__clear"
          type="button"
          @click="onClear"
        >
          <MIcon name="ControlTagCross24" class="mc-autocomplete__clear-icon" />
          <span class="mc-autocomplete__clear-text">
            {{ labelClearButton }}
          </span>
        </button>
      </div>
    </div>

    <Teleport :to="appendTo" :disabled="!appendTo">
      <Transition @before-enter="appendTo && onBeforeEnter()">
        <MListBox
          v-if="getOpenState"
          :id="listBoxId"
          ref="listboxRef"
          v-model="listboxValue"
          aria-live="polite"
          :class="classObjectListbox"
          :items="getListboxItems"
          :multiple="multiple"
          :data-text-expr="dataTextExpr"
          :data-value-expr="dataValueExpr"
          :data-disabled-expr="dataDisabledExpr"
          :select-all="selectAll"
          :label-select-all="labelSelectAll"
          :style="styleObjectListbox"
          :empty="getEmptyState"
          :empty-search-label="emptySearchLabel"
          :overlay="true"
          :max-width="maxWidth"
          width="100%"
          @update:model-value="onChangeListbox"
          @select-all="$emit('select-all')"
          @unselect-all="$emit('unselect-all')"
          @scrollend="$emit('scrollend')"
          @keydown.down="onChangeMadeWithArrowKey"
          @keydown.up="onChangeMadeWithArrowKey"
          @keydown.right="onChangeMadeWithArrowKey"
          @keydown.left="onChangeMadeWithArrowKey"
          @keydown.enter="onEnterKey"
          @keydown.space="onSpaceKey"
        >
          <template #item="{ item }">
            <!-- @slot Use this slot if you want to customize the label display of the listbox items -->
            <slot name="item" :item="item" />
          </template>
        </MListBox>
      </Transition>
    </Teleport>

    <!-- TODO: Plan to delete the "show-all" & "reduce" events, which are not very useful -->
    <!-- @slot Use this slot to insert a TagList component -->
    <slot name="taglist">
      <MTagList
        v-if="hasTagList"
        :items="getTagList"
        v-bind="tagList"
        @remove-tag="onRemoveTag"
        @show-all="(value) => $emit('show-all', value)"
        @reduce="(value) => $emit('reduce', value)"
        @click:show-all="(value) => $emit('taglist:show-all', value)"
      />
    </slot>
  </div>
</template>

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

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

// 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: '',
  },
  /**
   * The input placeholder
   */
  placeholder: {
    type: String,
    default: null,
  },
  /**
   * Your personal function to filter the items in the list
   */
  filter: {
    type: Boolean,
    default: true,
  },
  /**
   * 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,
  },
  /**
   * Allows to required the component
   */
  required: {
    type: Boolean,
    default: false,
  },
  /**
   * Allows to define the component as invalid
   */
  invalid: {
    type: Boolean,
    default: false,
  },
  /**
   * Allows to define the component as valid
   */
  valid: {
    type: Boolean,
    default: false,
  },
  /**
   * The input value
   */
  input: {
    type: String,
    default: '',
  },
  /**
   * The value of the `id` attribute of the input element
   */
  id: {
    type: String,
    default: null,
  },
  /**
   * Allows you to activate or deactivate the loader
   */
  loading: {
    type: Boolean,
    default: false,
  },
  /**
   * The clear button label. _(required for accessibility)_
   */
  labelClearButton: {
    type: String,
    default: 'Clear',
  },
  /**
   * 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: String, value: any, disabled: Boolean }]}
   */
  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,
  },
  /**
   * Allows to define the Listbox as empty _(hide the list items and display the `emptySearchLabel`)_
   */
  empty: {
    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 `label` to define the filtered label of the items in the Listbox
   */
  dataFilterExpr: {
    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',
  },
  /**
   * Allows to specify another key than `disabled` to define the value disabled of the items in the Listbox
   */
  dataDisabledExpr: {
    type: String,
    default: 'disabled',
  },
  /**
   * Enable/Disable the sorting of the listbox items. _(Only available for the multi-selection version of the component. Items are sorted each time the listbox is closed.)_
   */
  sort: {
    type: Boolean,
    default: true,
  },
  /**
   * Show or hide the clear button
   */
  clearable: {
    type: Boolean,
    default: true,
  },
  /**
   * Allows you to define a maximum size for the component
   */
  maxWidth: {
    type: String,
    default: '100%',
  },
  /**
   * An object that can contain all the accessories of the **MTagList** component.<br/>
   * **[DEPRECATED!!]** - This prop is deprecated and will be removed shortly. Use the `taglist` **slot** instead.
   * @type {Boolean}
   * @type {{ max: Number, showLabel: String, hideLabel: String, stateless: Boolean }}
   */
  tagList: {
    type: [Boolean, Object],
    default: false,
  },
  /**
   * Activate or not the "Select all" feature
   */
  selectAll: {
    type: Boolean,
    default: false,
  },
  /**
   * Label of the "Select all" item
   */
  labelSelectAll: {
    type: String,
    default: 'Select all',
  },
  /**
   * A query selector that defines the target element to which to append the content of the Teleport
   */
  appendTo: {
    type: String,
    default: undefined,
  },
});

// Emits
const emit = defineEmits([
  /** Triggered each time a value is selected. _(can be used with a v-model)_ */
  'update:modelValue',

  /** Triggered when the listbox is filtered following the input */
  'filter',

  /**  Triggered when the selected value is reset */
  'clear',

  /** Triggered when the Listbox opening status is updated. _(can be used with a v-model)_ */
  'update:open',

  /** Triggered when the Listbox is open */
  'open',

  /** Triggered when the Listbox is closed */
  'close',

  /** Triggered each time a value is entered in the text field. _(can be used with a v-model)_ */
  'update:input',

  /** Triggered when the input field is cleared */
  'clear-input',

  /** **[DEPRECATED!]** - Will be removed shortly. Use the `taglist` **slot** instead. */
  'show-all',

  /** **[DEPRECATED!]** - Will be removed shortly. Use the `taglist` **slot** instead. */
  'reduce',

  /** **[DEPRECATED!]** - Will be removed shortly. Use the `taglist` **slot** instead. */
  'taglist:show-all',

  /** Triggered when all Listbox items are selected */
  'select-all',

  /** Triggered when all Listbox items are unselected */
  'unselect-all',
  'scrollend',
]);

// Refs
const autocompleteRef = ref([]);
const tagRef = ref(null);
const triggerRef = ref(null);
const listboxRef = ref(null);
const listboxHeight = ref(221);
const shadowGap = ref(4);
const lastValueChangeMadeWithArrowKey = ref(false);
const listBoxId = `mc-autocomplete__listbox-${useId()}`;

// Data & state
const state = reactive({
  open: false,
  placeholder: '',
  triggerSize: null,
  visible: false,
});
const uuid = useId();
const tagSize = reactive(useElementBounding(tagRef));
const inputValue = ref('');
const localItems = 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;
}

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

const styleObject = computed(() => {
  return {
    '--autocomplete-tag-width': getTagWidth.value,
    '--autocomplete-width': props.maxWidth,
  };
});

const getOpenState = computed(() => {
  return (
    (state.open && !!props.items.length) ||
    (state.open && getEmptyState && !!inputValue.value.length)
  );
});

watch(
  () => props.open,
  (value) => {
    // Sort listbox when closed
    if (props.multiple && props.sort) {
      sortItems();
    }
    state.open = value;
  },
);

watch(getOpenState, (value) => {
  const eventName = value ? 'open' : 'close';
  emit(eventName);

  emit('update:open', value);
});

/**
 * TAGS
 *
 */
const getTagValue = computed(() => {
  return listboxValue?.value?.length;
});

const getTagLabel = computed(() => {
  return getTagValue.value.toString() + ' ' + props.tagLabel;
});

const getTagWidth = computed(() => {
  return tagRef.value && tagRef.value.$el ? `${tagSize.width}px` : '0px';
});

function onRemoveTag(value) {
  listboxValue.value = listboxValue.value.filter((item) => item !== value);

  onChangeListbox(listboxValue.value);
}

/**
 * TOOLS
 *
 */
const isClearable = computed(() => {
  return props.clearable && !!inputValue.value.length && !props.disabled;
});

function onClear() {
  // Clear Input
  clearInput();

  // Clear Listbox
  if (!props.multiple) {
    clearListbox();
  }
}

/**
 * INPUT
 *
 */

// Computed
const getRootClassInvalid = computed(() => {
  return (props.invalid && 'is-invalid') || null;
});

const getRootClassValid = computed(() => {
  return (props.valid && 'is-valid') || null;
});

const getRootClass = computed(() => {
  return getRootClassInvalid.value || getRootClassValid.value;
});

const getPlaceholder = computed(() => {
  return state.placeholder || props.placeholder;
});

// Methods
function clearInput() {
  inputValue.value = '';
}

function onInputFocusGained() {
  // Manage placeholder
  if (getSelectedLabels.value && !props.multiple) {
    clearInput();
    state.placeholder = getSelectedLabels.value;
  }

  // Open Listbox
  state.open = true;
}

// Watchers
watch(
  () => props.input,
  (value) => {
    inputValue.value = value;
  },
  { immediate: true },
);

watch(
  () => inputValue.value,
  (value) => {
    emit('update:input', value);

    if (!value.length) {
      emit('clear-input');
    }
  },
);

/**
 * LISTBOX
 *
 */
/* Computed */

const classObjectListbox = computed(() => {
  const spaceAbove = triggerSize.top;
  const spaceBelow = windowInnerHeight - triggerSize.bottom;
  const hasEnoughSpaceBelow = spaceBelow > listboxHeight.value;
  const hasEnoughSpaceAbove = spaceAbove > listboxHeight.value;

  return {
    'mc-autocomplete--above': hasEnoughSpaceBelow
      ? !hasEnoughSpaceBelow
      : hasEnoughSpaceAbove,
  };
});

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

const getListboxItems = computed(() => {
  return props.filter ? getFilteredList.value : localItems.value;
});

const getFilteredList = computed(() => {
  if (inputValue.value && props.filter) {
    return props.items.filter((item) =>
      item[props.dataFilterExpr ? props.dataFilterExpr : props.dataTextExpr]
        .toUpperCase()
        .includes(inputValue.value.toUpperCase()),
    );
  }

  return props.items;
});

const isFiltered = computed(() => {
  return !!inputValue.value.length && !getFilteredList.value.length;
});

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

  return selectedItems;
});

const getSelectedLabels = computed(() => {
  return getSelectedItems.value
    .map((item) => item[props.dataTextExpr])
    .join(', ');
});

const getEmptyState = computed(() => {
  return props.empty || isFiltered.value;
});

/* Methods */
function clearListbox() {
  listboxValue.value = props.multiple ? [] : undefined;

  emit('update:modelValue', listboxValue.value, getSelectedItems.value);

  emit('clear');
}

function closeListBox() {
  state.open = false;
  state.visible = false;

  // Manage placeholder
  if (getSelectedLabels.value && !props.multiple) {
    inputValue.value = getSelectedLabels.value;
    state.placeholder = '';
  }
}

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

  if (!props.multiple && !lastValueChangeMadeWithArrowKey.value) {
    inputValue.value = getSelectedLabels.value;
    closeListBox();
  }

  lastValueChangeMadeWithArrowKey.value = false;
}

function onChangeMadeWithArrowKey() {
  lastValueChangeMadeWithArrowKey.value = true;
}

function onEnterKey() {
  closeListBox();
}

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

async function setFocusOnListBox() {
  state.open = true;
  await nextTick();
  if (listboxRef.value) {
    listboxRef.value.focus();
  }
}

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

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

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

  localItems.value = sortedListItems;
}

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

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

      if (getSelectedLabels.value && !props.multiple) {
        inputValue.value = getSelectedLabels.value;
      }
    }
  },
  { immediate: true },
);

watch(
  () => getFilteredList.value,
  (value) => {
    emit('filter', value);
  },
);

/* rendering  */
onClickOutside(autocompleteRef, () => state.open && closeListBox(), {
  ignore: [listboxRef],
});

/**
 * TAGLIST
 *
 */
const getTagList = computed(() => {
  const selectedItems = getSelectedItems.value;
  const selectedTags = selectedItems.map((item) => ({
    ...item,
    size: 's',
    type: 'removable',
    id: item.id ?? item.value,
  }));

  return selectedTags;
});

const hasTagList = computed(() => {
  const hasTagList =
    typeof props.tagList === 'boolean'
      ? props.tagList
      : Object.keys(props.tagList).length;

  return props.multiple && props.tagList && hasTagList;
});
</script>

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

.mc-autocomplete {
  display: flex;
  flex-direction: column;
  gap: $mu150;

  &__main {
    position: relative;
  }

  &--above {
    transform: translateY(calc(-100% - var(--trigger-height)));
  }

  // Code duplication from @mozaic-ds/styles due to scoped usage
  &--multi {
    :deep(.mc-autocomplete__trigger) {
      padding-left: calc(2.9375rem + var(--autocomplete-tag-width, 0px));
    }
  }
}
</style>
