import { Listbox } from '@headlessui/react';
import { CrossCircle, ChevronDown, Cross, Check } from '@transferwise/icons';
import { clsx } from 'clsx';
import mergeProps from 'merge-props';
import { useState, useRef, useEffect, useDeferredValue, createContext, useContext, useMemo, useId, forwardRef } from 'react';
import { useIntl } from 'react-intl';
import { Virtualizer } from 'virtua';
import { useEffectEvent } from '../common/hooks/useEffectEvent.mjs';
import { useScreenSize } from '../common/hooks/useScreenSize.mjs';
import { PolymorphicWithOverrides } from '../common/polymorphicWithOverrides/PolymorphicWithOverrides.mjs';
import { Breakpoint } from '../common/propsValues/breakpoint.mjs';
import dateTriggerMessages from '../dateLookup/dateTrigger/DateTrigger.messages.mjs';
import { BottomSheet } from './_BottomSheet.mjs';
import { ButtonInput } from './_ButtonInput.mjs';
import { Popover } from './_Popover.mjs';
import { useInputAttributes } from './contexts.mjs';
import { InputGroup } from './InputGroup.mjs';
import { SearchInput } from './SearchInput.mjs';
import messages from './SelectInput.messages.mjs';
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';

const MAX_ITEMS_WITHOUT_VIRTUALIZATION = 50;
function searchableString(value) {
  return value.trim().replace(/\s+/gu, ' ').normalize('NFKC').toLowerCase();
}
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 [];
}
function dedupeSelectInputOptionItem(item, existingValues) {
  if (!existingValues.has(item.value)) {
    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) {
  const existingValues = new Set();
  return items.map(item => {
    switch (item.type) {
      case 'option':
        {
          return dedupeSelectInputOptionItem(item, existingValues);
        }
      case 'group':
        {
          return {
            ...item,
            options: item.options.map(option => dedupeSelectInputOptionItem(option, existingValues))
          };
        }
    }
    return item;
  });
}
function selectInputOptionItemIncludesNeedle(item, needle) {
  return inferSearchableStrings(item.filterMatchers ?? item.value).some(haystack => haystack.includes(needle));
}
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;
  });
}
const defaultRenderTrigger = ({
  content,
  placeholderShown,
  clear,
  disabled,
  size,
  className
}) => /*#__PURE__*/jsx(InputGroup, {
  addonEnd: {
    content: /*#__PURE__*/jsxs("span", {
      className: clsx('np-select-input-addon-container', disabled && 'disabled'),
      children: [clear != null && !placeholderShown ? /*#__PURE__*/jsxs(Fragment, {
        children: [/*#__PURE__*/jsx(SelectInputClearButton, {
          onClick: event => {
            event.preventDefault();
            clear();
          }
        }), /*#__PURE__*/jsx("span", {
          className: "np-select-input-addon-separator"
        })]
      }) : null, /*#__PURE__*/jsx("span", {
        className: "np-select-input-addon",
        children: /*#__PURE__*/jsx(ChevronDown, {
          size: 16
        })
      })]
    }),
    initialContentWidth: 24 + 4,
    padding: 'sm'
  },
  disabled: disabled,
  className: className,
  children: /*#__PURE__*/jsx(SelectInputTriggerButton, {
    as: ButtonInput,
    size: size,
    children: /*#__PURE__*/jsx("span", {
      className: clsx('np-select-input-content', placeholderShown && 'np-select-input-placeholder'),
      children: content
    })
  })
});
function SelectInputClearButton({
  className,
  onClick
}) {
  const intl = useIntl();
  return /*#__PURE__*/jsx("button", {
    type: "button",
    "aria-label": intl.formatMessage(dateTriggerMessages.ariaLabel),
    className: clsx(className, 'np-select-input-addon np-select-input-addon--interactive'),
    onClick: onClick,
    children: /*#__PURE__*/jsx(Cross, {
      size: 16
    })
  });
}
const noop = () => {};
function SelectInput({
  id: idProp,
  name,
  multiple,
  placeholder,
  items,
  defaultValue,
  value: controlledValue,
  compareValues,
  renderValue = String,
  renderFooter,
  renderTrigger = defaultRenderTrigger,
  filterable,
  filterPlaceholder,
  disabled,
  size = 'md',
  className,
  UNSAFE_triggerButtonProps,
  onFilterChange = noop,
  onChange,
  onClose,
  onClear
}) {
  const inputAttributes = useInputAttributes({
    nonLabelable: true
  });
  const id = idProp ?? inputAttributes.id;
  const [open, setOpen] = useState(false);
  const initialized = useRef(false);
  const handleClose = useEffectEvent(onClose ?? (() => {}));
  useEffect(() => {
    if (initialized.current) {
      if (!open) {
        handleClose?.();
      }
    } else {
      initialized.current = true;
    }
  }, [handleClose, open]);
  const [filterQuery, _setFilterQuery] = useState('');
  const deferredFilterQuery = useDeferredValue(filterQuery);
  const setFilterQuery = useEffectEvent(query => {
    _setFilterQuery(query);
    if (query !== filterQuery) {
      onFilterChange({
        query,
        queryNormalized: query ? searchableString(query) : null
      });
    }
  });
  const triggerRef = useRef(null);
  const screenSm = useScreenSize(Breakpoint.SMALL);
  const OptionsOverlay = screenSm ? Popover : BottomSheet;
  const searchInputRef = useRef(null);
  const listboxRef = useRef(null);
  const controllerRef = filterable ? searchInputRef : listboxRef;
  /**
   * Attempts to resolve the `listbox` label
   * @see https://storybook.wise.design/?path=/docs/forms-selectinput-accessibility--docs#labelling
   */
  const getListBoxLabelProps = () => {
    if (UNSAFE_triggerButtonProps?.['aria-label']) {
      return {
        listBoxLabel: UNSAFE_triggerButtonProps['aria-label']
      };
    }
    if (UNSAFE_triggerButtonProps?.['aria-labelledby']) {
      return {
        listBoxLabelledBy: UNSAFE_triggerButtonProps['aria-labelledby']
      };
    }
    if (inputAttributes['aria-labelledby']) {
      return {
        listBoxLabelledBy: inputAttributes['aria-labelledby']
      };
    }
    return {};
  };
  return /*#__PURE__*/jsx(Listbox, {
    name: name,
    multiple: multiple,
    defaultValue: defaultValue,
    value: controlledValue,
    by: compareValues,
    disabled: disabled,
    onChange: value => {
      if (!multiple) {
        setOpen(false);
      }
      onChange?.(value);
    },
    children: ({
      disabled: uiDisabled,
      value
    }) => {
      const placeholderShown = multiple && Array.isArray(value) ? value.length === 0 : value == null;
      return /*#__PURE__*/jsx(OptionsOverlay, {
        placement: "bottom-start",
        open: open,
        renderTrigger: ({
          ref,
          getInteractionProps
        }) => /*#__PURE__*/jsx(SelectInputTriggerButtonPropsContext.Provider, {
          // eslint-disable-next-line react/jsx-no-constructed-context-values
          value: {
            ref: node => {
              ref(node);
              triggerRef.current = node;
            },
            ...inputAttributes,
            ...UNSAFE_triggerButtonProps,
            id,
            ...mergeProps({
              onClick: () => {
                setOpen(prev => !prev);
              },
              onKeyDown: event => {
                if (event.key === ' ' || event.key === 'Enter' || event.key === 'ArrowDown' || event.key === 'ArrowUp') {
                  setOpen(prev => !prev);
                }
              }
            }, getInteractionProps())
          },
          children: renderTrigger({
            content: !placeholderShown ? /*#__PURE__*/jsx(SelectInputOptionContentWithinTriggerContext.Provider, {
              value: true,
              children: multiple && Array.isArray(value) ? value.map(option => renderValue(option, true)).filter(node => node != null).join(', ') : renderValue(value, true)
            }) : placeholder,
            placeholderShown,
            clear: onClear != null ? () => {
              onClear();
              triggerRef.current?.focus({
                preventScroll: true
              });
            } : undefined,
            disabled: uiDisabled,
            size,
            className
          })
        }),
        initialFocusRef: controllerRef,
        size: filterable ? 'lg' : 'md',
        padding: "none",
        onClose: () => {
          setOpen(false);
        },
        onCloseEnd: () => {
          setFilterQuery('');
        },
        children: /*#__PURE__*/jsx(SelectInputOptions, {
          id: id ? `${id}Search` : undefined,
          items: items,
          renderValue: renderValue,
          renderFooter: renderFooter,
          filterable: filterable,
          filterPlaceholder: filterPlaceholder,
          searchInputRef: searchInputRef,
          listboxRef: listboxRef,
          filterQuery: deferredFilterQuery,
          onFilterChange: setFilterQuery,
          ...getListBoxLabelProps()
        })
      });
    }
  });
}
const SelectInputTriggerButtonPropsContext = /*#__PURE__*/createContext({});
function SelectInputTriggerButton({
  as = 'button',
  ...restProps
}) {
  const {
    ref,
    onClick,
    onKeyDown,
    ...interactionProps
  } = useContext(SelectInputTriggerButtonPropsContext);
  return /*#__PURE__*/jsx(Listbox.Button, {
    ref: ref,
    as: PolymorphicWithOverrides,
    role: "combobox",
    __overrides: {
      as,
      ...interactionProps
    },
    ...mergeProps({
      onClick,
      onKeyDown
    }, restProps)
  });
}
const SelectInputOptionsContainer = /*#__PURE__*/forwardRef(function SelectInputOptionsContainer({
  'aria-orientation': ariaOrientation,
  'aria-activedescendant': ariaActiveDescendant,
  role,
  tabIndex,
  onAriaActiveDescendantChange,
  onKeyDown,
  ...restProps
}, ref) {
  const handleAriaActiveDescendantChange = useEffectEvent(onAriaActiveDescendantChange);
  useEffect(() => {
    handleAriaActiveDescendantChange(ariaActiveDescendant);
  }, [ariaActiveDescendant, handleAriaActiveDescendantChange]);
  return /*#__PURE__*/jsx("div", {
    ref: ref,
    role: "none",
    onKeyDown: event => {
      // Prevent confirmation close without an active item
      if (event.key === 'Enter' && ariaActiveDescendant == null) {
        return;
      }
      // Prevent absorbing actions early
      if (event.key === 'Escape' || event.key === 'Tab') {
        onKeyDown?.({
          ...event,
          preventDefault: () => {},
          stopPropagation: () => {}
        });
        return;
      }
      onKeyDown?.(event);
    },
    ...restProps
  });
});
function SelectInputOptions({
  id,
  items,
  renderValue = String,
  renderFooter,
  filterable = false,
  filterPlaceholder,
  searchInputRef,
  listboxRef,
  filterQuery,
  onFilterChange,
  listBoxLabel,
  listBoxLabelledBy
}) {
  const intl = useIntl();
  const controllerRef = filterable ? searchInputRef : listboxRef;
  const [initialRender, setInitialRender] = useState(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 filteredItems = needle != null ? filterSelectInputItems(dedupeSelectInputItems(items), item => selectInputOptionItemIncludesNeedle(item, needle)) : items;
  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 [mountedIndexes, setMountedIndexes] = useState([]);
  useEffect(() => {
    // Ensure the 'End' key works as intended by keeping the last item mounted
    setMountedIndexes(prevMountedIndexes => {
      const indexes = new Set(prevMountedIndexes);
      indexes.add(filteredItems.length - 1);
      return [...indexes]; // Sorting is redundant by nature here
    });
  }, [needle,
  // Needed as `filteredItems.length` may be equal between two updates
  filteredItems.length]);
  const listboxContainerRef = useRef(null);
  useEffect(() => {
    if (listboxContainerRef.current != null) {
      listboxContainerRef.current.style.setProperty('--initial-height', `${listboxContainerRef.current.offsetHeight}px`);
    }
  }, []);
  useEffect(() => {
    setInitialRender(false);
  }, []);
  const showStatus = resultsEmpty;
  const statusId = useId();
  const listboxId = useId();
  const getItemNode = index => {
    const item = filteredItems[index];
    return /*#__PURE__*/jsx(SelectInputItemView
    // eslint-disable-next-line react/no-array-index-key
    , {
      item: item,
      renderValue: renderValue,
      needle: needle
    }, index);
  };
  return /*#__PURE__*/jsxs(Listbox.Options, {
    as: SelectInputOptionsContainer,
    static: true,
    className: "np-select-input-options-container",
    onAriaActiveDescendantChange: value => {
      if (controllerRef.current != null) {
        if (!initialRender && 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,
        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
          setMountedIndexes([]);
          onFilterChange(event.currentTarget.value);
        }
      })
    }) : 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'),
      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, {
          count: filteredItems.length,
          keepMounted: mountedIndexes,
          scrollRef: listboxRef // `VList` doesn't expose this
          ,
          onRangeChange: (startIndex, endIndex) => {
            setMountedIndexes(prevMountedIndexes => {
              const indexes = new Set(prevMountedIndexes);
              for (let index = startIndex; index <= endIndex; index += 1) {
                indexes.add(index);
              }
              return [...indexes].sort((a, b) => a - b);
            });
          },
          children: 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)
            })
          })
        }, needle)
      }), 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]
    })]
  });
}
function SelectInputItemView({
  item,
  renderValue,
  needle
}) {
  switch (item.type) {
    case 'option':
      {
        if (item.value != null && (needle == null || selectInputOptionItemIncludesNeedle(item, needle))) {
          return /*#__PURE__*/jsx(SelectInputOption, {
            value: item.value,
            disabled: item.disabled,
            children: renderValue(item.value, false)
          });
        }
        break;
      }
    case 'group':
      {
        return /*#__PURE__*/jsx(SelectInputGroupItemView, {
          item: item,
          renderValue: renderValue,
          needle: needle
        });
      }
    case 'separator':
      {
        if (needle == null) {
          return /*#__PURE__*/jsx("hr", {
            className: "np-select-input-separator-item"
          });
        }
        break;
      }
  }
  return null;
}
function SelectInputGroupItemView({
  item,
  renderValue,
  needle
}) {
  const headerId = useId();
  return (
    /*#__PURE__*/
    // An empty container may be rendered when no options match `needle`
    // However, pre-filtering would result in worse performance overall
    jsxs("section", {
      role: "group",
      "aria-labelledby": headerId,
      className: clsx(needle === null && 'np-select-input-group-item--without-needle'),
      children: [needle == null ? /*#__PURE__*/jsx("header", {
        id: headerId,
        role: "none",
        className: "np-select-input-group-item-header np-text-title-group",
        children: item.label
      }) : null, item.options.map((option, index) => /*#__PURE__*/jsx(SelectInputItemView
      // eslint-disable-next-line react/no-array-index-key
      , {
        item: option,
        renderValue: renderValue,
        needle: needle
      }, index))]
    })
  );
}
const SelectInputItemsCountContext = /*#__PURE__*/createContext(undefined);
const SelectInputItemPositionContext = /*#__PURE__*/createContext(undefined);
function SelectInputOption({
  value,
  disabled,
  children
}) {
  const itemsCount = useContext(SelectInputItemsCountContext);
  const itemPosition = useContext(SelectInputItemPositionContext);
  return /*#__PURE__*/jsx(Listbox.Option, {
    as: "div",
    value: value,
    "aria-setsize": itemsCount,
    "aria-posinset": itemPosition,
    disabled: disabled,
    className: ({
      active,
      disabled: uiDisabled
    }) => clsx('np-select-input-option-container np-text-body-large', active && 'np-select-input-option-container--active', uiDisabled && 'np-select-input-option-container--disabled'),
    children: ({
      selected
    }) => /*#__PURE__*/jsxs(Fragment, {
      children: [/*#__PURE__*/jsx("div", {
        className: "np-select-input-option",
        children: children
      }), /*#__PURE__*/jsx(Check, {
        size: 16,
        className: clsx('np-select-input-option-check', !selected && 'np-select-input-option-check--not-selected')
      })]
    })
  });
}
const SelectInputOptionContentWithinTriggerContext = /*#__PURE__*/createContext(false);
function SelectInputOptionContent({
  title,
  note,
  description,
  icon
}) {
  const withinTrigger = useContext(SelectInputOptionContentWithinTriggerContext);
  return /*#__PURE__*/jsxs("div", {
    className: clsx('np-select-input-option-content-container', (note || description) && 'np-text-body-large'),
    children: [icon ? /*#__PURE__*/jsx("div", {
      className: clsx('np-select-input-option-content-icon', !withinTrigger && 'np-select-input-option-content-icon--not-within-trigger'),
      children: icon
    }) : null, /*#__PURE__*/jsxs("div", {
      className: "np-select-input-option-content-text",
      children: [/*#__PURE__*/jsxs("div", {
        className: clsx('np-select-input-option-content-text-line-1', withinTrigger && 'np-select-input-option-content-text-within-trigger'),
        children: [/*#__PURE__*/jsx("div", {
          className: "d-inline",
          children: title
        }), note ? /*#__PURE__*/jsx("span", {
          className: "np-select-input-option-content-text-secondary np-text-body-default",
          children: note
        }) : null]
      }), description ? /*#__PURE__*/jsx("div", {
        className: clsx('np-select-input-option-content-text-secondary np-text-body-default', withinTrigger && 'np-select-input-option-content-text-within-trigger'),
        children: description
      }) : null]
    })]
  });
}

export { SelectInput, SelectInputOptionContent, SelectInputTriggerButton };
//# sourceMappingURL=SelectInput.mjs.map
