const MAX_ITEMS_WITHOUT_VIRTUALIZATION = 50;
/**
 * Converts a string to a normalized, searchable format by:
 * - Trimming whitespace
 * - Normalizing whitespace (convert multiple spaces to single space)
 * - Converting to NFD normalization form to handle diacritics
 * - Removing combining diacritical marks
 * - Converting to lowercase
 */
function searchableString(value) {
  return value.trim().replace(/\s+/gu, ' ')
  // NFD converts an Å to A +  ̊ (and other special characters)
  .normalize('NFD')
  // and then this replaces the  ̊ with nothing (and other special characters)
  .replace(/[\u0300-\u036f]/g, '').toLowerCase();
}
/**
 * Extracts searchable strings from a value.
 * - If the value is a string, returns a normalized version.
 * - If the value is an object, extracts all string values and normalizes them.
 * - Otherwise returns an empty array.
 */
function inferSearchableStrings(value) {
  if (typeof value === 'string') {
    return [searchableString(value)];
  }
  if (typeof value === 'object' && value != null) {
    return Object.values(value).filter(innerValue => typeof innerValue === 'string').map(innerValue => searchableString(innerValue));
  }
  return [];
}
/**
 * Sets the value of a duplicate option item to undefined, effectively hiding it when rendered.
 */
function dedupeSelectInputOptionItem(item, existingValues, compareValues) {
  const isDuplicate = compareValues ? Array.from(existingValues).some(existingValue => compareValues(item.value, existingValue)) : existingValues.has(item.value);
  if (!isDuplicate) {
    existingValues.add(item.value);
    return item;
  }
  return {
    ...item,
    value: undefined
  };
}
/**
 * Sets the `value` of duplicate option items to `undefined`, hiding them when
 * rendered. Indexes are kept intact within groups to preserve the active item
 * between filter changes when possible.
 */
function dedupeSelectInputItems(items, compareValues) {
  const existingValues = new Set();
  return items.map(item => {
    switch (item.type) {
      case 'option':
        {
          return dedupeSelectInputOptionItem(item, existingValues, compareValues);
        }
      case 'group':
        {
          return {
            ...item,
            options: item.options.map(option => dedupeSelectInputOptionItem(option, existingValues, compareValues))
          };
        }
    }
    return item;
  });
}
/**
 * Checks if a SelectInputOptionItem matches the search needle.
 */
function selectInputOptionItemIncludesNeedle(item, needle) {
  return inferSearchableStrings(item.filterMatchers ?? item.value).some(haystack => haystack.includes(needle));
}
/**
 * Filters SelectInputItems based on the provided predicate function.
 * For group items, it checks if any of their options match the predicate.
 */
function filterSelectInputItems(items, predicate) {
  return items.filter(item => {
    switch (item.type) {
      case 'option':
        {
          return predicate(item);
        }
      case 'group':
        {
          return item.options.some(option => predicate(option));
        }
    }
    return false;
  });
}
/**
 * Flattens and sorts filtered options using the provided comparator.
 * Extracts all options from groups, filters out undefined values (deduplicated items),
 * sorts them, and returns as a flat list of option items.
 */
function sortSelectInputItems(items, compareFn, searchQuery) {
  const flattenedOption = items.flatMap(item => {
    if (item.type === 'option') {
      return item.value !== undefined ? [item] : [];
    }
    if (item.type === 'group') {
      return item.options.filter(option => option.value !== undefined);
    }
    return [];
  });
  // eslint-disable-next-line functional/immutable-data
  return flattenedOption.sort((a, b) => compareFn(a, b, searchQuery));
}
/**
 * A prebuilt sort function for `sortFilteredOptions` that sorts options by relevance to the search query.
 * Prioritizes: exact matches > starts with > contains > alphabetical.
 *
 * @param getLabel - Function to extract the label string from the option value. Defaults to using `title` property.
 *
 * @example
 * ```tsx
 * <SelectInput
 *   filterable
 *   sortFilteredOptions={sortByRelevance((value) => value.name)}
 *   // ...
 * />
 * ```
 */
function sortByRelevance(getLabel = value => value.title) {
  return (a, b, searchQuery) => {
    const normalizedQuery = searchQuery.toLowerCase();
    const labelA = getLabel(a.value).toLowerCase();
    const labelB = getLabel(b.value).toLowerCase();
    // Prioritize exact matches
    const aExactMatch = labelA === normalizedQuery;
    const bExactMatch = labelB === normalizedQuery;
    if (aExactMatch && !bExactMatch) return -1;
    if (!aExactMatch && bExactMatch) return 1;
    // Then prioritize options where label starts with the search query
    const aStartsWith = labelA.startsWith(normalizedQuery);
    const bStartsWith = labelB.startsWith(normalizedQuery);
    if (aStartsWith && !bStartsWith) return -1;
    if (!aStartsWith && bStartsWith) return 1;
    // Then prioritize options where label contains the search query
    const aContains = labelA.includes(normalizedQuery);
    const bContains = labelB.includes(normalizedQuery);
    if (aContains && !bContains) return -1;
    if (!aContains && bContains) return 1;
    // Finally sort alphabetically
    return labelA.localeCompare(labelB);
  };
}

export { MAX_ITEMS_WITHOUT_VIRTUALIZATION, dedupeSelectInputItems, dedupeSelectInputOptionItem, filterSelectInputItems, inferSearchableStrings, searchableString, selectInputOptionItemIncludesNeedle, sortByRelevance, sortSelectInputItems };
//# sourceMappingURL=SelectInput.utils.mjs.map
