///|
pub(all) enum ToastVariant {
  ToastDefault
  ToastDestructive
  ToastSuccess
} derive(Debug, Eq)

///|
priv struct ToastEntry {
  id : Int
  title : String
  description : String?
  variant : ToastVariant
  closing : Bool
  swipe_direction : Int
} derive(Eq)

///|
priv struct ToastModel {
  next_id : Int
  entries : Array[ToastEntry]
  pause_sources : Array[String]
  expired : Array[Int]
} derive(Eq)

///|
priv enum ToastMsg {
  ShowToast(String, String?, ToastVariant, Int)
  DismissToast(Int, Int)
  AutoDismissToast(Int)
  SetToastPaused(String, Bool)
  RemoveToast(Int)
  DismissAllToasts
}

///|
const ToastExitDurationMs : Int = 150

///|
const ToastSurfaceStyle : String = "position:relative;display:grid;width:100%;grid-template-columns:1fr auto;align-items:start;gap:0.25rem 0.75rem;border:1px solid;border-radius:var(--rui-toast-radius,var(--rui-toast-base-radius,1rem));padding:0.875rem 1rem;box-shadow:var(--rui-toast-shadow,var(--rui-toast-base-shadow,0 4px 12px rgb(0 0 0 / 0.1)));touch-action:pan-y;transition:opacity 150ms ease,transform 150ms ease,box-shadow 200ms ease"

///|
const ToastCardShadowStyle : String = "--rui-toast-base-shadow:0 4px 12px rgb(0 0 0 / 0.1)"

///|
const ToastViewportStyle : String = "position:fixed;z-index:100;display:flex;flex-direction:column;overflow:visible;border:0;background:transparent;box-shadow:none;pointer-events:none"

///|
const ToasterViewportStyle : String = "width:min(26rem,calc(100vw - 2rem));max-height:calc(100dvh - 2rem);gap:0.5rem;--rui-toast-base-offset:1rem"

///|
const SonnerViewportStyle : String = "width:min(22.25rem,calc(100vw - 3rem));max-height:calc(100dvh - 3rem);gap:0.875rem;--rui-toast-base-offset:1.5rem"

///|
/// Opaque command scope owned by `toaster` or `sonner`.
struct ToastScope {
  emit : @cmd.Emit[ToastMsg]
}

///|
pub fn ToastScope::show(
  self : ToastScope,
  title~ : String,
  description? : String,
  variant? : ToastVariant = ToastDefault,
  duration_ms? : Int = 4000,
) -> @cmd.Cmd {
  (self.emit)(ShowToast(title, description, variant, duration_ms))
}

///|
pub fn ToastScope::dismiss(self : ToastScope, id~ : Int) -> @cmd.Cmd {
  (self.emit)(DismissToast(id, 0))
}

///|
pub fn ToastScope::dismiss_all(self : ToastScope) -> @cmd.Cmd {
  (self.emit)(DismissAllToasts)
}

///|
fn toast_variant_value(variant : ToastVariant) -> String {
  match variant {
    ToastDefault => "default"
    ToastDestructive => "destructive"
    ToastSuccess => "success"
  }
}

///|
fn toast_variant_style(variant : ToastVariant, rich_colors : Bool) -> String {
  match variant {
    ToastDefault =>
      "border-color:var(--rui-border,oklch(0.922 0 0));background:var(--rui-background,white);color:var(--rui-foreground,oklch(0.145 0 0))"
    ToastDestructive =>
      if rich_colors {
        "border-color:color-mix(in oklch,var(--rui-destructive,oklch(0.577 0.245 27.325)) 35%,transparent);background:color-mix(in oklch,var(--rui-destructive,oklch(0.577 0.245 27.325)) 12%,var(--rui-background,white));color:var(--rui-destructive,oklch(0.577 0.245 27.325))"
      } else {
        "border-color:var(--rui-destructive-border,var(--rui-destructive,oklch(0.577 0.245 27.325)));background:var(--rui-background,white);color:var(--rui-destructive,oklch(0.577 0.245 27.325))"
      }
    ToastSuccess =>
      if rich_colors {
        "border-color:oklch(0.72 0.17 142 / 0.45);background:oklch(0.97 0.04 142);color:oklch(0.38 0.12 142)"
      } else {
        "border-color:var(--rui-border,oklch(0.922 0 0));background:var(--rui-background,white);color:var(--rui-foreground,oklch(0.145 0 0))"
      }
  }
}

///|
pub fn[C : @html.IsChildren] toast(
  variant? : ToastVariant = ToastDefault,
  open? : Bool = true,
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  @html.div(
    hidden=!open,
    id?,
    class?,
    title?,
    attrs=ui_attrs(attrs)
      .role(if variant is ToastDestructive { "alert" } else { "status" })
      .aria_live(
        if variant is ToastDestructive {
          "assertive"
        } else {
          "polite"
        },
      )
      .aria_atomic("true")
      .data_set("slot", "toast")
      .data_set("state", if open { "open" } else { "closed" })
      .data_set("variant", toast_variant_value(variant)),
    style=ui_styles(
      [
        UiBoxSizing,
        UiFontSans,
        UiTextRendering,
        ToastSurfaceStyle,
        ToastCardShadowStyle,
        if open {
          "opacity:1;transform:translateY(0) scale(1)"
        } else {
          "opacity:0;transform:translateY(0.5rem) scale(0.98);pointer-events:none"
        },
        toast_variant_style(variant, false),
      ],
      style,
    ),
    children,
  )
}

///|
pub fn[C : @html.IsChildren] toast_title(
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  @html.div(
    id?,
    class?,
    title?,
    attrs=ui_attrs(attrs).data_set("slot", "toast-title"),
    style=ui_styles(
      ["font-size:0.875rem;line-height:1.25rem;font-weight:500"],
      style,
    ),
    children,
  )
}

///|
pub fn[C : @html.IsChildren] toast_description(
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  @html.div(
    id?,
    class?,
    title?,
    attrs=ui_attrs(attrs).data_set("slot", "toast-description"),
    style=ui_styles(
      [
        "grid-column:1;color:var(--rui-muted-foreground,oklch(0.556 0 0));font-size:0.8125rem;line-height:1.25rem",
      ],
      style,
    ),
    children,
  )
}

///|
pub fn[C : @html.IsChildren] toast_action(
  on_click? : @cmd.Cmd,
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  button(
    variant=Outline,
    size=Sm,
    slot="toast-action",
    on_click?,
    id?,
    class?,
    title?,
    attrs=ui_attrs(attrs).data_set("slot", "toast-action"),
    style~,
    children,
  )
}

///|
pub fn toast_close(
  scope~ : ToastScope,
  toast_id~ : Int,
  label? : String = "Close notification",
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
) -> @html.Html {
  button(
    variant=Ghost,
    size=IconXs,
    slot="toast-close",
    on_click=scope.dismiss(id=toast_id),
    id?,
    class?,
    title?,
    attrs=ui_attrs(attrs).aria_label(label).data_set("slot", "toast-close"),
    style~,
    ui_x_icon(),
  )
}

///|
fn toast_trimmed(
  entries : Array[ToastEntry],
  max_toasts : Int,
) -> Array[ToastEntry] {
  let limit = if max_toasts < 1 { 1 } else { max_toasts }
  let start = if entries.length() > limit {
    entries.length() - limit
  } else {
    0
  }
  let result : Array[ToastEntry] = []
  for index, entry in entries {
    if index >= start {
      result.push(entry)
    }
  }
  result
}

///|
fn toast_remove(entries : Array[ToastEntry], id : Int) -> Array[ToastEntry] {
  let result : Array[ToastEntry] = []
  for entry in entries {
    if entry.id != id {
      result.push(entry)
    }
  }
  result
}

///|
fn toast_mark_closing(
  entries : Array[ToastEntry],
  id : Int,
  swipe_direction : Int,
) -> (Array[ToastEntry], Bool) {
  let result : Array[ToastEntry] = []
  let mut changed = false
  for entry in entries {
    if entry.id == id && !entry.closing {
      result.push({ ..entry, closing: true, swipe_direction })
      changed = true
    } else {
      result.push(entry)
    }
  }
  (result, changed)
}

///|
fn toast_mark_all_closing(
  entries : Array[ToastEntry],
) -> (Array[ToastEntry], Array[Int]) {
  let result : Array[ToastEntry] = []
  let ids : Array[Int] = []
  for entry in entries {
    if entry.closing {
      result.push(entry)
    } else {
      result.push({ ..entry, closing: true, swipe_direction: 0 })
      ids.push(entry.id)
    }
  }
  (result, ids)
}

///|
#cfg(target="js")
priv struct ToastPointerState {
  surface : @dom.Element
  pointer_id : Int
  start_x : Int
  start_y : Int
  start_time : Double
  direction : Int
  mut active : Bool
  transition : String
}

///|
#cfg(target="js")
let toast_pointer_states : Array[ToastPointerState] = []

///|
#cfg(target="js")
fn toast_pointer_state(surface : @dom.Element) -> ToastPointerState? {
  for state in toast_pointer_states {
    if state.surface.is_same_node(surface.as_node()) {
      return Some(state)
    }
  }
  None
}

///|
#cfg(target="js")
fn toast_remove_pointer_state(surface : @dom.Element) -> Unit {
  for index, state in toast_pointer_states {
    if state.surface.is_same_node(surface.as_node()) {
      ignore(toast_pointer_states.remove(index))
      return
    }
  }
}

///|
#cfg(target="js")
fn toast_restore_pointer_state(state : ToastPointerState) -> Unit {
  if state.surface.to_html_element() is Some(surface) {
    let style = surface.get_style()
    ignore(style.remove_property("--rui-toast-swipe-x"))
    if state.transition == "" {
      ignore(style.remove_property("transition"))
    } else {
      style.set_property("transition", state.transition)
    }
  }
  state.surface.remove_attribute("data-swipe")
  if state.surface.has_pointer_capture(state.pointer_id) {
    state.surface.release_pointer_capture(state.pointer_id)
  }
  toast_remove_pointer_state(state.surface)
}

///|
#cfg(target="js")
fn toast_pointer_start(event : @dom.MouseEvent) -> Bool {
  guard event.to_pointer_event() is Some(pointer) else { return false }
  if !pointer.get_is_primary() || pointer.get_button() != 0 {
    return false
  }
  guard pointer.current_target().to_option() is Some(current_target) else {
    return false
  }
  guard current_target.to_element() is Some(surface) else { return false }
  guard surface.to_html_element() is Some(surface_html) else { return false }
  if toast_pointer_state(surface) is Some(_) {
    return false
  }
  if pointer.target().to_element() is Some(target) &&
    target.closest("button,a,input,textarea,select,[contenteditable=true]")
    is Some(_) {
    return false
  }
  let state : ToastPointerState = {
    surface,
    pointer_id: pointer.get_pointer_id(),
    start_x: pointer.get_client_x(),
    start_y: pointer.get_client_y(),
    start_time: pointer.get_time_stamp(),
    direction: if ui_element_is_rtl(surface) {
      -1
    } else {
      1
    },
    active: false,
    transition: surface_html.get_style().get_property_value("transition"),
  }
  toast_pointer_states.push(state)
  surface.set_pointer_capture(state.pointer_id)
  true
}

///|
#cfg(target="js")
fn toast_pointer_move(event : @dom.MouseEvent) -> Bool {
  guard event.to_pointer_event() is Some(pointer) else { return false }
  guard pointer.current_target().to_option() is Some(current_target) else {
    return false
  }
  guard current_target.to_element() is Some(surface) else { return false }
  guard toast_pointer_state(surface) is Some(state) else { return false }
  guard state.pointer_id == pointer.get_pointer_id() else { return false }
  guard surface.to_html_element() is Some(surface_html) else { return false }
  let delta_x = pointer.get_client_x() - state.start_x
  let delta_y = pointer.get_client_y() - state.start_y
  if !state.active {
    if delta_x.abs() < 6 && delta_y.abs() < 6 {
      return false
    }
    if delta_x.abs() <= delta_y.abs() {
      toast_restore_pointer_state(state)
      return false
    }
    state.active = true
    surface.set_attribute("data-swipe", "move")
  }
  let progress = delta_x * state.direction
  let visual = if progress < 0 {
    delta_x.to_double() * 0.2
  } else {
    delta_x.to_double()
  }
  let style = surface_html.get_style()
  style.set_property("transition", "none")
  style.set_property("--rui-toast-swipe-x", "\{visual}px")
  true
}

///|
#cfg(target="js")
fn toast_pointer_end(event : @dom.MouseEvent) -> Int {
  guard event.to_pointer_event() is Some(pointer) else { return 0 }
  guard pointer.current_target().to_option() is Some(current_target) else {
    return 0
  }
  guard current_target.to_element() is Some(surface) else { return 0 }
  guard toast_pointer_state(surface) is Some(state) else { return 0 }
  guard state.pointer_id == pointer.get_pointer_id() else { return 0 }
  let delta_x = pointer.get_client_x() - state.start_x
  let progress = delta_x * state.direction
  let elapsed = (pointer.get_time_stamp() - state.start_time).max(1.0)
  let threshold = (surface.get_client_width() * 0.35).max(48.0).min(160.0)
  let dismiss = state.active &&
    progress > 0 &&
    (
      progress.to_double() >= threshold ||
      (progress >= 16 && progress.to_double() / elapsed >= 0.5)
    )
  let active = state.active
  toast_restore_pointer_state(state)
  if dismiss {
    state.direction * 2
  } else if active {
    1
  } else {
    0
  }
}

///|
#cfg(target="js")
fn toast_pointer_cancel(event : @dom.MouseEvent) -> Unit {
  guard event.to_pointer_event() is Some(pointer) else { return }
  guard pointer.current_target().to_option() is Some(current_target) else {
    return
  }
  guard current_target.to_element() is Some(surface) else { return }
  guard toast_pointer_state(surface) is Some(state) else { return }
  guard state.pointer_id == pointer.get_pointer_id() else { return }
  toast_restore_pointer_state(state)
}

///|
#cfg(target="js")
fn toast_bind_pointer_attrs(
  attrs : @html.Attrs,
  scope : ToastScope,
  id : Int,
) -> Unit {
  ignore(
    attrs
    .on_pointerdown(event => {
      ignore(toast_pointer_start(event))
      @cmd.none
    })
    .on_pointermove(event => {
      if toast_pointer_move(event) {
        event.prevent_default()
      }
      @cmd.none
    })
    .on_pointerup(event => {
      let result = toast_pointer_end(event)
      if result != 0 {
        event.prevent_default()
      }
      if result == 2 {
        (scope.emit)(DismissToast(id, 1))
      } else if result == -2 {
        (scope.emit)(DismissToast(id, -1))
      } else {
        @cmd.none
      }
    })
    .on_pointercancel(event => {
      toast_pointer_cancel(event)
      @cmd.none
    }),
  )
}

///|
#cfg(not(target="js"))
fn toast_bind_pointer_attrs(
  attrs : @html.Attrs,
  scope : ToastScope,
  id : Int,
) -> Unit {
  ignore((attrs, scope, id))
}

///|
fn toast_entry_view(
  scope : ToastScope,
  entry : ToastEntry,
  rich_colors : Bool,
) -> @html.Html {
  let variant = entry.variant
  let element_attrs = @html.Attrs::build()
    .role(if variant is ToastDestructive { "alert" } else { "status" })
    .aria_live(if variant is ToastDestructive { "assertive" } else { "polite" })
    .aria_atomic("true")
    .data_set("slot", "toast")
    .data_set("toast-id", "\{entry.id}")
    .data_set("state", if entry.closing { "closed" } else { "open" })
    .data_set("variant", toast_variant_value(variant))
    .data_set("swipe-direction", "inline-end")
    .on_mouseenter(_ => (scope.emit)(SetToastPaused("hover-\{entry.id}", true)))
    .on_mouseleave(_ => (scope.emit)(SetToastPaused("hover-\{entry.id}", false)))
  toast_bind_pointer_attrs(element_attrs, scope, entry.id)
  let close_attrs = @html.Attrs::build()
    .on_focus(_ => (scope.emit)(SetToastPaused("focus-\{entry.id}", true)))
    .on_blur(_ => (scope.emit)(SetToastPaused("focus-\{entry.id}", false)))
  @html.div(
    attrs=element_attrs,
    style=[
      UiBoxSizing,
      UiFontSans,
      ToastSurfaceStyle,
      ToastCardShadowStyle,
      if entry.closing {
        if entry.swipe_direction < 0 {
          "opacity:0;transform:translate3d(-110%,0,0) scale(0.98);pointer-events:none"
        } else if entry.swipe_direction > 0 {
          "opacity:0;transform:translate3d(110%,0,0) scale(0.98);pointer-events:none"
        } else {
          "opacity:0;transform:translate3d(0,0.5rem,0) scale(0.98);pointer-events:none"
        }
      } else {
        "opacity:1;transform:translate3d(var(--rui-toast-swipe-x,0px),0,0) scale(1);pointer-events:auto"
      },
      toast_variant_style(variant, rich_colors),
    ],
    [
      toast_title(entry.title),
      toast_close(scope~, toast_id=entry.id, attrs=close_attrs),
      if entry.description is Some(description) {
        toast_description(description)
      } else {
        @html.nothing
      },
    ],
  )
}

///|
fn render_toaster(
  model : ToastModel,
  scope : ToastScope,
  slot : String,
  rich_colors : Bool,
  position : String,
  class : String?,
  attrs : @html.Attrs?,
  style : Array[String],
  children : (ToastScope) -> @html.Html,
) -> @html.Html {
  let entries : Array[@html.Html] = []
  for entry in model.entries {
    entries.push(toast_entry_view(scope, entry, rich_colors))
  }
  @html.fragment([
    children(scope),
    @html.div(
      class?,
      attrs=ui_attrs(attrs)
        .role("region")
        .aria_label("Notifications")
        .data_set("slot", slot)
        .data_set("position", position),
      style=ui_styles(
        [
          UiBoxSizing,
          ToastViewportStyle,
          if rich_colors {
            SonnerViewportStyle
          } else {
            ToasterViewportStyle
          },
          if position == "top-left" {
            "top:var(--rui-toast-offset,var(--rui-toast-base-offset,1rem));left:var(--rui-toast-offset,var(--rui-toast-base-offset,1rem))"
          } else if position == "top-center" {
            "top:var(--rui-toast-offset,var(--rui-toast-base-offset,1rem));left:50%;transform:translateX(-50%)"
          } else if position == "top-right" {
            "top:var(--rui-toast-offset,var(--rui-toast-base-offset,1rem));right:var(--rui-toast-offset,var(--rui-toast-base-offset,1rem))"
          } else if position == "bottom-left" {
            "bottom:var(--rui-toast-offset,var(--rui-toast-base-offset,1rem));left:var(--rui-toast-offset,var(--rui-toast-base-offset,1rem))"
          } else if position == "bottom-center" {
            "bottom:var(--rui-toast-offset,var(--rui-toast-base-offset,1rem));left:50%;transform:translateX(-50%)"
          } else {
            "right:var(--rui-toast-offset,var(--rui-toast-base-offset,1rem));bottom:var(--rui-toast-offset,var(--rui-toast-base-offset,1rem))"
          },
        ],
        style,
      ),
      entries,
    ),
  ])
}

///|
#cfg(target="js")
let toast_document_pause_callbacks : Map[String, (Bool) -> Unit] = Map([])

///|
#cfg(target="js")
let toast_document_pause_bound_to : Ref[@dom.Document?] = Ref(None)

///|
#cfg(target="js")
fn toast_notify_document_pause(paused : Bool) -> Unit {
  for _, changed in toast_document_pause_callbacks {
    changed(paused)
  }
}

///|
#cfg(target="js")
fn bind_toast_document_pause(slot : String, changed : (Bool) -> Unit) -> Unit {
  toast_document_pause_callbacks[slot] = changed
  let document = @dom.document()
  let already_bound = if toast_document_pause_bound_to.val is Some(bound) {
    bound.to_node().is_same_node(document.to_node())
  } else {
    false
  }
  if !already_bound {
    toast_document_pause_bound_to.val = Some(document)
    document.add_event_listener("visibilitychange", _ => {
      toast_notify_document_pause(document.hidden())
    })
    let window = @dom.window()
    window.add_event_listener("blur", _ => toast_notify_document_pause(true))
    window.add_event_listener("focus", _ => {
      if !document.hidden() {
        toast_notify_document_pause(false)
      }
    })
  }
  changed(document.hidden())
}

///|
#cfg(target="js")
fn toast_bind_document_pause_cmd(
  slot : String,
  emit : @cmd.Emit[ToastMsg],
) -> @cmd.Cmd {
  @cmd.custom_cmd(kind=@cmd.AfterRender, scheduler => {
    bind_toast_document_pause(slot, paused => {
      scheduler.add(emit(SetToastPaused("document", paused)))
    })
  })
}

///|
fn toast_remove_pause_source(
  sources : Array[String],
  source : String,
) -> Array[String] {
  let result : Array[String] = []
  for value in sources {
    if value != source {
      result.push(value)
    }
  }
  result
}

///|
fn toast_remove_entry_pause_sources(
  sources : Array[String],
  id : Int,
) -> Array[String] {
  let result = toast_remove_pause_source(sources, "hover-\{id}")
  toast_remove_pause_source(result, "focus-\{id}")
}

///|
fn toast_contains_entry(entries : Array[ToastEntry], id : Int) -> Bool {
  for entry in entries {
    if entry.id == id {
      return true
    }
  }
  false
}

///|
fn toast_filter_expired(
  expired : Array[Int],
  entries : Array[ToastEntry],
) -> Array[Int] {
  let result : Array[Int] = []
  for id in expired {
    if toast_contains_entry(entries, id) {
      result.push(id)
    }
  }
  result
}

///|
fn toast_with_pause_sources(
  model : ToastModel,
  pause_sources : Array[String],
) -> (ToastModel, Array[Int]) {
  let resumed = model.pause_sources.length() > 0 && pause_sources.length() == 0
  (
    {
      ..model,
      pause_sources,
      expired: if resumed {
        []
      } else {
        model.expired
      },
    },
    if resumed {
      model.expired.copy()
    } else {
      []
    },
  )
}

///|
fn toast_set_paused(
  model : ToastModel,
  source : String,
  paused : Bool,
) -> (ToastModel, Array[Int]) {
  if paused {
    if model.pause_sources.contains(source) {
      (model, [])
    } else {
      let pause_sources = model.pause_sources.copy()
      pause_sources.push(source)
      toast_with_pause_sources(model, pause_sources)
    }
  } else if !model.pause_sources.contains(source) {
    (model, [])
  } else {
    toast_with_pause_sources(
      model,
      toast_remove_pause_source(model.pause_sources, source),
    )
  }
}

///|
#cfg(target="js")
fn toaster_impl(
  slot : String,
  rich_colors : Bool,
  max_toasts : Int,
  position : String,
  class : String?,
  attrs : @html.Attrs?,
  style : Array[String],
  children : (ToastScope) -> @html.Html,
) -> @rabbita.Val[@html.Html] {
  let (model, emit) = @rabbita.create_state_with_init(
    init=emit => {
      (
        { next_id: 1, entries: [], pause_sources: [], expired: [] },
        toast_bind_document_pause_cmd(slot, emit),
      )
    },
    update=fn(emit, msg, model) {
      match msg {
        ShowToast(title, description, variant, duration_ms) => {
          let id = model.next_id
          let entries = model.entries.copy()
          entries.push({
            id,
            title,
            description,
            variant,
            closing: false,
            swipe_direction: 0,
          })
          let entries = toast_trimmed(entries, max_toasts)
          let mut pause_sources = model.pause_sources
          for previous in model.entries {
            if !toast_contains_entry(entries, previous.id) {
              pause_sources = toast_remove_entry_pause_sources(
                pause_sources,
                previous.id,
              )
            }
          }
          let filtered = {
            ..model,
            next_id: id + 1,
            entries,
            expired: toast_filter_expired(model.expired, entries),
          }
          let (next, resume_ids) = toast_with_pause_sources(
            filtered, pause_sources,
          )
          let commands : Array[@cmd.Cmd] = []
          if duration_ms > 0 {
            commands.push(
              @rabbita.delay(emit(AutoDismissToast(id)), duration_ms),
            )
          }
          for expired_id in resume_ids {
            commands.push(emit(DismissToast(expired_id, 0)))
          }
          (next, @cmd.batch(commands))
        }
        DismissToast(id, swipe_direction) => {
          let (entries, changed) = toast_mark_closing(
            model.entries,
            id,
            swipe_direction,
          )
          let (unpaused, resume_ids) = toast_with_pause_sources(
            model,
            toast_remove_entry_pause_sources(model.pause_sources, id),
          )
          let commands : Array[@cmd.Cmd] = []
          if changed {
            commands.push(
              @rabbita.delay(emit(RemoveToast(id)), ToastExitDurationMs),
            )
          }
          for expired_id in resume_ids {
            commands.push(emit(DismissToast(expired_id, 0)))
          }
          ({ ..unpaused, entries, }, @cmd.batch(commands))
        }
        AutoDismissToast(id) =>
          if !toast_contains_entry(model.entries, id) {
            (model, @cmd.none)
          } else if model.pause_sources.length() > 0 {
            let expired = model.expired.copy()
            if !expired.contains(id) {
              expired.push(id)
            }
            ({ ..model, expired, }, @cmd.none)
          } else {
            let (entries, changed) = toast_mark_closing(model.entries, id, 0)
            (
              { ..model, entries, },
              if changed {
                @rabbita.delay(emit(RemoveToast(id)), ToastExitDurationMs)
              } else {
                @cmd.none
              },
            )
          }
        SetToastPaused(source, paused) => {
          let (next, resume_ids) = toast_set_paused(model, source, paused)
          let commands : Array[@cmd.Cmd] = []
          for expired_id in resume_ids {
            commands.push(emit(DismissToast(expired_id, 0)))
          }
          (next, @cmd.batch(commands))
        }
        RemoveToast(id) => {
          let entries = toast_remove(model.entries, id)
          let cleaned = {
            ..model,
            entries,
            expired: toast_filter_expired(model.expired, entries),
          }
          let (next, resume_ids) = toast_with_pause_sources(
            cleaned,
            toast_remove_entry_pause_sources(cleaned.pause_sources, id),
          )
          let commands : Array[@cmd.Cmd] = []
          for expired_id in resume_ids {
            commands.push(emit(DismissToast(expired_id, 0)))
          }
          (next, @cmd.batch(commands))
        }
        DismissAllToasts => {
          let (entries, ids) = toast_mark_all_closing(model.entries)
          let mut pause_sources = model.pause_sources
          for entry in model.entries {
            pause_sources = toast_remove_entry_pause_sources(
              pause_sources,
              entry.id,
            )
          }
          let commands : Array[@cmd.Cmd] = []
          for id in ids {
            commands.push(
              @rabbita.delay(emit(RemoveToast(id)), ToastExitDurationMs),
            )
          }
          (
            { ..model, entries, pause_sources, expired: [] },
            @cmd.batch(commands),
          )
        }
      }
    },
  )
  model.view(model => {
    render_toaster(
      model,
      { emit, },
      slot,
      rich_colors,
      position,
      class,
      attrs,
      style,
      children,
    )
  })
}

///|
#cfg(not(target="js"))
fn toaster_impl(
  slot : String,
  rich_colors : Bool,
  max_toasts : Int,
  position : String,
  class : String?,
  attrs : @html.Attrs?,
  style : Array[String],
  children : (ToastScope) -> @html.Html,
) -> @rabbita.Val[@html.Html] {
  ignore(max_toasts)
  let scope = { emit: @cmd.Emit(_ => @cmd.none) }
  @rabbita.Val::constant(
    render_toaster(
      { next_id: 1, entries: [], pause_sources: [], expired: [] },
      scope,
      slot,
      rich_colors,
      position,
      class,
      attrs,
      style,
      children,
    ),
  )
}

///|
pub fn toaster(
  max_toasts? : Int = 3,
  position? : String = "bottom-right",
  class? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : (ToastScope) -> @html.Html,
) -> @rabbita.Val[@html.Html] {
  toaster_impl(
    "toaster", false, max_toasts, position, class, attrs, style, children,
  )
}