<template>
  <div ref="rootRef" class="mc-listbox-container">
    <slot name="trigger">
      <button
        ref="triggerRef"
        type="button"
        class="mc-listbox-options__trigger"
        @click="triggerClick()"
        @keydown.escape="isOpen = !isOpen"
        @keydown.tab="isOpen = false"
        @keydown.enter.prevent="triggerEnter()"
        @keydown.down.prevent="navigate(0)"
        @keydown.up.prevent="navigate(-1)"
      >
        <MIcon :name="icon" :color="triggerIconColor" />
        <span class="mc-listbox-options__trigger-label">
          {{ triggerLabel }}
        </span>
      </button>
    </slot>
    <Teleport :to="appendTo">
      <Transition @before-enter="onBeforeEnter">
        <div
          v-show="isOpen"
          ref="listboxRef"
          class="mc-listbox mc-listbox--actions mc-listbox--overlay"
          :class="classObjectListBox"
          :style="styleObjectListBox"
          role="listbox"
          aria-labelledby="listbox"
          tabindex="0"
          :aria-expanded="isOpen"
          @keydown="handleKeydown"
        >
          <ul
            v-for="(list, i) in getItems"
            :key="`list${i}`"
            class="mc-listbox__list"
            role="listbox"
            :aria-activedescendant="ariaActivedescendant(i)"
          >
            <li
              v-for="(item, j) in list"
              :id="item.id"
              :key="`item${j}`"
              class="mc-listbox__tile"
              role="option"
              :class="{
                'is-disabled': item.disabled,
                'is-danger': item.danger,
              }"
            >
              <MIcon
                v-if="item.icon"
                :name="item.icon"
                class="mc-listbox__icon"
              />
              <component
                :is="item.href ? 'a' : 'button'"
                :id="`item-${i}-${j}`"
                :ref="`item-${i}-${j}`"
                :href="item.href ? item.href : null"
                :type="item.href ? null : 'button'"
                :disabled="item.disabled ? true : null"
                class="mc-listbox__label"
                :class="{
                  'is-disabled': item.disabled,
                }"
                :data-testid="item.testid"
                tabindex="-1"
                @click.self="onClickItem(item, i, j)"
              >
                {{ item.text }}
              </component>
            </li>
          </ul>
        </div>
      </Transition>
    </Teleport>
  </div>
</template>

<script>
import { reactive, ref } from 'vue';
import { onClickOutside, useWindowScroll, useWindowSize } from '@vueuse/core';
import MIcon from '../icon/MIcon.vue';

export default {
  name: 'MListBoxActions',
  components: {
    MIcon,
  },
  props: {
    /**
     * An array of objects that allows you to provide all the data needed to generate the items of the Listbox.
     * Can accept an array of arrays to create groups of actions.
     * @type {[{ text: 'First Item', value: 1 }, { text: 'Second Item', value: 2 }]}
     */
    items: {
      type: Array,
      default: () => [],
    },
    /**
     * Allows to make the Listbox visible or hide it
     */
    open: {
      type: Boolean,
      default: false,
    },
    /**
     * Allows you to define whether the list should be left _(default)_ or right
     * @values left, right
     */
    position: {
      type: String,
      default: 'left',
    },
    /**
     * Opening button label _(useful for accessibility reasons)_
     */
    triggerLabel: {
      type: String,
      default: 'Display options',
    },
    /**
     * Allows to set a colour for the opening button
     */
    triggerIconColor: {
      type: String,
      default: 'currentColor',
    },
    /**
     * Name of the icon to be used in the opening button
     */
    icon: {
      type: String,
      default: 'DisplayOptions24',
    },
    /**
     * A query selector that defines the target element to which to append the content of the Teleport
     */
    appendTo: {
      type: String,
      default: 'body',
    },
    /**
     * Allows to adapts the width of the Listbox to the width of the component's parent div
     */
    fitContent: {
      type: Boolean,
      default: false,
    },
    /**
     * Allows to set the first item to be focused, -1 will focus the last item _(useful for accessibility with custom trigger)_
     */
    firstItemIndexToFocus: {
      type: Number || null,
      default: null,
    },
    /**
     * Allows you pass the z-index
     */
    zIndex: {
      type: Number,
    },
    /**
     * Allows you pass the z-index overlay
     */
    zIndexOverlay: {
      type: Number,
    },
  },
  emits: [
    'update:open',
    'update:itemSelected',
    'item-clicked',
    'focus-trigger',
  ],
  setup(props, { emit }) {
    const isOpen = ref(props.open);
    const isVisible = ref(false);
    const triggerRef = ref(null);
    const listboxRef = ref(null);
    const rootRef = ref(null);
    const rootSize = ref(null);
    const { height: windowInnerHeight } = reactive(useWindowSize());
    const { y: windowScrollY } = reactive(useWindowScroll());
    const itemRefIndexToFocus = ref(null);

    function closeListbox() {
      if (!isOpen.value) {
        return;
      }

      isOpen.value = false;
      isVisible.value = false;

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

    onClickOutside(rootRef, closeListbox);

    return {
      isOpen,
      isVisible,
      triggerRef,
      listboxRef,
      rootRef,
      rootSize,
      windowInnerHeight,
      windowScrollY,
      closeListbox,
      itemRefIndexToFocus,
    };
  },
  data() {
    return {
      displayTop: false,
      currentFocusItemRef: '',
    };
  },
  computed: {
    classObjectListBox() {
      const listboxHeight = this.listboxRef?.clientHeight;
      const spaceAbove = this.rootSize?.top;
      const spaceBelow = this.windowInnerHeight - this.rootSize?.bottom;
      const hasEnoughSpaceBelow = spaceBelow > listboxHeight;
      const hasEnoughSpaceAbove = spaceAbove > listboxHeight;

      return {
        'is-open': this.isVisible,
        'is-align-right': this.position === 'right',
        'is-align-top': hasEnoughSpaceBelow
          ? !hasEnoughSpaceBelow
          : hasEnoughSpaceAbove,
      };
    },
    styleObjectListBox() {
      if (!this.rootSize) {
        return;
      }

      const { bottom, right, left, height, width } = this.rootSize;
      return {
        top: `${bottom + window.scrollY}px`,
        left: this.position === 'right' ? `${right}px` : `${left}px`,
        '--listbox-maxwidth': this.fitContent ? `${width}px` : null,
        '--listbox-width': this.fitContent ? '100%' : null,
        '--root-height': `${height}px`,
        '--listbox-label-z-index': this.zIndex,
        '--listbox-overlay-z-index': this.zIndexOverlay,
      };
    },
    getItems() {
      const hasNestedArray = this.items.filter(Array.isArray).length;

      if (hasNestedArray === 0) {
        const listItems = [];
        listItems.push(this.items);

        return [this.items];
      }
      return this.items;
    },
    getItemsRefs() {
      return this.getItems.flatMap((items, i) =>
        items.reduce((acc, item, j) => {
          if (!item.disabled) {
            acc.push(`item-${i}-${j}`);
          }
          return acc;
        }, []),
      );
    },
    ariaActivedescendant() {
      return (listIndex) =>
        !!this.currentFocusItemRef &&
        this.currentFocusItemRef.startsWith(`item-${listIndex}-`)
          ? this.currentFocusItemRef
          : null;
    },
  },
  watch: {
    open(newValue) {
      this.isOpen = newValue;
    },
    firstItemIndexToFocus(index) {
      this.itemRefIndexToFocus = index;
    },
    isOpen(open) {
      this.$nextTick(() => {
        if (open) {
          if (this.itemRefIndexToFocus !== null) {
            const itemIndexToFocus =
              this.itemRefIndexToFocus >= 0
                ? this.itemRefIndexToFocus
                : this.getItemsRefs.length - 1; // Compute the first option to focus
            this.currentFocusItemRef = this.getItemsRefs[itemIndexToFocus];
          }
        } else {
          this.itemRefIndexToFocus = null;
          this.currentFocusItemRef = '';
        }
      });
    },
    currentFocusItemRef(ref) {
      if (!!ref && !!this.$refs[ref]) {
        this.$refs[ref][0].focus();
      }
    },
  },
  created() {
    window.addEventListener('scroll', this.closeListbox, {
      capture: true,
    });
  },
  unmounted() {
    window.removeEventListener('scroll', this.closeListbox);
  },
  methods: {
    onClickItem(item, listIndex, itemIndex) {
      const valToEmit = { listIndex: listIndex, itemIndex: itemIndex, ...item };
      this.isOpen = false;
      this.$emit('update:itemSelected', valToEmit); // TODO: deprecated
      this.$emit('item-clicked', valToEmit);
    },
    onBeforeEnter() {
      this.rootSize = reactive(this.rootRef?.getBoundingClientRect());
      this.isVisible = true;
    },
    handleKeydown(event) {
      switch (event.key) {
        case 'ArrowUp':
          this.navigate(-1);
          event.preventDefault();
          break;
        case 'ArrowDown':
          this.navigate(1);
          event.preventDefault();
          break;
        case 'Tab':
        case 'Escape':
          this.focusTrigger();
          this.closeListbox();
          break;
        default:
          break;
      }
    },
    /**
     * Navigates forward or backward between options
     * @param direction Less than 0 to go to back, otherwise greater
     */
    navigate(direction) {
      if (!this.isOpen) {
        this.itemRefIndexToFocus = direction;
        this.isOpen = true;
      } else {
        let currentRefIndex = this.getItemsRefs.findIndex(
          (ref) => ref === this.currentFocusItemRef,
        );
        if (currentRefIndex < 0) {
          currentRefIndex = 0;
        }
        let newRefIndexToFocus =
          (currentRefIndex + direction + this.getItemsRefs.length) %
          this.getItemsRefs.length;
        this.currentFocusItemRef = this.getItemsRefs[newRefIndexToFocus];
      }
    },
    focusTrigger() {
      if (this.triggerRef) {
        this.$nextTick(() => this.triggerRef.focus());
      } else {
        this.$emit('focus-trigger');
      }
    },
    triggerClick() {
      this.itemRefIndexToFocus = null;
      this.isOpen = !this.isOpen;
    },
    triggerEnter() {
      if (!this.isOpen) {
        this.itemRefIndexToFocus = 0;
        this.isOpen = true;
      }
    },
  },
};
</script>

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

.mc-listbox {
  &-container {
    display: inline-block;
    position: relative;
  }

  &-options__trigger {
    align-items: center;
    background: none;
    border: none;
    cursor: pointer;
    display: flex;
    height: $mu150;
    justify-content: center;
    padding: 0;
    width: $mu150;

    &-label {
      @include visually-hidden();
    }
  }

  &--actions {
    &.is-align-top {
      transform: translate(0, calc(-100% - var(--root-height)));
    }

    &.is-align-right {
      transform: translate(-100%, 0);

      &.is-align-top {
        transform: translate(-100%, calc(-100% - var(--root-height)));
      }
    }
  }
}
</style>
