import { createSignal } from "solid-js";
import { useBaseUiId } from "../base-ui-id.jsx";
import { LabelableContext } from "./labelable-context.jsx";
/**
 * Provides a {@link LabelableContext} to descendants so label components and
 * the control they label can discover each other.
 *
 * Design notes (differences from Base UI React's `LabelableProvider`):
 *
 * - Control-id and label-id registries are kept in separate `Map<symbol, ...>`
 *   registries, each paired with a signal that holds the current computed
 *   view. When a registration changes, we recompute the view once and push
 *   the new value through the signal — no `tick` indirection.
 * - Registrations arrive from `useLabel` / `useLabelableId` during owned
 *   setup. Both registry signals opt into `ownedWrite` so those writes don't
 *   trip the Solid 2 `SIGNAL_WRITE_IN_OWNED_SCOPE` check (RFC 01). The writes
 *   are pure internal plumbing — no app-visible state depends on their
 *   timing within the owned scope.
 * - `getLabelIds` returns a fresh frozen array on each change so consumers
 *   that memoize off its reference (e.g. `useAriaLabelledBy` joining with
 *   spaces) see a stable identity between registrations.
 * - The `controlId` accessor reports the first still-registered control id
 *   (matching React's "first wins, ignore stale" logic). `null` means
 *   "implicit label", `undefined` means "nothing registered yet".
 */
export function LabelableProvider(props) {
  // Default id is generated lazily via `useBaseUiId` so the id slot is only
  // burned if the consumer actually wants one.
  const defaultId = useBaseUiId();
  const controlRegistrations = new Map();
  const labelRegistrations = new Map();

  // Seed the control id from props. `null` (explicit implicit) and string
  // values are kept as-is; `undefined` falls back to the generated default.
  //
  // We intentionally read `props.controlId` only once during setup. React's
  // `LabelableProvider` uses `useState(initialControlId)` which snapshots the
  // initial value and then lets `registerControlId` drive subsequent updates
  // — we match that.
  const initialControlId = props.controlId === undefined ? defaultId : props.controlId;
  const [controlIdSignal, setControlIdSignal] = createSignal(initialControlId, {
    ownedWrite: true
  });
  const [labelIdsSignal, setLabelIdsSignal] = createSignal(Object.freeze([]),
  // `equals: false` — we always push a fresh frozen array on change,
  // and we want consumers to re-run even when array contents happen to
  // match (e.g. a label swap that preserves length + order).
  {
    ownedWrite: true,
    equals: false
  });
  function recomputeControlId(prev) {
    // Mirror React: when nothing is registered, fall back to the seeded
    // initial id (generated default or explicit prop). Otherwise, reuse
    // `prev` if it's still registered (stable output), else pick the first
    // registered id in insertion order.
    if (controlRegistrations.size === 0) {
      return initialControlId;
    }
    let first;
    for (const id of controlRegistrations.values()) {
      if (prev !== undefined && id === prev) {
        return prev;
      }
      if (first === undefined) {
        first = id;
      }
    }
    return first;
  }
  function registerControlId(source, id) {
    if (id === undefined) {
      if (!controlRegistrations.has(source)) return;
      controlRegistrations.delete(source);
    } else {
      const existing = controlRegistrations.get(source);
      if (existing === id && controlRegistrations.has(source)) return;
      controlRegistrations.set(source, id);
    }
    // `ownedWrite` lets this run during owned setup/cleanup.
    setControlIdSignal(prev => recomputeControlId(prev));
  }
  function registerLabelId(source, id) {
    if (id === undefined) {
      if (!labelRegistrations.has(source)) return;
      labelRegistrations.delete(source);
    } else {
      const existing = labelRegistrations.get(source);
      if (existing === id) return;
      labelRegistrations.set(source, id);
    }
    // Snapshot the Map's insertion-order values into a fresh frozen array.
    // `isEqual: false` on the signal means every set fires subscribers — we
    // want that, because the array identity changes every time a label is
    // added or removed.
    setLabelIdsSignal(Object.freeze(Array.from(labelRegistrations.values())));
  }

  // Signals are already accessors — wrapping them in a `createMemo(() => sig())`
  // just adds a dependency layer that re-emits the same value. Expose the
  // signal accessors directly.
  const contextValue = {
    controlId: controlIdSignal,
    registerControlId,
    registerLabelId,
    getLabelIds: labelIdsSignal,
    present: true
  };
  return <LabelableContext value={contextValue}>{props.children}</LabelableContext>;
}
//# sourceMappingURL=labelable-provider.jsx.map
