<!--
Composant de sélection hiérarchique à plusieurs niveaux permettant de naviguer
et sélectionner des éléments organisés en catégories et sous-catégories.

## Exemple d'utilisation
```vue
<TreeSelect
  v-model="selectedValues"
  :items="hierarchicalItems"
  :max-visible-chips="3"
  label="Sélectionner des éléments"
/>
```
-->
<template>
  <v-select
    :model-value="modelValue"
    v-bind="$attrs"
    :items="flattenedItems"
    item-title="label"
    item-value="value"
    multiple
    chips
    closable-chips
    :menu-props="{ maxHeight: 400 }"
    @update:menu="onMenuUpdate"
    @update:model-value="$emit('update:modelValue', $event)"
  >
    <template #prepend-item>
      <!-- Champs de recherche textuelle -->
      <v-text-field
        v-model="searchText"
        density="compact"
        hide-details
        :placeholder="$t('search') + '...'"
        prepend-inner-icon="fas fa-magnifying-glass"
        variant="outlined"
        clearable
        class="mx-2 my-2"
        @click.stop
        @input="onSearchInput"
        @click:clear="onSearchInput"
      />
      <v-divider class="mt-2"/>
    </template>

    <template #selection="{ item, index }">
      <v-chip
        v-if="maxVisibleChips && index < maxVisibleChips"
        :key="item.raw.value"
        closable
        @click:close="removeItem(item.raw.value!)"
      >
        {{ item.raw.label }}
      </v-chip>
      <span
        v-if="maxVisibleChips && index === maxVisibleChips && modelValue.length > maxVisibleChips"
        class="text-grey text-caption align-self-center"
      >
        (+{{ modelValue.length - maxVisibleChips }} {{ $t('others') }})
      </span>
    </template>

    <template #item="{ item }">
      <template v-if="item.raw.type === 'category'">
        <v-list-item
          @click.stop="toggleCategory(item.raw.id)"
          :ripple="false"
          :class="{ 'v-list-item--active': expandedCategories.has(item.raw.id) }"
        >
          <template #prepend>
            <v-icon
              :icon="'fas ' + (expandedCategories.has(item.raw.id) ? 'fa-angle-down' : 'fa-angle-right')"
              size="small"
            />
          </template>
          <v-list-item-title class="font-weight-medium">
            {{ item.raw.label }}
          </v-list-item-title>
        </v-list-item>
      </template>

      <template v-else-if="item.raw.type === 'subcategory'">
        <v-list-item
          @click.stop="toggleSubcategory(item.raw.id)"
          :ripple="false"
          :class="{
            'v-list-item--active': expandedSubcategories.has(item.raw.id),
            'pl-8': true
          }"
        >
          <template #prepend>
            <v-icon
              :icon="'fas ' + (expandedSubcategories.has(item.raw.id) ? 'fa-angle-down' : 'fa-angle-right')"
              size="small"
            />
          </template>
          <v-list-item-title>
            {{ item.raw.label }}
          </v-list-item-title>
        </v-list-item>
      </template>

      <template v-else>
        <v-list-item
          @click="toggleItem(item.raw.value!)"
          :active="modelValue.includes(item.raw.value!)"
          :class="{ 'pl-12': item.raw.level === 2, 'pl-8': item.raw.level === 1 }"
        >
          <template #prepend>
            <v-checkbox
              :model-value="modelValue.includes(item.raw.value!)"
              @click.stop="toggleItem(item.raw.value!)"
              color="primary"
              :hide-details="true"
            />
          </template>
          <v-list-item-title>
            {{ item.raw.label }}
          </v-list-item-title>
        </v-list-item>
      </template>
    </template>
  </v-select>
</template>

<script setup lang="ts">
import StringUtils from '~/services/utils/stringUtils'

interface SelectItem {
  id: string
  label: string
  value?: string
  type: 'category' | 'subcategory' | 'item'
  parentId?: string
  level: number
}

const props = defineProps({
  modelValue: {
    type: Array as PropType<string[]>,
    required: true
  },
  items: {
    type: Array as PropType<SelectItem[]>,
    required: true
  },
  maxVisibleChips: {
    type: Number,
    required: false,
    default: null
  }
})

const emit = defineEmits(['update:modelValue'])

const expandedCategories: Ref<Set<string>> = ref(new Set())
const expandedSubcategories: Ref<Set<string>> = ref(new Set())
const searchText: Ref<string> = ref('')

/**
 * A callback function that is triggered when the menu's open state is updated.
 */
const onMenuUpdate = (isOpen: boolean) => {
  // Réinitialiser la recherche quand le menu se ferme
  if (!isOpen && searchText.value) {
    searchText.value = ''
    onSearchInput()
  }
}

/**
 * Toggles the expanded state of a given category. If the category is currently
 * expanded, it will collapse the category and also collapse its subcategories.
 * If the category is not expanded, it will expand the category.
 *
 * @param {string} categoryId - The unique identifier of the category to toggle.
 */
const toggleCategory = (categoryId: string) => {
  if (expandedCategories.value.has(categoryId)) {
    expandedCategories.value.delete(categoryId)
    // Fermer aussi les sous-catégories
    const subcategories = props.items.filter(i =>
      i.parentId === categoryId && i.type === 'subcategory'
    )
    subcategories.forEach(sub => {
      expandedSubcategories.value.delete(sub.id)
    })
  } else {
    expandedCategories.value.add(categoryId)
  }
}

/**
 * Toggles the expansion state of a subcategory.
 *
 * @param {string} subcategoryId - The unique identifier of the subcategory to be toggled.
 */
const toggleSubcategory = (subcategoryId: string) => {
  if (expandedSubcategories.value.has(subcategoryId)) {
    expandedSubcategories.value.delete(subcategoryId)
  } else {
    expandedSubcategories.value.add(subcategoryId)
  }
}

/**
 * A function that toggles the inclusion of a specific value in
 * the selected items list.
 *
 * @param {string} value - The value to toggle in the selected items list.
 */
const toggleItem = (value: string) => {
  const currentSelection = [...props.modelValue]
  const index = currentSelection.indexOf(value)

  if (index > -1) {
    currentSelection.splice(index, 1)
  } else {
    currentSelection.push(value)
  }

  emit('update:modelValue', currentSelection)
}

/**
 * Removes the specified item from the model value and emits an update event.
 *
 * @param {string} value - The item to be removed from the model value.
 * @emits update:modelValue - A custom event emitted with the updated model value
 * after the specified item has been removed.
 */
const removeItem = (value: string) => {
  emit('update:modelValue', props.modelValue.filter(item => item !== value))
}

/**
 * Fonction appellée lorsque l'input de recherche textuelle est modifié
 */
const onSearchInput = () => {
  // Réinitialiser les états d'expansion dans tous les cas
  expandedCategories.value.clear()
  expandedSubcategories.value.clear()

  if (searchText.value.trim()) {
    // Trouver tous les éléments qui correspondent à la recherche
    const matchingItems = props.items.filter(item =>
      item.type === 'item' &&
      item.level === 2 &&
      itemMatchesSearch(item)
    )

    // Pour chaque élément correspondant, ajouter ses parents aux ensembles d'expansion
    for (const item of matchingItems) {
      // Trouver et ajouter la sous-catégorie parente
      const subcategory = props.items.find(i => i.id === item.parentId)
      if (subcategory) {
        expandedSubcategories.value.add(subcategory.id)

        // Trouver et ajouter la catégorie parente
        const category = props.items.find(i => i.id === subcategory.parentId)
        if (category) {
          expandedCategories.value.add(category.id)
        }
      }
    }
  }
}

/**
 * Checks if any word in the normalized text starts with the normalized search term.
 *
 * @param {string} normalizedText - The normalized text to check.
 * @param {string} normalizedSearch - The normalized search term.
 * @returns {boolean} `true` if any word in the text starts with the search term; otherwise, `false`.
 */
const anyWordStartsWith = (normalizedText: string, normalizedSearch: string): boolean => {
  // Split the text into words
  const words = normalizedText.split(' ')

  // Check if any word starts with the search term
  return words.some(word => word.startsWith(normalizedSearch))
}

/**
 * Determines if a given item matches the current search text by checking its label
 * and, for certain items, the labels of its parent elements.
 *
 * The search text is normalized using `StringUtils.normalize` before comparison.
 * If no search text is provided, the item matches by default.
 *
 * For items of type `item` at level 2, the function checks:
 * - The label of the item itself
 * - The label of its parent subcategory
 * - The label of the grandparent category (if applicable)
 *
 * For all other item types, only the item's label is checked.
 *
 * The matching is done by checking if any word in the normalized label starts with the normalized search text.
 *
 * @param {SelectItem} item - The item to evaluate against the search text.
 * @returns {boolean} `true` if the item or its relevant parent(s) match the search text; otherwise, `false`.
 */
const itemMatchesSearch = (item: SelectItem): boolean => {
  if (!searchText.value) return true

  const normalizedSearch = StringUtils.normalize(searchText.value)

  // Si c'est un élément de niveau 2, vérifier son label et les labels de ses parents
  if (item.type === 'item' && item.level === 2) {
    // Vérifier le label de l'élément
    if (anyWordStartsWith(StringUtils.normalize(item.label), normalizedSearch)) return true

    // Trouver et vérifier le label de la sous-catégorie parente
    const subcategory = props.items.find(i => i.id === item.parentId)
    if (subcategory && anyWordStartsWith(StringUtils.normalize(subcategory.label), normalizedSearch)) return true

    // Trouver et vérifier le label de la catégorie parente
    if (subcategory && subcategory.parentId) {
      const category = props.items.find(i => i.id === subcategory.parentId)
      if (category && anyWordStartsWith(StringUtils.normalize(category.label), normalizedSearch)) return true
    }

    return false
  }

  // Pour les autres types d'éléments, vérifier simplement leur label
  return anyWordStartsWith(StringUtils.normalize(item.label), normalizedSearch)
}

/**
 * Filtre les éléments de niveau 2 qui correspondent au texte de recherche.
 *
 * @returns {SelectItem[]} Les éléments de niveau 2 qui correspondent à la recherche.
 */
const findMatchingLevel2Items = (): SelectItem[] => {
  return props.items.filter(item =>
    item.type === 'item' &&
    item.level === 2 &&
    itemMatchesSearch(item)
  )
}

/**
 * Construit une liste hiérarchique d'éléments basée sur les résultats de recherche.
 * Pour chaque élément correspondant, ajoute sa hiérarchie complète (catégorie et sous-catégorie).
 *
 * @param {SelectItem[]} matchingItems - Les éléments correspondant à la recherche.
 * @returns {SelectItem[]} Liste hiérarchique incluant les éléments et leurs parents.
 */
const buildSearchResultsList = (matchingItems: SelectItem[]): SelectItem[] => {
  const result: SelectItem[] = []
  const addedCategoryIds = new Set<string>()
  const addedSubcategoryIds = new Set<string>()

  for (const item of matchingItems) {
    // Trouver la sous-catégorie parente
    const subcategory = props.items.find(i => i.id === item.parentId)
    if (!subcategory) continue

    // Trouver la catégorie parente
    const category = props.items.find(i => i.id === subcategory.parentId)
    if (!category) continue

    // Ajouter la catégorie si elle n'est pas déjà présente
    if (!addedCategoryIds.has(category.id)) {
      result.push(category)
      addedCategoryIds.add(category.id)
      expandedCategories.value.add(category.id)
    }

    // Ajouter la sous-catégorie si elle n'est pas déjà présente
    if (!addedSubcategoryIds.has(subcategory.id)) {
      result.push(subcategory)
      addedSubcategoryIds.add(subcategory.id)
      expandedSubcategories.value.add(subcategory.id)
    }

    // Ajouter l'élément
    result.push(item)
  }

  return result
}

/**
 * Traite récursivement les éléments pour construire une liste hiérarchique
 * basée sur l'état d'expansion des catégories et sous-catégories.
 *
 * @param {SelectItem[]} items - Les éléments à traiter.
 * @param {SelectItem[]} result - Le tableau résultat à remplir.
 * @param {boolean} parentExpanded - Indique si le parent est développé.
 */
const processItemsRecursively = (
  items: SelectItem[],
  result: SelectItem[],
  parentExpanded = true
): void => {
  for (const item of items) {
    if (item.type === 'category') {
      result.push(item)
      if (expandedCategories.value.has(item.id)) {
        const subcategories = props.items.filter(i =>
          i.parentId === item.id && i.type === 'subcategory'
        )
        processItemsRecursively(subcategories, result, true)
      }
    } else if (item.type === 'subcategory') {
      if (parentExpanded) {
        result.push(item)
        if (expandedSubcategories.value.has(item.id)) {
          const subItems = props.items.filter(i =>
            i.parentId === item.id && i.type === 'item'
          )
          processItemsRecursively(subItems, result, true)
        }
      }
    } else if (item.type === 'item' && parentExpanded) {
      result.push(item)
    }
  }
}

/**
 * Construit une liste hiérarchique d'éléments en mode normal (sans recherche).
 *
 * @returns {SelectItem[]} Liste hiérarchique basée sur l'état d'expansion.
 */
const buildNormalModeList = (): SelectItem[] => {
  const result: SelectItem[] = []
  const topLevelItems = props.items.filter(item => !item.parentId)
  processItemsRecursively(topLevelItems, result)
  return result
}

/**
 * A computed property that generates a flattened and organized list of items
 * from a hierarchical structure, based on the current search text and
 * expanded categories/subcategories.
 *
 * @returns {SelectItem[]} Flattened and organized list of items.
 */
const flattenedItems = computed(() => {
  const hasSearch = !!searchText.value.trim()

  if (hasSearch) {
    const matchingItems = findMatchingLevel2Items()
    return buildSearchResultsList(matchingItems)
  }

  return buildNormalModeList()
})
</script>

<style scoped lang="scss">
.v-list-item--active {
  background-color: rgba(var(--v-theme-primary), 0.1);
}

.search-icon {
  color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
}

:deep(.v-field__prepend-inner) {
  padding-top: 0;
}

:deep(.v-list) {
  padding-top: 0;
}
</style>
