<template>
  <div
    class="mc-listbox"
    :class="classObject"
    :style="styleObject"
    :role="getRole"
    @scrollend="$emit('scrollend')"
  >
    <ul v-if="!empty" class="mc-listbox__list" :role="getRoleList" :aria-label="accessibilityLabel" :aria-activedescendant="activeDescendantRef">
      <li v-if="multiple && selectAll" class="mc-listbox__tile">
        <input
          :id="setItemId('selectall')"
          ref="selectAllRef"
          class="mc-checkbox__input mc-listbox__input"
          type="checkbox"
          :checked="getAllSelectedItems"
          :indeterminate="getIndeterminateState"
          @change="onChangeSelectAll"
          @keydown.up="focusPrevious"
          @keydown.down="focusNext"
        />
        <label :for="setItemId('selectall')" class="mc-listbox__label">
          {{ labelSelectAll }}
        </label>
      </li>
      <li
        v-for="(item, index) in localItems"
        :key="item.id"
        class="mc-listbox__tile"
        :class="setClassObjectTile(item)"
        role="option"
      >
        <MIcon
          v-if="item.icon"
          :name="item.icon"
          class="mc-listbox__icon"
          color="#666666"
        />
        <input
          :id="setItemId(index)"
          ref="inputsRef"
          v-model="localValue"
          class="mc-listbox__input"
          :class="classObjectInput"
          :checked="setCheckedAttr(item)"
          :disabled="setDisabledAttr(item)"
          :aria-disabled="setDisabledAttr(item)"
          :name="getInputName"
          :type="getInputType"
          :value="item[dataValueExpr]"
          @keydown.up.prevent="focusPrevious"
          @keydown.down.prevent="focusNext"
          @keydown.enter="onEnterKey"
          @keydown.space="onEnterKey"
        />
        <label :for="setItemId(index)" class="mc-listbox__label">
          <!-- @slot Use this slot if you want to customize the label display of the listbox items -->
          <slot name="item" :item="item">
            {{ item[dataTextExpr] }}
          </slot>
        </label>
      </li>
    </ul>
    <div v-else class="mc-listbox__tile mc-listbox__tile--empty">
      <span class="mc-listbox__label">{{ emptySearchLabel }}</span>
    </div>
  </div>
</template>

<script setup>
import { computed, ref, useId, watch } from 'vue';
import MIcon from '../icon/MIcon.vue';

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

/* props */
const props = defineProps({
  /**
   * An array of objects that allows you to provide all the data needed to generate the items of the Listbox.
   * @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
   * @deprecated
   * @ignore
   */
  open: {
    type: Boolean,
    default: false,
  },
  /**
   * Allows to activate the "multi-selection" version of the component
   */
  multiple: {
    type: Boolean,
    default: false,
  },
  /**
   * 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',
  },
  /**
   * Allows to specify another key than `disabled` to define the value disabled of the items in the Listbox
   */
  dataDisabledExpr: {
    type: String,
    default: 'disabled',
  },
  /**
   * Allows to disabled all the tiles in the component
   */
  disabled: {
    type: Boolean,
    default: false,
  },
  /**
   * Allows to define a width for the component
   */
  width: {
    type: String,
    default: undefined,
  },
  /**
   * Allows to define a maximum width for the component
   */
  maxWidth: {
    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',
  },
  /**
   * Select listbox type
   * @ignore
   */
  type: {
    type: String,
    default: 'options',
    validator: (value) => ['options', 'actions'].includes(value),
  },
  /**
   * Sets the "empty" state of the Listbox
   */
  empty: {
    type: Boolean,
    default: false,
  },
  /**
   * Text to be displayed in case of error or invalid search
   */
  emptySearchLabel: {
    type: String,
    default: 'No results found',
  },
  /**
   * Enables component display and positioning in overlay mode
   */
  overlay: {
    type: Boolean,
    default: false,
  },
  /**
   * Allows you pass the z-index
   */
  zIndex: {
    type: Number,
  },
  /**
   * Allows you pass the z-index overlay
   */
  zIndexOverlay: {
    type: Number,
  },
  /**
   * Aria-label attribute for the listbox/group element
   */
  accessibilityLabel: {
    type: String,
    default: null,
  },
});

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

/* exposes */
defineExpose({
  focus,
});

/* data & state */
const localItems = ref(null);
const localValue = ref(null);
const uuid = useId();
const inputsRef = ref([]);
const selectAllRef = ref(null);
const activeDescendantRef = ref(null);

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

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

watch(
  () => localValue.value,
  (value) => {
    /**
     * Triggered each time a value is selected. _(can be used with a v-model)_
     */
    emit('update:modelValue', value);
  },
);

/* computed */
const classObject = computed(() => {
  return {
    'mc-listbox--options': props.type === 'options',
    'mc-listbox--actions': props.type === 'actions',
    'mc-listbox--overlay': props.overlay,
    'mc-listbox--multi': props.multiple,
  };
});

const styleObject = computed(() => {
  return {
    '--listbox-maxwidth': props.maxWidth,
    '--listbox-width': props.width,
    '--listbox-label-z-index': props.zIndex,
    '--listbox-overlay-z-index': props.zIndexOverlay,
  };
});

const getRole = computed(() => {
  return props.type !== 'options' ? 'listbox' : null;
});

const getRoleList = computed(() => {
  return props.type === 'options' ? 'listbox' : 'group';
});

const classObjectInput = computed(() => {
  return {
    'mc-checkbox__input': props.multiple,
  };
});

const getLocalItemsValues = computed(() => {
  return localItems.value.map((item) => item[props.dataValueExpr]);
});

const getInputType = computed(() => {
  return props.multiple ? 'checkbox' : 'radio';
});

const getInputName = computed(() => {
  return !props.multiple ? `listboxradio-${uuid}` : null;
});

/* methods */
function focus() {
  if (props.selectAll) {
    activeDescendantRef.value = selectAllRef.value.id;
    selectAllRef.value.focus();
  } else if (inputsRef.value.length) {
    activeDescendantRef.value = inputsRef.value[0].id;
    inputsRef.value[0].focus();
  }
}

function setItemId(index) {
  return `listboxItem-${index + uuid}`;
}

function setClassObjectTile(item) {
  return {
    'is-disabled': item[props.dataDisabledExpr] || props.disabled,
  };
}

function setCheckedAttr(item) {
  const dataValueExpr = props.dataValueExpr;

  if (props.multiple) {
    return localValue.value.includes(item[dataValueExpr]);
  }

  return localValue.value === item[dataValueExpr];
}

function setDisabledAttr(item) {
  return item[props.dataDisabledExpr] || props.disabled;
}

function getAllFocusableInputs() {
  return selectAllRef.value
    ? [selectAllRef.value, ...inputsRef.value]
    : inputsRef.value;
}

function getFocusedInputIndex() {
  return getAllFocusableInputs().findIndex(
    (inputElement) => inputElement === document.activeElement,
  );
}

function focusInputFromCurrent(indexDelta) {
  const focusedInputIndex = getFocusedInputIndex();
  const allFocusableInputs = getAllFocusableInputs();

  let targetIndex = focusedInputIndex + indexDelta;
  if (targetIndex < 0) {
    targetIndex = allFocusableInputs.length - 1;
  } else if (targetIndex > allFocusableInputs.length - 1) {
    targetIndex = 0;
  }

  const focusCandidate = allFocusableInputs[targetIndex];
  if (focusCandidate.disabled) {
    focusInputFromCurrent(indexDelta >= 0 ? indexDelta + 1 : indexDelta - 1);
  } else {
    activeDescendantRef.value = allFocusableInputs[targetIndex].id;
    allFocusableInputs[targetIndex].focus();
  }
}

function focusPrevious(event) {
  if (props.multiple) {
    event.preventDefault();
  }
  focusInputFromCurrent(-1);
}

function focusNext(event) {
  if (props.multiple) {
    event.preventDefault();
  }
  focusInputFromCurrent(1);
}

function onEnterKey(event) {
  if (!props.multiple) {
    localValue.value = event.target.value;
  }
}

/**
 * SELECT ALL
 */
const getAllSelectedItems = computed(() => {
  return getLocalItemsValues.value.every((item) => {
    return localValue.value.includes(item);
  });
});

const getIndeterminateState = computed(() => {
  const hasSelectedValues = localValue.value.some((item) => {
    return getLocalItemsValues.value.includes(item);
  });

  return hasSelectedValues && !getAllSelectedItems.value;
});

function onChangeSelectAll(event) {
  const checked = event.target.checked;
  const selectedValues = [];

  if (checked) {
    localItems.value.map((item) => {
      selectedValues.push(item[props.dataValueExpr]);
    });
    localValue.value = selectedValues;
    emit('select-all');
  } else {
    emit('unselect-all');
    localValue.value = [];
  }
}
</script>

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

.mc-listbox__label {
  text-overflow: ellipsis;
  overflow: hidden;
  width: 100%;
}
</style>
