///|
pub fn select_option(
  key~ : String,
  label~ : String,
  disabled? : Bool = false,
  group? : String = "",
) -> ChoiceOption {
  choice_option(key~, label~, disabled~, group~)
}

///|
pub fn native_select_option(
  key~ : String,
  label~ : String,
  disabled? : Bool = false,
  group? : String = "",
) -> ChoiceOption {
  choice_option(key~, label~, disabled~, group~)
}

///|
pub fn native_select_optgroup(
  label~ : String,
  options : Array[ChoiceOption],
) -> Array[ChoiceOption] {
  options.map(option => { ..option, group: label, })
}

///|
pub fn select_value(
  options : Array[ChoiceOption],
  value : String,
  placeholder? : String = "Select…",
) -> String {
  for option in options {
    if option.key == value {
      return option.label
    }
  }
  placeholder
}

///|
pub fn[C : @minimoon.IsChildren] select_item(
  on_tap~ : @minimoon.Cmd,
  selected? : Bool = false,
  disabled? : Bool = false,
  id? : String = "",
  children : C,
) -> @minimoon.Node {
  @minimoon.tap_view(
    id~,
    on_tap=if disabled { @minimoon.none } else { on_tap },
    class="mmui-select-item " +
      (if selected { "mmui-selected " } else { "" }) +
      (if disabled { "mmui-disabled" } else { "" }),
    semantics=@minimoon.semantics(
      role=@minimoon.OptionRole,
      selected~,
      disabled~,
    ),
    children,
  )
}

///|
pub fn select_trigger(
  on_tap~ : @minimoon.Cmd,
  value~ : String,
  id~ : String,
  expanded? : Bool = false,
  disabled? : Bool = false,
  invalid? : Bool = false,
  required? : Bool = false,
  content? : @minimoon.Node,
) -> @minimoon.Node {
  @minimoon.button_children(
    id~,
    on_tap=if disabled { @minimoon.none } else { on_tap },
    disabled~,
    class="mmui-select-trigger",
    semantics=@minimoon.semantics(
      role=@minimoon.ComboboxRole,
      expanded~,
      disabled~,
      invalid~,
      required~,
      has_popup="listbox",
    ),
    content.unwrap_or(@minimoon.text(value)),
  )
}

///|
/// Select uses a page-owned native layer; application must declare MeasureNodes.
pub fn select_controlled(
  context : UiContext,
  id~ : String,
  options~ : @minimoon.Val[Array[ChoiceOption]],
  value~ : @minimoon.Val[String],
  on_change~ : @minimoon.Emit[String],
  placeholder? : String = "Select…",
  disabled? : Bool = false,
  open? : @minimoon.Val[Bool],
  initially_open? : Bool = false,
  on_open_change? : @minimoon.Emit[Bool],
  invalid? : Bool = false,
  required? : Bool = false,
  placement? : @headless.Placement = @headless.BottomStart,
  side_offset? : Double = 8.0,
  align_offset? : Double = 0.0,
  render_option? : (ChoiceOption, Bool) -> @minimoon.Node,
  render_value? : (String, Array[ChoiceOption]) -> @minimoon.Node,
  render_content? : (Array[@minimoon.Node]) -> @minimoon.Node,
) -> @minimoon.Val[@minimoon.Node] {
  validate_measured_id(id)
  guard open is None || on_open_change is Some(_) else {
    abort("controlled Select open requires on_open_change")
  }
  let (owned_open, update_open) = @minimoon.create_variable(initially_open)
  let open = open.unwrap_or(owned_open)
  let change_open = @minimoon.Emit((next : Bool) => {
    @minimoon.batch([
      update_open(_ => next),
      on_open_change.map(emit => emit(next)).unwrap_or(@minimoon.none),
    ])
  })
  let bounds = floating_bounds(
    context,
    id,
    open.map(open => open && !disabled),
    placement,
    side_offset~,
    align_offset~,
    surface_id=id + "-panel",
  )
  let _ = @minimoon.create_state(
    (),
    update=(_, _message : Unit, _) => @minimoon.no_cmd(()),
    subscriptions=(_, _) => context.page.on_hide(change_open(false)),
  )
  let show = change_open(true)
  let choose = form_dispatch(
    @minimoon.Val::map3(options, open, context.visible, (options, open, visible) => {
      (options, open, visible)
    }),
    (current, key : String) => {
      let (options, open, visible) = current
      if open &&
        visible &&
        !disabled &&
        options.any(option => option.key == key && !option.disabled) {
        @minimoon.batch([on_change(key), change_open(false)])
      } else {
        @minimoon.none
      }
    },
  )
  @minimoon.Val::view5(options, value, open, bounds, context.visible, (
    options,
    value,
    open,
    bounds,
    visible,
  ) => {
    validate_choices(options)
    let rows : Array[@minimoon.Node] = []
    let mut group = ""
    for option in options {
      if option.group != "" && option.group != group {
        rows.push(select_label(option.group))
        group = option.group
      }
      rows.push(
        select_item(
          id=id + "-option-" + option.key,
          selected=option.key == value,
          disabled=disabled || option.disabled,
          on_tap=choose(option.key),
          match render_option {
            Some(render) => render(option, option.key == value)
            None =>
              @minimoon.fragment([
                @minimoon.text(option.label),
                @minimoon.text(if option.key == value { "✓" } else { "" }),
              ])
          },
        ),
      )
    }
    ui_container(
      "select",
      [
        select_trigger(
          id=id + "-trigger",
          on_tap=show,
          value=select_value(options, value, placeholder~),
          expanded=open,
          disabled~,
          invalid~,
          required~,
          content?=render_value.map(render => render(value, options)),
        ),
        if open && visible && !disabled {
          @minimoon.layer(id=id + "-layer", [
            @minimoon.tap_view(
              class="mmui-overlay",
              style="position:fixed;top:0;left:0;right:0;bottom:0;",
              on_tap=change_open(false),
              @minimoon.nothing(),
            ),
            @minimoon.scroll_view(
              id=id + "-panel",
              class="mmui-select-panel",
              scroll_y=true,
              style=surface_style(PopoverSurface, bounds),
              match render_content {
                Some(render) => render(rows)
                None => select_content(rows)
              },
            ),
          ])
        } else {
          @minimoon.nothing()
        },
      ],
      id~,
    )
  })
}

///|
pub fn select_scroll_up_button(
  on_scroll~ : @minimoon.Cmd,
  disabled? : Bool = false,
) -> @minimoon.Node {
  button(on_tap=on_scroll, disabled~, variant=Ghost, label="Scroll up", "↑")
}

///|
pub fn select_scroll_down_button(
  on_scroll~ : @minimoon.Cmd,
  disabled? : Bool = false,
) -> @minimoon.Node {
  button(on_tap=on_scroll, disabled~, variant=Ghost, label="Scroll down", "↓")
}

///|
fn validate_measured_id(id : String) -> Unit {
  guard id != "" else { abort("measured component id must not be empty") }
  for character in id.iter() {
    guard (character >= 'a' && character <= 'z') ||
      (character >= 'A' && character <= 'Z') ||
      (character >= '0' && character <= '9') ||
      character == '_' ||
      character == '-' else {
      abort(
        "measured component id must contain only letters, digits, underscore or hyphen",
      )
    }
  }
}

///|
pub fn select(
  context : UiContext,
  id~ : String,
  options~ : @minimoon.Val[Array[ChoiceOption]],
  initial_value? : String = "",
  placeholder? : String = "Select…",
  disabled? : Bool = false,
) -> @minimoon.Val[@minimoon.Node] {
  let (value, update) = @minimoon.create_variable(initial_value)
  select_controlled(
    context,
    id~,
    options~,
    value~,
    on_change=update.map(value => _ => value),
    placeholder~,
    disabled~,
  )
}

///|
pub fn input_group_control(content : @minimoon.Node) -> @minimoon.Node {
  ui_container("input-group-control", [content])
}

///|
pub fn input_group_input(
  id~ : String,
  value~ : @minimoon.Val[String],
  on_change~ : @minimoon.Emit[String],
  options? : InputOptions = input_options(),
) -> @minimoon.Val[@minimoon.Node] {
  input_with_focus(id~, value~, on_input=on_change, options~).map(
    input_group_control,
  )
}

///|
pub fn input_group_textarea(
  id~ : String,
  value~ : @minimoon.Val[String],
  on_change~ : @minimoon.Emit[String],
  options? : InputOptions = input_options(),
) -> @minimoon.Val[@minimoon.Node] {
  textarea_with_focus(id~, value~, on_input=on_change, options~).map(
    input_group_control,
  )
}