// packages/components/src/autocomplete/index.tsx
import { renderToString, useEffect, useMemo, useReducer, useRef } from "@wordpress/element";
import { useInstanceId, useMergeRefs, useRefEffect, privateApis as composePrivateApis } from "@wordpress/compose";
import { create, slice, insert, isCollapsed, getTextContent } from "@wordpress/rich-text";
import { speak } from "@wordpress/a11y";
import { isAppleOS } from "@wordpress/keycodes";
import { AutocompleterUI } from "./autocompleter-ui.mjs";
import { getAutocompleteMatch } from "./get-autocomplete-match.mjs";
import { withIgnoreIMEEvents } from "../utils/with-ignore-ime-events.mjs";
import getNodeText from "../utils/get-node-text.mjs";
import { unlock } from "../lock-unlock.mjs";
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
var {
  subscribeDelegatedListener
} = unlock(composePrivateApis);
var EMPTY_FILTERED_OPTIONS = [];
var AUTOCOMPLETE_HOOK_REFERENCE = {};
function getCompletionObject(completion) {
  if (completion !== null && typeof completion === "object" && "action" in completion && completion.action !== void 0 && "value" in completion && completion.value !== void 0) {
    return completion;
  }
  return {
    action: "insert-at-caret",
    value: completion
  };
}
var initialState = {
  selectedIndex: 0,
  filteredOptions: EMPTY_FILTERED_OPTIONS,
  filterValue: "",
  autocompleter: null
};
function autocompleteReducer(state, action) {
  switch (action.type) {
    case "RESET":
      return initialState;
    case "SELECT":
      return {
        ...state,
        selectedIndex: action.index
      };
    case "OPTIONS":
      return {
        ...state,
        filteredOptions: action.options,
        selectedIndex: action.options.length === state.filteredOptions.length ? state.selectedIndex : 0
      };
    case "MATCH":
      return {
        ...state,
        autocompleter: action.completer,
        filterValue: action.query
      };
  }
}
function useAutocomplete({
  record,
  onChange,
  onReplace,
  completers,
  contentRef
}) {
  const instanceId = useInstanceId(AUTOCOMPLETE_HOOK_REFERENCE);
  const [state, dispatch] = useReducer(autocompleteReducer, initialState);
  const {
    selectedIndex,
    filteredOptions,
    filterValue,
    autocompleter
  } = state;
  const backspacingRef = useRef(false);
  const prevRecordTextRef = useRef("");
  const lastCompletionRef = useRef(null);
  function insertCompletion(replacement) {
    if (autocompleter === null) {
      return "";
    }
    const end = record.start;
    const start = end - autocompleter.triggerPrefix.length - filterValue.length;
    const toInsert = create({
      html: renderToString(replacement)
    });
    onChange(insert(record, toInsert, start, end));
    return getTextContent(toInsert);
  }
  function select(option) {
    if (option.isDisabled || !autocompleter) {
      return;
    }
    const {
      getOptionCompletion
    } = autocompleter;
    if (!getOptionCompletion) {
      dispatch({
        type: "RESET"
      });
      contentRef.current?.focus();
      return;
    }
    const completionObject = getCompletionObject(getOptionCompletion(option.value, filterValue));
    if ("replace" === completionObject.action) {
      onReplace([completionObject.value]);
      return;
    }
    if ("insert-at-caret" === completionObject.action) {
      const completionText = insertCompletion(completionObject.value);
      if (completionText.startsWith(autocompleter.triggerPrefix)) {
        const afterPrefix = completionText.slice(autocompleter.triggerPrefix.length);
        if (afterPrefix) {
          lastCompletionRef.current = {
            name: autocompleter.name,
            value: afterPrefix
          };
        }
      }
    }
    dispatch({
      type: "RESET"
    });
    contentRef.current?.focus();
  }
  function onChangeOptions(options) {
    dispatch({
      type: "OPTIONS",
      options
    });
  }
  function handleKeyDown(event) {
    backspacingRef.current = event.key === "Backspace";
    if (!autocompleter) {
      return;
    }
    if (filteredOptions.length === 0) {
      return;
    }
    if (event.defaultPrevented) {
      return;
    }
    switch (event.key) {
      case "ArrowUp":
      case "ArrowDown": {
        const offset = event.key === "ArrowUp" ? -1 : 1;
        const newIndex = (selectedIndex + offset + filteredOptions.length) % filteredOptions.length;
        dispatch({
          type: "SELECT",
          index: newIndex
        });
        if (isAppleOS()) {
          speak(getNodeText(filteredOptions[newIndex].label), "assertive");
        }
        break;
      }
      case "Escape":
        dispatch({
          type: "RESET"
        });
        event.preventDefault();
        break;
      case "Enter":
        select(filteredOptions[selectedIndex]);
        break;
      case "ArrowLeft":
      case "ArrowRight":
        dispatch({
          type: "RESET"
        });
        return;
      default:
        return;
    }
    event.preventDefault();
  }
  const textContent = useMemo(() => {
    if (isCollapsed(record)) {
      return getTextContent(slice(record, 0));
    }
    return "";
  }, [record]);
  useEffect(() => {
    const isTextChange = record.text !== prevRecordTextRef.current;
    prevRecordTextRef.current = record.text;
    function getTextAfterSelection() {
      return textContent ? getTextContent(slice(record, void 0, getTextContent(record).length)) : "";
    }
    const match = getAutocompleteMatch(textContent, completers, {
      matchCount: filteredOptions.length,
      isBackspacing: backspacingRef.current,
      getTextAfterSelection,
      lastCompletion: lastCompletionRef.current
    });
    if (!match) {
      if (autocompleter) {
        dispatch({
          type: "RESET"
        });
      }
      return;
    }
    const {
      completer,
      filterValue: query
    } = match;
    if (!autocompleter && !isTextChange) {
      return;
    }
    if (lastCompletionRef.current && lastCompletionRef.current.name === completer.name) {
      lastCompletionRef.current = null;
    }
    dispatch({
      type: "MATCH",
      completer,
      query
    });
  }, [textContent]);
  const {
    key: selectedKey = ""
  } = filteredOptions[selectedIndex] || {};
  const {
    className
  } = autocompleter || {};
  const isExpanded = !!autocompleter && filteredOptions.length > 0;
  const listBoxId = isExpanded ? `components-autocomplete-listbox-${instanceId}` : void 0;
  const activeId = isExpanded ? `components-autocomplete-item-${instanceId}-${selectedKey}` : null;
  const hasSelection = record.start !== void 0;
  const showPopover = !!textContent && hasSelection && !!autocompleter;
  return {
    listBoxId,
    activeId,
    onKeyDown: withIgnoreIMEEvents(handleKeyDown),
    popover: showPopover && /* @__PURE__ */ _jsx(AutocompleterUI, {
      autocompleter,
      className,
      filterValue,
      instanceId,
      listBoxId,
      selectedIndex,
      onChangeOptions,
      onSelect: select,
      contentRef,
      reset: () => dispatch({
        type: "RESET"
      })
    }, autocompleter.name + autocompleter.triggerPrefix)
  };
}
function recordValuesMatch(a, b) {
  return a.text === b.text && a.start === b.start && a.end === b.end;
}
function useLastDifferentValue(value) {
  const history = useRef([]);
  const lastEntry = history.current[history.current.length - 1];
  if (!lastEntry || !recordValuesMatch(value, lastEntry)) {
    history.current.push(value);
  }
  if (history.current.length > 2) {
    history.current.shift();
  }
  return history.current[0];
}
function useAutocompleteProps(options) {
  const ref = useRef(null);
  const onKeyDownRef = useRef(void 0);
  const {
    record
  } = options;
  const previousRecord = useLastDifferentValue(record);
  const {
    popover,
    listBoxId,
    activeId,
    onKeyDown
  } = useAutocomplete({
    ...options,
    contentRef: ref
  });
  onKeyDownRef.current = onKeyDown;
  const mergedRefs = useMergeRefs([ref, useRefEffect((element) => {
    function _onKeyDown(event) {
      onKeyDownRef.current?.(event);
    }
    return subscribeDelegatedListener(element, "keydown", _onKeyDown, true);
  }, [])]);
  const didUserInput = record.text !== previousRecord?.text;
  if (!didUserInput) {
    return {
      ref: mergedRefs
    };
  }
  return {
    ref: mergedRefs,
    children: popover,
    "aria-autocomplete": listBoxId ? "list" : void 0,
    "aria-owns": listBoxId,
    "aria-activedescendant": activeId
  };
}
function Autocomplete({
  children,
  isSelected,
  ...options
}) {
  const {
    popover,
    ...props
  } = useAutocomplete(options);
  return /* @__PURE__ */ _jsxs(_Fragment, {
    children: [children(props), isSelected && popover]
  });
}
export {
  Autocomplete as default,
  useAutocomplete,
  useAutocompleteProps,
  useLastDifferentValue
};
//# sourceMappingURL=index.mjs.map
