///|
const TooltipProviderStyle : String = "display:contents"

///|
const TooltipRootStyle : String = "display:contents"

///|
const TooltipContentStyle : String = "position:fixed;inset:auto;left:var(--rui-floating-left,auto);top:var(--rui-floating-top,auto);z-index:50;display:flex;width:max-content;max-width:calc(100vw - 1rem);transform-origin:var(--rui-floating-transform-origin,center);align-items:center;margin:0;border:0;border-radius:calc(var(--rui-radius,0.625rem) - 0.25rem);background:var(--rui-primary,oklch(0.205 0 0));padding:0.375rem 0.75rem;color:var(--rui-primary-foreground,oklch(0.985 0 0));font-size:0.75rem;line-height:1rem;font-weight:400;text-wrap:balance;box-shadow:0 4px 6px -1px rgb(0 0 0 / 0.1),0 2px 4px -2px rgb(0 0 0 / 0.1);outline:none;transition:opacity 100ms ease,transform 100ms ease,visibility 100ms ease"

///|
const TooltipArrowBaseStyle : String = "position:absolute;z-index:-1;top:var(--rui-tooltip-arrow-top,auto);right:var(--rui-tooltip-arrow-right,auto);bottom:var(--rui-tooltip-arrow-bottom,auto);left:var(--rui-tooltip-arrow-left,auto);width:0.625rem;height:0.625rem;margin-top:var(--rui-tooltip-arrow-margin-top,0);margin-left:var(--rui-tooltip-arrow-margin-left,0);border-radius:2px;background:var(--rui-primary,oklch(0.205 0 0));transform:rotate(45deg)"

///|
fn tooltip_delay(value : Int) -> Int {
  if value < 0 {
    0
  } else {
    value
  }
}

///|
fn tooltip_arrow_side_style(side : PopupSide) -> String {
  match side {
    Top =>
      "--rui-tooltip-arrow-top:auto;--rui-tooltip-arrow-right:auto;--rui-tooltip-arrow-bottom:-0.25rem;--rui-tooltip-arrow-left:50%;--rui-tooltip-arrow-margin-top:0;--rui-tooltip-arrow-margin-left:-0.3125rem"
    Right =>
      "--rui-tooltip-arrow-top:50%;--rui-tooltip-arrow-right:auto;--rui-tooltip-arrow-bottom:auto;--rui-tooltip-arrow-left:-0.25rem;--rui-tooltip-arrow-margin-top:-0.3125rem;--rui-tooltip-arrow-margin-left:0"
    Bottom =>
      "--rui-tooltip-arrow-top:-0.25rem;--rui-tooltip-arrow-right:auto;--rui-tooltip-arrow-bottom:auto;--rui-tooltip-arrow-left:50%;--rui-tooltip-arrow-margin-top:0;--rui-tooltip-arrow-margin-left:-0.3125rem"
    Left =>
      "--rui-tooltip-arrow-top:50%;--rui-tooltip-arrow-right:-0.25rem;--rui-tooltip-arrow-bottom:auto;--rui-tooltip-arrow-left:auto;--rui-tooltip-arrow-margin-top:-0.3125rem;--rui-tooltip-arrow-margin-left:0"
  }
}

///|
/// Supply shared open and close delays to descendant tooltips.
///
/// shadcn's Vega preset opens without a wait, so `delay` defaults to zero.
/// `timeout` is the instant-phase window: moving between tooltips within
/// that period bypasses a non-zero opening delay. An individual `tooltip` can
/// override the opening or closing delay.
pub fn[C : @html.IsChildren] tooltip_provider(
  delay? : Int = 0,
  close_delay? : Int = 0,
  timeout? : Int = 400,
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  let actual_delay = tooltip_delay(delay)
  let actual_close_delay = tooltip_delay(close_delay)
  let actual_timeout = tooltip_delay(timeout)
  let element_attrs = ui_attrs(attrs)
    .data_set("slot", "tooltip-provider")
    .data_set("delay", "\{actual_delay}")
    .data_set("close-delay", "\{actual_close_delay}")
    .data_set("timeout", "\{actual_timeout}")
  @html.span(
    style=ui_styles([UiBoxSizing, TooltipProviderStyle], style),
    id?,
    class?,
    title?,
    attrs=element_attrs,
    children,
  )
}

///|
pub fn[C : @html.IsChildren] tooltip_trigger(
  content_id~ : String,
  open? : Bool = false,
  disabled? : Bool = false,
  variant? : ButtonVariant = Ghost,
  size? : ButtonSize = Default,
  on_click? : @cmd.Cmd,
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  let resolved_id = id.unwrap_or(content_id + "-trigger")
  let element_attrs = popup_state_attrs(attrs, "tooltip-trigger", open)
    .data_set("variant", button_variant_value(variant))
    .data_set("size", button_size_value(size))
  if open {
    ignore(element_attrs.aria_describedby(content_id))
  }
  if disabled {
    ignore(element_attrs.aria_disabled("true").data_set("disabled", ""))
  }
  if on_click is Some(command) {
    ignore(element_attrs.on_click(_ => command))
  }
  @html.button(
    style=ui_styles(
      [
        UiBoxSizing,
        UiFontSans,
        UiTextRendering,
        UiTransition,
        ButtonBaseStyle,
        button_variant_style(variant),
        button_size_style(size),
        ui_disabled_style(disabled),
        popup_anchor_style(resolved_id),
      ],
      style,
    ),
    id=resolved_id,
    class?,
    title?,
    type_="button",
    disabled~,
    attrs=element_attrs,
    children,
  )
}

///|
pub fn tooltip_arrow(
  side? : PopupSide = Top,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
) -> @html.Html {
  @html.span(
    style=ui_styles(
      [UiBoxSizing, TooltipArrowBaseStyle, tooltip_arrow_side_style(side)],
      style,
    ),
    class?,
    title?,
    attrs=ui_attrs(attrs).data_set("slot", "tooltip-arrow").aria_hidden("true"),
    @html.nothing,
  )
}

///|
pub fn[C : @html.IsChildren] tooltip_content(
  id~ : String,
  trigger_id~ : String,
  open? : Bool = false,
  side? : PopupSide = Top,
  align? : PopupAlign = Center,
  side_offset? : Int = 4,
  align_offset? : Int = 0,
  show_arrow? : Bool = true,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  arrow_attrs? : @html.Attrs,
  style? : Array[String] = [],
  arrow_style? : Array[String] = [],
  children : C,
) -> @html.Html {
  let element_attrs = popup_state_attrs(attrs, "tooltip-content", open)
    .popover("manual")
    .role("tooltip")
    .data_set("initial-focus", "false")
    .data_set("restore-focus", "false")
    .data_set("trigger-id", trigger_id)
    .data_set("requested-side", popup_side_value(side))
    .data_set("requested-align", popup_align_value(align))
    .data_set("side", popup_side_value(side))
    .data_set("align", popup_align_value(align))
    .data_set("side-offset", "\{side_offset}")
    .data_set("align-offset", "\{align_offset}")
    .data_set("collision-padding", "0")
    .data_set("sticky", "partial")
  let body = @html.span(
    style=[UiBoxSizing, "display:contents"],
    attrs=@html.Attrs::build().data_set("slot", "tooltip-content-body"),
    children,
  )
  let content_children = if show_arrow {
    [
      body,
      @html.span(
        style=ui_styles([UiBoxSizing, TooltipArrowBaseStyle], arrow_style),
        attrs=ui_attrs(arrow_attrs)
          .data_set("slot", "tooltip-arrow")
          .aria_hidden("true"),
        @html.nothing,
      ),
    ]
  } else {
    [body]
  }
  @html.div(
    style=ui_styles(
      [
        UiBoxSizing,
        UiFontSans,
        UiTextRendering,
        TooltipContentStyle,
        PopupTransition100Style,
        popup_floating_anchor_style(
          trigger_id, side, align, side_offset, align_offset,
        ),
        tooltip_arrow_side_style(side),
        popup_state_style(open),
      ],
      style,
    ),
    id~,
    class?,
    title?,
    attrs=element_attrs,
    content_children,
  )
}

///|
#cfg(target="js")
priv struct TooltipEventBinding {
  popup : @dom.Element
  trigger : @dom.Element
  root : @dom.Element
  sidebar : @dom.Element?
  dispatch : Ref[(Int, Int) -> Unit]
  sidebar_observer : Ref[@dom.MutationObserver?]
}

///|
#cfg(target="js")
let tooltip_event_bindings : Map[String, TooltipEventBinding] = Map([])

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

///|
#cfg(target="js")
fn tooltip_live_binding(id : String) -> TooltipEventBinding? {
  guard tooltip_event_bindings.get(id) is Some(binding) else { return None }
  guard binding.popup.get_is_connected() &&
    binding.trigger.get_is_connected() &&
    binding.root.get_is_connected() else {
    ignore(tooltip_event_bindings.remove(id))
    return None
  }
  guard ui_element_by_id(id) is Some(popup) &&
    floating_same_element(binding.popup, popup) else {
    ignore(tooltip_event_bindings.remove(id))
    return None
  }
  Some(binding)
}

///|
#cfg(target="js")
fn tooltip_binding_provider(binding : TooltipEventBinding) -> @dom.Element? {
  binding.root.closest("[data-slot=\"tooltip-provider\"]")
}

///|
#cfg(target="js")
fn tooltip_number_config(
  binding : TooltipEventBinding,
  name : String,
  fallback : Int,
) -> Int {
  let attribute = "data-" + name
  let value = if binding.root.has_attribute(attribute) {
    binding.root.get_attribute(attribute).unwrap_or("")
  } else if tooltip_binding_provider(binding) is Some(provider) &&
    provider.has_attribute(attribute) {
    provider.get_attribute(attribute).unwrap_or("")
  } else {
    ""
  }
  ui_parse_int_or(value, fallback).max(0)
}

///|
#cfg(target="js")
fn tooltip_content_is_open(element : @dom.Element) -> Bool {
  let id = element.get_attribute("id").unwrap_or("")
  if floating_live_layer(id) is Some(layer) {
    floating_layer_is_open(layer)
  } else {
    element.get_attribute("data-state").unwrap_or("") == "open"
  }
}

///|
#cfg(target="js")
fn tooltip_open_delay(binding : TooltipEventBinding) -> Int {
  let configured = tooltip_number_config(binding, "delay", 0)
  guard tooltip_binding_provider(binding) is Some(provider) else {
    return configured
  }
  if configured <= 0 {
    return configured
  }
  let timeout = ui_parse_int_or(
    provider.get_attribute("data-timeout").unwrap_or(""),
    400,
  ).max(0)
  let mut has_open = false
  for content in provider.query_selector_all("[data-slot=\"tooltip-content\"]") {
    if tooltip_content_is_open(content) {
      has_open = true
    }
  }
  let instant = if tooltip_provider_last_close(provider) is Some(closed_at) &&
    ui_has_window() {
    @dom.window().get_performance().now() - closed_at <= timeout.to_double()
  } else {
    false
  }
  if has_open || instant {
    0
  } else {
    configured
  }
}

///|
#cfg(target="js")
fn tooltip_can_open(binding : TooltipEventBinding) -> Bool {
  if binding.root.get_attribute("data-sidebar-collapsed-only").unwrap_or("") !=
    "true" {
    return true
  }
  if binding.sidebar is Some(sidebar) {
    sidebar.get_attribute("data-collapsible").unwrap_or("") == "icon" &&
    sidebar.get_attribute("data-state").unwrap_or("") == "collapsed" &&
    sidebar.get_attribute("data-mobile").unwrap_or("") != "true"
  } else {
    false
  }
}

///|
#cfg(target="js")
fn tooltip_send(binding : TooltipEventBinding, kind : Int, wait : Int) -> Unit {
  let opening = kind == 0 || kind == 2 || kind == 5
  if opening &&
    (!ui_element_is_enabled(binding.trigger) || !tooltip_can_open(binding)) {
    return
  }
  (binding.dispatch.val)(kind, wait)
}

///|
#cfg(target="js")
fn tooltip_top_binding() -> TooltipEventBinding? {
  guard floating_top_layer() is Some(layer) else { return None }
  if layer.element.get_attribute("data-slot").unwrap_or("") != "tooltip-content" {
    return None
  }
  tooltip_live_binding(layer.id)
}

///|
#cfg(target="js")
fn tooltip_bind_document_events() -> Unit {
  let document = @dom.document()
  if tooltip_bound_document.val is Some(bound) &&
    bound.to_node().is_same_node(document.to_node()) {
    return
  }
  tooltip_bound_document.val = Some(document)
  document.add_event_listener("keydown", event => {
    guard event.to_keyboard_event() is Some(keyboard) else { return }
    if keyboard.key() != "Escape" || event.get_default_prevented() {
      return
    }
    guard tooltip_top_binding() is Some(binding) else { return }
    event.prevent_default()
    event.stop_propagation()
    tooltip_send(binding, 7, 0)
  })
  document.add_event_listener("pointerdown", event => {
    if event.get_default_prevented() {
      return
    }
    guard tooltip_top_binding() is Some(binding) else { return }
    if floating_event_inside(event, binding.popup) ||
      floating_event_inside(event, binding.trigger) {
      return
    }
    tooltip_send(binding, 8, 0)
  })
}

///|
#cfg(target="js")
fn bind_tooltip_events_for_trigger(
  id : String,
  trigger_id : String,
  root_id : String,
  dispatch : (Int, Int) -> Unit,
) -> Bool {
  guard ui_has_document() else { return false }
  guard ui_element_by_id(id) is Some(popup) else { return false }
  guard ui_element_by_id(trigger_id) is Some(trigger) else { return false }
  guard ui_element_by_id(root_id) is Some(root) else { return false }
  if !popup.get_is_connected() ||
    !trigger.get_is_connected() ||
    !root.get_is_connected() {
    return false
  }
  if tooltip_event_bindings.get(id) is Some(binding) &&
    floating_same_element(binding.popup, popup) &&
    floating_same_element(binding.trigger, trigger) &&
    floating_same_element(binding.root, root) {
    binding.dispatch.val = dispatch
    return true
  }
  if tooltip_event_bindings.get(id) is Some(previous) &&
    previous.sidebar_observer.val is Some(observer) {
    observer.disconnect()
  }
  let sidebar = if root
    .get_attribute("data-sidebar-collapsed-only")
    .unwrap_or("") ==
    "true" {
    trigger.closest("[data-slot=\"sidebar\"]")
  } else {
    None
  }
  let binding : TooltipEventBinding = {
    popup,
    trigger,
    root,
    sidebar,
    dispatch: Ref(dispatch),
    sidebar_observer: Ref(None),
  }
  tooltip_event_bindings[id] = binding
  trigger.add_event_listener("mouseenter", _ => {
    tooltip_send(binding, 0, tooltip_open_delay(binding))
  })
  trigger.add_event_listener("mouseleave", _ => {
    let hover_grace = if root.get_attribute("data-hoverable").unwrap_or("") ==
      "true" {
      80
    } else {
      0
    }
    tooltip_send(
      binding,
      1,
      tooltip_number_config(binding, "close-delay", 0).max(hover_grace),
    )
  })
  trigger.add_event_listener("focus", _ => {
    if trigger.matches(":focus-visible") {
      tooltip_send(binding, 2, 0)
    }
  })
  trigger.add_event_listener("blur", _ => {
    tooltip_send(binding, 3, tooltip_number_config(binding, "close-delay", 0))
  })
  trigger.add_event_listener("click", _ => {
    if root.get_attribute("data-close-on-click").unwrap_or("") == "true" {
      tooltip_send(binding, 4, 0)
    }
  })
  popup.add_event_listener("mouseenter", _ => {
    if root.get_attribute("data-hoverable").unwrap_or("") == "true" {
      tooltip_send(binding, 5, 0)
    }
  })
  popup.add_event_listener("mouseleave", _ => {
    if root.get_attribute("data-hoverable").unwrap_or("") == "true" {
      tooltip_send(binding, 6, tooltip_number_config(binding, "close-delay", 0))
    }
  })
  if sidebar is Some(sidebar_element) {
    let observer = @dom.MutationObserver::new((_records, _observer) => {
      if !tooltip_can_open(binding) {
        tooltip_send(binding, 9, 0)
      }
    })
    observer.observe(sidebar_element.as_node(), attributes=true, attribute_filter=[
      "data-collapsible", "data-state", "data-mobile",
    ])
    binding.sidebar_observer.val = Some(observer)
  }
  if !tooltip_can_open(binding) {
    tooltip_send(binding, 9, 0)
  }
  tooltip_bind_document_events()
  true
}

///|
#cfg(target="js")
fn bind_tooltip_events(id : String, dispatch : (Int, Int) -> Unit) -> Bool {
  bind_tooltip_events_for_trigger(id, id + "-trigger", id + "-root", dispatch)
}

///|
#cfg(target="js")
priv struct TooltipModel {
  open : Bool
  trigger_hovered : Bool
  trigger_focused : Bool
  content_hovered : Bool
  generation : Int
} derive(Eq)

///|
#cfg(target="js")
priv enum TooltipMsg {
  TooltipDomEvent(Int, Int)
  TooltipOpenAfter(Int)
  TooltipCloseAfter(Int)
  TooltipNativeChanged(Bool)
}

///|
#cfg(target="js")
fn tooltip_is_active(model : TooltipModel) -> Bool {
  model.trigger_hovered || model.trigger_focused || model.content_hovered
}

///|
#cfg(target="js")
fn tooltip_notify_cmd(
  on_open_change : @cmd.Emit[Bool]?,
  open : Bool,
) -> @cmd.Cmd {
  if on_open_change is Some(notify) {
    notify(open)
  } else {
    @cmd.none
  }
}

///|
#cfg(target="js")
fn tooltip_commit_open(
  id : String,
  on_open_change : @cmd.Emit[Bool]?,
  model : TooltipModel,
  open : Bool,
) -> (TooltipModel, @cmd.Cmd) {
  if model.open == open {
    (model, @cmd.none)
  } else {
    (
      { ..model, open, },
      @cmd.batch([
        floating_set_open_cmd(id, open),
        tooltip_notify_cmd(on_open_change, open),
      ]),
    )
  }
}

///|
#cfg(target="js")
fn tooltip_commit_native(
  on_open_change : @cmd.Emit[Bool]?,
  model : TooltipModel,
  open : Bool,
) -> (TooltipModel, @cmd.Cmd) {
  if open {
    if model.open {
      (model, @cmd.none)
    } else {
      ({ ..model, open: true }, tooltip_notify_cmd(on_open_change, true))
    }
  } else {
    let next : TooltipModel = {
      open: false,
      trigger_hovered: false,
      trigger_focused: false,
      content_hovered: false,
      generation: model.generation + 1,
    }
    if model.open {
      (next, tooltip_notify_cmd(on_open_change, false))
    } else {
      (next, @cmd.none)
    }
  }
}

///|
#cfg(target="js")
fn tooltip_bind_events_cmd(
  id : String,
  emit : @cmd.Emit[TooltipMsg],
) -> @cmd.Cmd {
  @cmd.custom_cmd(kind=@cmd.AfterRender, scheduler => {
    ui_after_ready("tooltip-events:" + id, () => {
      bind_tooltip_events(id, (kind, wait) => {
        scheduler.add(emit(TooltipDomEvent(kind, wait)))
      })
    })
  })
}

///|
#cfg(target="js")
fn tooltip_bind_trigger_events_cmd(
  id : String,
  trigger_id : String,
  root_id : String,
  emit : @cmd.Emit[TooltipMsg],
) -> @cmd.Cmd {
  @cmd.custom_cmd(kind=@cmd.AfterRender, scheduler => {
    ui_after_ready("tooltip-events:" + id, () => {
      bind_tooltip_events_for_trigger(id, trigger_id, root_id, (kind, wait) => {
        scheduler.add(emit(TooltipDomEvent(kind, wait)))
      })
    })
  })
}

///|
#cfg(target="js")
fn tooltip_update(
  id : String,
  close_on_click : Bool,
  on_open_change : @cmd.Emit[Bool]?,
  emit : @cmd.Emit[TooltipMsg],
  msg : TooltipMsg,
  current : TooltipModel,
) -> (TooltipModel, @cmd.Cmd) {
  match msg {
    TooltipDomEvent(kind, wait) =>
      match kind {
        // Trigger hover starts after the configured provider/root delay.
        0 => {
          let generation = current.generation + 1
          let next = { ..current, trigger_hovered: true, generation }
          if current.open {
            (next, @cmd.none)
          } else if wait <= 0 {
            tooltip_commit_open(id, on_open_change, next, true)
          } else {
            (next, @cmd.delay(emit(TooltipOpenAfter(generation)), wait))
          }
        }
        // Trigger hover ends; popup hover can cancel this delayed close.
        1 => {
          let generation = current.generation + 1
          let next = { ..current, trigger_hovered: false, generation }
          if tooltip_is_active(next) || !next.open {
            (next, @cmd.none)
          } else if wait <= 0 {
            tooltip_commit_open(id, on_open_change, next, false)
          } else {
            (next, @cmd.delay(emit(TooltipCloseAfter(generation)), wait))
          }
        }
        // Keyboard/programmatic focus opens immediately.
        2 => {
          let next = {
            ..current,
            trigger_focused: true,
            generation: current.generation + 1,
          }
          tooltip_commit_open(id, on_open_change, next, true)
        }
        3 => {
          let generation = current.generation + 1
          let next = { ..current, trigger_focused: false, generation }
          if tooltip_is_active(next) || !next.open {
            (next, @cmd.none)
          } else if wait <= 0 {
            tooltip_commit_open(id, on_open_change, next, false)
          } else {
            (next, @cmd.delay(emit(TooltipCloseAfter(generation)), wait))
          }
        }
        // Click-to-dismiss remains configurable.
        4 =>
          if close_on_click {
            tooltip_commit_open(
              id,
              on_open_change,
              {
                ..current,
                trigger_hovered: false,
                trigger_focused: false,
                content_hovered: false,
                generation: current.generation + 1,
              },
              false,
            )
          } else {
            (current, @cmd.none)
          }
        5 =>
          (
            {
              ..current,
              content_hovered: true,
              generation: current.generation + 1,
            },
            @cmd.none,
          )
        6 => {
          let generation = current.generation + 1
          let next = { ..current, content_hovered: false, generation }
          if tooltip_is_active(next) || !next.open {
            (next, @cmd.none)
          } else if wait <= 0 {
            tooltip_commit_open(id, on_open_change, next, false)
          } else {
            (next, @cmd.delay(emit(TooltipCloseAfter(generation)), wait))
          }
        }
        // Only the top floating layer dispatches Escape/outside dismissal.
        _ =>
          tooltip_commit_open(
            id,
            on_open_change,
            {
              ..current,
              trigger_hovered: false,
              trigger_focused: false,
              content_hovered: false,
              generation: current.generation + 1,
            },
            false,
          )
      }
    TooltipOpenAfter(generation) =>
      if generation == current.generation && tooltip_is_active(current) {
        tooltip_commit_open(id, on_open_change, current, true)
      } else {
        (current, @cmd.none)
      }
    TooltipCloseAfter(generation) =>
      if generation == current.generation && !tooltip_is_active(current) {
        tooltip_commit_open(id, on_open_change, current, false)
      } else {
        (current, @cmd.none)
      }
    TooltipNativeChanged(open) =>
      tooltip_commit_native(on_open_change, current, open)
  }
}

///|
fn[T : @html.IsChildren, C : @html.IsChildren] render_tooltip(
  id : String,
  open : Bool,
  disabled : Bool,
  delay : Int?,
  close_delay : Int?,
  close_on_click : Bool,
  hoverable : Bool,
  side : PopupSide,
  align : PopupAlign,
  side_offset : Int,
  align_offset : Int,
  show_arrow : Bool,
  trigger_variant : ButtonVariant,
  trigger_size : ButtonSize,
  attrs : @html.Attrs?,
  style : Array[String],
  trigger_attrs : @html.Attrs?,
  trigger_style : Array[String],
  content_attrs : @html.Attrs?,
  content_style : Array[String],
  arrow_attrs : @html.Attrs?,
  arrow_style : Array[String],
  on_trigger_click : @cmd.Cmd?,
  trigger : T,
  content : C,
) -> @html.Html {
  let root_id = id + "-root"
  let trigger_id = id + "-trigger"
  let root_attrs = popup_state_attrs(attrs, "tooltip", open)
    .data_set("close-on-click", ui_bool(close_on_click))
    .data_set("hoverable", ui_bool(hoverable))
  if delay is Some(delay) {
    ignore(root_attrs.data_set("delay", "\{delay}"))
  }
  if close_delay is Some(close_delay) {
    ignore(root_attrs.data_set("close-delay", "\{close_delay}"))
  }
  let on_click = on_trigger_click
  let trigger_element_attrs = trigger_attrs
  let popup_attrs = content_attrs
  @html.span(
    style=ui_styles([UiBoxSizing, TooltipRootStyle], style),
    id=root_id,
    attrs=root_attrs,
    [
      tooltip_trigger(
        content_id=id,
        open~,
        disabled~,
        variant=trigger_variant,
        size=trigger_size,
        on_click?,
        id=trigger_id,
        attrs?=trigger_element_attrs,
        style=trigger_style,
        trigger,
      ),
      tooltip_content(
        id~,
        trigger_id~,
        open~,
        side~,
        align~,
        side_offset~,
        align_offset~,
        show_arrow~,
        attrs?=popup_attrs,
        arrow_attrs?,
        style=content_style,
        arrow_style~,
        content,
      ),
    ],
  )
}

///|
/// Imperatively open a mounted tooltip. The owning incremental component
/// receives the resulting native `toggle` event and synchronizes its state.
pub fn tooltip_open(id : String) -> @cmd.Cmd {
  floating_set_open_cmd(id, true)
}

///|
pub fn tooltip_close(id : String) -> @cmd.Cmd {
  floating_set_open_cmd(id, false)
}

///|
pub fn tooltip_toggle(id : String) -> @cmd.Cmd {
  floating_toggle_cmd(id)
}

///|
/// Build a black-box tooltip with Rabbita-owned incremental interaction state.
///
/// Hover, keyboard focus, click dismissal, Escape, hoverable content, provider
/// delays, and native Popover top-layer synchronization are handled internally.
#cfg(target="js")
pub fn[T : @html.IsChildren, C : @html.IsChildren] tooltip(
  id~ : String,
  default_open? : Bool = false,
  disabled? : Bool = false,
  delay? : Int,
  close_delay? : Int,
  close_on_click? : Bool = true,
  hoverable? : Bool = true,
  side? : PopupSide = Top,
  align? : PopupAlign = Center,
  side_offset? : Int = 4,
  align_offset? : Int = 0,
  show_arrow? : Bool = true,
  trigger_variant? : ButtonVariant = Ghost,
  trigger_size? : ButtonSize = Default,
  on_open_change? : @cmd.Emit[Bool],
  on_trigger_click? : @cmd.Cmd,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  trigger_attrs? : @html.Attrs,
  trigger_style? : Array[String] = [],
  content_attrs? : @html.Attrs,
  content_style? : Array[String] = [],
  arrow_attrs? : @html.Attrs,
  arrow_style? : Array[String] = [],
  trigger : T,
  content : C,
) -> @rabbita.Val[@html.Html] {
  let initial_open = default_open && !disabled
  let initial_model : TooltipModel = {
    open: initial_open,
    trigger_hovered: false,
    trigger_focused: false,
    content_hovered: false,
    generation: 0,
  }
  let (model, _) = @rabbita.create_state_with_init(
    init=emit => {
      let sync = emit.map(value => TooltipNativeChanged(value))
      (
        initial_model,
        @cmd.batch([
          floating_bind_sync_cmd(id, sync),
          tooltip_bind_events_cmd(id, emit),
          if initial_open {
            floating_set_open_cmd(id, true)
          } else {
            @cmd.none
          },
        ]),
      )
    },
    update=(emit, msg, current) => {
      tooltip_update(id, close_on_click, on_open_change, emit, msg, current)
    },
  )
  model.view(model => {
    render_tooltip(
      id,
      model.open,
      disabled,
      delay,
      close_delay,
      close_on_click,
      hoverable,
      side,
      align,
      side_offset,
      align_offset,
      show_arrow,
      trigger_variant,
      trigger_size,
      attrs,
      style,
      trigger_attrs,
      trigger_style,
      content_attrs,
      content_style,
      arrow_attrs,
      arrow_style,
      on_trigger_click,
      trigger,
      content,
    )
  })
}

///|
/// Native/SSR fallback: render the requested initial state. Browser event and
/// top-layer behavior is enabled by the JavaScript target.
#cfg(not(target="js"))
pub fn[T : @html.IsChildren, C : @html.IsChildren] tooltip(
  id~ : String,
  default_open? : Bool = false,
  disabled? : Bool = false,
  delay? : Int,
  close_delay? : Int,
  close_on_click? : Bool = true,
  hoverable? : Bool = true,
  side? : PopupSide = Top,
  align? : PopupAlign = Center,
  side_offset? : Int = 4,
  align_offset? : Int = 0,
  show_arrow? : Bool = true,
  trigger_variant? : ButtonVariant = Ghost,
  trigger_size? : ButtonSize = Default,
  on_open_change? : @cmd.Emit[Bool],
  on_trigger_click? : @cmd.Cmd,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  trigger_attrs? : @html.Attrs,
  trigger_style? : Array[String] = [],
  content_attrs? : @html.Attrs,
  content_style? : Array[String] = [],
  arrow_attrs? : @html.Attrs,
  arrow_style? : Array[String] = [],
  trigger : T,
  content : C,
) -> @rabbita.Val[@html.Html] {
  ignore(on_open_change)
  @rabbita.Val::constant(
    render_tooltip(
      id,
      default_open && !disabled,
      disabled,
      delay,
      close_delay,
      close_on_click,
      hoverable,
      side,
      align,
      side_offset,
      align_offset,
      show_arrow,
      trigger_variant,
      trigger_size,
      attrs,
      style,
      trigger_attrs,
      trigger_style,
      content_attrs,
      content_style,
      arrow_attrs,
      arrow_style,
      on_trigger_click,
      trigger,
      content,
    ),
  )
}