import { createEffect, createSignal } from "solid-js";
import { createStableCallback } from "../stable-callback.jsx";
import { useFocusableWhenDisabled } from "../use-focusable-when-disabled.jsx";

/**
 * Accessor-or-value input. Consumers may pass a plain boolean/number or a
 * reactive accessor; we always coerce via `read()` at call time.
 */

function read(value) {
  return typeof value === "function" ? value() : value;
}

/**
 * Parameters for {@link useButton}. Each flag may be a plain value or an
 * accessor so consumers can thread reactive state in without us having to
 * re-create the primitive on every change.
 */

/**
 * Minimal event shape we look at in our chained handlers. We keep the typing
 * structural so we don't over-commit to a specific JSX event type — callers
 * hand us whichever synthetic/native event the JSX runtime produced.
 */

/**
 * Return value of {@link useButton}.
 */

const BUTTON_TAG = "BUTTON";
const ANCHOR_TAG = "A";
function isButtonElement(element) {
  return element !== null && element.tagName === BUTTON_TAG;
}
function isValidLinkElement(element) {
  return element !== null && element.tagName === ANCHOR_TAG && typeof element.href === "string" && element.href.length > 0;
}

/**
 * Shared primitive powering `<Button>`, `<Toggle>`, and other button-shaped
 * components.
 *
 * Responsibilities mirror Base UI React's `useButton`:
 *
 * 1. Native path: lets the browser handle activation; threads `disabled` and
 *    defaults `type="button"`.
 * 2. Non-native path: adds `role="button"`, synthesizes click events on
 *    Enter/Space via `HTMLElement.click()` so consumer `onClick` handlers
 *    still fire through the normal DOM path, and sets `tabIndex`.
 * 3. Disabled handling: suppresses pointer/keyboard events via Base UI's
 *    `preventBaseUIHandler()` so chained consumer handlers don't run. Focus
 *    and blur events are NOT suppressed — this matches React Base UI, which
 *    deliberately lets focusableWhenDisabled components still observe focus
 *    transitions.
 *
 * Solid-specific decisions:
 *
 * - Inputs accept accessors so consumers can pass `() => props.disabled`
 *   without materializing a memo. All reads happen inside event handlers
 *   (call-time), so top-level reactive reads don't fire.
 * - Clicks are synthesized via `currentTarget.click()` rather than by calling
 *   a captured `externalOnClick` closure — the Solid merge pipeline keeps
 *   consumer handlers separate, so dispatching a real DOM click lets them
 *   fire through the normal right-to-left chain in `mergePropsN`.
 * - The `buttonRef` is a stable callback; Solid's native array-ref form
 *   (RFC 07) is used at the call site to compose with other refs.
 *
 * Reactivity notes:
 *
 * - The element is tracked in an internal signal with `ownedWrite: true` so
 *   the ref callback (unowned) can write to it without tripping the
 *   `SIGNAL_WRITE_IN_OWNED_SCOPE` diagnostic.
 * - The dev-mode native-button mismatch warning is wired up with a split
 *   `createEffect` — the compute half reads the element and the `native`
 *   flag; the apply half does the console.error.
 */
export function useButton(options = {}) {
  // Internal host-element signal. `ownedWrite` opts out of the
  // SIGNAL_WRITE_IN_OWNED_SCOPE check because the ref callback fires from
  // unowned scope and this signal is pure internal plumbing.
  const [element, setElement] = createSignal(null, {
    ownedWrite: true
  });

  // Read helpers, with Base UI React's default values applied.
  const disabled = () => read(options.disabled) ?? false;
  const focusableWhenDisabled = () => read(options.focusableWhenDisabled);
  const tabIndexOption = () => read(options.tabIndex) ?? 0;
  const isNativeButton = () => read(options.native) ?? true;
  const isCompositeItem = () => read(options.composite) ?? false;

  // Delegate tabIndex / aria-disabled / disabled to the shared primitive so
  // we mirror React's `useFocusableWhenDisabled` exactly. Pass everything as
  // accessors — the hook reads through `read()` on every getter access, so
  // our reactive inputs stay live.
  const fwd = useFocusableWhenDisabled({
    disabled,
    focusableWhenDisabled,
    composite: isCompositeItem,
    tabIndex: tabIndexOption,
    isNativeButton
  });

  // Dev-mode mismatch warning: if `native` disagrees with the actual tag.
  if (process.env.NODE_ENV !== "production") {
    createEffect(() => {
      const el = element();
      if (!el) return null;
      return {
        el,
        native: isNativeButton()
      };
    }, payload => {
      if (!payload) return;
      const isButtonTag = isButtonElement(payload.el);
      if (payload.native && !isButtonTag) {
        console.error("Base UI: A component that acts as a button expected a native <button> because the " + "`nativeButton` prop is true. Rendering a non-<button> removes native button " + "semantics, which can impact forms and accessibility. Use a real <button> in the " + "`render` prop, or set `nativeButton` to `false`.");
      } else if (!payload.native && isButtonTag) {
        console.error("Base UI: A component that acts as a button expected a non-<button> because the " + "`nativeButton` prop is false. Rendering a <button> keeps native behavior while Base UI " + "applies non-native attributes and handlers, which can add unintended extra attributes " + "(such as `role` or `aria-disabled`). Use a non-<button> in the `render` prop, or set " + "`nativeButton` to `true`.");
      }
    });

    // Composite edge case: a disabled composite button nested under another
    // (already-disabled) composite button needs its `disabled` DOM attr
    // cleared so the outer composite can still focus it. Runs whenever the
    // tracked inputs change.
    createEffect(() => {
      const el = element();
      if (!el) return null;
      return {
        el,
        disabled: disabled(),
        composite: isCompositeItem(),
        focusableWhenDisabled: focusableWhenDisabled()
      };
    }, payload => {
      if (!payload) return;
      if (!isButtonElement(payload.el)) return;
      // If composite and disabled, but `disabled` wasn't set on the
      // returned props (focusableWhenDisabled path), clear the
      // element's `disabled` property so focus isn't blocked.
      const shouldSuppress = payload.composite && payload.disabled && payload.focusableWhenDisabled;
      if (shouldSuppress && payload.el.disabled) {
        payload.el.disabled = false;
      }
    });
  }

  // The returned props object uses getters so every read re-evaluates — the
  // `renderElement` merge proxy calls `resolveKey` on every access, and
  // consumers may swap accessor-backed flags over time.
  const handlers = {
    onClick(event) {
      if (disabled()) {
        event.preventDefault?.();
        event.preventBaseUIHandler?.();
      }
    },
    onMouseDown(event) {
      if (disabled()) {
        event.preventBaseUIHandler?.();
      }
    },
    onKeyDown(event) {
      if (disabled()) {
        // Delegate the focusable-when-disabled key swallow to the shared
        // hook (no-op when not disabled+focusable; otherwise it
        // preventDefault()s every non-Tab key).
        fwd.props.onKeyDown(event);
        event.preventBaseUIHandler?.();
        return;
      }
      const currentTarget = event.currentTarget;
      if (!currentTarget) return;
      const isCurrentTarget = event.target === event.currentTarget;
      const native = isNativeButton();
      const composite = isCompositeItem();
      const isButton = isButtonElement(currentTarget);
      const isLink = !native && isValidLinkElement(currentTarget);
      const shouldClick = isCurrentTarget && (native ? isButton : !isLink);
      const isEnterKey = event.key === "Enter";
      const isSpaceKey = event.key === " ";
      const role = currentTarget.getAttribute("role");
      const isTextNavigationRole = role?.startsWith("menuitem") || role === "option" || role === "gridcell";
      if (isCurrentTarget && composite && isSpaceKey) {
        if (event.defaultPrevented && isTextNavigationRole) {
          return;
        }
        event.preventDefault?.();
        if (isLink || native && isButton) {
          // Native button / real link: let the browser's own `.click()`
          // run so form-submit/reset and link navigation semantics are
          // preserved. Consumer's onClick fires via the dispatched
          // click event through the normal merge chain; cancel our
          // pipeline so we don't double-handle.
          currentTarget.click();
          event.preventBaseUIHandler?.();
        } else if (shouldClick) {
          // Non-native composite: synthesize a click so the consumer's
          // onClick handler runs via the DOM event path.
          currentTarget.click();
          event.preventBaseUIHandler?.();
        }
        return;
      }
      if (shouldClick) {
        if (!native && (isSpaceKey || isEnterKey)) {
          event.preventDefault?.();
        }
        if (!native && isEnterKey) {
          currentTarget.click();
        }
      }
    },
    onKeyUp(event) {
      if (disabled()) {
        event.preventBaseUIHandler?.();
        return;
      }
      const currentTarget = event.currentTarget;
      if (!currentTarget) return;
      const native = isNativeButton();
      const composite = isCompositeItem();
      if (event.target === event.currentTarget && native && composite && isButtonElement(currentTarget) && event.key === " ") {
        // Native composite buttons get activation on keydown; swallow the
        // browser's keyup-driven click so we don't fire twice.
        event.preventDefault?.();
        return;
      }
      if (event.target === event.currentTarget && !native && !composite && event.key === " ") {
        // Non-native, non-composite Space activation happens on keyup
        // (matching native <button> semantics).
        currentTarget.click();
      }
    },
    onPointerDown(event) {
      if (disabled()) {
        event.preventDefault?.();
        event.preventBaseUIHandler?.();
      }
    }
  };

  // Build the reactive props object. Every access re-evaluates the getter
  // so that accessor-backed inputs stay live.
  const props = Object.defineProperties(Object.create(null), {
    type: {
      enumerable: true,
      configurable: true,
      get: () => isNativeButton() ? "button" : undefined
    },
    role: {
      enumerable: true,
      configurable: true,
      get: () => isNativeButton() ? undefined : "button"
    },
    tabIndex: {
      enumerable: true,
      configurable: true,
      // Per Solid 2 RFC 07, `aria-*` strings flow through unchanged;
      // the focusable-when-disabled hook owns the policy.
      get: () => fwd.props.tabIndex
    },
    "aria-disabled": {
      enumerable: true,
      configurable: true,
      get: () => fwd.props["aria-disabled"]
    },
    disabled: {
      enumerable: true,
      configurable: true,
      get: () => fwd.props.disabled
    },
    onClick: {
      enumerable: true,
      configurable: true,
      value: handlers.onClick
    },
    onMouseDown: {
      enumerable: true,
      configurable: true,
      value: handlers.onMouseDown
    },
    onKeyDown: {
      enumerable: true,
      configurable: true,
      value: handlers.onKeyDown
    },
    onKeyUp: {
      enumerable: true,
      configurable: true,
      value: handlers.onKeyUp
    },
    onPointerDown: {
      enumerable: true,
      configurable: true,
      value: handlers.onPointerDown
    }
  });
  const buttonRef = createStableCallback(() => el => {
    setElement(el);
  });
  return {
    props,
    ref: buttonRef
  };
}
//# sourceMappingURL=use-button.jsx.map
