import { createContext, createMemo, runWithOwner, untrack, useContext } from "solid-js";
import { readOption } from "../internals/maybe-accessor.jsx";
import { SNAPSHOT_KEYS, snapshotKeys } from "../internals/observability/snapshot.jsx";
import { createCollatorItemFilter, createSingleSelectionCollatorFilter, getFilter } from "./internals/filter.jsx";
import { defaultItemEquality, selectedValueIncludes } from "./internals/item-equality.jsx";
import { isGroupedItems, stringifyAsLabel } from "./internals/resolve-value-label.jsx";

// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------

// ---------------------------------------------------------------------------
// Context
// ---------------------------------------------------------------------------

const ComboboxContext = createContext(null);
export function useCombobox() {
  const ctx = useContext(ComboboxContext);
  if (ctx == null) {
    throw new Error("Base UI: useCombobox() must be called inside a <c.Provider>.");
  }
  return ctx;
}

// ---------------------------------------------------------------------------
// Primitive
// ---------------------------------------------------------------------------

/**
 * Solid 2-native headless primitive for the combobox selection + filtering
 * logic. Owns: value / inputValue / open state, items / filteredItems /
 * query derivation, and `isItemSelected`. DOM concerns (floating-ui
 * positioning, input element, keyboard nav, portal) live in the structural
 * `<Combobox.Root>` wrapper that composes this primitive.
 */
export function createCombobox(options) {
  // Freeze multi-select at init — the scalar vs. array value shape cannot
  // flip after mount (same pattern as the structural wrapper).
  const isMultipleAtInit = untrack(() => readOption(options.multiple) === true || readOption(options.selectionMode) === "multiple");
  const selectionMode = createMemo(() => {
    if (readOption(options.multiple) === true) return "multiple";
    return readOption(options.selectionMode) ?? "single";
  }, {
    name: "combobox.selectionMode"
  });
  const isSingle = createMemo(() => selectionMode() === "single", {
    name: "combobox.isSingle"
  });
  const disabled = createMemo(() => Boolean(readOption(options.disabled)), {
    name: "combobox.disabled"
  });
  const readOnly = createMemo(() => Boolean(readOption(options.readOnly)), {
    name: "combobox.readOnly"
  });
  const itemToStringLabel = createMemo(() => readOption(options.itemToStringLabel), {
    name: "combobox.itemToStringLabel"
  });
  const isItemEqualToValue = createMemo(() => readOption(options.isItemEqualToValue) ?? defaultItemEquality, {
    name: "combobox.isItemEqualToValue"
  });

  // Value — reads through the consumer's accessor.
  const emptyArray = [];
  const value = createMemo(() => {
    const v = readOption(options.value);
    if (v === undefined) return isMultipleAtInit ? emptyArray : null;
    return v ?? null;
  }, {
    name: "combobox.value"
  });

  // inputValue — reads through the consumer's accessor if provided; otherwise
  // derives from the selected value label in single mode (computed default,
  // NOT internal state).
  const inputValue = createMemo(() => {
    const iv = readOption(options.inputValue);
    if (iv !== undefined) return iv;
    if (isSingle()) {
      const sv = readOption(options.value);
      if (sv === undefined || sv === null) return "";
      if (Array.isArray(sv)) return "";
      return stringifyAsLabel(sv, itemToStringLabel());
    }
    return "";
  }, {
    name: "combobox.inputValue"
  });

  // Open — reads through the consumer's accessor.
  const open = createMemo(() => Boolean(readOption(options.open)), {
    name: "combobox.open"
  });

  // Items
  const items = createMemo(() => readOption(options.items), {
    name: "combobox.items"
  });
  const isGrouped = createMemo(() => isGroupedItems(items()), {
    name: "combobox.isGrouped"
  });
  const query = createMemo(() => inputValue().trim(), {
    name: "combobox.query"
  });

  // Filtering
  const collatorFilter = createMemo(() => getFilter({
    locale: readOption(options.locale)
  }), {
    name: "combobox.collatorFilter"
  });
  const resolvedFilter = createMemo(() => {
    const userFilter = readOption(options.filter);
    if (userFilter === null) return () => true;
    if (userFilter !== undefined) return userFilter;
    const coll = collatorFilter();
    if (isSingle() && !readOption(options.queryChangedAfterOpen)) {
      // Read `value()` reactively so the filter re-derives when the
      // single selection changes (mirrors React's `selectedValue` dep).
      const v = value();
      return createSingleSelectionCollatorFilter(coll, itemToStringLabel(), Array.isArray(v) ? null : v);
    }
    return createCollatorItemFilter(coll, itemToStringLabel());
  }, {
    name: "combobox.resolvedFilter"
  });

  // Mirrors React's `shouldBypassFiltering` + `filterQuery`: in single-select
  // mode, when the user hasn't typed since opening and the query exactly equals
  // the selected label, blank the effective query so the full list is shown.
  const filterQuery = createMemo(() => {
    const q = query();
    if (q === "") return q;
    if (!isSingle() || readOption(options.queryChangedAfterOpen)) return q;
    const v = value();
    const selectedLabelString = v == null || Array.isArray(v) ? "" : stringifyAsLabel(v, itemToStringLabel());
    const shouldBypassFiltering = selectedLabelString !== "" && selectedLabelString.length === q.length && collatorFilter().contains(selectedLabelString, q);
    return shouldBypassFiltering ? "" : q;
  }, {
    name: "combobox.filterQuery"
  });
  const filteredItems = createMemo(() => {
    const list = items();
    if (!list) return [];
    const filt = resolvedFilter();
    const q = filterQuery();
    const label = itemToStringLabel();
    if (isGrouped()) {
      const groups = list;
      const out = [];
      for (const g of groups) {
        const candidates = q === "" ? g.items : g.items.filter(item => filt(item, q, label));
        if (candidates.length > 0) {
          out.push({
            ...g,
            items: candidates
          });
        }
      }
      return out;
    }
    if (q === "") return list;
    return list.filter(item => filt(item, q, label));
  }, {
    name: "combobox.filteredItems"
  });
  const flatFilteredItems = createMemo(() => {
    const fi = filteredItems();
    if (isGrouped()) {
      return fi.flatMap(g => g.items);
    }
    return fi;
  }, {
    name: "combobox.flatFilteredItems"
  });

  // isItemSelected — pure derivation, no reactive subscription needed.
  const isItemSelected = item => {
    const sv = value();
    if (sv == null) return false;
    if (Array.isArray(sv)) {
      return selectedValueIncludes(sv, item, isItemEqualToValue());
    }
    return isItemEqualToValue()(item, sv);
  };

  // Pure event-emitters — fire onXxxChange; the consumer is the only writer
  // of their signal. runWithOwner(null, ...) escapes the reactive owner so
  // signal writes inside the callback don't trigger SIGNAL_WRITE_IN_OWNED_SCOPE.
  const setValue = (next, reason = "imperative") => {
    runWithOwner(null, () => options.onValueChange?.(next, reason));
  };
  const setInputValue = (next, reason = "imperative") => {
    runWithOwner(null, () => options.onInputValueChange?.(next, reason));
  };
  const setOpen = (next, reason = "imperative") => {
    runWithOwner(null, () => options.onOpenChange?.(next, reason));
  };
  const api = {
    value,
    inputValue,
    open,
    selectionMode,
    disabled,
    readOnly,
    items,
    filteredItems,
    flatFilteredItems,
    query,
    isGrouped,
    isItemSelected,
    setValue,
    setInputValue,
    setOpen,
    Provider: props => <ComboboxContext value={api}>
				{props.children}
			</ComboboxContext>
  };
  api[SNAPSHOT_KEYS] = COMBOBOX_SNAPSHOT_KEYS;
  return api;
}
const COMBOBOX_SNAPSHOT_KEYS = snapshotKeys("value", "inputValue", "open", "selectionMode", "disabled", "readOnly", "query");
//# sourceMappingURL=create-combobox.jsx.map
