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

///|
const CollapsibleTriggerStyle : String = AccordionTriggerStyle

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

///|
const CollapsibleIndicatorBoxStyle : 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 CollapsibleIndicatorStyle : 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 CollapsibleIndicatorClosedStyle : String = "transform:translateY(-0.125rem) rotate(45deg)"

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

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

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

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

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

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

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

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

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

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

///|
fn collapsible_indicator(open : Bool) -> @html.Html {
  let state = collapsible_state(open)
  @html.span(
    style=[UiBoxSizing, CollapsibleIndicatorBoxStyle],
    attrs=@html.Attrs::build()
      .aria_hidden("true")
      .data_set("slot", "collapsible-indicator")
      .data_set("state", state),
    @html.span(
      style=[
        UiBoxSizing,
        CollapsibleIndicatorStyle,
        if open {
          CollapsibleIndicatorOpenStyle
        } else {
          CollapsibleIndicatorClosedStyle
        },
      ],
      attrs=@html.Attrs::build().data_set("state", state),
      @html.nothing,
    ),
  )
}

///|
/// Render the root used by a controlled collapsible.
pub fn[C : @html.IsChildren] collapsible_root(
  open? : Bool = false,
  disabled? : Bool = false,
  id? : String,
  class? : String,
  title? : String,
  hidden? : Bool,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  let element_attrs = collapsible_state_attrs(attrs, "collapsible", open).aria_disabled(
    ui_bool(disabled),
  )
  if disabled {
    ignore(element_attrs.data_set("disabled", ""))
  }
  @html.div(
    style=ui_styles(
      [
        UiBoxSizing,
        UiFontSans,
        UiTextRendering,
        CollapsibleRootStyle,
        ui_disabled_visual_style(disabled),
      ],
      style,
    ),
    id?,
    class?,
    title?,
    hidden?,
    attrs=element_attrs,
    children,
  )
}

///|
/// Render the native button trigger for a controlled collapsible.
///
/// The native button supplies pointer, Enter, and Space activation. The
/// component appends its state request after any caller click handler stored
/// in `attrs`.
pub fn[C : @html.IsChildren] collapsible_trigger(
  open? : Bool = false,
  disabled? : Bool = false,
  controls? : String,
  show_indicator? : Bool = true,
  on_open_change? : @cmd.Emit[Bool],
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  let element_attrs = collapsible_state_attrs(
      attrs, "collapsible-trigger", open,
    )
    .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_open_change is Some(emit) {
    collapsible_bind_open_change(element_attrs, open, emit)
  }
  @html.button(
    style=ui_styles(
      [
        UiBoxSizing,
        UiFontSans,
        UiTextRendering,
        UiTransition,
        CollapsibleTriggerStyle,
        if open {
          AccordionTriggerOpenStyle
        } else {
          ""
        },
        ui_disabled_style(disabled),
      ],
      style,
    ),
    id?,
    class?,
    title?,
    type_="button",
    disabled~,
    attrs=element_attrs,
    [
      @html.span(
        style=[UiBoxSizing, CollapsibleTriggerLabelStyle],
        attrs=@html.Attrs::build().data_set("slot", "collapsible-trigger-label"),
        children,
      ),
      if show_indicator {
        collapsible_indicator(open)
      } else {
        @html.nothing
      },
    ],
  )
}

///|
/// Render the collapsible panel. Content remains mounted so both entering and
/// leaving states can animate without a component-specific script.
pub fn[C : @html.IsChildren] collapsible_content(
  open? : Bool = false,
  force_mount? : Bool = false,
  labelledby? : String,
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  body_style? : Array[String] = [],
  children : C,
) -> @html.Html {
  let element_attrs = collapsible_state_attrs(
      attrs, "collapsible-content", open,
    )
    .data_set("force-mount", ui_bool(force_mount))
    .aria_hidden(ui_bool(!open))
    .inert(!open)
  if labelledby is Some(labelledby) {
    ignore(element_attrs.aria_labelledby(labelledby))
  }
  @html.div(
    style=ui_styles(
      [
        UiBoxSizing,
        CollapsibleContentOuterStyle,
        if open {
          CollapsibleContentOpenStyle
        } else {
          CollapsibleContentClosedStyle
        },
      ],
      style,
    ),
    id?,
    class?,
    title?,
    attrs=element_attrs,
    @html.div(
      style=[UiBoxSizing, CollapsibleContentClipStyle],
      attrs=@html.Attrs::build().data_set("slot", "collapsible-content-clip"),
      @html.div(
        style=ui_styles([UiBoxSizing, CollapsibleContentStyle], body_style),
        attrs=@html.Attrs::build().data_set("slot", "collapsible-content-body"),
        children,
      ),
    ),
  )
}

///|
/// Render the current collapsible state for the public incremental component.
fn[T : @html.IsChildren, C : @html.IsChildren] render_collapsible(
  open~ : Bool,
  disabled? : Bool = false,
  show_indicator? : Bool = true,
  force_mount? : Bool = false,
  on_open_change? : @cmd.Emit[Bool],
  id? : String,
  trigger_id? : String,
  content_id? : String,
  class? : String,
  title? : String,
  hidden? : Bool,
  attrs? : @html.Attrs,
  trigger_attrs? : @html.Attrs,
  content_attrs? : @html.Attrs,
  style? : Array[String] = [],
  trigger_style? : Array[String] = [],
  content_style? : Array[String] = [],
  content_body_style? : Array[String] = [],
  trigger~ : T,
  content~ : C,
) -> @html.Html {
  let resolved_trigger_id = match trigger_id {
    Some(trigger_id) => Some(trigger_id)
    None => id.map(id => id + "-trigger")
  }
  let resolved_content_id = match content_id {
    Some(content_id) => Some(content_id)
    None => id.map(id => id + "-content")
  }
  let trigger_html = {
    let controls = resolved_content_id
    let id = resolved_trigger_id
    let attrs = trigger_attrs
    collapsible_trigger(
      open~,
      disabled~,
      controls?,
      show_indicator~,
      on_open_change?,
      id?,
      attrs?,
      style=trigger_style,
      trigger,
    )
  }
  let content_html = {
    let labelledby = resolved_trigger_id
    let id = resolved_content_id
    let attrs = content_attrs
    collapsible_content(
      open~,
      force_mount~,
      labelledby?,
      id?,
      attrs?,
      style=content_style,
      body_style=content_body_style,
      content,
    )
  }
  collapsible_root(
    open~,
    disabled~,
    id?,
    class?,
    title?,
    hidden?,
    attrs?,
    style~,
    [trigger_html, content_html],
  )
}

///|
/// Build a self-contained collapsible with component-local incremental state.
#cfg(target="js")
pub fn[T : @html.IsChildren, C : @html.IsChildren] collapsible(
  default_open? : Bool = false,
  disabled? : Bool = false,
  show_indicator? : Bool = true,
  force_mount? : Bool = false,
  id? : String,
  trigger_id? : String,
  content_id? : String,
  class? : String,
  title? : String,
  hidden? : Bool,
  attrs? : @html.Attrs,
  trigger_attrs? : @html.Attrs,
  content_attrs? : @html.Attrs,
  style? : Array[String] = [],
  trigger_style? : Array[String] = [],
  content_style? : Array[String] = [],
  content_body_style? : Array[String] = [],
  trigger~ : T,
  content~ : C,
) -> @rabbita.Val[@html.Html] {
  let (open, set_open) = @rabbita.create_variable(default_open)
  let on_open_change = set_open.map(next => _ => next)
  open.view(open => {
    render_collapsible(
      open~,
      disabled~,
      show_indicator~,
      force_mount~,
      on_open_change~,
      id?,
      trigger_id?,
      content_id?,
      class?,
      title?,
      hidden?,
      attrs?,
      trigger_attrs?,
      content_attrs?,
      style~,
      trigger_style~,
      content_style~,
      content_body_style~,
      trigger~,
      content~,
    )
  })
}

///|
/// Native/SSR fallback: render the requested initial state as a constant
/// incremental value.
#cfg(not(target="js"))
pub fn[T : @html.IsChildren, C : @html.IsChildren] collapsible(
  default_open? : Bool = false,
  disabled? : Bool = false,
  show_indicator? : Bool = true,
  force_mount? : Bool = false,
  id? : String,
  trigger_id? : String,
  content_id? : String,
  class? : String,
  title? : String,
  hidden? : Bool,
  attrs? : @html.Attrs,
  trigger_attrs? : @html.Attrs,
  content_attrs? : @html.Attrs,
  style? : Array[String] = [],
  trigger_style? : Array[String] = [],
  content_style? : Array[String] = [],
  content_body_style? : Array[String] = [],
  trigger~ : T,
  content~ : C,
) -> @rabbita.Val[@html.Html] {
  @rabbita.Val::constant(
    render_collapsible(
      open=default_open,
      disabled~,
      show_indicator~,
      force_mount~,
      id?,
      trigger_id?,
      content_id?,
      class?,
      title?,
      hidden?,
      attrs?,
      trigger_attrs?,
      content_attrs?,
      style~,
      trigger_style~,
      content_style~,
      content_body_style~,
      trigger~,
      content~,
    ),
  )
}