/**
 * `<CompositeList>` provider + `useCompositeListItem` hook.
 *
 * Owns a per-item `Element → metadata` map, observes DOM mutations to
 * discover re-ordering, and exposes:
 *   1. a plain-array `elementsRef` (ordered by DOM position)
 *   2. a `subscribeMapChange` helper so items can learn their index
 *   3. a `mapTick` signal that bumps whenever `register`/`unregister` fire,
 *      driving the sorted-map memo in a Solid-idiomatic way
 *
 * Mirrors `packages/react/src/internals/composite/list/CompositeList.tsx` in
 * Base UI React. The Solid translation:
 *
 * - React's `useState(0)` counter becomes a `createSignal(0)` + a hidden
 *   `latestTick` integer (we write to it from `register`/`unregister` which
 *   are called from unowned event/effect scope; the signal uses
 *   `ownedWrite: true` so we don't trip `SIGNAL_WRITE_IN_OWNED_SCOPE` when an
 *   `onCleanup` removes an item).
 * - React's `useMemo` becomes `createMemo`. The memo re-reads on `tick`
 *   changes and walks the map in document order.
 * - React's `useIsoLayoutEffect` calls become split `createRenderEffect`s
 *   (for DOM-level sync) and `createEffect`s (for listener dispatch).
 * - The sibling-unit `MutationObserver` is hooked up in a render-effect
 *   keyed on the sorted map; when elements are re-ordered in the DOM, the
 *   observer bumps `tick` so consumers re-sort on the next microtask.
 *
 * `useCompositeListItem` takes a per-call `source` symbol (created once per
 * invocation) which is how we identify items across re-registrations — Solid
 * has no component-instance identity for refs to hang off of, so we use an
 * explicit token. The returned ref callback registers the element on first
 * call and unregisters via `onCleanup` when the owning scope disposes.
 */

import { createMemo, createRenderEffect, createSignal, onCleanup, untrack } from "solid-js";
import { CompositeListContext, eraseCompositeListContext, useCompositeListContext } from "./composite-context.jsx";
import { sortByDocumentPosition } from "./composite-utils.jsx";

/**
 * Internal entry for the per-item source registry. We keep the `source`
 * symbol as the primary key so re-registrations from the same item (e.g.
 * when its element is unmounted and a new one is mounted in the same
 * lifetime) replace the old entry cleanly.
 */

export function CompositeList(props) {
  // Source-of-truth registry. Keyed by the per-item `source` symbol so
  // registration identity survives element swaps.
  const sources = new Map();

  // Listener set for `subscribeMapChange`. We keep it off-signal since
  // subscription lifetimes are managed by disposer functions, not tracked.
  const listeners = new Set();

  // Rerender tick. This is the "re-read" signal — every mutation bumps it
  // and the `sortedMap` memo re-runs. The signal uses `ownedWrite: true`
  // because we write to it from the unowned `register`/`unregister`
  // callbacks (and from the mutation observer's microtask), not from
  // owned reactive scope.
  const [tick, setTick] = createSignal(0, {
    ownedWrite: true
  });
  const bump = () => {
    setTick(t => t + 1);
  };
  const register = (source, node, metadata) => {
    sources.set(source, {
      node,
      metadata: metadata ?? null
    });
    bump();
  };
  const unregister = source => {
    sources.delete(source);
    bump();
  };
  const subscribeMapChange = fn => {
    listeners.add(fn);
    return () => {
      listeners.delete(fn);
    };
  };

  // Sorted map. Recomputes on `tick` — filters disconnected elements (so
  // `compareDocumentPosition` doesn't return inconsistent results) and
  // stamps each entry with its `index` for consumers.
  const sortedMap = createMemo(() => {
    // Track the tick signal so the memo re-runs on register/unregister.
    tick();
    const out = new Map();
    const nodes = [];
    const meta = new Map();
    for (const entry of sources.values()) {
      if (!entry.node.isConnected) continue;
      nodes.push(entry.node);
      meta.set(entry.node, entry.metadata);
    }
    nodes.sort(sortByDocumentPosition);
    nodes.forEach((node, index) => {
      const m = meta.get(node);
      out.set(node, {
        ...(m ?? {}),
        index
      });
    });
    return out;
  });

  // Wire up the MutationObserver. Runs in a render effect so ordering
  // changes (e.g. a <For> re-ordering its rows) bump the tick before the
  // next flush. We re-bind the observer when the sorted map's node set
  // changes so we're always observing the current parents.
  createRenderEffect(() => {
    // Track sortedMap so re-attach happens on real ordering changes.
    return sortedMap();
  }, map => {
    if (typeof MutationObserver !== "function" || map.size === 0) {
      return undefined;
    }
    const observer = new MutationObserver(entries => {
      // Count add/remove pairs: when the same node is both added and
      // removed (i.e. re-ordered), it cancels out. Only re-sort when
      // the churn set ends up *empty*, matching Base UI React.
      const diff = new Set();
      const update = node => {
        if (diff.has(node)) diff.delete(node);else diff.add(node);
      };
      entries.forEach(entry => {
        entry.removedNodes.forEach(update);
        entry.addedNodes.forEach(update);
      });
      if (diff.size === 0) bump();
    });
    const parents = new Set();
    map.forEach((_, node) => {
      if (node.parentElement) parents.add(node.parentElement);
    });
    parents.forEach(parent => {
      observer.observe(parent, {
        childList: true
      });
    });
    return () => {
      observer.disconnect();
    };
  });

  // Sync the shared elementsRef + labelsRef arrays with the sorted map and
  // fan out map changes to subscribers + the optional `onMapChange` prop.
  // Runs as a render effect (layout-timing) so consumers that read the map
  // during the same flush — e.g. `<Tabs.Indicator>` measurements or the
  // composite root's keyboard handler — see registered items before paint.
  // Matches Base UI React's `useIsoLayoutEffect` dispatch in `CompositeList`.
  createRenderEffect(() => sortedMap(), map => {
    const elements = props.elementsRef;
    const labels = props.labelsRef;
    elements.length = map.size;
    if (labels) labels.length = map.size;
    let i = 0;
    for (const [node] of map) {
      elements[i] = node;
      i += 1;
    }
    listeners.forEach(fn => {
      fn(map);
    });
    props.onMapChange?.(map);
    return undefined;
  });

  // `props.elementsRef` / `props.labelsRef` are captured once at setup —
  // they're stable mutable refs from the consumer, not a value that needs
  // to track. Wrap in `untrack` so the reads don't trip
  // `[STRICT_READ_UNTRACKED]` from this list's component-body scope.
  const contextValue = untrack(() => eraseCompositeListContext({
    register,
    unregister,
    subscribeMapChange,
    elementsRef: props.elementsRef,
    labelsRef: props.labelsRef
  }));
  return <CompositeListContext value={contextValue}>
			{props.children}
		</CompositeListContext>;
}
/**
 * Register the rendered element with the nearest `<CompositeList>`. Returns
 * a ref callback + an accessor for the item's current index.
 *
 * The `source` symbol is created once per call so re-registrations (e.g. the
 * owner scope re-running) stay identified across element swaps. Cleanup on
 * scope disposal unregisters automatically.
 */
export function useCompositeListItem(params = {}) {
  const ctx = useCompositeListContext();

  // Per-call identity token. Using a symbol (DECISION) avoids React's
  // useRef-of-symbol dance and makes the registration entry unambiguous
  // across element swaps.
  const source = Symbol("composite-list-item");

  // Hold onto the registered node so we can keep our own index in sync
  // whenever the list re-sorts. We also need it for unregister.
  let currentNode = null;

  // `index` signal. `-1` means "not yet registered" (matches React). Uses
  // ownedWrite because the writes happen in the subscription callback /
  // register path, which runs from unowned scope. The `params.index` read
  // is one-shot setup (consumer-supplied initial index), so untrack it so
  // callers that invoke this hook from an effect-apply scope don't trip
  // `[STRICT_READ_UNTRACKED]` on the synchronous signal-init read.
  const [index, setIndex] = createSignal(untrack(() => params.index) ?? -1, {
    ownedWrite: true
  });

  // Subscribe to map changes so our index stays live. We need the cleanup
  // to fire on scope disposal and to deregister if we leave the list
  // entirely.
  const unsubscribe = ctx.subscribeMapChange(map => {
    // Subscription fires from inside the list's `createEffect` apply
    // when items register/unregister. The `params.*` reads here are
    // snapshots driving a one-shot label-cache write, not
    // subscriptions. We pull each `params` access through its own
    // `untrack` call so any nested reactive accessors (props proxies
    // can wrap memos around getters) don't inherit the surrounding
    // effect-apply strict-read context.
    const paramsIndex = untrack(() => params.index);
    if (paramsIndex != null) return;
    if (!currentNode) return;
    const entry = map.get(currentNode);
    const i = entry?.index;
    if (typeof i !== "number") return;
    setIndex(i);
    const labels = ctx.labelsRef;
    if (!labels) return;
    const paramsLabel = untrack(() => params.label);
    const paramsTextContent = untrack(() => params.textRef?.current?.textContent);
    labels[i] = paramsLabel !== undefined ? paramsLabel ?? null : paramsTextContent ?? currentNode.textContent ?? null;
  });
  onCleanup(() => {
    unsubscribe();
    if (currentNode) {
      ctx.unregister(source);
      currentNode = null;
    }
  });

  // Beta 8: ref callbacks fire *before* the element is inserted into the
  // DOM. If we called `ctx.register` directly from the ref, the
  // downstream `sortedMap` memo (which filters on `isConnected`) would
  // see the node as disconnected and drop it — and since the list's
  // MutationObserver only re-bumps on re-ordering, the subsequent
  // insertion never triggers a re-sort. We capture the node in the ref
  // but defer the `register` call to a render effect, which fires during
  // `flush()` after DOM insertion has completed.
  const [nodeSignal, setNodeSignal] = createSignal(null, {
    ownedWrite: true
  });

  // `params.index` and `params.metadata` are consumer inputs read here as
  // snapshots — index for the static-explicit branch, metadata for the
  // register call. Wrapping in `untrack` keeps the ref callback and
  // register-effect's apply scope from tripping
  // `[STRICT_READ_UNTRACKED]` on those reads.
  const hasExplicitIndex = untrack(() => params.index) != null;
  const ref = node => {
    if (hasExplicitIndex) {
      currentNode = node;
      return;
    }
    setNodeSignal(node);
  };
  if (!hasExplicitIndex) {
    createRenderEffect(() => nodeSignal(), node => {
      if (currentNode && currentNode !== node) {
        ctx.unregister(source);
      }
      currentNode = node;
      if (node) {
        const metadata = untrack(() => params.metadata);
        ctx.register(source, node, metadata ?? null);
      }
    });
  }
  return {
    ref,
    index
  };
}
//# sourceMappingURL=composite-list.jsx.map
