<template>
  <div>
    <div class="mb-1 flex w-full items-center justify-between" v-if="label">
      <div class="flex items-center">
        <!-- label of multiselect field -->
        <label v-if="label" class="text-sm font-medium text-gray-900">{{
          label
        }}</label>
        <!-- asterisk sign to render if field is required -->
        <p v-if="isRequired" class="ml-1 text-red-600">*</p>
      </div>
      <span v-if="hint" class="text-sm text-gray-500">{{ hint }}</span>
    </div>
    <div class="relative">
      <v-select
        ref="vselect"
        :class="[
          'disabled:custom-disabled-state absolute inline-block h-full w-full text-sm',
          isMultiple && showOverFlowCount ? 'c-multiselect--capped-pills' : '',
        ]"
        :placeholder="placeholder"
        :multiple="isMultiple"
        :taggable="isTaggable"
        :push-tags="pushTags"
        :filterable="filterable"
        @keydown.native="handleKeydown"
        v-model="value"
        :options="isGrouped ? groupedArray : options"
        @input="setSelectedOptions"
        @search="fetchOptions"
        @search:focus="$emit('search:focus')"
        @option:selecting="handleSingleSelect"
        @option:selected="($event) => (addCheckBox ? null : $event)"
        @option:deselected="($event) => (addCheckBox ? null : $event)"
        :label="optionLabel"
        :clearable="clearable"
        :loading="loading"
        :id="id"
        @open="onOpen"
        @close="onClose"
        :disabled="isDisabled"
        :selectable="
          isGrouped
            ? (option) => (option.header ? false : getDisableOptions(option))
            : (option) => getDisableOptions(option)
        "
        :closeOnSelect="addCheckBox ? false : isGrouped ? false : true"
      >
        <template #open-indicator="{ attributes }">
          <span
            v-bind="attributes"
            :style="!shouldIconRotate ? 'transform:none;' : ''"
          >
            <c-icon
              v-if="icon"
              :name="icon.name"
              :type="icon.type"
              class="h-5 w-5 text-gray-400"
            />
            <c-icon
              v-else
              name="chevron-down"
              type="solid"
              class="h-5 w-5 text-gray-400"
            />
          </span>
        </template>
        <template
          #selected-option-container="{ option, deselect, multiple, disabled }"
        >
          <template v-for="row in [getPillRow(option)]">
            <div
              v-if="row && row.showPill"
              :key="row.slotKey"
              class="vs__selected vs__selected--capped inline-flex items-center gap-1.5"
            >
              <p
                v-if="!option.header"
                :class="[
                  'vs__selected-inner inline-flex min-w-0 max-w-[120px] items-center gap-1.5 rounded bg-gray-100 px-2 py-1 text-gray-800',
                  isDisabled ? 'text-gray-500' : '',
                ]"
              >
                <c-icon
                  v-if="option.icon"
                  :name="option.icon.name"
                  :type="option.icon.type"
                  :class="[
                    'h-4 w-4 flex-shrink-0 text-gray-400',
                    option.icon.optionIconClass,
                  ]"
                />
                <c-avatar
                  v-if="option.image && displaySelectedOptionAvatar"
                  :size="
                    option.description && showAdditionalText
                      ? 'extrasmall'
                      : 'extraextrasmall'
                  "
                  :image="option.image"
                  :rounded="true"
                  :description="option.description"
                  class="flex-shrink-0"
                ></c-avatar>
                <c-avatar
                  v-else-if="option.initials && displaySelectedOptionAvatar"
                  size="extraextrasmall"
                  :nameInitials="option.initials"
                  :description="option.description"
                  :rounded="true"
                  :isDynamicallyColored="true"
                  class="flex-shrink-0"
                ></c-avatar>
                <span class="min-w-0 flex-1 truncate">{{ row.label }}</span>
                <button
                  v-if="multiple"
                  type="button"
                  class="flex h-2 w-2 flex-shrink-0 items-center justify-center rounded text-gray-400 focus:outline-none"
                  :disabled="disabled"
                  :title="`Deselect ${row.label}`"
                  :aria-label="`Deselect ${row.label}`"
                  @click.stop.prevent="deselect(option)"
                >
                  <span class="text-base leading-none" aria-hidden="true"
                    >×</span
                  >
                </button>
              </p>
              <span
                v-if="row.showOverflowBadge"
                class="vs__selected-overflow inline-flex w-max rounded bg-gray-100 px-2 py-1 text-gray-800"
                aria-live="polite"
              >
                +{{ selectedOverflowCount }}
              </span>
            </div>
          </template>
        </template>
        <!-- eslint-disable-next-line vue/no-unused-vars  -->
        <template #no-options="{ search, searching, loading }">
          <span v-if="search.length < 1"
            >Start typing to search for options...</span
          >
          <span v-else>No options found, try searching something else.</span>
        </template>
        <template #spinner="{ loading }">
          <svg
            class="h-5 w-5 animate-spin text-gray-400"
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 24 24"
            v-if="loading"
          >
            <circle
              class="opacity-25"
              cx="12"
              cy="12"
              r="10"
              stroke="currentColor"
              stroke-width="4"
            ></circle>
            <path
              class="opacity-75"
              fill="currentColor"
              d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
            ></path>
          </svg>
        </template>
        <template v-slot:option="option">
          <slot
            v-if="$scopedSlots['custom-option']"
            name="custom-option"
            :option="option"
          />
          <template v-else>
            <div v-if="!isGrouped">
              <div v-if="option.hasNoData" class="dropdown-menu-bordered"></div>
              <div v-else class="mt-1 flex h-full w-full items-center">
                <div @click.stop v-if="addCheckBox" class="flex-shrink-0">
                  <c-checkbox
                    :value="isChecked(option)"
                    @onChange="handleSingleSelect(option)"
                  />
                </div>
                <p v-if="showOptionImage" class="flex-shrink-0">
                  <c-avatar
                    v-if="option[imageLabel]"
                    :size="
                      option.description && showAdditionalText
                        ? 'extrasmall'
                        : 'extraextrasmall'
                    "
                    :image="option[imageLabel]"
                    :rounded="true"
                    :description="option.description"
                    class="mr-2"
                  ></c-avatar>
                  <c-avatar
                    v-if="
                      !option.icon && !option[imageLabel] && option.initials
                    "
                    :size="
                      option.description && showAdditionalText
                        ? 'extrasmall'
                        : 'extraextrasmall'
                    "
                    :nameInitials="option.initials"
                    :description="option.description"
                    :rounded="true"
                    :isDynamicallyColored="true"
                    class="mr-2"
                  ></c-avatar>
                  <c-icon
                    v-if="
                      option.icon && !option[imageLabel] && !option.initials
                    "
                    :name="option.icon.name"
                    :type="option.icon.type"
                    :class="`mr-2 h-5 w-5 text-gray-400 ${
                      option.icon.optionIconClass
                        ? option.icon.optionIconClass
                        : ''
                    }`"
                    cursorType="cursor-pointer"
                  />
                </p>
                <div class="w-full">
                  <div
                    :class="`flex items-center justify-between ${checkboxWrapperClass}`"
                  >
                    <div :class="`flex items-center ${tagWrapperClass}`">
                      <p>
                        {{
                          option[optionLabelSecondary]
                            ? option[optionLabelSecondary]
                            : option[optionLabel]
                        }}
                      </p>
                      <div class="ml-3">
                        <c-tag
                          v-if="tags && option.info && option.info !== ''"
                          :label="option.info"
                          color="gray"
                        ></c-tag>
                        <span v-else class="-ml-3 text-gray-300">
                          &nbsp;{{ option.info }}
                        </span>
                      </div>
                    </div>
                  </div>
                  <p
                    class="text-xs"
                    v-if="option.description && showAdditionalText"
                  >
                    {{ option.description }}
                  </p>
                </div>
              </div>
            </div>
            <div
              v-if="isGrouped"
              :class="[
                option.header
                  ? 'sticky-group-header -mx-4 -my-2 bg-gray-200 px-3 py-2'
                  : '',
              ]"
            >
              <v-list-subheader
                v-if="option.header"
                class="flex items-center gap-1 text-gray-900"
              >
                <c-icon
                  v-if="option.icon"
                  :name="option.icon.name"
                  :type="option.icon.type"
                  :class="`${
                    option.icon.headerIconClass
                      ? option.icon.headerIconClass
                      : 'h-4 w-4'
                  } text-gray-400`"
                />
                {{ option.header }}
              </v-list-subheader>
              <div
                :class="[
                  'mt-1 flex h-full w-full',
                  option.description ? 'items-start' : 'items-center',
                ]"
              >
                <div @click.stop v-if="addCheckBox && !option.header">
                  <c-checkbox
                    :value="isChecked(option)"
                    @onChange="handleSingleSelect(option)"
                  />
                </div>
                <div v-if="!option.header && showOptionImage">
                  <c-avatar
                    v-if="option.image"
                    size="extraextrasmall"
                    :image="option.image"
                    :rounded="true"
                    :description="option.description"
                    class="mr-2"
                  ></c-avatar>
                  <c-avatar
                    v-else
                    size="extraextrasmall"
                    :nameInitials="option.initials"
                    :description="option.description"
                    :rounded="true"
                    :isDynamicallyColored="true"
                    class="mr-2"
                  ></c-avatar>
                </div>
                <div class="w-full">
                  <div class="flex items-center">
                    <p>
                      {{
                        option[optionLabelSecondary]
                          ? option[optionLabelSecondary]
                          : option[optionLabel]
                      }}
                    </p>
                    <div class="ml-3">
                      <span
                        v-if="tags && option.info && option.info !== ''"
                        class="inline-flex rounded-md bg-gray-400 px-2 py-px text-sm leading-5 text-white"
                      >
                        {{ option.info }}
                      </span>
                      <span v-else class="text-gray-300">
                        {{ option.info }}
                      </span>
                    </div>
                  </div>

                  <p class="text-xs" v-if="option.description">
                    {{ option.description }}
                  </p>
                </div>
              </div>
            </div>
          </template>
        </template>
        <template #list-footer="{ search }">
          <li ref="load" class="loader" v-show="hasNextPage">
            Loading more options...
          </li>

          <!-- Footer buttons when footer buttons are visible -->
          <div
            v-if="showFooterButton || showFooterButton2"
            :class="[
              'group sticky bottom-0 mt-1 bg-white',
              showFooterButton && showFooterButton2 ? 'space-y-2' : '',
            ]"
          >
            <li
              v-if="showFooterButton"
              :class="footerBtnClasses"
              class="cursor-pointer select-none bg-gray-50 py-2 pl-3 pr-9 text-gray-700 hover:bg-gray-100 hover:text-gray-900"
              :id="getActionIDFn(footerButtonText)"
              role="option"
              @click="$emit('footerButtonClick')"
            >
              <div class="flex items-center">
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  class="h-6 w-6 flex-shrink-0 text-gray-400"
                  viewBox="0 0 20 20"
                  fill="currentColor"
                >
                  <path
                    fill-rule="evenodd"
                    d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z"
                    clip-rule="evenodd"
                  />
                </svg>
                <span class="ml-3 block truncate font-semibold">
                  {{ footerButtonText }}
                </span>
              </div>
            </li>
            <li
              v-if="showFooterButton2"
              class="cursor-pointer select-none bg-gray-50 py-2 pl-3 pr-9 text-gray-700 hover:bg-gray-100 hover:text-gray-900"
              :class="footerBtnClasses2"
              :id="getActionIDFn(footerButtonText2)"
              role="option"
              @click="$emit('footerButtonClick2')"
            >
              <div class="flex items-center">
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  class="h-6 w-6 flex-shrink-0 text-gray-400"
                  viewBox="0 0 20 20"
                  fill="currentColor"
                >
                  <path
                    fill-rule="evenodd"
                    d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z"
                    clip-rule="evenodd"
                  />
                </svg>
                <span class="ml-3 block truncate font-semibold">
                  {{ footerButtonText2 }}
                </span>
              </div>
            </li>
          </div>

          <!-- Create option button when showCreateOption is true and there is a search term -->
          <div
            v-if="
              showCreateOption &&
              showCreateOptionAfterSearch &&
              search &&
              search.trim()
            "
            ref="createOption"
            class="sticky z-10 flex min-h-[44px] items-start gap-3 bg-white px-3 py-2 shadow-lg ring-1 ring-gray-900 ring-opacity-5"
            :style="footerStickyStyles.create"
          >
            <span class="word-break flex-1 text-sm text-gray-700">{{
              search
            }}</span>

            <button
              type="button"
              :id="id + '_create_button'"
              @mousedown="handleCreateOption($event, search)"
              class="flex items-center gap-1 text-sm text-indigo-700"
            >
              <c-icon
                type="solid"
                name="plus-circle-solid-v2"
                class="h-5 w-5 text-indigo-600"
                viewBox="0 0 24 24"
              ></c-icon>
              Create
            </button>
          </div>

          <!-- Other option checkbox when otherOption is true -->
          <div
            v-if="otherOption"
            ref="otherOption"
            class="sticky z-10 border-t border-gray-100 bg-gray-50 px-3 py-2"
            :style="footerStickyStyles.other"
          >
            <div class="flex" @mousedown.prevent.stop>
              <c-checkbox
                :value="isChecked(otherOption)"
                @onChange="handleOtherOptionChange"
              />
              <div class="flex-1" @click.stop="handleOtherOptionChange">
                <p class="text-sm font-medium text-gray-700">
                  {{ otherOption[optionLabel] }}
                </p>
                <p class="mt-1 text-sm text-gray-500">
                  {{ otherOption.description }}
                </p>
              </div>
            </div>
          </div>

          <!-- Clear all button when multiple is true and there are selected values -->
          <button
            v-if="isMultiple && hasSelectedValues"
            ref="clearAllButton"
            type="button"
            class="sticky z-10 w-full border-t border-gray-200 bg-gray-50 px-4 py-3 text-left text-sm text-gray-500 hover:text-gray-900 focus:outline-none"
            :style="footerStickyStyles.clear"
            :id="id + '_clear_all_button'"
            aria-label="Clear all selections"
            @mousedown.prevent.stop="handleClearAll"
          >
            Clear all
          </button>
        </template>
      </v-select>
      <button
        class="z-100 absolute top-2.5 right-10"
        :id="id + '_close_button'"
        v-if="showCloseButton && (value == null || value.length == 0)"
        @click.stop="handleCrossClick"
      >
        <c-icon
          name="close"
          type="outline-v2"
          class="h-4 w-4 text-gray-400"
          cursorType="cursor-pointer"
        />
      </button>
    </div>
    <p v-if="subLabel" class="mt-2 text-sm text-gray-500">{{ subLabel }}</p>
    <p
      v-if="!isValidate && errorMessage"
      class="mt-2 text-left text-sm text-red-600"
    >
      {{ errorMessage }}
    </p>
    <p v-if="helpText && isValidate == true" class="mt-2 text-sm text-gray-500">
      {{ helpText }}
    </p>
  </div>
</template>
<script>
import vSelect from "vue-select";
import CAvatar from "../CAvatar/CAvatar.vue";
import CIcon from "../CIcon/CIcon.vue";
import CCheckbox from "../CCheckbox/CCheckbox.vue";
import { debounce } from "lodash-es";
import "vue-select/dist/vue-select.css";
import { getActionID } from "../../helper";
import CTag from "../CTag/CTag.vue";
export default {
  name: "CMultiselect",
  components: { vSelect, CAvatar, CCheckbox, CIcon, CTag },
  props: {
    id: {
      type: String,
    },
    clearInput: {
      type: Boolean,
      default: false,
    },
    options: {
      type: Array,
      required: true,
    },
    extraDetails: {
      type: Boolean,
      default: false,
    },
    displaySelectedOptionAvatar: {
      type: Boolean,
      default: false,
    },
    showFooterButton: {
      type: Boolean,
      default: false,
    },
    footerButtonText: {
      type: String,
      default: "",
    },
    label: String,
    isRequired: {
      type: Boolean,
    },
    subLabel: String,
    placeholder: {
      type: String,
    },
    // whether multiple fields are allowed or not
    isMultiple: {
      type: Boolean,
    },
    // append newly created tags in options
    pushTags: {
      type: Boolean,
    },
    // whether to allow user to create new options
    isTaggable: {
      type: Boolean,
    },
    getOptions: {
      // function to emit on search{
      type: Function,
    },
    customiseOption: {
      type: Boolean,
    },
    // whether to show profile picture in option
    showOptionImage: {
      type: Boolean,
    },
    // the name of the option to render in the selectpicker
    optionLabel: {
      type: String,
    },
    // the name of the option to render in the selectpicker
    optionLabelSecondary: {
      type: String,
    },
    selectedOptionLabel: {
      type: String,
    },
    // the key to compare the options when addCheckBox is true
    primaryComparisonKey: {
      type: String,
      default: "id",
    },
    // action to trigger after selecting option from dropdown
    onSelectOptions: {
      type: Function,
    },
    optionsSelected: {
      type: [Array, Object],
    },
    filterable: {
      type: Boolean,
      default: false,
    },
    clearable: {
      type: Boolean,
      default: true,
    },
    isValidate: {
      type: Boolean,
      default: true,
    },
    errorMessage: {
      type: String,
    },
    loading: {
      type: Boolean,
      default: false,
    },
    hasNextPage: {
      type: Boolean,
      default: false,
    },
    isDisabled: {
      type: Boolean,
      default: false,
    },
    // text adjacent to label of dropdown to provide hint about field
    hint: { type: String },
    helpText: {
      type: String,
    },
    tags: {
      type: Boolean,
      default: false,
    },
    isGrouped: {
      type: Boolean,
      default: false,
    },
    //to show the additional text, default true to prevent failure at any other place
    showAdditionalText: {
      type: Boolean,
      default: true,
    },
    addCheckBox: {
      type: Boolean,
      default: false,
    },
    showSelectedAvatar: {
      type: Boolean,
      default: false,
    },
    icon: {
      type: Object,
    },
    footerBtnClasses: {
      type: String,
    },
    shouldIconRotate: {
      type: Boolean,
      default: true,
    },
    imageLabel: {
      type: String,
      default: "image",
    },
    showFooterButton2: {
      type: Boolean,
      default: false,
    },
    footerButtonText2: {
      type: String,
      default: "",
    },
    footerBtnClasses2: {
      type: String,
    },
    showCloseButton: {
      type: Boolean,
      default: false,
    },
    showCreateOption: {
      type: Boolean,
      default: false,
    },
    otherOption: {
      type: Object,
    },
    tagWrapperClass: {
      type: String,
      default: "",
    },
    checkboxWrapperClass: {
      type: String,
      default: "",
    },
    // Enables/disables chip insert animation flow (phase 1 + phase 2 sweep).
    chipAnimationEnabled: {
      type: Boolean,
      default: false,
    },
    // When true, render all selected pills and hide "+N" overflow counter.
    showOverFlowCount: {
      type: Boolean,
      default: true,
    },
  },
  computed: {
    groupedArray() {
      const groups = {};

      this.options.forEach((x) => {
        // create empty object if it doesn't exists.
        groups[x.category] = groups[x.category] || {
          text: x.category,
          list: [],
          ...(x.icon ? { icon: x.icon } : {}),
        };

        groups[x.category].list.push(x);
      });

      // The flattened list of items that holds items as well as unique headers
      const flattened = [];

      // Iterate over all the unique categories and
      // then flatten into a list that v-select needs.
      Object.keys(groups).forEach((categoryId) => {
        const category = groups[categoryId];
        const categoryName = category.text;

        // Create a group
        flattened.push({
          header: categoryName,
          ...(category.icon ? { icon: category.icon } : {}),
        });

        // Add all the items followed by category header
        flattened.push(...category.list);
      });

      return flattened;
    },
    showCreateOptionAfterSearch() {
      if (!this.showCreateOption) return false;

      // Get current search term from v-select
      const vSelectRef = this.$refs.vselect;
      const search = (vSelectRef && vSelectRef.search) || "";
      const term = search.trim().toLowerCase();
      if (!term) return false;

      // Check if any options exactly match the search term
      const hasExactMatch = this.options.some((option) => {
        const label = option[this.optionLabel];
        return label && String(label).toLowerCase() === term;
      });

      return !hasExactMatch;
    },
    /*
      This function is used to get the remaining count of options that are not shown in the dropdown.
    */
    selectedOverflowCount() {
      if (!this.isMultiple || !this.showOverFlowCount) return 0;
      const totalSelectedOptions = Array.isArray(this.value)
        ? this.value.length
        : 0;
      return totalSelectedOptions > 2 ? totalSelectedOptions - 2 : 0;
    },
    hasSelectedValues() {
      return Array.isArray(this.value) && this.value.length > 0;
    },
    /**
     * Pre-built pill rows keyed by `value[i]` and by vue-select option key.
     * Recomputes only when selection / multi / overflow-relevant state changes — not on dropdown hover.
     * Index comes from `value` order (same order vue-select uses for selected-option-container).
     */
    pillRowsBySlotLookup() {
      if (!Array.isArray(this.value)) {
        return { byRef: new Map(), byKey: new Map() };
      }
      const byRef = new Map();
      const byKey = new Map();
      const overflow = this.selectedOverflowCount;

      this.value.forEach((item, index) => {
        const label = this.getDisplayLabelForOption(item);
        const key = this.getVueSelectOptionKey(item);
        const slotKey = `slot-${index}-${String(key)}`;
        const meta = { index, key, label, slotKey };
        const showPill = this.isMultiple;
        const showOverflowBadge =
          this.isMultiple &&
          this.showOverFlowCount &&
          index === 1 &&
          overflow > 0;
        const row = {
          meta,
          showPill,
          showOverflowBadge,
          label,
          slotKey,
        };
        byRef.set(item, row);
        if (!byKey.has(key)) {
          byKey.set(key, row);
        }
      });
      return { byRef, byKey };
    },
    /**
     * Sticky `bottom` offsets so create → other → clear stack without overlap.
     * DOM order is create (top of sticky group), other, clear (anchored to dropdown bottom).
     */
    footerStickyStyles() {
      const clearH = Number(this.footerMetaData.clearAllHeight) || 0;
      const otherH = Number(this.footerMetaData.otherOptionHeight) || 0;
      return {
        create: { bottom: `${clearH + otherH}px` },
        other: { bottom: `${clearH}px` },
        clear: { bottom: "0px" },
      };
    },
  },
  data() {
    return {
      loaderSearching: true,
      value:
        this.optionsSelected && this.optionsSelected.length
          ? this.optionsSelected
          : [],
      observer: null,
      chipAnimationTimeout: null,
      chipAnimationCycle: 0,
      hasMounted: false,
      footerMetaData: {
        clearAllHeight: 0,
        otherOptionHeight: 0,
        createOptionHeight: 0,
      },
    };
  },
  methods: {
    clearChipAnimationTimeout() {
      if (this.chipAnimationTimeout) {
        clearTimeout(this.chipAnimationTimeout);
        this.chipAnimationTimeout = null;
      }
    },
    startChipInsertAnimation(insertedCount) {
      if (!this.isMultiple || !this.chipAnimationEnabled || !insertedCount)
        return;

      this.clearChipAnimationTimeout();
      // Increment a cycle token so any previously scheduled async animation work
      // can detect it is stale and bail out.
      this.chipAnimationCycle += 1;
      const animationCycle = this.chipAnimationCycle;
      this.$nextTick(() => {
        // Guard against race conditions: if another insert happened before this
        // tick runs, skip this stale cycle.
        if (animationCycle !== this.chipAnimationCycle || !this.$refs.vselect)
          return;

        const vSelectEl = this.$refs.vselect.$el;
        if (!vSelectEl) return;

        requestAnimationFrame(() => {
          // Re-check at frame time because DOM work is deferred one more step.
          if (animationCycle !== this.chipAnimationCycle) return;

          const chipElements = Array.from(
            vSelectEl.querySelectorAll(".vs__selected"),
          );
          const chipsToAnimate = chipElements.slice(-insertedCount);

          if (!chipsToAnimate.length) {
            return;
          }

          const staggerMs = chipsToAnimate.length > 1 ? 80 : 0;
          chipsToAnimate.forEach((chipEl, index) => {
            chipEl.classList.remove("chip-entering");
            void chipEl.offsetWidth;
            chipEl.style.setProperty(
              "--chip-enter-delay",
              `${index * staggerMs}ms`,
            );
            chipEl.classList.add("chip-entering");
          });

          const totalPhaseOneDuration =
            (chipsToAnimate.length - 1) * staggerMs + 320;
          this.chipAnimationTimeout = setTimeout(() => {
            if (animationCycle !== this.chipAnimationCycle) return;
            chipsToAnimate.forEach((chipEl) => {
              chipEl.classList.remove("chip-entering");
              chipEl.style.removeProperty("--chip-enter-delay");
            });
          }, totalPhaseOneDuration);
        });
      });
    },
    getActionIDFn(name) {
      return getActionID(name, this.id);
    },
    handleCrossClick(event) {
      event.preventDefault();
      this.$emit("hide-multiselect-dropdown");
    },
    fetchOptions(search, loaderSearching) {
      this.emitGetOptions(search, loaderSearching);

      this._updateFooterHeights();
    },
    emitGetOptions: debounce(function (search, loaderSearching) {
      this.$emit("getOptions", search.trim(), loaderSearching);
    }, 300),
    setSelectedOptions(params) {
      //this prevents animation to run, we have a watcher that will instate value correctly
      if (!this.chipAnimationEnabled) {
        this.value = params;
      }
      this.$emit("onSelectOptions", params);
      if (this.clearInput) {
        this.value = [];
        this.$nextTick(() => {
          if (this.$refs.vselect) {
            this.$refs.vselect.search = "";
          }
        });
      }
    },
    async onOpen() {
      if (this.hasNextPage) {
        await this.$nextTick();
        if (this.$refs ? this.$refs.load : false && this.observer) {
          this.observer.observe(this.$refs.load);
        } else {
          setTimeout(() => {
            this.onOpen();
          }, 500);
        }
      }
      this.emitGetOptions("", true);
    },
    onClose() {
      if (this.observer) {
        this.observer.disconnect();
      }
    },
    async infiniteScroll([{ isIntersecting }]) {
      if (isIntersecting) {
        this.emitLoadNextPage();
      }
    },
    /*
      This function is used to create a unique key for the option.
    */
    createUniqueKeyForOption(sortable) {
      const ordered = {};
      Object.keys(sortable)
        .sort()
        .forEach((key) => {
          ordered[key] = sortable[key];
        });
      return JSON.stringify(ordered);
    },
    /*
      This function is used to get the key of the option for the selected pills.
      Aligns with vue-select default getOptionKey.
      Reference: https://vue-select.org/api/props.html#getoptionkey
    */
    getVueSelectOptionKey(option) {
      if (typeof option !== "object" || option === null) {
        return option;
      }
      try {
        return Object.prototype.hasOwnProperty.call(option, "id")
          ? option.id
          : this.createUniqueKeyForOption(option);
      } catch (e) {
        return String(option);
      }
    },
    getDisplayLabelForOption(option) {
      if (!option) return "";
      if (this.selectedOptionLabel && option[this.selectedOptionLabel]) {
        return option[this.selectedOptionLabel];
      }
      if (this.optionLabelSecondary && option[this.optionLabelSecondary]) {
        return option[this.optionLabelSecondary];
      }
      if (this.optionLabel && option[this.optionLabel]) {
        return option[this.optionLabel];
      }
      return "";
    },
    /**
     * Easy lookup for each selected-option slot.
     */
    getPillRow(option) {
      const { byRef, byKey } = this.pillRowsBySlotLookup;
      if (byRef.has(option)) {
        return byRef.get(option);
      }
      const k = this.getVueSelectOptionKey(option);
      return byKey.get(k) || null;
    },
    handleClearAll() {
      this.value = [];
      this.$emit("onSelectOptions", []);
      this.$nextTick(() => {
        if (this.$refs.vselect) {
          this.$refs.vselect.search = "";
        }
      });
    },
    isChecked(option) {
      return this.value.some(
        (item) =>
          item[this.primaryComparisonKey] === option[this.primaryComparisonKey],
      )
        ? 1
        : 0;
    },
    handleSingleSelect(option) {
      if (this.addCheckBox) {
        if (this.isChecked(option) === 1) {
          this.value = this.value.filter(
            (item) =>
              item[this.primaryComparisonKey] !==
              option[this.primaryComparisonKey],
          );
        } else {
          this.value = [...this.value, option];
        }
        this.$emit("onSelectOptions", this.value);

        return false;
      }
    },
    handleOtherOptionChange() {
      const isSelected = this.isChecked(this.otherOption) === 1;
      if (isSelected) {
        this.value = this.value.filter(
          (item) =>
            item[this.primaryComparisonKey] !==
            this.otherOption[this.primaryComparisonKey],
        );
      } else {
        this.value = [...this.value, this.otherOption];
      }
      this.$emit("onSelectOptions", this.value);

      this.$nextTick(() => {
        if (this.$refs.vselect) {
          this.$refs.vselect.open = false;
          this.$refs.vselect.search = "";
        }

        const input = this.$refs.vselect.$el.querySelector("input");
        if (input) {
          input.blur();
        }
      });
    },
    getDisableOptions(option) {
      if (!option) return true;
      if (option.header) return false;
      if (option.isDisabled || option.disabled) return false;
      return true;
    },

    // a function that will set the location when the component has mounted and the location is passed after that
    forceSetData(params) {
      this.value = params;
    },
    emitLoadNextPage: debounce(function () {
      this.$emit("loadNextPage");
    }, 300),
    handleCreateOption(event, searchText) {
      event.preventDefault();

      this.$emit("handleCreateOption", {
        event,
        searchText: searchText.trim(),
      });
      this.$refs.vselect.open = false;
      this.$refs.vselect.search = "";

      this.$nextTick(() => {
        const input = this.$refs.vselect.$el.querySelector("input");
        if (input) {
          input.blur();
        }
      });
    },
    handleKeydown(event) {
      if (event.key === "Enter") {
        event.preventDefault();
        event.stopPropagation();

        const vSelectRef = this.$refs.vselect;
        const search = (vSelectRef && vSelectRef.search) || "";

        if (
          this.showCreateOption &&
          this.showCreateOptionAfterSearch &&
          search &&
          search.trim()
        ) {
          this.handleCreateOption(event, search.trim());
        }
      }
    },
    _updateFooterHeights() {
      this.$nextTick(() => {
        const createEl = this.$refs.createOption;
        const otherEl = this.$refs.otherOption;
        const clearEl = this.$refs.clearAllButton;

        this.footerMetaData = {
          createOptionHeight: createEl ? createEl.offsetHeight : 0,
          otherOptionHeight: otherEl ? otherEl.offsetHeight : 0,
          clearAllHeight: clearEl ? clearEl.offsetHeight : 0,
        };
      });
    },
  },
  watch: {
    optionsSelected: {
      handler(newVal, oldVal) {
        this.value = newVal;
        if (!this.hasMounted) return;
        if (!this.isMultiple || !this.chipAnimationEnabled) return;
        const newLength = Array.isArray(newVal) ? newVal.length : 0;
        const oldLength = Array.isArray(oldVal) ? oldVal.length : 0;
        const insertedCount = Math.max(0, newLength - oldLength);
        if (insertedCount > 0) {
          this.startChipInsertAnimation(insertedCount);
        }
      },
      deep: true,
    },
    value() {
      this._updateFooterHeights();
    },
  },
  mounted() {
    this.hasMounted = true;
    this.observer = new IntersectionObserver(this.infiniteScroll);
  },
  beforeDestroy() {
    this.clearChipAnimationTimeout();
    if (this.observer) {
      this.observer.disconnect();
      this.observer = null;
    }
  },
};
</script>
<style>
.v-select {
  @apply cursor-pointer;
}

.vs__dropdown-toggle {
  @apply min-h-9 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-left shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm;
}

.vs__selected-options {
  @apply flex items-center gap-1.5 p-0;
}

.c-multiselect--capped-pills .vs__selected-options {
  @apply min-w-0 flex-nowrap overflow-hidden;
}

/* Hide 3rd+ chips if they still mount (key mismatch); keeps one row */
.c-multiselect--capped-pills
  .vs__selected-options
  > .vs__selected:nth-child(n + 3) {
  display: none !important;
}

.vs--open .vs__dropdown-toggle {
  border-bottom-color: rgba(212, 212, 216, var(--tw-border-opacity));
  @apply rounded-b-md;
}
.vs__selected {
  @apply m-0 !bg-transparent !p-0 text-gray-700;
  border: 1px solid transparent;
  transition: background-color 220ms ease, border-color 220ms ease,
    color 220ms ease;
}

.vs__selected.chip-entering {
  background-color: #f0f9ff;
  border-color: #7dd3fc;
  color: #0284c7;
  animation: chip-entry-pop 320ms cubic-bezier(0.22, 1, 0.36, 1) both;
  animation-delay: var(--chip-enter-delay, 0ms);
}

.vs__selected.chip-entering .vs__selected-inner {
  background-color: #f0f9ff;
  border-color: #7dd3fc;
  color: #0284c7;
}

@keyframes chip-entry-pop {
  0% {
    opacity: 0;
    transform: translateY(6px) scale(0.94);
  }
  70% {
    opacity: 1;
    transform: translateY(-1px) scale(1.01);
  }
  100% {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}
.extra:hover {
  color: white;
}
.vs__search,
.vs__search:focus {
  @apply m-0 p-0;
}
.vs__search::placeholder {
  @apply text-gray-500;
}
.vs__actions {
  @apply p-0;
}
.vs__dropdown-menu {
  @apply mt-2 max-h-60 overflow-auto rounded-md bg-white py-0 text-base shadow-lg ring-1 ring-opacity-5 focus:outline-none sm:text-sm;
}

.dropdown-menu-bordered {
  @apply border-b border-gray-200;
}

.vs__dropdown-menu li {
  min-height: fit-content;
}

.vs__dropdown-menu li:has(.sticky-group-header) {
  @apply sticky top-0 z-10 bg-gray-200;
}

/* Remove li padding if it contains a bordered option */
.vs__dropdown-menu li:has(.dropdown-menu-bordered) {
  padding: 0 !important;
}

.vs__dropdown-option {
  @apply whitespace-normal !px-3 !py-2 text-gray-700;
}

.vs__dropdown-toggle.vs__dropdown-option--highlight {
  @apply bg-indigo-700 text-white;
}

.vs__dropdown-option--highlight {
  @apply bg-indigo-700 text-white;
}

.vs__dropdown-option--disabled {
  @apply cursor-not-allowed !border-gray-200 !bg-gray-50 !text-gray-500 opacity-100 shadow-none;
}

.vs__dropdown-option--disabled * {
  @apply text-gray-500;
}

.vs__dropdown-menu li {
  @apply opacity-100;
}

.v-list-subheader {
  @apply pointer-events-none opacity-100;
}

.vs__no-options {
  @apply px-4 py-3 text-sm text-gray-500;
}

.vs__deselect {
  @apply m-0;
}
</style>
