///|
struct DropdownMenuScope {
  menu : MenuScope
}

///|
struct DropdownMenuRadioGroupScope {
  menu : MenuScope
  group : String
}

///|
struct DropdownMenuSubScope {
  menu : MenuScope
  key : String
}

///|
#cfg(target="js")
priv enum DropdownMenuMsg {
  DropdownSetOpen(Bool)
  DropdownNativeOpen(Bool)
  DropdownSetActive(Int)
  DropdownToggleChecked(String)
  DropdownSetRadio(String, String)
  DropdownSetSubmenu(MenuSubmenuChange)
}

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

///|
fn dropdown_scope(
  id : String,
  model : MenuCoreModel,
  set_open : @cmd.Emit[Bool],
  toggle_checked : @cmd.Emit[String],
  set_radio : @cmd.Emit[(String, String)],
) -> DropdownMenuScope {
  {
    menu: {
      id,
      content_id: id + "-content",
      trigger_id: id + "-trigger",
      model,
      counter: Ref(0),
      close_command: set_open(false),
      toggle_command: set_open(!model.open),
      toggle_checked,
      set_radio,
    },
  }
}

///|
/// Stateful shadcn/Vega dropdown menu. The builder receives an opaque scope
/// shared by all compound parts; no registration or stylesheet is required.
#cfg(target="js")
pub fn dropdown_menu(
  id~ : String,
  default_open? : Bool = false,
  default_checked_values? : Array[String] = [],
  default_radio_values? : Array[(String, String)] = [],
  on_open_change? : @cmd.Emit[Bool],
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : (DropdownMenuScope) -> @html.Html,
) -> @rabbita.Val[@html.Html] {
  let initial = menu_core_model(
    default_open, default_checked_values, default_radio_values,
  )
  let (model, emit) = @rabbita.create_state_with_init(
    init=emit => {
      let set_active = emit.map(index => DropdownSetActive(index))
      let set_submenu = emit.map(value => DropdownSetSubmenu(value))
      (
        initial,
        @cmd.batch([
          floating_bind_sync_cmd(
            id + "-content",
            emit.map(open => DropdownNativeOpen(open)),
          ),
          menu_bind_surface_cmd(
            id + "-root",
            set_active,
            emit(DropdownSetOpen(false)),
            set_submenu,
          ),
          if default_open {
            @cmd.batch([
              floating_set_open_cmd(id + "-content", true),
              menu_focus_first_cmd(id + "-root", set_active),
            ])
          } else {
            @cmd.none
          },
        ]),
      )
    },
    update=(emit, msg, current) => {
      match msg {
        DropdownSetOpen(open) =>
          if open == current.open {
            (current, @cmd.none)
          } else {
            let next = {
              ..current,
              open,
              active: if open {
                -1
              } else {
                current.active
              },
              submenus: [],
            }
            (
              next,
              @cmd.batch([
                floating_set_open_cmd(id + "-content", open),
                if open {
                  menu_focus_first_cmd(
                    id + "-root",
                    emit.map(index => DropdownSetActive(index)),
                  )
                } else {
                  ui_focus_id(id + "-trigger")
                },
                if open {
                  @cmd.none
                } else {
                  menu_position_submenus_cmd(id + "-root")
                },
                dropdown_notify(on_open_change, open),
              ]),
            )
          }
        DropdownNativeOpen(open) =>
          if open == current.open {
            (current, @cmd.none)
          } else {
            (
              { ..current, open, submenus: [] },
              @cmd.batch([
                if open {
                  @cmd.none
                } else {
                  menu_position_submenus_cmd(id + "-root")
                },
                dropdown_notify(on_open_change, open),
              ]),
            )
          }
        DropdownSetActive(index) =>
          if current.active == index {
            (current, @cmd.none)
          } else {
            (
              { ..current, active: index },
              menu_focus_item_cmd(id + "-root", index),
            )
          }
        DropdownToggleChecked(value) =>
          (
            { ..current, checked: menu_toggle_value(current.checked, value) },
            @cmd.none,
          )
        DropdownSetRadio(group, value) =>
          (
            {
              ..current,
              radios: menu_set_radio_value(current.radios, group, value),
            },
            @cmd.none,
          )
        DropdownSetSubmenu(change) => {
          let submenus = menu_update_submenus(current.submenus, change)
          if submenus == current.submenus {
            (current, @cmd.none)
          } else {
            ({ ..current, submenus, }, menu_position_submenus_cmd(id + "-root"))
          }
        }
      }
    },
  )
  model.view(model => {
    let scope = dropdown_scope(
      id,
      model,
      emit.map(open => DropdownSetOpen(open)),
      emit.map(value => DropdownToggleChecked(value)),
      emit.map(pair => DropdownSetRadio(pair.0, pair.1)),
    )
    menu_root_surface(
      "dropdown-menu",
      id + "-root",
      model.open,
      attrs,
      style,
      children(scope),
    )
  })
}

///|
#cfg(not(target="js"))
pub fn dropdown_menu(
  id~ : String,
  default_open? : Bool = false,
  default_checked_values? : Array[String] = [],
  default_radio_values? : Array[(String, String)] = [],
  on_open_change? : @cmd.Emit[Bool],
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : (DropdownMenuScope) -> @html.Html,
) -> @rabbita.Val[@html.Html] {
  ignore(on_open_change)
  let scope = dropdown_scope(
    id,
    menu_core_model(default_open, default_checked_values, default_radio_values),
    menu_noop_emit(),
    menu_noop_emit(),
    menu_noop_emit(),
  )
  @rabbita.Val::constant(
    menu_root_surface(
      "dropdown-menu",
      id + "-root",
      default_open,
      attrs,
      style,
      children(scope),
    ),
  )
}

///|
/// Explicit Root spelling matching the compound-component API.
pub fn dropdown_menu_root(
  id~ : String,
  default_open? : Bool = false,
  default_checked_values? : Array[String] = [],
  default_radio_values? : Array[(String, String)] = [],
  on_open_change? : @cmd.Emit[Bool],
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : (DropdownMenuScope) -> @html.Html,
) -> @rabbita.Val[@html.Html] {
  dropdown_menu(
    id~,
    default_open~,
    default_checked_values~,
    default_radio_values~,
    on_open_change?,
    attrs?,
    style~,
    children,
  )
}

///|
/// Zero-DOM Portal adapter for shadcn-compatible compound composition.
/// Native `popover="auto"` promotes Content to the top layer when opened, so
/// no registration, DOM relocation, or build-time portal runtime is required.
pub fn dropdown_menu_portal(children : Array[@html.Html]) -> @html.Html {
  @html.fragment(children)
}

///|
pub fn[C : @html.IsChildren] dropdown_menu_trigger(
  scope : DropdownMenuScope,
  variant? : ButtonVariant = Outline,
  size? : ButtonSize = Default,
  disabled? : Bool = false,
  aria_label? : String,
  on_click? : @cmd.Cmd,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  menu_trigger_button(
    "dropdown-menu-trigger",
    scope.menu,
    variant,
    size,
    disabled,
    aria_label,
    on_click,
    class,
    title,
    attrs,
    style,
    children,
  )
}

///|
pub fn[C : @html.IsChildren] dropdown_menu_content(
  scope : DropdownMenuScope,
  side? : PopupSide = Bottom,
  align? : PopupAlign = Start,
  side_offset? : Int = 4,
  align_offset? : Int = 0,
  aria_label? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  menu_content_surface(
    "dropdown-menu-content",
    scope.menu,
    side,
    align,
    side_offset,
    align_offset,
    aria_label,
    class,
    title,
    attrs,
    style,
    children,
  )
}

///|
pub fn[C : @html.IsChildren] dropdown_menu_group(
  aria_label? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  menu_group_surface(
    "dropdown-menu-group", aria_label, class, title, attrs, style, children,
  )
}

///|
pub fn[C : @html.IsChildren] dropdown_menu_item(
  scope : DropdownMenuScope,
  disabled? : Bool = false,
  inset? : Bool = false,
  destructive? : Bool = false,
  close_on_select? : Bool = true,
  on_select? : @cmd.Cmd,
  aria_label? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  menu_item_surface(
    "dropdown-menu-item",
    scope.menu,
    "menuitem",
    None,
    MenuIndicatorInlineStart,
    disabled,
    inset,
    destructive,
    None,
    close_on_select,
    None,
    on_select,
    aria_label,
    class,
    title,
    attrs,
    style,
    children,
  )
}

///|
pub fn[C : @html.IsChildren] dropdown_menu_checkbox_item(
  scope : DropdownMenuScope,
  value~ : String,
  disabled? : Bool = false,
  close_on_select? : Bool = false,
  on_checked_change? : @cmd.Emit[Bool],
  on_select? : @cmd.Cmd,
  aria_label? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  let checked = menu_contains(scope.menu.model.checked, value)
  let notify = if on_checked_change is Some(notify) {
    notify(!checked)
  } else {
    @cmd.none
  }
  menu_item_surface(
    "dropdown-menu-checkbox-item",
    scope.menu,
    "menuitemcheckbox",
    Some(checked),
    MenuIndicatorInlineEnd,
    disabled,
    false,
    false,
    None,
    close_on_select,
    Some(@cmd.batch([notify, (scope.menu.toggle_checked)(value)])),
    on_select,
    aria_label,
    class,
    title,
    attrs,
    style,
    children,
  )
}

///|
pub fn[C : @html.IsChildren] dropdown_menu_radio_group(
  scope : DropdownMenuScope,
  value~ : String,
  aria_label? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : (DropdownMenuRadioGroupScope) -> C,
) -> @html.Html {
  menu_group_surface(
    "dropdown-menu-radio-group",
    aria_label,
    class,
    title,
    attrs,
    style,
    children({ menu: scope.menu, group: value }),
  )
}

///|
pub fn[C : @html.IsChildren] dropdown_menu_radio_item(
  scope : DropdownMenuRadioGroupScope,
  value~ : String,
  disabled? : Bool = false,
  close_on_select? : Bool = false,
  on_value_change? : @cmd.Emit[String],
  on_select? : @cmd.Cmd,
  aria_label? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  let checked = menu_radio_value(scope.menu.model.radios, scope.group) ==
    Some(value)
  let notify = if on_value_change is Some(notify) {
    notify(value)
  } else {
    @cmd.none
  }
  menu_item_surface(
    "dropdown-menu-radio-item",
    scope.menu,
    "menuitemradio",
    Some(checked),
    MenuIndicatorInlineEnd,
    disabled,
    false,
    false,
    None,
    close_on_select,
    Some(@cmd.batch([notify, (scope.menu.set_radio)((scope.group, value))])),
    on_select,
    aria_label,
    class,
    title,
    attrs,
    style,
    children,
  )
}

///|
pub fn[C : @html.IsChildren] dropdown_menu_label(
  inset? : Bool = false,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  menu_label_surface(
    "dropdown-menu-label", inset, class, title, attrs, style, children,
  )
}

///|
pub fn dropdown_menu_separator(
  class? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
) -> @html.Html {
  menu_separator_surface("dropdown-menu-separator", class, attrs, style)
}

///|
pub fn[C : @html.IsChildren] dropdown_menu_shortcut(
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  menu_shortcut_surface(
    "dropdown-menu-shortcut", class, title, attrs, style, children,
  )
}

///|
pub fn[C : @html.IsChildren] dropdown_menu_sub(
  scope : DropdownMenuScope,
  value~ : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : (DropdownMenuSubScope) -> C,
) -> @html.Html {
  let key = menu_submenu_key(scope.menu, value)
  let open = menu_submenu_open(scope.menu.model.submenus, key)
  menu_sub_root_surface(
    "dropdown-menu-sub",
    open,
    attrs,
    style,
    children({ menu: scope.menu, key }),
  )
}

///|
pub fn[C : @html.IsChildren] dropdown_menu_sub_trigger(
  scope : DropdownMenuSubScope,
  disabled? : Bool = false,
  inset? : Bool = false,
  aria_label? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  menu_item_surface(
    "dropdown-menu-sub-trigger",
    scope.menu,
    "menuitem",
    None,
    MenuIndicatorInlineStart,
    disabled,
    inset,
    false,
    Some(scope.key),
    false,
    None,
    None,
    aria_label,
    class,
    title,
    attrs,
    style,
    children,
  )
}

///|
pub fn[C : @html.IsChildren] dropdown_menu_sub_content(
  scope : DropdownMenuSubScope,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  menu_sub_content_surface(
    "dropdown-menu-sub-content",
    scope.key,
    menu_submenu_open(scope.menu.model.submenus, scope.key),
    class,
    title,
    attrs,
    style,
    children,
  )
}