/**
 * `useCompositeRoot` — keyboard navigation primitive for 1D lists.
 *
 * Translates the keyboard half of Base UI React's `useCompositeRoot`
 * (`packages/react/src/internals/composite/root/useCompositeRoot.ts`) to Solid
 * 2. This is intentionally a 1D-only engine — grid / dense / itemSizes /
 * onLoop / cols are deliberately not implemented here. Every non-floating
 * consumer (toolbar, tabs, radio-group, toggle-group) is 1D. Grid navigation
 * lives in the floating path (`floating-ui/hooks/use-list-navigation.ts` +
 * `floating-ui/utils/composite.ts`, `getGridNavigatedIndex`), where any grid
 * consumer (menu / select / combobox) already gets it. Build a non-floating
 * grid path only if a non-floating grid consumer surfaces — none is planned.
 *
 * Shape:
 *
 * - Returns `{ props, highlightedIndex, onHighlightedIndexChange,
 *   elementsRef, onMapChange, relayKeyboardEvent }`.
 * - `props` is a plain object containing the keyboard handler + focus
 *   handler, ready to be dropped into `renderElement`'s `params.props`
 *   array. Handlers read reactive values at call time inside the body; no
 *   top-level reactive reads.
 * - `highlightedIndex` is an accessor; `onHighlightedIndexChange(index,
 *   shouldScrollIntoView)` mirrors React Base UI's signature exactly.
 * - `elementsRef` is a plain array (NOT a signal) — downstream `<CompositeList>`
 *   owns its contents and the keyboard handler reads it at call time.
 */

import { createSignal, untrack } from "solid-js";
import { ACTIVE_COMPOSITE_ITEM, ALL_KEYS, ARROW_DOWN, ARROW_KEYS, ARROW_LEFT, ARROW_RIGHT, ARROW_UP, END, HOME, HORIZONTAL_KEYS, HORIZONTAL_KEYS_WITH_EXTRA_KEYS, MODIFIER_KEYS, VERTICAL_KEYS, VERTICAL_KEYS_WITH_EXTRA_KEYS } from "./composite-constants.jsx";
import { findNonDisabledListIndex, getMaxListIndex, getMinListIndex, isElementDisabled, isIndexOutOfListBounds, isNativeInput, scrollIntoViewIfNeeded } from "./composite-utils.jsx";

/**
 * A `MouseEvent`/`KeyboardEvent` with Base UI's handler-chain bailout.
 */

const EMPTY_MODIFIERS = [];
export function useCompositeRoot(params) {
  // Internal highlighted-index state. Composite is internal infrastructure
  // (no public consumer ever passes `highlightedIndex` directly — only
  // outer compounds like Tabs.List / ToggleGroup forward an accessor for
  // composite-to-composite coordination). The optional consumer-controlled
  // override stays via the `params.highlightedIndex` accessor; if absent,
  // the internal signal is the source of truth.
  //
  // Why this internal signal stays as part of §4.1: composite is a 100%
  // internal helper, not a public-API surface. Forcing every wrapper to
  // bring its own signal would push controlled/uncontrolled friction onto
  // internals where the consumer is never exposed. The pattern we removed
  // from public wrappers (drop defaultX, force consumer to bring signal)
  // doesn't apply here.
  const [internalHighlightedIndex, setInternalHighlightedIndex] = createSignal(0, {
    ownedWrite: true,
    name: "composite.internalHighlightedIndex"
  });
  const highlightedIndex = () => params.highlightedIndex?.() ?? internalHighlightedIndex();
  const setHighlightedIndex = next => {
    if (next === undefined) return;
    // Always notify the external onChange — consumers may use this to
    // observe even when they don't pass a controlled `highlightedIndex`.
    params.onHighlightedIndexChange?.(next);
    // Only write the internal signal when the consumer isn't driving
    // `highlightedIndex` themselves; mirrors consumer-owned signal mechanics
    // ("once-controlled-always-controlled" pattern, see solidify.md §6).
    if (params.highlightedIndex === undefined) {
      setInternalHighlightedIndex(next);
    }
  };

  // Synchronous mirror for keyboard-handler reads. In Solid 2 signal writes
  // settle on the next microtask (MIGRATION.md), so two arrow presses inside
  // the same tick would both see the pre-write value — making the second
  // press read a stale `highlightedIndex()`. We mirror the latest written
  // value into a plain variable so the handler can advance monotonically.
  // The accessor remains the source of truth for external readers.
  //
  // Strategy (see B3 in the wave-0 review):
  //
  // - On every handler entry we read the external controlled value (when
  //   the consumer passes one) and compare it against `lastSeenControlled`.
  // - If the consumer drove it elsewhere (external change across ticks),
  //   the value differs from our last observation; we resync
  //   `committedIndex` to the external value.
  // - Otherwise, we assume the caller is still mid-tick and prefer
  //   `committedIndex` — which reflects all writes we've issued so far in
  //   this tick, even if the controlled accessor hasn't flushed yet.
  //
  // The controlled accessor remains the single source of truth across
  // ticks; `committedIndex` is an intra-tick overlay only.
  let committedIndex = 0;
  let lastSeenControlled;

  // Root DOM element, captured via ref callback. `ownedWrite` because the
  // callback fires from unowned scope.
  const [rootEl, setRootEl] = createSignal(null, {
    ownedWrite: true
  });
  const rootRef = el => {
    setRootEl(el);
  };

  // The elements array lives here; `<CompositeList>` mutates it on our
  // behalf. Keeping it as a plain array (not a signal) matches the
  // `ref.current`-shaped contract floating-ui uses and keeps keyboard
  // handler reads synchronous.
  const elementsRef = [];

  // Once-only latch for the "initial active item" scan.
  let hasSetDefaultIndex = false;
  const onHighlightedIndexChange = (index, shouldScrollIntoView = false) => {
    committedIndex = index;
    setHighlightedIndex(index);
    if (shouldScrollIntoView) {
      const el = elementsRef[index] ?? null;
      scrollIntoViewIfNeeded(rootEl(), el, params.direction(), params.orientation ?? "both");
    }
  };
  const onMapChange = map => {
    // Invoked from `<CompositeList>` inside an effect apply when items
    // register/unregister; reads of `rootEl()` / `params.direction()`
    // are snapshots driving a one-shot scroll, not subscriptions.
    // `untrack` keeps the strict-read context from flagging them.
    untrack(() => {
      if (map.size === 0 || hasSetDefaultIndex) return;
      hasSetDefaultIndex = true;
      const sortedElements = Array.from(map.keys());
      const active = sortedElements.find(el => el.hasAttribute(ACTIVE_COMPOSITE_ITEM));
      const activeIndex = active ? sortedElements.indexOf(active) : -1;
      if (activeIndex !== -1) {
        onHighlightedIndexChange(activeIndex);
      }
      scrollIntoViewIfNeeded(rootEl(), active ?? null, params.direction(), params.orientation ?? "both");
    });
  };
  const disabledIndicesAccessor = () => params.disabledIndices?.();
  const getDisabledIndicesInput = () => {
    const list = disabledIndicesAccessor();
    return list;
  };
  const handleKeyDown = event => {
    const enableHomeEnd = params.enableHomeAndEndKeys ?? false;
    const orientation = params.orientation ?? "both";
    const loopFocus = params.loopFocus ?? true;
    const stopEventPropagation = params.stopEventPropagation ?? false;
    const modifierKeys = params.modifierKeys ?? EMPTY_MODIFIERS;
    const RELEVANT_KEYS = enableHomeEnd ? ALL_KEYS : ARROW_KEYS;
    if (!RELEVANT_KEYS.has(event.key)) return;
    if (isModifierKeySet(event, modifierKeys)) return;
    const root = rootEl();
    if (!root) return;
    const isRtl = params.direction() === "rtl";
    const keys = resolveDirectionalKeys(orientation, isRtl, enableHomeEnd, RELEVANT_KEYS);

    // Textbox bailout — defer arrow keys to the input when the cursor
    // isn't at the extent in the travel direction.
    const target = event.target;
    if (shouldDeferToNativeInput(event, target, keys.forwardKey, keys.backwardKey)) {
      return;
    }
    const list = elementsRef;
    const disabledInput = getDisabledIndicesInput();
    // See `committedIndex` declaration: prefer the mirror for
    // intra-tick reads, only resync to the controlled accessor when the
    // external value changed between handler calls (across ticks).
    const controlledValue = params.highlightedIndex?.();
    if (controlledValue !== undefined && controlledValue !== lastSeenControlled) {
      committedIndex = controlledValue;
    }
    lastSeenControlled = controlledValue;
    const currentIndex = committedIndex;
    const minIndex = getMinListIndex(list, disabledInput);
    const maxIndex = getMaxListIndex(list, disabledInput);
    const nextIndex = computeNextIndex(event, currentIndex, list, disabledInput, minIndex, maxIndex, enableHomeEnd, loopFocus, keys);
    if (nextIndex !== currentIndex && !isIndexOutOfListBounds(list, nextIndex)) {
      if (stopEventPropagation) event.stopPropagation();
      if (keys.preventedKeys.has(event.key)) event.preventDefault();
      onHighlightedIndexChange(nextIndex, true);

      // Defer focus until after the flush completes — matches React's
      // `queueMicrotask` placement so any concurrent focus-manager work
      // (e.g. `returnFocus`) has already settled.
      queueMicrotask(() => {
        elementsRef[nextIndex]?.focus();
      });
    }
  };
  const handleFocus = event => {
    const root = rootEl();
    const target = event.target;
    if (!root || target == null || !isNativeInput(target)) return;
    const input = target;
    input.setSelectionRange(0, input.value?.length ?? 0);
  };
  const orientationAttr = () => {
    const o = params.orientation ?? "both";
    return o === "both" ? undefined : o;
  };
  const props = {
    get "aria-orientation"() {
      return orientationAttr();
    },
    onFocus: handleFocus,
    onKeyDown: handleKeyDown
  };
  return {
    props,
    highlightedIndex,
    onHighlightedIndexChange,
    elementsRef,
    rootRef,
    onMapChange,
    relayKeyboardEvent: handleKeyDown
  };
}
function isModifierKeySet(event, ignoredModifierKeys) {
  for (const key of MODIFIER_KEYS.values()) {
    if (ignoredModifierKeys.includes(key)) continue;
    if (event.getModifierState(key)) return true;
  }
  return false;
}
// Resolve the orientation/rtl-aware key sets used to drive navigation. Pulled
// out of the handler so the (otherwise dense) lookup tables read as data.
function resolveDirectionalKeys(orientation, isRtl, enableHomeEnd, relevantKeys) {
  const horizontalForwardKey = isRtl ? ARROW_LEFT : ARROW_RIGHT;
  const horizontalBackwardKey = isRtl ? ARROW_RIGHT : ARROW_LEFT;
  return {
    forwardKey: {
      horizontal: horizontalForwardKey,
      vertical: ARROW_DOWN,
      both: horizontalForwardKey
    }[orientation],
    backwardKey: {
      horizontal: horizontalBackwardKey,
      vertical: ARROW_UP,
      both: horizontalBackwardKey
    }[orientation],
    forwardKeys: {
      horizontal: [horizontalForwardKey],
      vertical: [ARROW_DOWN],
      both: [horizontalForwardKey, ARROW_DOWN]
    }[orientation],
    backwardKeys: {
      horizontal: [horizontalBackwardKey],
      vertical: [ARROW_UP],
      both: [horizontalBackwardKey, ARROW_UP]
    }[orientation],
    preventedKeys: {
      horizontal: enableHomeEnd ? HORIZONTAL_KEYS_WITH_EXTRA_KEYS : HORIZONTAL_KEYS,
      vertical: enableHomeEnd ? VERTICAL_KEYS_WITH_EXTRA_KEYS : VERTICAL_KEYS,
      both: relevantKeys
    }[orientation]
  };
}

// Textbox bailout: when the event originated in a native input, arrow keys
// should defer to the input unless the caret sits at the extent in the travel
// direction. Returns `true` when the composite handler should stand down.
function shouldDeferToNativeInput(event, target, forwardKey, backwardKey) {
  if (!isNativeInput(target) || isElementDisabled(target)) return false;
  const input = target;
  const selectionStart = input.selectionStart;
  const selectionEnd = input.selectionEnd;
  const textContent = input.value ?? "";
  if (selectionStart == null || event.shiftKey || selectionStart !== selectionEnd) {
    return true;
  }
  if (event.key !== backwardKey && selectionStart < textContent.length) {
    return true;
  }
  if (event.key !== forwardKey && selectionStart > 0) {
    return true;
  }
  return false;
}

// The next-index math: Home/End jumps, edge looping, and the find-next-enabled
// scan. Returns the index the highlight should move to (possibly unchanged).
function computeNextIndex(event, currentIndex, list, disabledInput, minIndex, maxIndex, enableHomeEnd, loopFocus, keys) {
  let nextIndex = currentIndex;
  if (enableHomeEnd) {
    if (event.key === HOME) nextIndex = minIndex;else if (event.key === END) nextIndex = maxIndex;
  }
  const isForward = keys.forwardKeys.includes(event.key);
  const isBackward = keys.backwardKeys.includes(event.key);
  if (nextIndex === currentIndex && (isForward || isBackward)) {
    if (loopFocus && nextIndex === maxIndex && isForward) {
      nextIndex = minIndex;
    } else if (loopFocus && nextIndex === minIndex && isBackward) {
      nextIndex = maxIndex;
    } else {
      nextIndex = findNonDisabledListIndex(list, {
        startingIndex: nextIndex,
        decrement: isBackward,
        disabledIndices: disabledInput
      });
    }
  }
  return nextIndex;
}
//# sourceMappingURL=composite-root.jsx.map
