import { createContext, createMemo, createSignal, runWithOwner, untrack, useContext } from "solid-js";
import { createChangeEventDetails, REASONS } from "../internals/change-event-details.jsx";
import { readOption } from "../internals/maybe-accessor.jsx";
import { SNAPSHOT_KEYS, snapshotKeys } from "../internals/observability/snapshot.jsx";
import { useIsoLayoutEffect } from "../internals/use-iso-layout-effect.jsx";
import { DEFAULT_STEP } from "./utils/constants.jsx";
import { formatNumber, formatNumberMaxPrecision } from "./utils/format-number.jsx";
import { BASE_NON_NUMERIC_SYMBOLS, getNumberLocaleDetails, MINUS_SIGNS_WITH_ASCII, PERCENTAGES, PERMILLE, PLUS_SIGNS_WITH_ASCII, SPACE_SEPARATOR_RE } from "./utils/parse.jsx";
import { toValidatedNumber } from "./utils/validate.jsx";

/**
 * Options for {@link createNumberField}.
 *
 * All reactive inputs accept `MaybeAccessor<T>`. No internal state — the
 * consumer's signal is the sole source of truth.
 */

/**
 * Public API returned by {@link createNumberField}.
 */

const NumberFieldContext = createContext(null);

/**
 * Read the nearest {@link NumberFieldApi} provided by `c.Provider`. Throws if
 * called outside a Provider.
 */
export function useNumberField() {
  const ctx = useContext(NumberFieldContext);
  if (ctx == null) {
    throw new Error("Base UI: useNumberField() must be called inside a <c.Provider>.");
  }
  return ctx;
}
function getControlledInputValue(value, locale, format) {
  const explicitPrecision = format?.maximumFractionDigits != null || format?.minimumFractionDigits != null;
  return explicitPrecision ? formatNumber(value, locale, format) : formatNumberMaxPrecision(value, locale, format);
}
const IS_IOS = typeof navigator !== "undefined" && /iP(ad|hone|od)/.test(navigator.platform ?? "");

/**
 * Solid 2.0-native headless primitive for a number field. Owns logical
 * state: value normalization, step math, formatting, and increment/decrement.
 * DOM concerns (visible input ref, hidden input, wheel listener, field
 * validation wiring) stay in `<NumberField.Root>`.
 */
export function createNumberField(options) {
  const value = createMemo(() => readOption(options.value) ?? null, {
    name: "numberField.value"
  });
  const smallStep = createMemo(() => readOption(options.smallStep) ?? 0.1, {
    name: "numberField.smallStep"
  });
  const largeStep = createMemo(() => readOption(options.largeStep) ?? 10, {
    name: "numberField.largeStep"
  });
  const stepRaw = createMemo(() => readOption(options.step) ?? 1, {
    name: "numberField.step"
  });
  const step = createMemo(() => {
    const s = stepRaw();
    return s === "any" ? 1 : s;
  }, {
    name: "numberField.stepResolved"
  });
  const min = createMemo(() => readOption(options.min), {
    name: "numberField.min"
  });
  const max = createMemo(() => readOption(options.max), {
    name: "numberField.max"
  });
  const minWithDefault = createMemo(() => min() ?? Number.MIN_SAFE_INTEGER, {
    name: "numberField.minWithDefault"
  });
  const maxWithDefault = createMemo(() => max() ?? Number.MAX_SAFE_INTEGER, {
    name: "numberField.maxWithDefault"
  });
  const minWithZeroDefault = createMemo(() => min() ?? 0, {
    name: "numberField.minWithZeroDefault"
  });
  const format = createMemo(() => readOption(options.format), {
    name: "numberField.format"
  });
  const locale = createMemo(() => readOption(options.locale), {
    name: "numberField.locale"
  });
  const allowOutOfRange = createMemo(() => readOption(options.allowOutOfRange) === true, {
    name: "numberField.allowOutOfRange"
  });
  const snapOnStep = createMemo(() => readOption(options.snapOnStep) === true, {
    name: "numberField.snapOnStep"
  });

  // Visible input text. Initialized from the starting value.
  const [inputValue, setInputValueState] = createSignal(untrack(() => getControlledInputValue(value(), locale(), format())), {
    ownedWrite: true,
    name: "numberField.inputValue"
  });
  const [inputMode, setInputMode] = createSignal("numeric", {
    ownedWrite: true,
    name: "numberField.inputMode"
  });

  // Keep inputMode in sync for iOS.
  if (IS_IOS) {
    createMemo(() => {
      const lo = minWithDefault();
      const newMode = lo >= 0 ? "decimal" : "text";
      setInputMode(newMode);
    });
  }

  // Sync inputValue whenever the external value / locale / format changes
  // (e.g. consumer writes their signal from outside). The shouldSyncInput
  // guard suppresses this during IME composition.
  useIsoLayoutEffect(() => [value(), locale(), format()], () => untrack(() => {
    const shouldSync = readOption(options.shouldSyncInput) ?? true;
    if (!shouldSync) return undefined;
    const current = value();
    const nextInputValue = getControlledInputValue(current, locale(), format());
    if (nextInputValue !== inputValue()) {
      setInputValueState(nextInputValue);
    }
    return undefined;
  }));

  // Mutable ref — read synchronously from event handlers.
  const formatOptionsRef = {
    current: untrack(format)
  };
  function getStepAmount(event) {
    if (event?.altKey) return smallStep();
    if (event?.shiftKey) return largeStep();
    return step();
  }
  function getAllowedNonNumericKeys() {
    const {
      decimal,
      group,
      currency,
      literal
    } = getNumberLocaleDetails(locale(), format());
    const keys = new Set();
    for (const symbol of BASE_NON_NUMERIC_SYMBOLS) keys.add(symbol);
    if (decimal) keys.add(decimal);
    if (group) {
      keys.add(group);
      if (SPACE_SEPARATOR_RE.test(group)) keys.add(" ");
    }
    const style = format()?.style;
    const fmtUnit = format()?.unit;
    const allowPercentSymbols = style === "percent" || style === "unit" && fmtUnit === "percent";
    const allowPermilleSymbols = style === "percent" || style === "unit" && fmtUnit === "permille";
    if (allowPercentSymbols) for (const k of PERCENTAGES) keys.add(k);
    if (allowPermilleSymbols) for (const k of PERMILLE) keys.add(k);
    if (style === "currency" && currency) keys.add(currency);
    if (literal) {
      for (const ch of Array.from(literal)) keys.add(ch);
      if (SPACE_SEPARATOR_RE.test(literal)) keys.add(" ");
    }
    for (const k of PLUS_SIGNS_WITH_ASCII) keys.add(k);
    if (minWithDefault() < 0) {
      for (const k of MINUS_SIGNS_WITH_ASCII) keys.add(k);
    }
    return keys;
  }
  function setValue(raw, details) {
    const eventWithKeys = details.event;
    const dir = details.direction;
    const reason = details.reason;
    const shouldClamp = !allowOutOfRange() || !(reason === REASONS.inputChange || reason === REASONS.inputBlur || reason === REASONS.inputPaste || reason === REASONS.inputClear || reason === REASONS.none);
    const validatedValue = toValidatedNumber(raw, {
      step: dir ? (getStepAmount(eventWithKeys) ?? DEFAULT_STEP) * dir : undefined,
      format: formatOptionsRef.current,
      minWithDefault: minWithDefault(),
      maxWithDefault: maxWithDefault(),
      minWithZeroDefault: minWithZeroDefault(),
      snapOnStep: snapOnStep(),
      small: eventWithKeys?.altKey ?? false,
      clamp: shouldClamp
    });
    const isInputReason = reason === REASONS.inputChange || reason === REASONS.inputClear || reason === REASONS.inputBlur || reason === REASONS.inputPaste || reason === REASONS.none;
    const currentValue = value();
    const shouldFireChange = validatedValue !== currentValue || isInputReason;
    if (shouldFireChange) {
      // Pure event-emitter — runWithOwner(null) escapes the reactive owner
      // so consumer signal writes don't trip SIGNAL_WRITE_IN_OWNED_SCOPE.
      runWithOwner(null, () => options.onValueChange?.(validatedValue, details));
      if (details.isCanceled) {
        return shouldFireChange;
      }

      // Respect the wrapper's IME-composition guard; defaults to true.
      const shouldSync = readOption(options.shouldSyncInput) ?? true;
      if (shouldSync) {
        setInputValueState(formatNumber(validatedValue, locale(), format()));
      }
    }
    return shouldFireChange;
  }
  function increment(amount, {
    direction,
    currentValue,
    event,
    reason
  }) {
    const prevValue = currentValue == null ? untrack(value) : currentValue;
    const nextValue = typeof prevValue === "number" ? prevValue + amount * direction : Math.max(0, untrack(min) ?? 0);
    return setValue(nextValue, createChangeEventDetails(reason, event, undefined, {
      direction
    }));
  }
  function commitValue(next, details) {
    runWithOwner(null, () => options.onValueCommitted?.(next, details));
  }
  function setInputValue(next) {
    setInputValueState(next);
  }
  const api = {
    value,
    inputValue,
    minWithDefault,
    maxWithDefault,
    min,
    max,
    step,
    smallStep,
    largeStep,
    inputMode,
    locale,
    setValue,
    increment,
    commitValue,
    getStepAmount,
    getAllowedNonNumericKeys,
    setInputValue,
    Provider: props => <NumberFieldContext value={api}>{props.children}</NumberFieldContext>
  };
  api[SNAPSHOT_KEYS] = NUMBER_FIELD_SNAPSHOT_KEYS;
  return api;
}
const NUMBER_FIELD_SNAPSHOT_KEYS = snapshotKeys("value", "inputValue", "min", "max", "step", "smallStep", "largeStep", "inputMode");
//# sourceMappingURL=create-number-field.jsx.map
