///|
/// One option consumed by the black-box [`select`] component.
struct SelectOption {
  value : String
  label : String
  disabled : Bool
}

///|
pub fn select_option(
  value~ : String,
  label~ : String,
  disabled? : Bool = false,
) -> SelectOption {
  { value, label, disabled }
}

///|
priv struct SelectModel {
  open : Bool
  value : String?
  active : Int
  search : String
  search_generation : Int
} derive(Eq)

///|
priv enum SelectMsg {
  SelectToggle
  SelectNativeChanged(Bool)
  SelectMove(Int)
  SelectHighlight(Int)
  SelectFirst
  SelectLast
  SelectChoose(Int)
  SelectClose
  SelectDismiss
  SelectTypeahead(String)
  SelectClearTypeahead(Int)
}

///|
/// Opaque state handle passed by [`select`] to its compound rendering helpers.
struct SelectScope {
  id : String
  options : Array[SelectOption]
  model : SelectModel
  placeholder : String
  disabled : Bool
  invalid : Bool
  required : Bool
  name : String?
  side : PopupSide
  align : PopupAlign
  side_offset : Int
  align_offset : Int
  emit : @cmd.Emit[SelectMsg]
  content_id : Ref[String]
  trigger_id : Ref[String]
}

///|
const SelectRootStyle : String = "position:relative;display:inline-flex;width:100%;min-width:0"

///|
const SelectTriggerStyle : String = "display:flex;width:100%;height:2.25rem;min-width:0;align-items:center;justify-content:space-between;gap:0.5rem;--rui-control-base-bg:var(--rui-input-background,transparent);--rui-control-base-fg:var(--rui-foreground,oklch(0.145 0 0));--rui-control-base-border:var(--rui-input,oklch(0.922 0 0));--rui-control-base-shadow:0 1px 2px rgb(0 0 0 / 0.05);border:1px solid transparent;border-color:var(--rui-control-border,var(--rui-control-base-border,var(--rui-input,oklch(0.922 0 0))));border-radius:calc(var(--rui-radius,0.625rem) - 0.125rem);background:var(--rui-control-bg,var(--rui-control-base-bg,var(--rui-input-background,transparent)));padding:0.25rem 0.625rem;color:var(--rui-control-fg,var(--rui-control-base-fg,var(--rui-foreground,oklch(0.145 0 0))));font-size:0.875rem;line-height:1.25rem;text-align:start;white-space:nowrap;outline-offset:2px;box-shadow:var(--rui-control-shadow,var(--rui-control-base-shadow,0 1px 2px rgb(0 0 0 / 0.05)));cursor:pointer"

///|
const SelectContentStyle : String = "position:fixed;inset:auto;left:var(--rui-floating-left,auto);top:var(--rui-floating-top,auto);z-index:50;display:flex;width:var(--rui-floating-anchor-width,anchor-size(width));min-width:max(8rem,var(--rui-floating-anchor-width,anchor-size(width)));max-width:var(--rui-floating-available-width,calc(100vw - 1rem));max-height:var(--rui-floating-available-height,calc(100vh - 1rem));transform-origin:var(--rui-floating-transform-origin,center);flex-direction:column;overflow-x:hidden;overflow-y:auto;margin:0;border:1px solid var(--rui-border,oklch(0.922 0 0));border-radius:calc(var(--rui-radius,0.625rem) - 0.125rem);background:var(--rui-popover,oklch(1 0 0));padding:0;color:var(--rui-popover-foreground,oklch(0.145 0 0));box-shadow:0 10px 15px -3px rgb(0 0 0 / 0.1),0 4px 6px -4px rgb(0 0 0 / 0.1);outline:none;transition:opacity 100ms ease,transform 100ms ease,visibility 100ms ease"

///|
const SelectItemStyle : String = "position:relative;display:flex;width:100%;min-height:2rem;align-items:center;gap:0.5rem;border:0;border-radius:calc(var(--rui-radius,0.625rem) - 0.25rem);background:var(--rui-menu-item-bg,var(--rui-menu-item-base-bg,transparent));padding-block:0.375rem;padding-inline:0.5rem 2rem;color:var(--rui-menu-item-fg,var(--rui-menu-item-base-fg,inherit));font:inherit;font-size:0.875rem;line-height:1.25rem;text-align:start;white-space:nowrap;outline:none;cursor:pointer"

///|
fn select_find_value(options : Array[SelectOption], value : String?) -> Int {
  if value is Some(value) {
    for index, option in options {
      if option.value == value && !option.disabled {
        return index
      }
    }
  }
  -1
}

///|
fn select_first_enabled(options : Array[SelectOption], reverse : Bool) -> Int {
  if reverse {
    let mut index = options.length() - 1
    while index >= 0 {
      if !options[index].disabled {
        return index
      }
      index -= 1
    }
  } else {
    for index, option in options {
      if !option.disabled {
        return index
      }
    }
  }
  -1
}

///|
fn select_next_enabled(
  options : Array[SelectOption],
  current : Int,
  direction : Int,
) -> Int {
  let length = options.length()
  if length == 0 {
    return -1
  }
  let mut index = current
  let mut remaining = length
  while remaining > 0 {
    index = if direction < 0 {
      if index <= 0 {
        length - 1
      } else {
        index - 1
      }
    } else if index < 0 || index >= length - 1 {
      0
    } else {
      index + 1
    }
    if !options[index].disabled {
      return index
    }
    remaining -= 1
  }
  -1
}

///|
fn select_typeahead_index(
  options : Array[SelectOption],
  current : Int,
  search : String,
) -> Int {
  if search == "" || options.length() == 0 {
    return -1
  }
  let needle = search.to_lower()
  let mut offset = 1
  while offset <= options.length() {
    let start = if current < 0 { -1 } else { current }
    let index = (start + offset) % options.length()
    let option = options[index]
    if !option.disabled && option.label.to_lower().has_prefix(needle) {
      return index
    }
    offset += 1
  }
  -1
}

///|
fn select_typeahead_search(current : String, key : String) -> String {
  let normalized_key = key.to_lower()
  if current == normalized_key {
    normalized_key
  } else {
    current + normalized_key
  }
}

///|
fn select_label_for(scope : SelectScope) -> String {
  let index = select_find_value(scope.options, scope.model.value)
  if index >= 0 {
    scope.options[index].label
  } else {
    scope.placeholder
  }
}

///|
fn select_item_id(scope : SelectScope, index : Int) -> String {
  "\{scope.id}-item-\{index}"
}

///|
fn ui_listbox_run_open_sequence(
  set_layer : () -> Unit,
  focus : (() -> Unit)?,
) -> Unit {
  set_layer()
  if focus is Some(focus) {
    focus()
  }
}

///|
#cfg(target="js")
fn find_listbox_content_id(root_id : String, slot : String) -> String {
  guard ui_element_by_id(root_id + "-root") is Some(root) else { return "" }
  guard ui_find_element(root.query_selector_all("[data-slot]"), element => {
      element.get_attribute("data-slot").unwrap_or("") == slot
    })
    is Some(content) else {
    return ""
  }
  content.get_attribute("id").unwrap_or("")
}

///|
#cfg(target="js")
fn sync_select_trigger_id(root_id : String, content_id : String) -> Unit {
  guard ui_element_by_id(root_id + "-root") is Some(root) else { return }
  guard ui_element_by_id(content_id) is Some(content) else { return }
  guard ui_find_element(root.query_selector_all("[data-slot]"), element => {
      element.get_attribute("data-slot").unwrap_or("") == "select-trigger"
    })
    is Some(trigger) else {
    return
  }
  let trigger_id = trigger.get_attribute("id").unwrap_or("")
  if trigger_id != "" {
    content.set_attribute("data-trigger-id", trigger_id)
  }
}

///|
#cfg(target="js")
fn ui_listbox_set_open_now(
  root_id : String,
  slot : String,
  open : Bool,
) -> Unit {
  let content_id = find_listbox_content_id(root_id, slot)
  if content_id != "" {
    ignore(set_floating_open(content_id, open))
    if open {
      position_floating(content_id)
    }
  }
}

///|
#cfg(not(target="js"))
fn ui_listbox_set_open_now(
  root_id : String,
  slot : String,
  open : Bool,
) -> Unit {
  ignore((root_id, slot, open))
}

///|
#cfg(target="js")
fn ui_listbox_bind_sync_cmd(
  root_id : String,
  slot : String,
  sync : @cmd.Emit[Bool],
) -> @cmd.Cmd {
  @cmd.custom_cmd(kind=@cmd.AfterRender, scheduler => {
    let content_id = find_listbox_content_id(root_id, slot)
    if content_id != "" {
      bind_floating_sync(content_id, open => scheduler.add(sync(open)))
    }
  })
}

///|
#cfg(not(target="js"))
fn ui_listbox_bind_sync_cmd(
  root_id : String,
  slot : String,
  sync : @cmd.Emit[Bool],
) -> @cmd.Cmd {
  ignore((root_id, slot, sync))
  @cmd.none
}

///|
#cfg(target="js")
fn focus_select_part(root_id : String, slot : String, value : String) -> Unit {
  // A click focuses its trigger as the browser's default action after the
  // handler returns. Move focus on the following frame so native activation
  // cannot overwrite the listbox focus target.
  ui_after_request_frame(() => {
    guard ui_element_by_id(root_id + "-root") is Some(root) else { return }
    let candidates = root
      .query_selector_all("[data-slot]")
      .filter(element => {
        element.get_attribute("data-slot").unwrap_or("") == slot
      })
    let target = if slot == "select-item" {
      ui_find_element(candidates, element => {
        element.get_attribute("data-value").unwrap_or("") == value
      })
    } else {
      candidates.get(0)
    }
    if target is Some(target) {
      ui_focus_element_without_scroll(target)
    }
  })
}

///|
#cfg(target="js")
fn select_popup_cmd(
  id : String,
  open : Bool?,
  focus_slot : String?,
  focus_value : String,
) -> @cmd.Cmd {
  @cmd.custom_cmd(kind=@cmd.AfterRender, _ => {
    let content_id = find_listbox_content_id(id, "select-content")
    if content_id != "" {
      // Resolve the rendered trigger instead of assuming the default id. This
      // also works when compound parts are evaluated in a different order.
      sync_select_trigger_id(id, content_id)
    }
    let set_layer = () => {
      if open is Some(open) {
        ui_listbox_set_open_now(id, "select-content", open)
      }
    }
    let focus : (() -> Unit)? = if focus_slot is Some(slot) {
      Some(() => focus_select_part(id, slot, focus_value))
    } else {
      None
    }
    ui_listbox_run_open_sequence(set_layer, focus)
  })
}

///|
#cfg(not(target="js"))
fn select_popup_cmd(
  id : String,
  open : Bool?,
  focus_slot : String?,
  focus_value : String,
) -> @cmd.Cmd {
  ignore((id, open, focus_slot, focus_value))
  @cmd.none
}

///|
fn select_open_commands(scope : SelectScope, open : Bool) -> @cmd.Cmd {
  if open &&
    scope.model.active >= 0 &&
    scope.model.active < scope.options.length() {
    select_popup_cmd(
      scope.id,
      Some(true),
      Some("select-item"),
      scope.options[scope.model.active].value,
    )
  } else if open {
    select_popup_cmd(scope.id, Some(true), None, "")
  } else {
    select_popup_cmd(scope.id, Some(false), Some("select-trigger"), "")
  }
}

///|
#cfg(target="js")
fn select_root_keydown(
  scope : SelectScope,
  event : @dom.KeyboardEvent,
) -> @cmd.Cmd {
  if event.alt_key() ||
    event.ctrl_key() ||
    event.meta_key() ||
    event.is_composing() {
    return @cmd.none
  }
  guard event.current_target().to_option() is Some(current_target) &&
    current_target.to_element() is Some(root) else {
    return @cmd.none
  }
  guard event.target().to_element() is Some(event_target) else {
    return @cmd.none
  }
  guard event_target.closest(
      "[data-slot=\"select-trigger\"],[data-slot=\"select-item\"]",
    )
    is Some(target) &&
    target.closest("[data-slot=\"select\"]") is Some(owner) &&
    owner.is_same_node(root.as_node()) else {
    return @cmd.none
  }
  let key = event.key()
  let slot = target.get_attribute("data-slot").unwrap_or("")
  if key == "Tab" && scope.model.open {
    return (scope.emit)(SelectDismiss)
  }
  let message : SelectMsg? = match key {
    "ArrowDown" => Some(SelectMove(1))
    "ArrowUp" => Some(SelectMove(-1))
    "Home" => Some(SelectFirst)
    "End" => Some(SelectLast)
    "Escape" => Some(SelectClose)
    "Enter" if slot == "select-item" => Some(SelectChoose(-1))
    " " if slot == "select-item" && scope.model.search == "" =>
      Some(SelectChoose(-1))
    "Enter" | " " if slot == "select-trigger" => Some(SelectToggle)
    _ => if key.length() == 1 { Some(SelectTypeahead(key)) } else { None }
  }
  if message is Some(message) {
    event.prevent_default()
    (scope.emit)(message)
  } else {
    @cmd.none
  }
}

///|
#cfg(target="js")
fn select_bind_root_keyboard(attrs : @html.Attrs, scope : SelectScope) -> Unit {
  ignore(attrs.on_keydown(event => select_root_keydown(scope, event)))
}

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

///|
fn select_notify_bool(notify : @cmd.Emit[Bool]?, value : Bool) -> @cmd.Cmd {
  if notify is Some(notify) {
    notify(value)
  } else {
    @cmd.none
  }
}

///|
fn select_notify_value(notify : @cmd.Emit[String]?, value : String) -> @cmd.Cmd {
  if notify is Some(notify) {
    notify(value)
  } else {
    @cmd.none
  }
}

///|
fn select_resolve_active(
  options : Array[SelectOption],
  model : SelectModel,
) -> Int {
  if model.active >= 0 &&
    model.active < options.length() &&
    !options[model.active].disabled {
    model.active
  } else {
    let selected = select_find_value(options, model.value)
    if selected >= 0 {
      selected
    } else {
      select_first_enabled(options, false)
    }
  }
}

///|
fn select_scope_root_attrs(
  scope : SelectScope,
  attrs : @html.Attrs?,
) -> @html.Attrs {
  let root_attrs = popup_state_attrs(attrs, "select", scope.model.open)
    .data_set("value", scope.model.value.unwrap_or(""))
    .data_set("searching", ui_bool(scope.model.search != ""))
  if scope.disabled {
    ignore(root_attrs.data_set("disabled", "").aria_disabled("true"))
  }
  if scope.invalid {
    ignore(root_attrs.data_set("invalid", "").aria_invalid("true"))
  }
  select_bind_root_keyboard(root_attrs, scope)
  root_attrs
}

///|
pub fn select_value(
  scope~ : SelectScope,
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  prefix? : @html.Html,
) -> @html.Html {
  let empty = scope.model.value is None
  @html.span(
    id?,
    class?,
    title?,
    attrs=ui_attrs(attrs)
      .data_set("slot", "select-value")
      .data_set("placeholder", ui_bool(empty)),
    style=ui_styles(
      [
        "display:flex;min-width:0;flex:1;align-items:center;gap:0.375rem;overflow:hidden;text-overflow:ellipsis",
        if empty {
          "color:var(--rui-muted-foreground,oklch(0.556 0 0))"
        } else {
          ""
        },
      ],
      style,
    ),
    [
      if prefix is Some(prefix) {
        prefix
      } else {
        @html.nothing
      },
      @html.span(
        style=["min-width:0;overflow:hidden;text-overflow:ellipsis"],
        select_label_for(scope),
      ),
    ],
  )
}

///|
pub fn select_trigger(
  scope~ : SelectScope,
  aria_label? : String,
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
) -> @html.Html {
  let resolved_id = id.unwrap_or("\{scope.id}-trigger")
  scope.trigger_id.val = resolved_id
  let trigger_attrs = popup_state_attrs(
      attrs,
      "select-trigger",
      scope.model.open,
    )
    .role("combobox")
    .aria_haspopup("listbox")
    .aria_expanded(ui_bool(scope.model.open))
    .aria_invalid(ui_bool(scope.invalid))
  if scope.model.open {
    ignore(trigger_attrs.aria_controls(scope.content_id.val))
  }
  if aria_label is Some(label) {
    ignore(trigger_attrs.aria_label(label))
  }
  if !scope.disabled {
    ignore(trigger_attrs.on_click(_ => (scope.emit)(SelectToggle)))
  }
  @html.button(
    id=resolved_id,
    class?,
    title?,
    type_="button",
    disabled=scope.disabled,
    attrs=trigger_attrs,
    style=ui_styles(
      [
        UiBoxSizing,
        UiFontSans,
        UiTextRendering,
        UiTransition,
        SelectTriggerStyle,
        input_invalid_style(scope.invalid),
        ui_disabled_style(scope.disabled),
        popup_anchor_style(resolved_id),
      ],
      style,
    ),
    [
      select_value(scope~),
      @html.span(
        attrs=@html.Attrs::build().aria_hidden("true"),
        style=[
          "display:inline-flex;flex:none;align-items:center;color:var(--rui-muted-foreground,oklch(0.556 0 0));font-size:0.75rem;line-height:1",
        ],
        ui_chevron_down_icon(),
      ),
    ],
  )
}

///|
pub fn[C : @html.IsChildren] select_group(
  label_id? : String,
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  let group_attrs = ui_attrs(attrs)
    .role("group")
    .data_set("slot", "select-group")
  if label_id is Some(label_id) {
    ignore(group_attrs.aria_labelledby(label_id))
  }
  @html.div(
    id?,
    class?,
    title?,
    attrs=group_attrs,
    style=ui_styles(["padding:0.125rem 0"], style),
    children,
  )
}

///|
pub fn[C : @html.IsChildren] select_label(
  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", "select-label"),
    style=ui_styles(
      [
        "padding:0.375rem 0.5rem;color:var(--rui-muted-foreground,oklch(0.556 0 0));font-size:0.75rem;line-height:1rem;font-weight:500",
      ],
      style,
    ),
    children,
  )
}

///|
pub fn[C : @html.IsChildren] select_item(
  scope~ : SelectScope,
  value~ : String,
  disabled? : Bool,
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  let mut index = -1
  for option_index, option in scope.options {
    if option.value == value && index < 0 {
      index = option_index
    }
  }
  let option_disabled = if disabled is Some(disabled) {
    disabled
  } else if index >= 0 {
    scope.options[index].disabled
  } else {
    false
  }
  let selected = scope.model.value is Some(current) && current == value
  let active = scope.model.open && index >= 0 && scope.model.active == index
  let item_attrs = ui_attrs(attrs)
    .role("option")
    .aria_selected(ui_bool(selected))
    .aria_disabled(ui_bool(option_disabled))
    .data_set("slot", "select-item")
    .data_set("value", value)
    .data_set("state", if selected { "checked" } else { "unchecked" })
    .data_set("highlighted", ui_bool(active))
    .tabindex(if active { 0 } else { -1 })
  if option_disabled {
    ignore(item_attrs.data_set("disabled", ""))
  }
  if !option_disabled && index >= 0 {
    ignore(item_attrs.on_click(_ => (scope.emit)(SelectChoose(index))))
    ignore(item_attrs.on_mouseenter(_ => (scope.emit)(SelectHighlight(index))))
  }
  @html.button(
    id=id.unwrap_or(if index >= 0 { select_item_id(scope, index) } else { "" }),
    class?,
    title?,
    type_="button",
    disabled=option_disabled,
    attrs=item_attrs,
    style=ui_styles(
      [
        UiBoxSizing,
        UiFontSans,
        UiTransition,
        SelectItemStyle,
        if active {
          "--rui-menu-item-base-bg:var(--rui-accent,oklch(0.97 0 0));--rui-menu-item-base-fg:var(--rui-accent-foreground,oklch(0.205 0 0))"
        } else {
          ""
        },
        ui_disabled_visual_style(option_disabled),
      ],
      style,
    ),
    [
      @html.span(
        style=["min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis"],
        children,
      ),
      if selected {
        @html.span(
          attrs=@html.Attrs::build()
            .aria_hidden("true")
            .data_set("slot", "select-item-indicator"),
          style=[
            "position:absolute;inset-inline-end:0.5rem;display:inline-flex;width:1rem;height:1rem;align-items:center;justify-content:center",
          ],
          ui_check_icon(),
        )
      } else {
        @html.nothing
      },
    ],
  )
}

///|
pub fn select_separator(
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
) -> @html.Html {
  @html.div(
    id?,
    class?,
    title?,
    attrs=ui_attrs(attrs).role("separator").data_set("slot", "select-separator"),
    style=ui_styles(
      [
        "height:1px;margin:0.25rem -0.25rem;background:var(--rui-border,oklch(0.922 0 0));pointer-events:none",
      ],
      style,
    ),
    "",
  )
}

///|
fn select_scroll_button(
  scope : SelectScope,
  direction : Int,
  slot : String,
  label : String,
  attrs : @html.Attrs?,
  style : Array[String],
) -> @html.Html {
  let button_attrs = ui_attrs(attrs)
    .aria_label(label)
    .data_set("slot", slot)
    .tabindex(-1)
    .on_click(_ => (scope.emit)(SelectMove(direction)))
  @html.button(
    type_="button",
    attrs=button_attrs,
    style=ui_styles(
      [
        "display:flex;width:100%;height:1.5rem;align-items:center;justify-content:center;border:0;background:var(--rui-popover,white);color:var(--rui-muted-foreground,oklch(0.556 0 0));font-size:0.75rem;cursor:default",
      ],
      style,
    ),
    if direction < 0 {
      ui_chevron_up_icon()
    } else {
      ui_chevron_down_icon()
    },
  )
}

///|
pub fn select_scroll_up_button(
  scope~ : SelectScope,
  label? : String = "Scroll up",
  attrs? : @html.Attrs,
  style? : Array[String] = [],
) -> @html.Html {
  select_scroll_button(
    scope, -1, "select-scroll-up-button", label, attrs, style,
  )
}

///|
pub fn select_scroll_down_button(
  scope~ : SelectScope,
  label? : String = "Scroll down",
  attrs? : @html.Attrs,
  style? : Array[String] = [],
) -> @html.Html {
  select_scroll_button(
    scope, 1, "select-scroll-down-button", label, attrs, style,
  )
}

///|
pub fn[C : @html.IsChildren] select_content(
  scope~ : SelectScope,
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  let resolved_id = id.unwrap_or("\{scope.id}-content")
  scope.content_id.val = resolved_id
  let content_attrs = popup_state_attrs(
      attrs,
      "select-content",
      scope.model.open,
    )
    .popover("auto")
    .role("listbox")
    .tabindex(-1)
    .aria_required(ui_bool(scope.required))
    .aria_invalid(ui_bool(scope.invalid))
    .data_set("trigger-id", scope.trigger_id.val)
    .data_set("requested-side", popup_side_value(scope.side))
    .data_set("requested-align", popup_align_value(scope.align))
    .data_set("side", popup_side_value(scope.side))
    .data_set("align", popup_align_value(scope.align))
    .data_set("side-offset", "\{scope.side_offset}")
    .data_set("align-offset", "\{scope.align_offset}")
  @html.div(
    id=resolved_id,
    class?,
    title?,
    attrs=content_attrs,
    style=ui_styles(
      [
        UiBoxSizing,
        UiFontSans,
        UiTextRendering,
        SelectContentStyle,
        PopupTransition100Style,
        popup_floating_anchor_style(
          scope.trigger_id.val,
          scope.side,
          scope.align,
          scope.side_offset,
          scope.align_offset,
        ),
        popup_state_style(scope.model.open),
      ],
      style,
    ),
    @html.div(
      attrs=@html.Attrs::build().data_set("slot", "select-viewport"),
      style=["min-height:0;overflow-y:auto;padding:0.25rem"],
      children,
    ),
  )
}

///|
fn select_default_view(scope : SelectScope) -> @html.Html {
  let items : Array[@html.Html] = []
  for option in scope.options {
    items.push(select_item(scope~, value=option.value, option.label))
  }
  @html.fragment([select_trigger(scope~), select_content(scope~, items)])
}

///|
fn select_render(
  scope : SelectScope,
  class : String?,
  title : String?,
  attrs : @html.Attrs?,
  style : Array[String],
  render : ((SelectScope) -> @html.Html)?,
) -> @html.Html {
  let body = if render is Some(render) {
    // Compound renderers are pure. The discovery pass lets a custom Content
    // id participate in Trigger's aria-controls even when Trigger is declared
    // first, matching the two-pass Command catalog.
    ignore(render(scope))
    render(scope)
  } else {
    select_default_view(scope)
  }
  let hidden_control = if scope.name is Some(name) {
    @html.input(
      input_type=@html.Hidden,
      name~,
      value=scope.model.value.unwrap_or(""),
      attrs=@html.Attrs::build()
        .disabled(scope.disabled)
        .data_set("slot", "select-hidden-input")
        .aria_hidden("true"),
    )
  } else {
    @html.nothing
  }
  @html.span(
    id="\{scope.id}-root",
    class?,
    title?,
    attrs=select_scope_root_attrs(scope, attrs),
    style=ui_styles(
      [UiBoxSizing, UiFontSans, UiTextRendering, SelectRootStyle],
      style,
    ),
    [hidden_control, body],
  )
}

///|
#cfg(target="js")
pub fn select(
  id~ : String,
  options~ : Array[SelectOption],
  default_value? : String,
  default_open? : Bool = false,
  placeholder? : String = "Select an option",
  disabled? : Bool = false,
  invalid? : Bool = false,
  required? : Bool = false,
  name? : String,
  side? : PopupSide = Bottom,
  align? : PopupAlign = Start,
  side_offset? : Int = 4,
  align_offset? : Int = 0,
  on_value_change? : @cmd.Emit[String],
  on_open_change? : @cmd.Emit[Bool],
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  render? : (SelectScope) -> @html.Html,
) -> @rabbita.Val[@html.Html] {
  let selected = select_find_value(options, default_value)
  let initial_value = if selected >= 0 {
    Some(options[selected].value)
  } else {
    None
  }
  let initial_active = if selected >= 0 {
    selected
  } else {
    select_first_enabled(options, false)
  }
  let initial : SelectModel = {
    open: default_open && !disabled,
    value: initial_value,
    active: initial_active,
    search: "",
    search_generation: 0,
  }
  let (model, emit) = @rabbita.create_state_with_init(
    init=emit => {
      (
        initial,
        @cmd.batch([
          ui_listbox_bind_sync_cmd(
            id,
            "select-content",
            emit.map(value => SelectNativeChanged(value)),
          ),
          if initial.open {
            select_popup_cmd(id, Some(true), None, "")
          } else {
            @cmd.none
          },
        ]),
      )
    },
    update=(emit, message, current) => {
      let base_scope : SelectScope = {
        id,
        options,
        model: current,
        placeholder,
        disabled,
        invalid,
        required,
        name,
        side,
        align,
        side_offset,
        align_offset,
        emit,
        content_id: Ref("\{id}-content"),
        trigger_id: Ref("\{id}-trigger"),
      }
      match message {
        SelectToggle =>
          if disabled {
            (current, @cmd.none)
          } else {
            let open = !current.open
            let active = select_resolve_active(options, current)
            let next = { ..current, open, active }
            let next_scope = { ..base_scope, model: next }
            (
              next,
              @cmd.batch([
                select_open_commands(next_scope, open),
                select_notify_bool(on_open_change, open),
              ]),
            )
          }
        SelectNativeChanged(open) =>
          if current.open == open {
            (current, @cmd.none)
          } else if open {
            let active = select_resolve_active(options, current)
            (
              { ..current, open: true, active },
              @cmd.batch([
                select_notify_bool(on_open_change, true),
                if active >= 0 && active < options.length() {
                  select_popup_cmd(
                    id,
                    None,
                    Some("select-item"),
                    options[active].value,
                  )
                } else {
                  @cmd.none
                },
              ]),
            )
          } else {
            (
              { ..current, open: false },
              select_notify_bool(on_open_change, false),
            )
          }
        SelectMove(direction) => {
          let active = if !current.open {
            let selected = select_find_value(options, current.value)
            if selected >= 0 {
              selected
            } else {
              select_first_enabled(options, direction < 0)
            }
          } else {
            select_next_enabled(options, current.active, direction)
          }
          let open = true
          let next = { ..current, open, active }
          (
            next,
            @cmd.batch([
              if active >= 0 && active < options.length() {
                select_popup_cmd(
                  id,
                  Some(true),
                  Some("select-item"),
                  options[active].value,
                )
              } else {
                select_popup_cmd(id, Some(true), None, "")
              },
              if current.open {
                @cmd.none
              } else {
                select_notify_bool(on_open_change, true)
              },
            ]),
          )
        }
        SelectHighlight(index) =>
          if !current.open ||
            index < 0 ||
            index >= options.length() ||
            options[index].disabled {
            (current, @cmd.none)
          } else if current.active == index {
            (current, @cmd.none)
          } else {
            ({ ..current, active: index }, @cmd.none)
          }
        SelectFirst => {
          let active = select_first_enabled(options, false)
          let opened = !current.open
          (
            { ..current, open: true, active },
            @cmd.batch([
              if active >= 0 && active < options.length() {
                select_popup_cmd(
                  id,
                  Some(true),
                  Some("select-item"),
                  options[active].value,
                )
              } else {
                select_popup_cmd(id, Some(true), None, "")
              },
              if opened {
                select_notify_bool(on_open_change, true)
              } else {
                @cmd.none
              },
            ]),
          )
        }
        SelectLast => {
          let active = select_first_enabled(options, true)
          let opened = !current.open
          (
            { ..current, open: true, active },
            @cmd.batch([
              if active >= 0 && active < options.length() {
                select_popup_cmd(
                  id,
                  Some(true),
                  Some("select-item"),
                  options[active].value,
                )
              } else {
                select_popup_cmd(id, Some(true), None, "")
              },
              if opened {
                select_notify_bool(on_open_change, true)
              } else {
                @cmd.none
              },
            ]),
          )
        }
        SelectChoose(requested) => {
          let index = if requested < 0 { current.active } else { requested }
          if index < 0 || index >= options.length() || options[index].disabled {
            (current, @cmd.none)
          } else {
            let option = options[index]
            let changed = current.value != Some(option.value)
            (
              {
                ..current,
                open: false,
                value: Some(option.value),
                active: index,
              },
              @cmd.batch([
                select_popup_cmd(id, Some(false), Some("select-trigger"), ""),
                select_notify_bool(on_open_change, false),
                if changed {
                  select_notify_value(on_value_change, option.value)
                } else {
                  @cmd.none
                },
              ]),
            )
          }
        }
        SelectClose =>
          if current.open {
            (
              { ..current, open: false },
              @cmd.batch([
                select_popup_cmd(id, Some(false), Some("select-trigger"), ""),
                select_notify_bool(on_open_change, false),
              ]),
            )
          } else {
            (current, @cmd.none)
          }
        SelectDismiss =>
          if current.open {
            (
              { ..current, open: false },
              @cmd.batch([
                select_popup_cmd(id, Some(false), None, ""),
                select_notify_bool(on_open_change, false),
              ]),
            )
          } else {
            (current, @cmd.none)
          }
        SelectTypeahead(key) => {
          let search = select_typeahead_search(current.search, key)
          let search_start = if current.open {
            current.active
          } else {
            select_find_value(options, current.value)
          }
          let matched = select_typeahead_index(options, search_start, search)
          let active = if matched >= 0 { matched } else { current.active }
          let generation = current.search_generation + 1
          if current.open {
            (
              { ..current, active, search, search_generation: generation },
              @cmd.batch([
                if matched >= 0 {
                  select_popup_cmd(
                    id,
                    Some(true),
                    Some("select-item"),
                    options[matched].value,
                  )
                } else {
                  @cmd.none
                },
                @cmd.delay(emit(SelectClearTypeahead(generation)), 700),
              ]),
            )
          } else if matched >= 0 {
            let option = options[matched]
            let changed = current.value != Some(option.value)
            (
              {
                ..current,
                value: Some(option.value),
                active: matched,
                search,
                search_generation: generation,
              },
              @cmd.batch([
                if changed {
                  select_notify_value(on_value_change, option.value)
                } else {
                  @cmd.none
                },
                @cmd.delay(emit(SelectClearTypeahead(generation)), 700),
              ]),
            )
          } else {
            (
              { ..current, search, search_generation: generation },
              @cmd.delay(emit(SelectClearTypeahead(generation)), 700),
            )
          }
        }
        SelectClearTypeahead(generation) =>
          if generation == current.search_generation {
            ({ ..current, search: "" }, @cmd.none)
          } else {
            (current, @cmd.none)
          }
      }
    },
  )
  model.view(model => {
    let scope : SelectScope = {
      id,
      options,
      model,
      placeholder,
      disabled,
      invalid,
      required,
      name,
      side,
      align,
      side_offset,
      align_offset,
      emit,
      content_id: Ref("\{id}-content"),
      trigger_id: Ref("\{id}-trigger"),
    }
    select_render(scope, class, title, attrs, style, render)
  })
}

///|
#cfg(not(target="js"))
pub fn select(
  id~ : String,
  options~ : Array[SelectOption],
  default_value? : String,
  default_open? : Bool = false,
  placeholder? : String = "Select an option",
  disabled? : Bool = false,
  invalid? : Bool = false,
  required? : Bool = false,
  name? : String,
  side? : PopupSide = Bottom,
  align? : PopupAlign = Start,
  side_offset? : Int = 4,
  align_offset? : Int = 0,
  on_value_change? : @cmd.Emit[String],
  on_open_change? : @cmd.Emit[Bool],
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  render? : (SelectScope) -> @html.Html,
) -> @rabbita.Val[@html.Html] {
  ignore((on_value_change, on_open_change))
  let selected = select_find_value(options, default_value)
  let model : SelectModel = {
    open: default_open && !disabled,
    value: if selected >= 0 {
      Some(options[selected].value)
    } else {
      None
    },
    active: if selected >= 0 {
      selected
    } else {
      select_first_enabled(options, false)
    },
    search: "",
    search_generation: 0,
  }
  let scope : SelectScope = {
    id,
    options,
    model,
    placeholder,
    disabled,
    invalid,
    required,
    name,
    side,
    align,
    side_offset,
    align_offset,
    emit: @cmd.Emit(_ => @cmd.none),
    content_id: Ref("\{id}-content"),
    trigger_id: Ref("\{id}-trigger"),
  }
  @rabbita.Val::constant(
    select_render(scope, class, title, attrs, style, render),
  )
}