///|
/// Compound carousel state uses native swiper physics and typed MiniApp events.
struct CarouselScope {
  id : String
  current : Int
  count : Int
  circular : Bool
  orientation : Orientation
  change : @minimoon.Emit[CarouselRequest]
}

///|
priv enum CarouselRequest {
  Target(Int)
  Step(Int)
}

///|
fn carousel_dispatch(
  count : @minimoon.Val[Int],
  current : @minimoon.Val[Int],
  change : @minimoon.Emit[Int],
  circular : Bool,
) -> @minimoon.Emit[CarouselRequest] {
  let input = @minimoon.Val::map2(count, current, (count, current) => {
    (count.max(0), current)
  })
  let (_, emit) = @minimoon.create_state_with_input(
    input~,
    init=(_, _) => @minimoon.no_cmd(()),
    update=(_, input, request : CarouselRequest, _) => {
      let (count, current) = input
      let current = current.clamp(min=0, max=(count - 1).max(0))
      let next = match request {
        Target(index) => index.clamp(min=0, max=(count - 1).max(0))
        Step(delta) =>
          if count <= 1 {
            current
          } else if circular {
            (current + count + delta) % count
          } else {
            (current + delta).clamp(min=0, max=count - 1)
          }
      }
      ((), if next == current { @minimoon.none } else { change(next) })
    },
  )
  emit
}

///|
pub fn carousel_active_index(scope : CarouselScope) -> Int {
  scope.current
}

///|
pub fn carousel_slide_count(scope : CarouselScope) -> Int {
  scope.count
}

///|
pub fn carousel_orientation(scope : CarouselScope) -> Orientation {
  scope.orientation
}

///|
pub fn carousel_is_looping(scope : CarouselScope) -> Bool {
  scope.circular
}

///|
pub fn carousel_can_previous(scope : CarouselScope) -> Bool {
  scope.count > 1 && (scope.circular || scope.current > 0)
}

///|
pub fn carousel_can_next(scope : CarouselScope) -> Bool {
  scope.count > 1 && (scope.circular || scope.current + 1 < scope.count)
}

///|
pub fn carousel_scroll_to(scope : CarouselScope, index : Int) -> @minimoon.Cmd {
  (scope.change)(Target(index))
}

///|
pub fn carousel_scroll_previous(scope : CarouselScope) -> @minimoon.Cmd {
  (scope.change)(Step(-1))
}

///|
pub fn carousel_scroll_next(scope : CarouselScope) -> @minimoon.Cmd {
  (scope.change)(Step(1))
}

///|
pub fn[Input : Eq] carousel_provider_controlled_with_input(
  id~ : String,
  count~ : @minimoon.Val[Int],
  current~ : @minimoon.Val[Int],
  on_change~ : @minimoon.Emit[Int],
  input~ : @minimoon.Val[Input],
  render~ : (CarouselScope, Input) -> @minimoon.Node,
  circular? : Bool = false,
  orientation? : Orientation = Horizontal,
  aria_label? : String = "Carousel",
  class? : String = "",
  style? : String = "",
) -> @minimoon.Val[@minimoon.Node] {
  let change = carousel_dispatch(count, current, on_change, circular)
  @minimoon.Val::view3(count, current, input, (count, current, input) => {
    let count = count.max(0)
    let current = current.clamp(min=0, max=(count - 1).max(0))
    let scope = CarouselScope::{
      id,
      count,
      current,
      circular,
      orientation,
      change,
    }
    ui_container(
      "carousel",
      id~,
      class~,
      style="position:relative;min-width:0;" + style,
      semantics=@minimoon.semantics(role=@minimoon.RegionRole, label=aria_label),
      [render(scope, input)],
    )
  })
}

///|
pub fn carousel_provider_controlled(
  id~ : String,
  count~ : @minimoon.Val[Int],
  current~ : @minimoon.Val[Int],
  on_change~ : @minimoon.Emit[Int],
  render~ : (CarouselScope) -> @minimoon.Node,
  circular? : Bool = false,
  orientation? : Orientation = Horizontal,
  aria_label? : String = "Carousel",
  class? : String = "",
  style? : String = "",
) -> @minimoon.Val[@minimoon.Node] {
  carousel_provider_controlled_with_input(
    id~,
    count~,
    current~,
    on_change~,
    input=@minimoon.Val::constant(()),
    render=(scope, _) => render(scope),
    circular~,
    orientation~,
    aria_label~,
    class~,
    style~,
  )
}

///|
pub fn[Input : Eq] carousel_provider_with_input(
  id~ : String,
  count~ : @minimoon.Val[Int],
  input~ : @minimoon.Val[Input],
  render~ : (CarouselScope, Input) -> @minimoon.Node,
  default_index? : Int = 0,
  on_change? : @minimoon.Emit[Int],
  circular? : Bool = false,
  orientation? : Orientation = Horizontal,
  aria_label? : String = "Carousel",
  class? : String = "",
  style? : String = "",
) -> @minimoon.Val[@minimoon.Node] {
  let (index, emit) = @minimoon.create_state_with_input(
    input=count,
    init=(_, count) => {
      @minimoon.no_cmd(default_index.clamp(min=0, max=(count - 1).max(0)))
    },
    update=(old, count, requested : Int, _) => {
      let next = requested.clamp(min=0, max=(count - 1).max(0))
      (
        next,
        if next == old {
          @minimoon.none
        } else {
          on_change.map(emit => emit(next)).unwrap_or(@minimoon.none)
        },
      )
    },
  )
  let watch = count.switch_by(
    (count, _) => {
      let (_, _) = @minimoon.create_state_with_input(
        input=index,
        init=(_, index) => {
          (
            (),
            if index >= count.max(0) {
              emit((count - 1).max(0))
            } else {
              @minimoon.none
            },
          )
        },
        update=(_, _, _ : Unit, _) => @minimoon.no_cmd(()),
      )
      @minimoon.Val::constant(@minimoon.nothing())
    },
    by=count => count.to_string(),
  )
  let content = carousel_provider_controlled_with_input(
    id~,
    count~,
    current=index,
    on_change=emit,
    input~,
    render~,
    circular~,
    orientation~,
    aria_label~,
    class~,
    style~,
  )
  @minimoon.Val::view2(content, watch, (content, watch) => {
    @minimoon.fragment([content, watch])
  })
}

///|
pub fn carousel_provider(
  id~ : String,
  count~ : @minimoon.Val[Int],
  render~ : (CarouselScope) -> @minimoon.Node,
  default_index? : Int = 0,
  on_change? : @minimoon.Emit[Int],
  circular? : Bool = false,
  orientation? : Orientation = Horizontal,
  aria_label? : String = "Carousel",
  class? : String = "",
  style? : String = "",
) -> @minimoon.Val[@minimoon.Node] {
  carousel_provider_with_input(
    id~,
    count~,
    input=@minimoon.Val::constant(()),
    render=(scope, _) => render(scope),
    default_index~,
    on_change?,
    circular~,
    orientation~,
    aria_label~,
    class~,
    style~,
  )
}

///|
pub fn[C : @minimoon.IsChildren] carousel_content(
  scope : CarouselScope,
  children : C,
  id? : String,
  autoplay? : Bool = false,
  interval? : Int = 5000,
  height? : String = "200px",
  class? : String = "",
  style? : String = "",
) -> @minimoon.Node {
  @minimoon.swiper(
    id=id.unwrap_or(scope.id + "-swiper"),
    event_key=scope.id + "/change",
    current=scope.current,
    circular=scope.circular,
    vertical=scope.orientation == Vertical,
    autoplay=autoplay && scope.count > 1,
    interval~,
    on_change=@minimoon.Emit(index => carousel_scroll_to(scope, index)),
    class="mmui-carousel-content " + class,
    style="height:" + height + ";" + style,
    children,
  )
}

///|
pub fn[C : @minimoon.IsChildren] carousel_item(
  scope : CarouselScope,
  index~ : Int,
  children : C,
  key? : String,
  aria_label? : String,
  class? : String = "",
  style? : String = "",
) -> @minimoon.Node {
  guard index >= 0 && index < scope.count else {
    abort("carousel item index must be within slide count")
  }
  @minimoon.swiper_item(
    item_id=key.unwrap_or(index.to_string()),
    ui_container(
      "carousel-item",
      class~,
      style="height:100%;box-sizing:border-box;" + style,
      semantics=@minimoon.semantics(
        role=@minimoon.GroupRole,
        label=aria_label.unwrap_or(
          "Slide " + (index + 1).to_string() + " of " + scope.count.to_string(),
        ),
        selected=index == scope.current,
        hidden=index != scope.current,
      ),
      children.to_nodes(),
    ),
  )
}

///|
pub fn carousel_items(
  scope : CarouselScope,
  items~ : Array[Panel],
  autoplay? : Bool = false,
  interval? : Int = 5000,
  height? : String = "200px",
  class? : String = "",
  style? : String = "",
) -> @minimoon.Node {
  guard items.length() == scope.count else {
    abort("carousel items must match slide count")
  }
  let slides : Array[@minimoon.Node] = []
  for index, item in items {
    slides.push(carousel_item(scope, index~, key=item.key, item.content))
  }
  carousel_content(scope, slides, autoplay~, interval~, height~, class~, style~)
}

///|
pub fn[C : @minimoon.IsChildren] carousel_previous(
  scope : CarouselScope,
  children : C,
  disabled? : Bool = false,
  aria_label? : String = "Previous slide",
  class? : String = "",
) -> @minimoon.Node {
  let disabled = disabled || !carousel_can_previous(scope)
  @minimoon.button_children(
    disabled~,
    event_key=scope.id + "/previous",
    class="mmui-carousel-previous " + class,
    semantics=@minimoon.semantics(label=aria_label),
    on_tap=if disabled {
      @minimoon.none
    } else {
      carousel_scroll_previous(scope)
    },
    children,
  )
}

///|
pub fn[C : @minimoon.IsChildren] carousel_next(
  scope : CarouselScope,
  children : C,
  disabled? : Bool = false,
  aria_label? : String = "Next slide",
  class? : String = "",
) -> @minimoon.Node {
  let disabled = disabled || !carousel_can_next(scope)
  @minimoon.button_children(
    disabled~,
    event_key=scope.id + "/next",
    class="mmui-carousel-next " + class,
    semantics=@minimoon.semantics(label=aria_label),
    on_tap=if disabled { @minimoon.none } else { carousel_scroll_next(scope) },
    children,
  )
}