///|
priv struct CalendarRangeModel {
  visible : CalendarDate
  selected : DateRange?
  focused : CalendarDate
} derive(Eq)

///|
priv struct CalendarMultipleModel {
  visible : CalendarDate
  selected : Array[CalendarDate]
  focused : CalendarDate
} derive(Eq)

///|
#cfg(target="js")
priv enum CalendarRangeMsg {
  CalendarRangeNavigate(Int)
  CalendarRangeChoose(CalendarDate)
  CalendarRangeSetFocused(CalendarDate)
  CalendarRangeMoveFocus(CalendarDate)
}

///|
#cfg(target="js")
priv enum CalendarMultipleMsg {
  CalendarMultipleNavigate(Int)
  CalendarMultipleChoose(CalendarDate)
  CalendarMultipleSetFocused(CalendarDate)
  CalendarMultipleMoveFocus(CalendarDate)
}

///|
fn calendar_selection_mode_name(mode : CalendarSelectionMode) -> String {
  match mode {
    CalendarSingleSelection => "single"
    CalendarRangeSelection => "range"
    CalendarMultipleSelection => "multiple"
  }
}

///|
fn calendar_contains_date(
  dates : Array[CalendarDate],
  target : CalendarDate,
) -> Bool {
  for date in dates {
    if date == target {
      return true
    }
  }
  false
}

///|
fn calendar_normalize_dates(dates : Array[CalendarDate]) -> Array[CalendarDate] {
  let normalized : Array[CalendarDate] = []
  for date in dates {
    if !calendar_contains_date(normalized, date) {
      normalized.push(date)
    }
  }
  normalized
}

///|
fn calendar_multiple_next_value(
  current : Array[CalendarDate],
  chosen : CalendarDate,
) -> Array[CalendarDate] {
  let next : Array[CalendarDate] = []
  let mut removed = false
  for date in current {
    if date == chosen {
      removed = true
    } else {
      next.push(date)
    }
  }
  if !removed {
    next.push(chosen)
  }
  next
}

///|
fn calendar_range_next_value(
  current : DateRange?,
  chosen : CalendarDate,
) -> DateRange {
  if current is Some(current) && current.end is None {
    date_range(start=current.start, end=chosen)
  } else {
    date_range(start=chosen)
  }
}

///|
fn calendar_range_initial_model(
  default_month : CalendarDate?,
  default_selected : DateRange?,
  today : CalendarDate?,
) -> CalendarRangeModel {
  let selected = default_selected.map(range => range.start)
  let calendar = calendar_initial_model(default_month, selected, today)
  {
    visible: calendar.visible,
    selected: default_selected,
    focused: calendar.focused,
  }
}

///|
fn calendar_multiple_initial_model(
  default_month : CalendarDate?,
  default_selected : Array[CalendarDate],
  today : CalendarDate?,
) -> CalendarMultipleModel {
  let selected = calendar_normalize_dates(default_selected)
  let calendar = calendar_initial_model(default_month, selected.get(0), today)
  { visible: calendar.visible, selected, focused: calendar.focused }
}

///|
fn render_calendar_range_model(
  model : CalendarRangeModel,
  today : CalendarDate?,
  show_outside_days : Bool,
  disabled : Bool,
  aria_label : String,
  on_previous : @cmd.Cmd?,
  on_next : @cmd.Cmd?,
  on_select : @cmd.Emit[CalendarDate]?,
  on_focus_change : @cmd.Emit[CalendarDate]?,
  on_focus_move : @cmd.Emit[CalendarDate]?,
  id : String?,
  class : String?,
  title : String?,
  hidden : Bool?,
  attrs : @html.Attrs?,
  previous_attrs : @html.Attrs?,
  next_attrs : @html.Attrs?,
  grid_attrs : @html.Attrs?,
  day_attrs : @html.Attrs?,
  style : Array[String],
  header_style : Array[String],
  caption_style : Array[String],
  nav_style : Array[String],
  previous_style : Array[String],
  next_style : Array[String],
  grid_style : Array[String],
  weekday_style : Array[String],
  cell_style : Array[String],
  day_style : Array[String],
) -> @html.Html {
  let range_start = model.selected.map(range => range.start)
  let range_end = if model.selected is Some(range) { range.end } else { None }
  render_calendar(
    month=model.visible,
    selection_mode=CalendarRangeSelection,
    range_start?,
    range_end?,
    today?,
    focused=model.focused,
    show_outside_days~,
    disabled~,
    aria_label~,
    on_previous?,
    on_next?,
    on_select?,
    on_focus_change?,
    on_focus_move?,
    id?,
    class?,
    title?,
    hidden?,
    attrs?,
    previous_attrs?,
    next_attrs?,
    grid_attrs?,
    day_attrs?,
    style~,
    header_style~,
    caption_style~,
    nav_style~,
    previous_style~,
    next_style~,
    grid_style~,
    weekday_style~,
    cell_style~,
    day_style~,
  )
}

///|
fn render_calendar_multiple_model(
  model : CalendarMultipleModel,
  today : CalendarDate?,
  show_outside_days : Bool,
  disabled : Bool,
  aria_label : String,
  on_previous : @cmd.Cmd?,
  on_next : @cmd.Cmd?,
  on_select : @cmd.Emit[CalendarDate]?,
  on_focus_change : @cmd.Emit[CalendarDate]?,
  on_focus_move : @cmd.Emit[CalendarDate]?,
  id : String?,
  class : String?,
  title : String?,
  hidden : Bool?,
  attrs : @html.Attrs?,
  previous_attrs : @html.Attrs?,
  next_attrs : @html.Attrs?,
  grid_attrs : @html.Attrs?,
  day_attrs : @html.Attrs?,
  style : Array[String],
  header_style : Array[String],
  caption_style : Array[String],
  nav_style : Array[String],
  previous_style : Array[String],
  next_style : Array[String],
  grid_style : Array[String],
  weekday_style : Array[String],
  cell_style : Array[String],
  day_style : Array[String],
) -> @html.Html {
  render_calendar(
    month=model.visible,
    selection_mode=CalendarMultipleSelection,
    selected_dates=model.selected,
    today?,
    focused=model.focused,
    show_outside_days~,
    disabled~,
    aria_label~,
    on_previous?,
    on_next?,
    on_select?,
    on_focus_change?,
    on_focus_move?,
    id?,
    class?,
    title?,
    hidden?,
    attrs?,
    previous_attrs?,
    next_attrs?,
    grid_attrs?,
    day_attrs?,
    style~,
    header_style~,
    caption_style~,
    nav_style~,
    previous_style~,
    next_style~,
    grid_style~,
    weekday_style~,
    cell_style~,
    day_style~,
  )
}

///|
#cfg(target="js")
fn calendar_range_update(
  root_id : String?,
  on_select : @cmd.Emit[DateRange]?,
  on_month_change : @cmd.Emit[CalendarDate]?,
  today : CalendarDate?,
  msg : CalendarRangeMsg,
  model : CalendarRangeModel,
) -> (CalendarRangeModel, @cmd.Cmd) {
  match msg {
    CalendarRangeNavigate(delta) => {
      let visible = calendar_shift_month(model.visible, delta)
      let selected = model.selected.map(range => range.start)
      let focused = calendar_focus_target(visible, selected, today)
      (
        { ..model, visible, focused },
        calendar_emit_command(on_month_change, visible),
      )
    }
    CalendarRangeChoose(date) => {
      let selected = calendar_range_next_value(model.selected, date)
      let visible = calendar_first_of_month(date)
      let commands : Array[@cmd.Cmd] = [
        calendar_emit_command(on_select, selected),
      ]
      if visible != model.visible {
        commands.push(calendar_emit_command(on_month_change, visible))
      }
      (
        { visible, selected: Some(selected), focused: date },
        @cmd.batch(commands),
      )
    }
    CalendarRangeSetFocused(date) => ({ ..model, focused: date }, @cmd.none)
    CalendarRangeMoveFocus(date) => {
      let visible = calendar_first_of_month(date)
      let commands : Array[@cmd.Cmd] = [calendar_focus_day_cmd(root_id, date)]
      if visible != model.visible {
        commands.push(calendar_emit_command(on_month_change, visible))
      }
      ({ ..model, visible, focused: date }, @cmd.batch(commands))
    }
  }
}

///|
#cfg(target="js")
fn calendar_multiple_update(
  root_id : String?,
  on_select : @cmd.Emit[Array[CalendarDate]]?,
  on_month_change : @cmd.Emit[CalendarDate]?,
  today : CalendarDate?,
  msg : CalendarMultipleMsg,
  model : CalendarMultipleModel,
) -> (CalendarMultipleModel, @cmd.Cmd) {
  match msg {
    CalendarMultipleNavigate(delta) => {
      let visible = calendar_shift_month(model.visible, delta)
      let focused = calendar_focus_target(visible, model.selected.get(0), today)
      (
        { ..model, visible, focused },
        calendar_emit_command(on_month_change, visible),
      )
    }
    CalendarMultipleChoose(date) => {
      let selected = calendar_multiple_next_value(model.selected, date)
      let visible = calendar_first_of_month(date)
      let commands : Array[@cmd.Cmd] = [
        calendar_emit_command(on_select, selected.copy()),
      ]
      if visible != model.visible {
        commands.push(calendar_emit_command(on_month_change, visible))
      }
      ({ visible, selected, focused: date }, @cmd.batch(commands))
    }
    CalendarMultipleSetFocused(date) => ({ ..model, focused: date }, @cmd.none)
    CalendarMultipleMoveFocus(date) => {
      let visible = calendar_first_of_month(date)
      let commands : Array[@cmd.Cmd] = [calendar_focus_day_cmd(root_id, date)]
      if visible != model.visible {
        commands.push(calendar_emit_command(on_month_change, visible))
      }
      ({ ..model, visible, focused: date }, @cmd.batch(commands))
    }
  }
}

///|
/// Stateful range-selection Calendar. The first activation starts a range;
/// the second normalizes and completes it. Activating again starts a new
/// range. Every change is reported through `on_select`.
#cfg(target="js")
pub fn calendar_range(
  default_month? : CalendarDate,
  default_selected? : DateRange,
  today? : CalendarDate,
  show_outside_days? : Bool = true,
  disabled? : Bool = false,
  aria_label? : String = "Calendar",
  on_select? : @cmd.Emit[DateRange],
  on_month_change? : @cmd.Emit[CalendarDate],
  id? : String,
  class? : String,
  title? : String,
  hidden? : Bool,
  attrs? : @html.Attrs,
  previous_attrs? : @html.Attrs,
  next_attrs? : @html.Attrs,
  grid_attrs? : @html.Attrs,
  day_attrs? : @html.Attrs,
  style? : Array[String] = [],
  header_style? : Array[String] = [],
  caption_style? : Array[String] = [],
  nav_style? : Array[String] = [],
  previous_style? : Array[String] = [],
  next_style? : Array[String] = [],
  grid_style? : Array[String] = [],
  weekday_style? : Array[String] = [],
  cell_style? : Array[String] = [],
  day_style? : Array[String] = [],
) -> @rabbita.Val[@html.Html] {
  let initial = calendar_range_initial_model(
    default_month, default_selected, today,
  )
  let (model, emit) = @rabbita.create_state(initial, update=fn(_, msg, model) {
    calendar_range_update(id, on_select, on_month_change, today, msg, model)
  })
  let select = emit.map(date => CalendarRangeChoose(date))
  let set_focused = emit.map(date => CalendarRangeSetFocused(date))
  let move_focus = emit.map(date => CalendarRangeMoveFocus(date))
  model.view(model => {
    render_calendar_range_model(
      model,
      today,
      show_outside_days,
      disabled,
      aria_label,
      Some(emit(CalendarRangeNavigate(-1))),
      Some(emit(CalendarRangeNavigate(1))),
      Some(select),
      Some(set_focused),
      Some(move_focus),
      id,
      class,
      title,
      hidden,
      attrs,
      previous_attrs,
      next_attrs,
      grid_attrs,
      day_attrs,
      style,
      header_style,
      caption_style,
      nav_style,
      previous_style,
      next_style,
      grid_style,
      weekday_style,
      cell_style,
      day_style,
    )
  })
}

///|
/// Native/SSR fallback for [`calendar_range`].
#cfg(not(target="js"))
pub fn calendar_range(
  default_month? : CalendarDate,
  default_selected? : DateRange,
  today? : CalendarDate,
  show_outside_days? : Bool = true,
  disabled? : Bool = false,
  aria_label? : String = "Calendar",
  on_select? : @cmd.Emit[DateRange],
  on_month_change? : @cmd.Emit[CalendarDate],
  id? : String,
  class? : String,
  title? : String,
  hidden? : Bool,
  attrs? : @html.Attrs,
  previous_attrs? : @html.Attrs,
  next_attrs? : @html.Attrs,
  grid_attrs? : @html.Attrs,
  day_attrs? : @html.Attrs,
  style? : Array[String] = [],
  header_style? : Array[String] = [],
  caption_style? : Array[String] = [],
  nav_style? : Array[String] = [],
  previous_style? : Array[String] = [],
  next_style? : Array[String] = [],
  grid_style? : Array[String] = [],
  weekday_style? : Array[String] = [],
  cell_style? : Array[String] = [],
  day_style? : Array[String] = [],
) -> @rabbita.Val[@html.Html] {
  ignore((on_select, on_month_change))
  let model = calendar_range_initial_model(
    default_month, default_selected, today,
  )
  let select : @cmd.Emit[CalendarDate] = Emit(_ => @cmd.none)
  @rabbita.Val::constant(
    render_calendar_range_model(
      model,
      today,
      show_outside_days,
      disabled,
      aria_label,
      Some(@cmd.none),
      Some(@cmd.none),
      Some(select),
      None,
      None,
      id,
      class,
      title,
      hidden,
      attrs,
      previous_attrs,
      next_attrs,
      grid_attrs,
      day_attrs,
      style,
      header_style,
      caption_style,
      nav_style,
      previous_style,
      next_style,
      grid_style,
      weekday_style,
      cell_style,
      day_style,
    ),
  )
}

///|
/// Stateful multiple-selection Calendar. Activating a date toggles it while
/// preserving all other selected dates. `on_select` receives a fresh array
/// after every toggle, including the empty selection.
#cfg(target="js")
pub fn calendar_multiple(
  default_month? : CalendarDate,
  default_selected? : Array[CalendarDate] = [],
  today? : CalendarDate,
  show_outside_days? : Bool = true,
  disabled? : Bool = false,
  aria_label? : String = "Calendar",
  on_select? : @cmd.Emit[Array[CalendarDate]],
  on_month_change? : @cmd.Emit[CalendarDate],
  id? : String,
  class? : String,
  title? : String,
  hidden? : Bool,
  attrs? : @html.Attrs,
  previous_attrs? : @html.Attrs,
  next_attrs? : @html.Attrs,
  grid_attrs? : @html.Attrs,
  day_attrs? : @html.Attrs,
  style? : Array[String] = [],
  header_style? : Array[String] = [],
  caption_style? : Array[String] = [],
  nav_style? : Array[String] = [],
  previous_style? : Array[String] = [],
  next_style? : Array[String] = [],
  grid_style? : Array[String] = [],
  weekday_style? : Array[String] = [],
  cell_style? : Array[String] = [],
  day_style? : Array[String] = [],
) -> @rabbita.Val[@html.Html] {
  let initial = calendar_multiple_initial_model(
    default_month, default_selected, today,
  )
  let (model, emit) = @rabbita.create_state(initial, update=fn(_, msg, model) {
    calendar_multiple_update(id, on_select, on_month_change, today, msg, model)
  })
  let select = emit.map(date => CalendarMultipleChoose(date))
  let set_focused = emit.map(date => CalendarMultipleSetFocused(date))
  let move_focus = emit.map(date => CalendarMultipleMoveFocus(date))
  model.view(model => {
    render_calendar_multiple_model(
      model,
      today,
      show_outside_days,
      disabled,
      aria_label,
      Some(emit(CalendarMultipleNavigate(-1))),
      Some(emit(CalendarMultipleNavigate(1))),
      Some(select),
      Some(set_focused),
      Some(move_focus),
      id,
      class,
      title,
      hidden,
      attrs,
      previous_attrs,
      next_attrs,
      grid_attrs,
      day_attrs,
      style,
      header_style,
      caption_style,
      nav_style,
      previous_style,
      next_style,
      grid_style,
      weekday_style,
      cell_style,
      day_style,
    )
  })
}

///|
/// Native/SSR fallback for [`calendar_multiple`].
#cfg(not(target="js"))
pub fn calendar_multiple(
  default_month? : CalendarDate,
  default_selected? : Array[CalendarDate] = [],
  today? : CalendarDate,
  show_outside_days? : Bool = true,
  disabled? : Bool = false,
  aria_label? : String = "Calendar",
  on_select? : @cmd.Emit[Array[CalendarDate]],
  on_month_change? : @cmd.Emit[CalendarDate],
  id? : String,
  class? : String,
  title? : String,
  hidden? : Bool,
  attrs? : @html.Attrs,
  previous_attrs? : @html.Attrs,
  next_attrs? : @html.Attrs,
  grid_attrs? : @html.Attrs,
  day_attrs? : @html.Attrs,
  style? : Array[String] = [],
  header_style? : Array[String] = [],
  caption_style? : Array[String] = [],
  nav_style? : Array[String] = [],
  previous_style? : Array[String] = [],
  next_style? : Array[String] = [],
  grid_style? : Array[String] = [],
  weekday_style? : Array[String] = [],
  cell_style? : Array[String] = [],
  day_style? : Array[String] = [],
) -> @rabbita.Val[@html.Html] {
  ignore((on_select, on_month_change))
  let model = calendar_multiple_initial_model(
    default_month, default_selected, today,
  )
  let select : @cmd.Emit[CalendarDate] = Emit(_ => @cmd.none)
  @rabbita.Val::constant(
    render_calendar_multiple_model(
      model,
      today,
      show_outside_days,
      disabled,
      aria_label,
      Some(@cmd.none),
      Some(@cmd.none),
      Some(select),
      None,
      None,
      id,
      class,
      title,
      hidden,
      attrs,
      previous_attrs,
      next_attrs,
      grid_attrs,
      day_attrs,
      style,
      header_style,
      caption_style,
      nav_style,
      previous_style,
      next_style,
      grid_style,
      weekday_style,
      cell_style,
      day_style,
    ),
  )
}