import { dynamic } from "@solidjs/web";
import { createSignal, omit, untrack } from "solid-js";
import { FormContext } from "../internals/form-context.jsx";
import { mergeRootProps } from "../internals/merge-root-props.jsx";
import { resolveClassStyle } from "../internals/resolve-class-style.jsx";
import { createStableCallback } from "../internals/stable-callback.jsx";
import { useValueChanged } from "../internals/use-value-changed.jsx";

/**
 * Focus helper mirroring React's `focusControl` in `Form.tsx`: focuses the
 * element and, when it's an `<input>`, selects its contents so the user can
 * immediately re-type.
 */
function focusInvalidControl(control) {
  if (!control) return;
  control.focus();
  if (control.tagName === "INPUT") {
    control.select();
  }
}

/**
 * Pick the "first" invalid field. Prefers DOM order via
 * `compareDocumentPosition` when the controls are connected to a document;
 * falls back to registration order (the Map's insertion order — which matches
 * mount order and therefore DOM order for most apps) when either control is
 * detached or the positions are identical.
 */
function firstInvalidInDomOrder(invalidFields) {
  if (invalidFields.length === 0) return undefined;
  if (invalidFields.length === 1) return invalidFields[0];
  let best = invalidFields[0];
  for (let i = 1; i < invalidFields.length; i++) {
    const candidate = invalidFields[i];
    const a = best?.controlRef.current;
    const b = candidate?.controlRef.current;
    if (!a || !b) continue;
    // Node.DOCUMENT_POSITION_FOLLOWING (0x04) — `b` follows `a`, keep `a`.
    // Node.DOCUMENT_POSITION_PRECEDING (0x02) — `b` precedes `a`, swap.
    const position = a.compareDocumentPosition(b);
    if (position & 0x02) {
      best = candidate;
    }
  }
  return best;
}

// The form has no state exposed to children — state-driven `class`/`style`
// callbacks will receive an empty object. Using `Record<string, never>`
// communicates "intentionally empty shape" (biome's `noEmptyInterface` /
// `noEmptyBlockStatements` don't flag it).

/**
 * A native `<form>` element that coordinates cross-field validation with
 * any `<Field.Root>` descendants.
 *
 * On native submit:
 *   1. Flip `submitAttemptedRef.current` to `true`.
 *   2. Iterate `formRef.current.fields` and call each field's `validate`.
 *   3. If any field reports `validityData.state.valid === false`, call
 *      `preventDefault()` on the submit event and invoke
 *      `onSubmitValidationFailed`.
 *   4. Otherwise pass the event through to the consumer's `onSubmit`.
 *
 * On native reset: clear the internal error map and reset
 * `submitAttemptedRef`.
 *
 * Documentation: [Base UI Form](https://base-ui.com/react/components/form)
 */
export function Form(props) {
  // `props.errors` can change across renders. We surface an internal
  // accessor so consumers of `FormContext.errors` re-run when the external
  // prop changes OR when we clear entries locally (e.g. via
  // `clearErrors(name)` on reset).
  //
  // Starting value comes from the current prop. `useValueChanged` below
  // mirrors later changes of the external prop into this signal — the
  // clear-errors handler writes into it too, so we own it outright.
  //
  // `ownedWrite: true` because we write the initial mirrored value from
  // `useValueChanged`'s apply callback (unowned — fine) AND from the
  // native reset handler (event scope — fine); the opt-in only exists to
  // silence the dev guard uniformly.
  //
  // The option is `ownedWrite` (Solid's runtime name — the migration doc
  // calls it `ownedWrite` but the actual API is `ownedWrite`). Used here
  // because the clear-errors handler and the reset handler both want to
  // write into this from inside owned-like scopes (event scope is
  // unowned, but Solid's guard is conservative). Matches the pattern
  // from `render-element.tsx` for the host-element signal.
  // Initial value is a body-time snapshot — `useValueChanged` below
  // mirrors `props.errors` changes into this signal, so the seed read
  // is intentionally non-tracking.
  const [errors, setErrorsSignal] = createSignal(untrack(() => props.errors ?? {}), {
    ownedWrite: true
  });
  const errorsAccessor = () => errors();

  // Mirror external `errors` prop changes into the internal signal. The
  // apply callback runs in unowned scope per `useValueChanged`'s contract
  // so the write is safe.
  useValueChanged(() => props.errors, () => {
    setErrorsSignal(props.errors ?? {});
  });

  // Plain mutable refs — identity is stable for the `<Form>`'s lifetime
  // and never flows through the reactive graph. Each `<Form>` instance
  // gets its own fresh `fields` map; this is what prevents cross-instance
  // leaks from the shared `NOOP_FORM_CONTEXT` sentinel.
  const formRef = {
    current: {
      fields: new Map()
    }
  };
  const submitAttemptedRef = {
    current: false
  };
  const setErrors = next => {
    setErrorsSignal(next);
  };

  // `clearErrors` is a stable trampoline so long-lived consumers (fields
  // stashing the handler in a registration record) don't get stale
  // closures. The inner function sees the current `errors()` snapshot on
  // each invocation.
  const clearErrors = createStableCallback(() => name => {
    if (name === undefined) {
      setErrorsSignal({});
      return;
    }
    const current = errors();
    if (!Object.hasOwn(current, name)) return;
    const next = {
      ...current
    };
    delete next[name];
    setErrorsSignal(next);
  });
  const validationMode = () => props.validationMode ?? "onSubmit";
  const contextValue = {
    formRef,
    errors: errorsAccessor,
    setErrors,
    clearErrors,
    validationMode,
    submitAttemptedRef,
    present: true
  };

  // Wrap consumer-supplied handlers so fields that capture the trampoline
  // at mount time always dispatch to the latest prop closure.
  const onSubmitCallback = createStableCallback(() => props.onSubmit);
  const onSubmitValidationFailedCallback = createStableCallback(() => props.onSubmitValidationFailed);

  // The native submit handler. Runs in event scope (unowned) so all
  // writes are safe — this is the single orchestration point between
  // native form submit and the field registry.
  //
  // Sync vs async: each field's `validate()` returns `void` for sync
  // validators (writes already landed) or `Promise<void>` for async
  // validators (`zod.parseAsync`, async refinements, server-side
  // checks). When ALL fields are sync we keep React-parity sync
  // behaviour; when ANY field is async we `preventDefault` upfront,
  // `Promise.all` the pending validators, then read validity and
  // dispatch. The async branch is the path that lets server-side
  // validation gate submission cleanly.
  const finishSubmit = event => {
    const invalidFields = Array.from(formRef.current.fields.values()).filter(field => field.validityData.state.valid === false);
    if (invalidFields.length > 0) {
      if (!event.defaultPrevented) {
        event.preventDefault();
      }
      // Match React's `SubmitOnInvalid` behavior: focus the first invalid
      // control (in DOM order) so keyboard users land on the field that
      // blocked the submit. Happens BEFORE dispatching to the consumer's
      // `onSubmitValidationFailed` so the handler sees the focused state
      // if it inspects `document.activeElement`.
      focusInvalidControl(firstInvalidInDomOrder(invalidFields)?.controlRef.current ?? null);
      onSubmitValidationFailedCallback(event, invalidFields);
      return;
    }
    onSubmitCallback(event);
  };
  const handleSubmit = event => {
    submitAttemptedRef.current = true;
    const fields = Array.from(formRef.current.fields.values());
    const pending = [];
    for (const field of fields) {
      const r = field.validate(true);
      if (r) pending.push(r);
    }
    if (pending.length === 0) {
      // All-sync path: identical to the React-parity behavior. Keeps
      // native form auto-submission alive when validation passes and
      // the consumer doesn't `preventDefault` themselves.
      finishSubmit(event);
      return;
    }

    // At least one async validator is in flight. Native auto-submission
    // must be cancelled NOW (before crossing the microtask boundary)
    // because the default action runs synchronously after this handler
    // returns. The Promise.all chain then runs in a microtask, by which
    // point native submission is no longer reachable for `event`.
    event.preventDefault();
    void Promise.all(pending).then(() => finishSubmit(event));
  };
  const handleReset = () => {
    setErrorsSignal({});
    submitAttemptedRef.current = false;
  };

  // Strip our own API props (and `as`) before spreading user HTML attrs.
  // `children` is stripped because our provider wrapper owns the children
  // subtree below. `onSubmit` is stripped because our chained handler is
  // the sole submit handler — `createStableCallback` already dispatches to
  // the latest consumer closure.
  const userStyling = resolveClassStyle(props, () => ({}));
  const userHtmlProps = omit(props, "class", "style", "attrs", "as", "children", "errors", "validationMode", "onSubmit", "onSubmitValidationFailed");
  const internalProps = {
    noValidate: true,
    onSubmit: handleSubmit,
    onReset: handleReset
  };
  const styledUserProps = mergeRootProps(userStyling, userHtmlProps);
  const merged = mergeRootProps(internalProps, styledUserProps);
  const Tag = dynamic(() => props.as ?? "form");
  return <Tag {...merged}>
			<FormContext value={contextValue}>{props.children}</FormContext>
		</Tag>;
}
//# sourceMappingURL=form.jsx.map
