///|
pub(all) enum AccordionType {
  Single
  Multiple
} derive(Debug, Eq)

///|
/// One entry for the self-contained accordion component.
///
/// Use the compound `accordion_*` functions directly when the trigger needs
/// richer content than this entry's text label.
struct AccordionEntry {
  value : String
  label : String
  content : @html.Html
  disabled : Bool
}

///|
const AccordionRootStyle : String = "display:block;width:100%;min-width:0;color:var(--rui-foreground,oklch(0.145 0 0))"

///|
const AccordionItemStyle : String = "display:block;width:100%;min-width:0;border-bottom:1px solid var(--rui-border,oklch(0.922 0 0))"

///|
const AccordionTriggerStyle : String = "display:flex;width:100%;min-height:3rem;align-items:center;justify-content:space-between;gap:0.75rem;border:1px solid var(--rui-accordion-border,var(--rui-accordion-base-border,transparent));border-radius:calc(var(--rui-radius,0.625rem) - 0.125rem);margin:0;padding:0.75rem 0;background:var(--rui-accordion-bg,var(--rui-accordion-base-bg,transparent));color:var(--rui-accordion-fg,var(--rui-accordion-base-fg,inherit));box-shadow:var(--rui-accordion-shadow,var(--rui-accordion-base-shadow,none));font-size:0.875rem;line-height:1.25rem;font-weight:500;text-align:start;list-style:none;cursor:pointer;user-select:none;outline-offset:2px"

///|
const AccordionTriggerOpenStyle : String = "--rui-accordion-base-fg:var(--rui-foreground,oklch(0.145 0 0))"

///|
const AccordionTriggerLabelStyle : String = "display:block;min-width:0;flex:1"

///|
const AccordionIndicatorBoxStyle : String = "display:inline-flex;width:1rem;height:1rem;flex-shrink:0;align-items:center;justify-content:center;color:var(--rui-muted-foreground,oklch(0.556 0 0));pointer-events:none"

///|
const AccordionIndicatorStyle : String = "display:block;width:0.4375rem;height:0.4375rem;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform-origin:center;transition:transform 150ms ease"

///|
const AccordionIndicatorClosedStyle : String = "transform:translateY(-0.125rem) rotate(45deg)"

///|
const AccordionIndicatorOpenStyle : String = "transform:translateY(0.125rem) rotate(225deg)"

///|
const AccordionContentOuterStyle : String = "display:grid;min-width:0;grid-template-rows:var(--rui-accordion-content-rows,0fr);opacity:var(--rui-accordion-content-opacity,0);visibility:var(--rui-accordion-content-visibility,hidden);transition:grid-template-rows 200ms cubic-bezier(0.22,1,0.36,1),opacity 150ms ease,visibility 0s linear var(--rui-accordion-content-visibility-delay,200ms)"

///|
const AccordionContentOpenStyle : String = "--rui-accordion-content-rows:1fr;--rui-accordion-content-opacity:1;--rui-accordion-content-visibility:visible;--rui-accordion-content-visibility-delay:0s"

///|
const AccordionContentClosedStyle : String = "--rui-accordion-content-rows:0fr;--rui-accordion-content-opacity:0;--rui-accordion-content-visibility:hidden;--rui-accordion-content-visibility-delay:200ms"

///|
const AccordionContentClipStyle : String = "min-width:0;min-height:0;overflow:hidden"

///|
const AccordionContentStyle : String = "min-width:0;padding:0 0 1rem;color:var(--rui-muted-foreground,oklch(0.556 0 0));font-size:0.875rem;line-height:1.5;overflow-wrap:anywhere"

///|
fn accordion_type_value(type_ : AccordionType) -> String {
  match type_ {
    Single => "single"
    Multiple => "multiple"
  }
}

///|
fn accordion_state(open : Bool) -> String {
  if open {
    "open"
  } else {
    "closed"
  }
}

///|
fn accordion_state_attrs(
  attrs : @html.Attrs?,
  slot : String,
  open : Bool,
) -> @html.Attrs {
  let element_attrs = ui_attrs(attrs)
    .data_set("slot", slot)
    .data_set("state", accordion_state(open))
  if open {
    ignore(element_attrs.data_set("open", ""))
  } else {
    ignore(element_attrs.data_set("closed", ""))
  }
  element_attrs
}

///|
#cfg(target="js")
fn accordion_bind_value_change(
  attrs : @html.Attrs,
  value : String,
  emit : @cmd.Emit[String],
) -> Unit {
  ignore(
    attrs.on_click(event => {
      event.prevent_default()
      emit(value)
    }),
  )
}

///|
#cfg(not(target="js"))
fn accordion_bind_value_change(
  attrs : @html.Attrs,
  value : String,
  emit : @cmd.Emit[String],
) -> Unit {
  ignore((attrs, value, emit))
}

///|
#cfg(target="js")
fn accordion_move_focus(event : @dom.KeyboardEvent) -> Bool {
  if event.alt_key() ||
    event.ctrl_key() ||
    event.meta_key() ||
    event.shift_key() {
    return false
  }
  guard ui_roving_context(
      event, "[data-slot=\"accordion-trigger\"]", "[data-slot=\"accordion\"]", "[data-slot=\"accordion-trigger\"]",
    )
    is Some(context) else {
    return false
  }
  if context.root.get_attribute("aria-disabled").unwrap_or("") == "true" {
    return false
  }
  guard ui_roving_target(context, event.key(), "vertical", false)
    is Some(target) else {
    return false
  }
  ui_focus_element(target)
  true
}

///|
#cfg(target="js")
fn accordion_bind_keyboard(attrs : @html.Attrs) -> Unit {
  ignore(
    attrs.on_keydown(event => {
      if accordion_move_focus(event) {
        event.prevent_default()
      }
      @cmd.none
    }),
  )
}

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

///|
fn accordion_indicator(open : Bool) -> @html.Html {
  let state = accordion_state(open)
  @html.span(
    style=[UiBoxSizing, AccordionIndicatorBoxStyle],
    attrs=@html.Attrs::build()
      .aria_hidden("true")
      .data_set("slot", "accordion-indicator")
      .data_set("state", state),
    @html.span(
      style=[
        UiBoxSizing,
        AccordionIndicatorStyle,
        if open {
          AccordionIndicatorOpenStyle
        } else {
          AccordionIndicatorClosedStyle
        },
      ],
      attrs=@html.Attrs::build().data_set("state", state),
      @html.nothing,
    ),
  )
}

///|
fn accordion_entry_enabled(
  items : Array[AccordionEntry],
  value : String,
) -> Bool {
  for item in items {
    if item.value == value {
      return !item.disabled
    }
  }
  false
}

///|
fn accordion_initial_values(
  items : Array[AccordionEntry],
  type_ : AccordionType,
  default_value : String?,
  default_values : Array[String],
) -> Array[String] {
  match type_ {
    Single =>
      if default_value is Some(value) && accordion_entry_enabled(items, value) {
        [value]
      } else {
        []
      }
    Multiple => {
      let values : Array[String] = []
      for value in default_values {
        if accordion_entry_enabled(items, value) && !values.contains(value) {
          values.push(value)
        }
      }
      values
    }
  }
}

///|
#cfg(target="js")
fn accordion_next_values(
  current : Array[String],
  value : String,
  type_ : AccordionType,
  collapsible : Bool,
) -> Array[String] {
  match type_ {
    Single =>
      if current.contains(value) {
        if collapsible {
          []
        } else {
          current
        }
      } else {
        [value]
      }
    Multiple =>
      if current.contains(value) {
        current.filter(item => item != value)
      } else {
        let next = current.copy()
        next.push(value)
        next
      }
  }
}

///|
/// Construct one black-box accordion entry.
pub fn accordion_entry(
  value~ : String,
  label~ : String,
  content~ : @html.Html,
  disabled? : Bool = false,
) -> AccordionEntry {
  { value, label, content, disabled }
}

///|
/// Render the root for a composed accordion.
pub fn[C : @html.IsChildren] accordion_root(
  type_? : AccordionType = Single,
  disabled? : Bool = false,
  id? : String,
  class? : String,
  title? : String,
  hidden? : Bool,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  let element_attrs = ui_attrs(attrs)
    .data_set("slot", "accordion")
    .data_set("type", accordion_type_value(type_))
    .data_set("orientation", "vertical")
    .aria_disabled(ui_bool(disabled))
  if disabled {
    ignore(element_attrs.data_set("disabled", ""))
  }
  accordion_bind_keyboard(element_attrs)
  @html.div(
    style=ui_styles(
      [
        UiBoxSizing,
        UiFontSans,
        UiTextRendering,
        AccordionRootStyle,
        ui_disabled_visual_style(disabled),
      ],
      style,
    ),
    id?,
    class?,
    title?,
    hidden?,
    attrs=element_attrs,
    children,
  )
}

///|
/// Render one accordion item container.
pub fn[C : @html.IsChildren] accordion_item(
  value~ : String,
  open? : Bool = false,
  disabled? : Bool = false,
  name? : String,
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  let element_attrs = accordion_state_attrs(attrs, "accordion-item", open)
    .data_set("value", value)
    .aria_disabled(ui_bool(disabled))
  if name is Some(name) {
    ignore(element_attrs.data_set("group", name))
  }
  if disabled {
    ignore(element_attrs.data_set("disabled", ""))
  }
  @html.div(
    style=ui_styles(
      [UiBoxSizing, AccordionItemStyle, ui_disabled_visual_style(disabled)],
      style,
    ),
    id?,
    class?,
    title?,
    attrs=element_attrs,
    children,
  )
}

///|
/// Render one native button accordion trigger.
pub fn[C : @html.IsChildren] accordion_trigger(
  value~ : String,
  open? : Bool = false,
  disabled? : Bool = false,
  controls? : String,
  show_indicator? : Bool = true,
  on_value_change? : @cmd.Emit[String],
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  let element_attrs = accordion_state_attrs(attrs, "accordion-trigger", open)
    .data_set("value", value)
    .aria_expanded(ui_bool(open))
    .aria_disabled(ui_bool(disabled))
  if controls is Some(controls) {
    ignore(element_attrs.aria_controls(controls))
  }
  if disabled {
    ignore(element_attrs.data_set("disabled", "").tabindex(-1))
  }
  if !disabled && on_value_change is Some(emit) {
    accordion_bind_value_change(element_attrs, value, emit)
  }
  @html.button(
    style=ui_styles(
      [
        UiBoxSizing,
        UiFontSans,
        UiTextRendering,
        UiTransition,
        AccordionTriggerStyle,
        if open {
          AccordionTriggerOpenStyle
        } else {
          ""
        },
        ui_disabled_style(disabled),
      ],
      style,
    ),
    id?,
    class?,
    title?,
    type_="button",
    disabled~,
    attrs=element_attrs,
    [
      @html.span(
        style=[UiBoxSizing, AccordionTriggerLabelStyle],
        attrs=@html.Attrs::build().data_set("slot", "accordion-trigger-label"),
        children,
      ),
      if show_indicator {
        accordion_indicator(open)
      } else {
        @html.nothing
      },
    ],
  )
}

///|
/// Render one accordion panel.
pub fn[C : @html.IsChildren] accordion_content(
  value~ : String,
  open? : Bool = false,
  force_mount? : Bool = false,
  labelledby? : String,
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  let element_attrs = accordion_state_attrs(attrs, "accordion-content", open)
    .data_set("value", value)
    .data_set("force-mount", ui_bool(force_mount))
    .role("region")
    .aria_hidden(ui_bool(!open))
    .inert(!open)
  if labelledby is Some(labelledby) {
    ignore(element_attrs.aria_labelledby(labelledby))
  }
  @html.div(
    style=ui_styles(
      [
        UiBoxSizing,
        AccordionContentOuterStyle,
        if open {
          AccordionContentOpenStyle
        } else {
          AccordionContentClosedStyle
        },
      ],
      style,
    ),
    id?,
    class?,
    title?,
    attrs=element_attrs,
    @html.div(
      style=[UiBoxSizing, AccordionContentClipStyle],
      attrs=@html.Attrs::build().data_set("slot", "accordion-content-clip"),
      @html.div(
        style=[UiBoxSizing, AccordionContentStyle],
        attrs=@html.Attrs::build().data_set("slot", "accordion-content-body"),
        children,
      ),
    ),
  )
}

///|
/// Render the current accordion state for the public incremental component.
fn render_accordion(
  items~ : Array[AccordionEntry],
  values~ : Array[String],
  type_? : AccordionType = Single,
  collapsible? : Bool = true,
  disabled? : Bool = false,
  show_indicator? : Bool = true,
  force_mount? : Bool = false,
  on_value_change? : @cmd.Emit[String],
  id? : String,
  class? : String,
  title? : String,
  hidden? : Bool,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  item_style? : Array[String] = [],
  trigger_style? : Array[String] = [],
  content_style? : Array[String] = [],
) -> @html.Html {
  let item_nodes : Array[@html.Html] = []
  for index, item in items {
    let open = values.contains(item.value)
    let item_disabled = disabled || item.disabled
    let trigger_id = id.map(id => "\{id}-trigger-\{index}")
    let content_id = id.map(id => "\{id}-content-\{index}")
    let group_name = if type_ is Single {
      id.map(id => "\{id}-group")
    } else {
      None
    }
    let trigger_html = {
      let controls = content_id
      let id = trigger_id
      accordion_trigger(
        value=item.value,
        open~,
        disabled=item_disabled,
        controls?,
        show_indicator~,
        on_value_change?,
        id?,
        style=trigger_style,
        item.label,
      )
    }
    let content_html = {
      let labelledby = trigger_id
      let id = content_id
      accordion_content(
        value=item.value,
        open~,
        force_mount~,
        labelledby?,
        id?,
        style=content_style,
        item.content,
      )
    }
    let item_html = {
      let name = group_name
      accordion_item(
        value=item.value,
        open~,
        disabled=item_disabled,
        name?,
        style=item_style,
        [trigger_html, content_html],
      )
    }
    item_nodes.push(item_html)
  }
  let root_attrs = ui_attrs(attrs).data_set("collapsible", ui_bool(collapsible))
  accordion_root(
    type_~,
    disabled~,
    id?,
    class?,
    title?,
    hidden?,
    attrs=root_attrs,
    style~,
    item_nodes,
  )
}

///|
/// Build a self-contained accordion with component-local incremental state.
#cfg(target="js")
pub fn accordion(
  items~ : Array[AccordionEntry],
  type_? : AccordionType = Single,
  collapsible? : Bool = true,
  disabled? : Bool = false,
  default_value? : String,
  default_values? : Array[String] = [],
  show_indicator? : Bool = true,
  force_mount? : Bool = false,
  id? : String,
  class? : String,
  title? : String,
  hidden? : Bool,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  item_style? : Array[String] = [],
  trigger_style? : Array[String] = [],
  content_style? : Array[String] = [],
) -> @rabbita.Val[@html.Html] {
  let initial = accordion_initial_values(
    items, type_, default_value, default_values,
  )
  let (values, set_values) = @rabbita.create_variable(initial)
  let on_value_change = set_values.map(value => {
    current => accordion_next_values(current, value, type_, collapsible)
  })
  values.view(values => {
    render_accordion(
      items~,
      values~,
      type_~,
      collapsible~,
      disabled~,
      show_indicator~,
      force_mount~,
      on_value_change~,
      id?,
      class?,
      title?,
      hidden?,
      attrs?,
      style~,
      item_style~,
      trigger_style~,
      content_style~,
    )
  })
}

///|
/// Native/SSR fallback: render the requested initial selection as a constant
/// incremental value.
#cfg(not(target="js"))
pub fn accordion(
  items~ : Array[AccordionEntry],
  type_? : AccordionType = Single,
  collapsible? : Bool = true,
  disabled? : Bool = false,
  default_value? : String,
  default_values? : Array[String] = [],
  show_indicator? : Bool = true,
  force_mount? : Bool = false,
  id? : String,
  class? : String,
  title? : String,
  hidden? : Bool,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  item_style? : Array[String] = [],
  trigger_style? : Array[String] = [],
  content_style? : Array[String] = [],
) -> @rabbita.Val[@html.Html] {
  let values = accordion_initial_values(
    items, type_, default_value, default_values,
  )
  @rabbita.Val::constant(
    render_accordion(
      items~,
      values~,
      type_~,
      collapsible~,
      disabled~,
      show_indicator~,
      force_mount~,
      id?,
      class?,
      title?,
      hidden?,
      attrs?,
      style~,
      item_style~,
      trigger_style~,
      content_style~,
    ),
  )
}