import { createSignal, onCleanup, untrack } from "solid-js";
import { contains } from "../internals/dom/contains.jsx";
import { Timeout } from "../internals/use-timeout.jsx";

/**
 * Tier-3b port of Base UI React's `ToastStore`
 * (`packages/react/src/toast/store.ts`). The Solid port mirrors the React
 * store's shape but swaps the `ReactStore`/selector plumbing for plain
 * Solid signals. Every external reader (`Toast.Viewport`, `Toast.Root`,
 * `Toast.Close`) subscribes to the signals directly through accessors —
 * there are no selector memos because each reader only cares about one or
 * two slots.
 *
 * Scope differences vs React:
 *
 * - Heights are measured on `Toast.Root` and flow into stack CSS vars.
 * - Pause/resume follows React: when ANY of `hovering` / `focused` is true,
 *   or `isWindowFocused` is false, timers are paused; when all three
 *   release the lock, timers resume from their recorded remaining delay.
 */

/** The shape that consumers read (via accessors) from the store. */

let nextToastCounter = 0;
function generateToastId() {
  nextToastCounter += 1;
  return `toast-${nextToastCounter}-${Math.random().toString(36).slice(2, 8)}`;
}
function resolvePromiseOption(option, value) {
  if (option === undefined) return {};
  const resolved = typeof option === "function" ? option(value) : option;
  if (typeof resolved === "string") return {
    title: resolved
  };
  return resolved;
}
/**
 * Factory — owns all store signals + timers for the owning component. The
 * returned object is stable for the scope's lifetime; owner-dispose tears
 * every pending timer down via `onCleanup`.
 *
 * Signals opt into `ownedWrite` because the store's setters are invoked
 * from unowned surfaces (event handlers, timers) but the signals are
 * created inside the provider's owned scope.
 */
export function createToastStore(initial) {
  // Solid 2 signals update readers on the next flush — reads between
  // synchronous writes still see the stale value. Our multi-add / close
  // paths reshape the list multiple times per call site (e.g. two
  // `manager.add()` in the same frame), so we keep a private mutable
  // mirror and notify the signal after every mutation.
  //
  // `createStore` was explored as a replacement (see `spikes/toast-store/`)
  // — proxy reads give same-tick visibility natively, but its `<For>`
  // reconciliation preserves item proxies across array reassignment,
  // which breaks reorder behaviour when an id moves position. The mirror
  // stays until we rewrite the add/close paths as fine-grained draft
  // mutations (splice/push) rather than whole-array replacements.
  let toastsMirror = [];
  const [toasts, setToasts] = createSignal(toastsMirror, {
    ownedWrite: true,
    // Identity-based equality: every write produces a fresh array, so
    // the signal must treat "different array reference" as a change.
    // Default `isEqual` is a value compare — we shortcut that here.
    equals: false
  });
  const writeList = next => {
    toastsMirror = next;
    setToasts(next);
  };
  const readListRaw = () => toastsMirror;
  const [hovering, setHoveringSignal] = createSignal(false, {
    ownedWrite: true
  });
  const [focused, setFocusedSignal] = createSignal(false, {
    ownedWrite: true
  });
  const [isWindowFocused, setIsWindowFocusedSignal] = createSignal(true, {
    ownedWrite: true
  });
  const [prevFocusElement, setPrevFocusElement] = createSignal(null, {
    ownedWrite: true
  });
  const [viewport, setViewport] = createSignal(null, {
    ownedWrite: true
  });
  const [timeoutOption, setTimeoutOption] = createSignal(initial.timeout, {
    ownedWrite: true
  });
  const [limitOption, setLimitOption] = createSignal(initial.limit, {
    ownedWrite: true
  });
  const timers = new Map();
  let timersPaused = false;
  onCleanup(() => {
    for (const [, t] of timers) t.timeout?.clear();
    timers.clear();
  });
  const hasDifferingHeights = () => {
    const heights = readListRaw().map(t => t.height ?? 0).filter(h => h > 0);
    return heights.length > 0 && new Set(heights).size > 1;
  };
  const getExpanded = () => hovering() || focused() || hasDifferingHeights();
  const getExpandedOrOutOfFocus = () => hovering() || focused() || !isWindowFocused();
  const shouldStartPaused = () => untrack(getExpandedOrOutOfFocus);
  const scheduleTimer = (id, delay, cb) => {
    const startActive = !shouldStartPaused();
    const t = startActive ? Timeout.create() : undefined;
    t?.start(delay, () => {
      timers.delete(id);
      cb();
    });
    timers.set(id, {
      timeout: t,
      start: startActive ? Date.now() : 0,
      delay,
      remaining: delay,
      callback: cb
    });
  };
  const pauseTimers = () => {
    if (timersPaused) return;
    timersPaused = true;
    for (const [, t] of timers) {
      if (t.timeout) {
        t.timeout.clear();
        const elapsed = Date.now() - t.start;
        const remaining = t.delay - elapsed;
        t.remaining = remaining > 0 ? remaining : 0;
        t.timeout = undefined;
      }
    }
  };
  const resumeTimers = () => {
    if (!timersPaused) return;
    timersPaused = false;
    for (const [id, t] of timers) {
      const remaining = t.remaining > 0 ? t.remaining : t.delay;
      const handle = Timeout.create();
      t.timeout = handle;
      t.start = Date.now();
      handle.start(remaining, () => {
        timers.delete(id);
        t.callback();
      });
    }
  };
  const setHovering = next => {
    setHoveringSignal(next);
    if (next) pauseTimers();else if (!untrack(focused) && untrack(isWindowFocused)) resumeTimers();
  };
  const setFocused = next => {
    setFocusedSignal(next);
    if (next) pauseTimers();else if (!untrack(hovering) && untrack(isWindowFocused)) resumeTimers();
  };
  const setIsWindowFocused = next => {
    setIsWindowFocusedSignal(next);
    if (!next) pauseTimers();else if (!untrack(hovering) && !untrack(focused)) resumeTimers();
  };
  const findIndex = id => readListRaw().findIndex(t => t.id === id);
  const writeToasts = next => {
    writeList(next);
    if (next.length === 0) {
      setHoveringSignal(false);
      setFocusedSignal(false);
    }
  };
  const removeToast = (id, opts = {}) => {
    const index = findIndex(id);
    if (index === -1) return;
    const current = readListRaw();
    const toast = current[index];
    if (!opts.skipOnRemove) toast?.onRemove?.();
    const next = current.slice();
    next.splice(index, 1);
    writeToasts(next);
  };
  const updateToastInternal = (id, updates, behavior = {}) => {
    const current = readListRaw();
    const index = current.findIndex(t => t.id === id);
    if (index === -1) return;
    const prev = current[index];
    if (!prev) return;
    if (prev.transitionStatus === "ending") return;

    // Snapshot the fields the timer logic below needs from the PRE-update
    // toast — we mutate `prev` in place further down, so reading these after
    // the mutation would see the new values.
    const prevTimeout = prev.timeout;
    const prevType = prev.type;
    const nextToast = {
      ...prev,
      ...updates,
      id: prev.id,
      ...(behavior.markUpdated ? {
        updateKey: (prev.updateKey ?? 0) + 1
      } : {})
    };

    // Skip no-op internal writes so ResizeObserver ↔ store ↔ style does not
    // spin when only `height` / `transitionStatus` are unchanged.
    if (!behavior.markUpdated && (nextToast.height ?? 0) === (prev.height ?? 0) && nextToast.transitionStatus === prev.transitionStatus && nextToast.limited === prev.limited && nextToast.type === prev.type && nextToast.timeout === prev.timeout) {
      return;
    }

    // Mutate the EXISTING toast object in place rather than swapping in a
    // fresh `{ ...prev, ...updates }` reference. Consumers render the queue
    // with a keyed `<For each={toasts()}>`, which keys rows by item
    // IDENTITY — replacing the object reference makes `<For>` drop and
    // rebuild the row, re-firing `Toast.Root`'s mount `ref` →
    // `recalculateHeight()` → another internal write → another rebuild. In
    // happy-dom the no-op guard above collapses that after one pass (height
    // is constant `0`), but in a real browser sub-pixel height jitter keeps
    // each measurement different, so the rebuild/measure cycle spins at
    // 100% CPU. Preserving identity keeps the row mounted; the `toast` memo
    // in `ToastRoot` (created with `equals: false`) still re-notifies style
    // readers off the queue signal. Add / close / reorder still allocate
    // new references where a genuine identity change is intended.
    Object.assign(prev, updates, {
      id: prev.id
    });
    if (behavior.markUpdated) {
      prev.updateKey = (prev.updateKey ?? 0) + 1;
    }
    const nextList = current.slice();
    writeToasts(nextList);
    const defaultTimeout = untrack(timeoutOption);
    const nextDuration = nextToast.timeout ?? defaultTimeout;
    const prevDuration = prevTimeout ?? defaultTimeout;
    const timeoutUpdated = Object.hasOwn(updates, "timeout");
    const shouldHaveTimer = nextToast.transitionStatus !== "ending" && nextToast.type !== "loading" && nextDuration > 0;
    const hasTimer = timers.has(id);
    const changed = prevDuration !== nextDuration;
    const wasLoading = prevType === "loading";
    if (!shouldHaveTimer && hasTimer) {
      timers.get(id)?.timeout?.clear();
      timers.delete(id);
      return;
    }
    if (shouldHaveTimer && (!hasTimer || changed || timeoutUpdated || wasLoading || behavior.resetTimer)) {
      timers.get(id)?.timeout?.clear();
      timers.delete(id);
      scheduleTimer(id, nextDuration, () => closeToast(id));
      if (untrack(getExpandedOrOutOfFocus)) pauseTimers();
    }
  };
  const addToast = options => {
    const defaultTimeout = untrack(timeoutOption);
    const currentLimit = untrack(limitOption);
    const id = options.id ?? generateToastId();
    const current = readListRaw();
    if (options.id) {
      const existing = current.find(t => t.id === options.id);
      if (existing) {
        if (existing.transitionStatus === "ending") {
          removeToast(options.id, {
            skipOnRemove: true
          });
        } else {
          const {
            id: _ignored,
            transitionStatus: _ts,
            ...updates
          } = options;
          updateToastInternal(options.id, updates, {
            resetTimer: true,
            markUpdated: true
          });
          return options.id;
        }
      }
    }
    const toastToAdd = {
      ...options,
      id,
      updateKey: 0,
      transitionStatus: "starting"
    };
    const withNew = [toastToAdd, ...readListRaw()];
    const active = withNew.filter(t => t.transitionStatus !== "ending");
    let nextList;
    if (active.length > currentLimit) {
      const excess = active.length - currentLimit;
      const oldestActive = active.slice(-excess);
      const limitedIds = new Set(oldestActive.map(t => t.id));
      nextList = withNew.map(t => {
        const limited = limitedIds.has(t.id);
        return t.limited === limited ? t : {
          ...t,
          limited
        };
      });
    } else {
      nextList = withNew.map(t => t.limited ? {
        ...t,
        limited: false
      } : t);
    }
    writeToasts(nextList);
    const duration = toastToAdd.timeout ?? defaultTimeout;
    if (toastToAdd.type !== "loading" && duration > 0) {
      scheduleTimer(id, duration, () => closeToast(id));
    }
    if (untrack(getExpandedOrOutOfFocus)) pauseTimers();
    return id;
  };
  const updateToast = (id, updates) => {
    updateToastInternal(id, updates, {
      markUpdated: true
    });
  };
  const closeToast = id => {
    const closeAll = id === undefined;
    const current = readListRaw();
    if (!closeAll && !current.some(t => t.id === id)) return;
    let toastsToClose;
    if (closeAll) {
      toastsToClose = current;
      for (const [, t] of timers) t.timeout?.clear();
      timers.clear();
    } else {
      toastsToClose = current.filter(t => t.id === id);
      const t = timers.get(id);
      if (t) {
        t.timeout?.clear();
        timers.delete(id);
      }
    }
    const currentLimit = untrack(limitOption);
    let activeIndex = 0;
    const nextList = current.map(item => {
      if (closeAll || item.id === id) {
        return {
          ...item,
          transitionStatus: "ending",
          height: 0
        };
      }
      if (item.transitionStatus === "ending") return item;
      const isLimited = activeIndex >= currentLimit;
      activeIndex += 1;
      return item.limited === isLimited ? item : {
        ...item,
        limited: isLimited
      };
    });
    writeList(nextList);
    if (closeAll || current.length === 1) {
      setHoveringSignal(false);
      setFocusedSignal(false);
    }
    for (const toast of toastsToClose) {
      if (toast.transitionStatus !== "ending") toast.onClose?.();
    }

    // Focus recovery — delegate to the provider's current viewport +
    // prevFocusElement. We keep the check simple: if active element is
    // inside the viewport, refocus the previous element. No next-toast
    // focus traversal (tier-3b simplification).
    const vp = untrack(viewport);
    if (vp?.contains(vp.ownerDocument.activeElement)) {
      restoreFocusToPrevElement();
    }
  };
  const restoreFocusToPrevElement = () => {
    const el = untrack(prevFocusElement);
    try {
      el?.focus({
        preventScroll: true
      });
    } catch {
      // happy-dom can throw on .focus() for detached nodes — swallow.
    }
  };
  const promiseToast = (value, options) => {
    const loading = resolvePromiseOption(options.loading);
    const id = addToast({
      ...loading,
      type: "loading"
    });
    const handled = value.then(result => {
      const success = resolvePromiseOption(options.success, result);
      updateToast(id, {
        ...success,
        type: "success",
        timeout: success.timeout
      });
      return result;
    }).catch(error => {
      const err = resolvePromiseOption(options.error, error);
      updateToast(id, {
        ...err,
        type: "error",
        timeout: err.timeout
      });
      return Promise.reject(error);
    });
    const optionsAsAny = "setPromise" in options ? options : {
      setPromise: undefined
    };
    if (Object.hasOwn(optionsAsAny, "setPromise") && typeof optionsAsAny.setPromise === "function") {
      optionsAsAny.setPromise(handled);
    }
    return handled;
  };
  const isEmpty = () => toasts().length === 0;
  const handleDocumentPointerDown = event => {
    if (event.pointerType !== "touch") return;
    const vp = untrack(viewport);
    if (!vp) return;
    const target = event.target;
    if (contains(vp, target)) return;
    setHovering(false);
    setFocused(false);
  };
  return {
    toasts,
    hovering,
    focused,
    expanded: getExpanded,
    isEmpty,
    prevFocusElement,
    viewport,
    isWindowFocused,
    timeout: timeoutOption,
    limit: limitOption,
    addToast,
    updateToast,
    updateToastInternal,
    closeToast,
    removeToast,
    promiseToast,
    setHovering,
    setFocused,
    setIsWindowFocused,
    setViewport,
    setPrevFocusElement,
    restoreFocusToPrevElement,
    pauseTimers,
    resumeTimers,
    setTimeoutOption,
    setLimitOption,
    handleDocumentPointerDown
  };
}
//# sourceMappingURL=toast-store.jsx.map
