import { CrossCircle } from '@transferwise/icons';
import { ListboxOptions } from '@headlessui/react';
import { clsx } from 'clsx';
import { useRef, useMemo, useEffect, useState, useId } from 'react';
import { useIntl } from 'react-intl';
import { Virtualizer } from 'virtua';
import { SearchInput } from '../../SearchInput.mjs';
import { SelectInputItemsCountContext, SelectInputItemPositionContext } from '../SelectInput.contexts.mjs';
import { searchableString, dedupeSelectInputItems, selectInputOptionItemIncludesNeedle, sortSelectInputItems, filterSelectInputItems, MAX_ITEMS_WITHOUT_VIRTUALIZATION } from '../SelectInput.utils.mjs';
import messages from '../SelectInput.messages.mjs';
import { SelectInputItemView } from '../ItemView/SelectInputItemView.mjs';
import '../../../header/Header.mjs';
import { jsxs, jsx } from 'react/jsx-runtime';
import { SelectInputOptionsContainer } from './OptionsContainer/SelectInputOptionsContainer.mjs';

function SelectInputOptions({
  id,
  parentId,
  items,
  compareValues: compareValuesProp,
  renderValue = String,
  renderFooter,
  filterable = false,
  filterPlaceholder,
  sortFilteredOptions,
  searchInputRef,
  listboxRef,
  filterQuery,
  onFilterChange,
  listBoxLabel,
  listBoxLabelledBy,
  autocomplete,
  name,
  onAutocompleteSelect
}) {
  const intl = useIntl();
  const virtualiserHandlerRef = useRef(null);
  const controllerRef = filterable ? searchInputRef : listboxRef;
  const initialRenderRef = useRef(true);
  const needle = useMemo(() => {
    if (filterable) {
      return filterQuery ? searchableString(filterQuery) : null;
    }
    return undefined;
  }, [filterQuery, filterable]);
  useEffect(() => {
    if (needle) {
      // Ensure having an active option while filtering.
      // Without `requestAnimationFrame` upon which React depends for scheduling
      // updates, the active status would only show for a split second and then
      // disappear inadvertently.
      requestAnimationFrame(() => {
        if (controllerRef.current != null && !controllerRef.current.hasAttribute('aria-activedescendant')) {
          // Activate first option via synthetic key press
          controllerRef.current.dispatchEvent(new KeyboardEvent('keydown', {
            key: 'Home',
            bubbles: true
          }));
        }
      });
    }
  }, [controllerRef, needle]);
  const compareValues = useMemo(() => {
    if (!compareValuesProp) {
      return undefined;
    }
    if (typeof compareValuesProp === 'function') {
      return (a, b) => compareValuesProp(a, b);
    }
    const key = compareValuesProp;
    return (a, b) => {
      if (typeof a === 'object' && a != null && typeof b === 'object' && b != null) {
        return a[key] === b[key];
      }
      return a === b;
    };
  }, [compareValuesProp]);
  const filteredItems = useMemo(() => {
    if (needle == null) {
      return items;
    }
    const dedupedItems = dedupeSelectInputItems(items, compareValues);
    if (sortFilteredOptions) {
      // When sorting, filter out non-matching items completely to avoid ghost items
      const filtered = dedupedItems.map(item => {
        if (item.type === 'option') {
          return selectInputOptionItemIncludesNeedle(item, needle) ? item : {
            ...item,
            value: undefined
          };
        }
        if (item.type === 'group') {
          return {
            ...item,
            options: item.options.map(option => selectInputOptionItemIncludesNeedle(option, needle) ? option : {
              ...option,
              value: undefined
            })
          };
        }
        return item;
      });
      return sortSelectInputItems(filtered, sortFilteredOptions, filterQuery);
    }
    return filterSelectInputItems(dedupedItems, item => selectInputOptionItemIncludesNeedle(item, needle));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [needle, items, compareValues]);
  const resultsEmpty = needle != null && filteredItems.length === 0;
  const virtualized = filteredItems.length > MAX_ITEMS_WITHOUT_VIRTUALIZATION;
  // Items shown once shall be kept mounted until the needle changes, otherwise
  // the scroll position may jump around inadvertently. Pattern adopted from:
  // https://inokawa.github.io/virtua/?path=/story/advanced-keep-offscreen-items--append-only
  const [virtualState, setVirtualState] = useState({
    needle,
    length: filteredItems.length,
    mountedIndexes: []
  });
  // Note: virtualState.mountedIndexes is in deps but only read in the guarded branch.
  // This means external updates to mountedIndexes will trigger this effect but hit the guard
  // and bail out early. This is intentional and harmless - the guard ensures no unnecessary work.
  useEffect(() => {
    if (virtualState.needle !== needle || virtualState.length !== filteredItems.length) {
      const needleChanged = virtualState.needle !== needle;
      // eslint-disable-next-line react-hooks/set-state-in-effect -- Syncing virtual scroll state with filtered items
      setVirtualState({
        needle,
        length: filteredItems.length,
        mountedIndexes: needleChanged ? [] // Reset on needle change
        : filteredItems.length > 0 ? [...new Set([...virtualState.mountedIndexes, filteredItems.length - 1])] // Add last index
        : virtualState.mountedIndexes
      });
    }
  }, [needle, filteredItems.length, virtualState.needle, virtualState.length, virtualState.mountedIndexes]);
  const {
    mountedIndexes
  } = virtualState;
  const listboxContainerRef = useRef(null);
  useEffect(() => {
    if (listboxContainerRef.current != null) {
      listboxContainerRef.current.style.setProperty('--initial-height', `${listboxContainerRef.current.offsetHeight}px`);
    }
  }, []);
  useEffect(() => {
    initialRenderRef.current = false;
  }, []);
  const showStatus = resultsEmpty;
  const statusId = useId();
  const listboxId = useId();
  const getItemNode = index => {
    const item = filteredItems[index];
    return /*#__PURE__*/jsx(SelectInputItemView, {
      item: item,
      renderValue: renderValue,
      needle: needle
    }, index);
  };
  const findMatchingItem = autocompleteValue => {
    const flatOptions = items.flatMap(item => item.type === 'group' ? item.options : item.type === 'option' ? [item] : []).filter(item => item.type === 'option' && item.value != null);
    const exactMatch = flatOptions.find(option => String(option.value) === autocompleteValue || option.filterMatchers?.some(matcher => matcher === autocompleteValue));
    if (exactMatch) {
      return exactMatch.value;
    }
    const fuzzyMatch = flatOptions.find(option => option.filterMatchers?.some(matcher => matcher.toLowerCase().includes(autocompleteValue.toLowerCase())));
    return fuzzyMatch ? fuzzyMatch.value : null;
  };
  return /*#__PURE__*/jsxs(ListboxOptions, {
    modal: true,
    as: SelectInputOptionsContainer,
    static: true,
    className: "np-select-input-options-container",
    onAriaActiveDescendantChange: value => {
      if (controllerRef.current != null) {
        if (!initialRenderRef.current && value != null) {
          controllerRef.current.setAttribute('aria-activedescendant', value);
        } else {
          controllerRef.current.removeAttribute('aria-activedescendant');
        }
      }
    },
    children: [filterable ? /*#__PURE__*/jsx("div", {
      className: "np-select-input-query-container",
      children: /*#__PURE__*/jsx(SearchInput, {
        ref: searchInputRef,
        id: id,
        name: name,
        autoComplete: autocomplete,
        role: "combobox",
        shape: "rectangle",
        placeholder: filterPlaceholder,
        "aria-label": filterPlaceholder,
        defaultValue: filterQuery,
        "aria-autocomplete": "list",
        "aria-expanded": true,
        "aria-controls": listboxId,
        "aria-describedby": showStatus ? statusId : undefined,
        onKeyDown: event => {
          // Prevent interfering with the matcher of Headless UI
          // https://mathiasbynens.be/notes/javascript-unicode#regex
          if (/^.$/u.test(event.key)) {
            event.stopPropagation();
          }
        },
        onChange: event => {
          // Free up resources and ensure not to go out of bounds when the
          // resulting item count is less than before
          const inputValue = event.currentTarget.value;
          // Free up resources and ensure not to go out of bounds
          setVirtualState(prev => ({
            ...prev,
            mountedIndexes: []
          }));
          onFilterChange(inputValue);
        },
        onInput: event => {
          const inputValue = event.currentTarget.value;
          const inputElement = event.currentTarget;
          if (autocomplete && onAutocompleteSelect && inputValue) {
            setTimeout(() => {
              if (inputElement.value === inputValue && inputValue.length > 2) {
                const matchedValue = findMatchingItem(inputValue);
                if (matchedValue !== null) {
                  onAutocompleteSelect(matchedValue);
                }
              }
            }, 50);
          }
        }
      })
    }) : null, /*#__PURE__*/jsxs("section", {
      ref: listboxContainerRef,
      tabIndex: -1,
      className: clsx('np-select-input-listbox-container', virtualized && 'np-select-input-listbox-container--virtualized', needle == null &&
      // Groups aren't shown when filtering
      items.some(item => item.type === 'group') && 'np-select-input-listbox-container--has-group'),
      "data-wds-parent": parentId ?? undefined,
      children: [resultsEmpty ? /*#__PURE__*/jsxs("div", {
        id: statusId,
        className: "np-select-input-options-status",
        children: [/*#__PURE__*/jsx(CrossCircle, {
          size: 16,
          className: "np-select-input-options-status-icon"
        }), intl.formatMessage(messages.noResultsFound)]
      }) : null, /*#__PURE__*/jsx("div", {
        ref: listboxRef,
        id: listboxId,
        role: "listbox",
        "aria-orientation": "vertical",
        "aria-label": listBoxLabel,
        "aria-labelledby": listBoxLabelledBy,
        tabIndex: 0,
        className: "np-select-input-listbox",
        children: !virtualized ? filteredItems.map((_, index) => getItemNode(index)) : /*#__PURE__*/jsx(Virtualizer, {
          ref: virtualiserHandlerRef,
          data: filteredItems,
          keepMounted: mountedIndexes,
          scrollRef: listboxRef // `VList` doesn't expose this
          ,
          onScroll: async () => {
            if (!virtualiserHandlerRef.current) return;
            const startIndex = virtualiserHandlerRef.current.findItemIndex(virtualiserHandlerRef.current.scrollOffset);
            const endIndex = virtualiserHandlerRef.current.findItemIndex(virtualiserHandlerRef.current.scrollOffset + virtualiserHandlerRef.current.viewportSize);
            setVirtualState(prev => {
              // Create an array of all indexes that should be visible
              const visibleIndexes = [];
              for (let index = startIndex; index <= endIndex; index += 1) {
                // eslint-disable-next-line functional/immutable-data
                visibleIndexes.push(index);
              }
              // Combine with previous indexes and sort
              const newMountedIndexes = [...new Set([...prev.mountedIndexes, ...visibleIndexes])].sort((a, b) => a - b);
              return {
                ...prev,
                mountedIndexes: newMountedIndexes
              };
            });
          },
          children: (item, index) =>
          /*#__PURE__*/
          // The position of each item can't be inferred by browsers when
          // virtualizing, as some of the items may not be in the DOM
          jsx(SelectInputItemsCountContext.Provider, {
            value: filteredItems.length,
            children: /*#__PURE__*/jsx(SelectInputItemPositionContext.Provider, {
              value: index + 1,
              children: getItemNode(index)
            })
          })
        })
      }), renderFooter != null ? /*#__PURE__*/jsx("footer", {
        className: "np-select-input-footer",
        children: /*#__PURE__*/jsx("div", {
          role: "none",
          onKeyDown: event => {
            // Prevent interfering with Headless UI
            if (event.key !== 'Escape') {
              event.stopPropagation();
            }
          },
          children: renderFooter({
            resultsEmpty,
            queryNormalized: needle
          })
        })
      }) : null]
    })]
  });
}

export { SelectInputOptions };
//# sourceMappingURL=SelectInputOptions.mjs.map
