import { dynamic } from "@solidjs/web";
import { createMemo, createSignal, omit, untrack } from "solid-js";
import { useBaseUiId } from "../../internals/base-ui-id.jsx";
import { createChangeEventDetails, REASONS } from "../../internals/change-event-details.jsx";
import { userRef } from "../../internals/compose-refs.jsx";
import { createControlledState } from "../../internals/create-controlled-state.jsx";
import { activeElement } from "../../internals/dom/active-element.jsx";
import { ownerDocument } from "../../internals/dom/owner-document.jsx";
import { useFieldRootContext } from "../../internals/field-context.jsx";
import { readOption } from "../../internals/maybe-accessor.jsx";
import { mergeRootProps } from "../../internals/merge-root-props.jsx";
import { resolveClassStyle } from "../../internals/resolve-class-style.jsx";
import { useIsoLayoutEffect } from "../../internals/use-iso-layout-effect.jsx";
import { VISUALLY_HIDDEN_STYLE } from "../../internals/visually-hidden-style.jsx";
import { createNumberField } from "../create-number-field.jsx";
import { DEFAULT_STEP } from "../utils/constants.jsx";
import { numberFieldDataAttrs } from "../utils/state-attributes-mapping.jsx";
import { NumberFieldRootContext } from "./number-field-root-context.jsx";

/** Overall state used by every `<NumberField.*>` part's `data-*` projection. */

/**
 * Groups all parts of the number field and manages its state. Renders a
 * `<div>`.
 *
 * Logical state (value normalization, step math, formatting, increment /
 * decrement) is owned by `createNumberField`. This wrapper retains DOM
 * concerns: field registration, hidden input, wheel listener, `disabled` /
 * `readOnly` / `required` resolved against the `<Field.Root>`, and the
 * shared mutable refs that child event handlers need synchronously.
 */
export function NumberFieldRoot(props) {
  const field = useFieldRootContext();
  const id = useBaseUiId(untrack(() => typeof props.id === "string" ? props.id : undefined));

  // DOM-concern prop thunks. `disabled` depends on the field context so it
  // stays here. `name` is similarly field-aware.
  const disabled = () => field.disabled() || untrack(() => props.disabled === true);
  const readOnly = () => untrack(() => props.readOnly === true);
  const required = () => untrack(() => props.required === true);
  const name = () => field.name() ?? untrack(() => props.name);
  const invalid = () => field.invalid();

  // `stepProp` preserves the "any" sentinel for the hidden input's `step`
  // attribute. The primitive resolves "any" → 1 for numeric math.
  const stepProp = () => untrack(() => readOption(props.step) ?? 1);

  // IME guard: set to `false` by `<NumberField.Input>` during composition to
  // suppress mid-composition input-value flicker. Read synchronously by the
  // primitive's `setValue` via `shouldSyncInput`.
  const allowInputSyncRef = {
    current: true
  };

  // Mutable refs read synchronously from event handlers in child parts.
  const lastChangedValueRef = {
    current: null
  };
  const hasPendingCommitRef = {
    current: false
  };
  const formatOptionsRef = {
    current: untrack(() => readOption(props.format))
  };

  // Shared input ref — `<NumberField.Input>` writes its element here; the
  // hidden input's onFocus redirects focus to it.
  const inputRef = {
    current: null
  };
  const hiddenInputRef = {
    current: null
  };

  // Keep `formatOptionsRef` synced so event-handler closures always read
  // the current format options.
  useIsoLayoutEffect(() => readOption(props.format), next => {
    formatOptionsRef.current = next;
  });

  // Controlled / uncontrolled value (Base UI `value` / `defaultValue`
  // contract): the `value` prop wins while its resolved value is defined
  // (an explicit `null` IS a controlled empty value); otherwise an internal
  // signal (seeded once from `defaultValue`, default `null`) backs the
  // state and the change flow below writes it.
  const [resolvedValue, setUncontrolledValue] = createControlledState({
    controlled: () => readOption(props.value),
    default: () => readOption(props.defaultValue),
    fallback: null,
    name: "<NumberField.Root>",
    state: "value"
  });

  // Logical state primitive. The `shouldSyncInput` bridge threads the IME
  // guard so the primitive skips input-value formatting during composition.
  const nf = createNumberField({
    value: resolvedValue,
    smallStep: () => readOption(props.smallStep),
    step: () => readOption(props.step),
    largeStep: () => readOption(props.largeStep),
    min: () => readOption(props.min),
    max: () => readOption(props.max),
    allowOutOfRange: () => readOption(props.allowOutOfRange),
    snapOnStep: () => readOption(props.snapOnStep),
    format: () => readOption(props.format),
    locale: () => readOption(props.locale),
    shouldSyncInput: () => allowInputSyncRef.current,
    onValueChange: (next, details) => {
      lastChangedValueRef.current = next;
      props.onValueChange?.(next, details);
      // Cancel gate matches React Base UI `NumberFieldRoot.setValue`:
      // `details.cancel()` keeps the uncontrolled state, dirtiness, and
      // pending-commit bookkeeping unchanged (input sync is skipped by
      // the primitive's own `isCanceled` check).
      if (details.isCanceled) return;
      setUncontrolledValue(next);
      field.setDirty(next !== field.validityData().initialValue);
      hasPendingCommitRef.current = true;
    },
    onValueCommitted: (next, details) => {
      hasPendingCommitRef.current = false;
      props.onValueCommitted?.(next, details);
    }
  });

  // Mutable value ref — child event handlers read this synchronously to
  // avoid a stale-closure read of the prop.
  const valueRef = {
    current: untrack(nf.value)
  };
  useIsoLayoutEffect(nf.value, next => {
    valueRef.current = next;
  });

  // Keep `field.filled` in sync.
  useIsoLayoutEffect(nf.value, next => {
    field.setFilled(next !== null);
  });
  const [isScrubbing, setIsScrubbing] = createSignal(false, {
    ownedWrite: true
  });

  // Wheel listener — `onWheel` in JSX can't call `preventDefault()`.
  useIsoLayoutEffect(() => [disabled(), readOnly(), props.allowWheelScrub, inputRef.current], ([isDisabled, isReadOnly, allowScrub, element]) => {
    if (isDisabled || isReadOnly || !allowScrub || !element) {
      return undefined;
    }
    function handleWheel(event) {
      if (event.ctrlKey || activeElement(ownerDocument(element)) !== element) {
        return;
      }
      event.preventDefault();
      const amount = nf.getStepAmount(event) ?? DEFAULT_STEP;
      nf.increment(amount, {
        direction: event.deltaY > 0 ? -1 : 1,
        event,
        reason: REASONS.wheel
      });
    }
    element.addEventListener("wheel", handleWheel, {
      passive: false
    });
    return () => {
      element.removeEventListener("wheel", handleWheel);
    };
  });
  const state = createMemo(() => ({
    ...field.state(),
    disabled: disabled(),
    readOnly: readOnly(),
    required: required(),
    value: nf.value(),
    inputValue: nf.inputValue(),
    scrubbing: isScrubbing()
  }));
  const contextValue = {
    inputRef,
    inputValue: nf.inputValue,
    value: nf.value,
    minWithDefault: nf.minWithDefault,
    maxWithDefault: nf.maxWithDefault,
    disabled,
    readOnly,
    id,
    setValue: nf.setValue,
    incrementValue: nf.increment,
    getStepAmount: nf.getStepAmount,
    allowInputSyncRef,
    formatOptionsRef,
    valueRef,
    lastChangedValueRef,
    hasPendingCommitRef,
    name,
    required,
    invalid,
    inputMode: nf.inputMode,
    getAllowedNonNumericKeys: nf.getAllowedNonNumericKeys,
    min: nf.min,
    max: nf.max,
    setInputValue: nf.setInputValue,
    locale: nf.locale,
    isScrubbing,
    setIsScrubbing: next => {
      setIsScrubbing(next);
    },
    state,
    onValueCommitted: nf.commitValue
  };
  const userStyling = resolveClassStyle(props, () => state());
  const userProps = omit(props, "class", "style", "attrs", "as", "allowOutOfRange", "allowWheelScrub", "children", "defaultValue", "disabled", "form", "format", "inputRef", "largeStep", "locale", "max", "min", "name", "onValueChange", "onValueCommitted", "readOnly", "required", "smallStep", "snapOnStep", "step", "value");
  const internalSlot = {
    get children() {
      return <NumberFieldRootContext value={contextValue}>
					{props.children}
					{/* The hidden input combines `tabindex={-1}` with
         `aria-hidden="true"` so it stays out of the
         tab order and AT tree (matches React Base UI).
         biome-ignore lint/a11y/noAriaHiddenOnFocusable: the `tabindex={-1}` removes the element from focus; the redundant `aria-hidden` mirrors React for belt-and-braces AT coverage. */}
					<input ref={el => {
          hiddenInputRef.current = el;
          // Bridge the hidden input into the field
          // validation pipeline so native validity
          // (rangeUnderflow / stepMismatch / etc.) flows
          // through the same path as `<Field.Control>`.
          field.validation.inputRef.current = el;
          props.inputRef?.(el);
        }} type="number" form={props.form} name={name()} value={nf.value() ?? ""} min={readOption(props.min)} max={readOption(props.max)} step={String(stepProp())} disabled={disabled()} required={required()} aria-hidden="true" tabindex={-1} style={{
          ...VISUALLY_HIDDEN_STYLE
        }} onFocus={() => {
          inputRef.current?.focus();
        }} onInput={event => {
          const nextValue = event.currentTarget.valueAsNumber;
          const parsedValue = Number.isNaN(nextValue) ? null : nextValue;
          const details = createChangeEventDetails(REASONS.none, event);
          field.setDirty(parsedValue !== field.validityData().initialValue);
          nf.setValue(parsedValue, details);
          if (field.shouldValidateOnChange()) {
            void field.validation.commit(parsedValue);
          }
        }} />
				</NumberFieldRootContext>;
    }
  };
  const styledUserProps = mergeRootProps(userStyling, userProps);
  let merged = mergeRootProps(internalSlot, numberFieldDataAttrs(state));
  merged = mergeRootProps(merged, styledUserProps);
  const Tag = dynamic(() => props.as ?? "div");
  return <Tag {...merged} ref={userRef(props)} />;
}
//# sourceMappingURL=number-field-root.jsx.map
