///|
priv struct InputOtpModel {
  value : String
} derive(Eq)

///|
priv enum InputOtpMsg {
  SetOtpValue(String)
}

///|
/// Opaque render scope owned by `input_otp`.
struct InputOtpScope {
  model : InputOtpModel
  max_length : Int
  invalid : Bool
  disabled : Bool
  input_id : String
  emit : @cmd.Emit[InputOtpMsg]
}

///|
fn otp_normalize(value : String, max_length : Int) -> String {
  let mut result = ""
  let mut count = 0
  for character in value {
    if count < max_length {
      result = result + "\{character}"
      count += 1
    }
  }
  result
}

///|
fn otp_chars(value : String) -> Array[Char] {
  let chars : Array[Char] = []
  for character in value {
    chars.push(character)
  }
  chars
}

///|
fn input_otp_control_attrs(
  scope : InputOtpScope,
  attrs : @html.Attrs?,
) -> @html.Attrs {
  let control_attrs = ui_attrs(attrs).data_set("slot", "input-otp-control")
  if scope.disabled {
    ignore(control_attrs.disabled(true))
  }
  control_attrs
}

///|
pub fn[C : @html.IsChildren] input_otp_group(
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
  children : C,
) -> @html.Html {
  @html.div(
    id?,
    class?,
    title?,
    attrs=ui_attrs(attrs).data_set("slot", "input-otp-group"),
    style=ui_styles(["display:flex;align-items:center"], style),
    children,
  )
}

///|
pub fn input_otp_slot(
  scope~ : InputOtpScope,
  index~ : Int,
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
) -> @html.Html {
  let chars = otp_chars(scope.model.value)
  let char_count = chars.length()
  let active_index = if char_count >= scope.max_length {
    scope.max_length - 1
  } else {
    char_count
  }
  let current = index == active_index
  let value = if index >= 0 && index < char_count {
    "\{chars[index]}"
  } else {
    ""
  }
  let slot_attrs = ui_attrs(attrs)
    .aria_hidden("true")
    .data_set("slot", "input-otp-slot")
    .data_set("index", "\{index}")
    .data_set("active", ui_bool(current))
    .data_set("current", ui_bool(current))
  if scope.invalid {
    ignore(slot_attrs.aria_invalid("true"))
  }
  @html.div(
    id?,
    class?,
    title?,
    attrs=slot_attrs,
    style=ui_styles(
      [
        UiBoxSizing,
        UiTransition,
        "position:relative;display:flex;width:2.25rem;height:2.25rem;align-items:center;justify-content:center;border-block:1px solid var(--rui-otp-border,var(--rui-input,oklch(0.922 0 0)));border-inline-end:1px solid var(--rui-otp-border,var(--rui-input,oklch(0.922 0 0)));background:var(--rui-input-background,transparent);font-size:0.875rem;line-height:1.25rem;box-shadow:var(--rui-otp-shadow,0 1px 2px rgb(0 0 0 / 0.05));outline:none",
        if scope.invalid {
          "--rui-otp-border:var(--rui-destructive-border,var(--rui-destructive,oklch(0.577 0.245 27.325)))"
        } else {
          ""
        },
      ],
      style,
    ),
    [
      @html.span(style=["display:contents"], value),
      if current && index >= char_count {
        @html.span(
          attrs=@html.Attrs::build()
            .aria_hidden("true")
            .data_set("slot", "input-otp-caret"),
          style=[
            "pointer-events:none;position:absolute;inset:0;display:flex;align-items:center;justify-content:center",
          ],
          @html.span(
            style=[
              "display:block;width:1px;height:1rem;background:var(--rui-foreground,oklch(0.145 0 0));animation:rui-caret-blink 1s ease-out infinite",
            ],
            "",
          ),
        )
      } else {
        @html.nothing
      },
    ],
  )
}

///|
pub fn input_otp_separator(
  id? : String,
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
) -> @html.Html {
  @html.div(
    id?,
    class?,
    title?,
    attrs=ui_attrs(attrs)
      .role("separator")
      .data_set("slot", "input-otp-separator"),
    style=ui_styles(
      [
        "display:flex;width:1.5rem;height:1rem;flex:none;align-items:center;justify-content:center;color:var(--rui-muted-foreground,oklch(0.556 0 0))",
      ],
      style,
    ),
    ui_minus_icon(),
  )
}

///|
fn input_otp_default_view(
  scope : InputOtpScope,
  separator_after : Int,
) -> @html.Html {
  let split = if separator_after > 0 && separator_after < scope.max_length {
    separator_after
  } else {
    -1
  }
  if split < 0 {
    let slots : Array[@html.Html] = []
    for index in 0.. @html.Html)?,
) -> @html.Html {
  let content = if render is Some(render) {
    render(scope)
  } else {
    input_otp_default_view(scope, separator_after)
  }
  @html.div(
    class?,
    title?,
    attrs=ui_attrs(attrs).data_set("slot", "input-otp"),
    style=ui_styles(
      [
        UiBoxSizing,
        UiFontSans,
        "position:relative;display:inline-flex;width:fit-content;align-items:center;gap:0.5rem",
        ui_disabled_visual_style(scope.disabled),
      ],
      style,
    ),
    [
      @html.input(
        input_type=@html.Text,
        value=scope.model.value,
        maxlength=scope.max_length,
        inputmode~,
        pattern?,
        id=scope.input_id,
        on_input=scope.emit.map(value => SetOtpValue(value)),
        attrs=input_otp_control_attrs(scope, input_attrs)
          .aria_label(aria_label)
          .aria_invalid(ui_bool(scope.invalid)),
        style=[
          "box-sizing:border-box;position:absolute;inset:0;z-index:20;width:100%;height:100%;margin:0;border:0;background:transparent;padding:0;opacity:0.001;color:transparent;caret-color:transparent;cursor:text;appearance:none;-webkit-appearance:none",
        ],
      ),
      content,
    ],
  )
}

///|
#cfg(target="js")
pub fn input_otp(
  id~ : String,
  max_length? : Int = 6,
  default_value? : String = "",
  separator_after? : Int = -1,
  disabled? : Bool = false,
  invalid? : Bool = false,
  aria_label? : String = "One-time password",
  inputmode? : String = "numeric",
  pattern? : String,
  on_value_change? : @cmd.Emit[String],
  on_complete? : @cmd.Emit[String],
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  input_attrs? : @html.Attrs,
  style? : Array[String] = [],
  render? : (InputOtpScope) -> @html.Html,
) -> @rabbita.Val[@html.Html] {
  let initial_value = otp_normalize(default_value, max_length)
  let (model, emit) = @rabbita.create_state({ value: initial_value }, update=fn(
    _,
    msg,
    model,
  ) {
    match msg {
      SetOtpValue(value) => {
        let value = otp_normalize(value, max_length)
        let commands : Array[@cmd.Cmd] = []
        if on_value_change is Some(notify) {
          commands.push(notify(value))
        }
        if value.char_length() == max_length &&
          model.value.char_length() != max_length &&
          on_complete is Some(complete) {
          commands.push(complete(value))
        }
        ({ value, }, @cmd.batch(commands))
      }
    }
  })
  model.view(model => {
    render_input_otp(
      { model, max_length, invalid, disabled, input_id: id, emit },
      separator_after,
      aria_label,
      inputmode,
      pattern,
      class,
      title,
      attrs,
      input_attrs,
      style,
      render,
    )
  })
}

///|
#cfg(not(target="js"))
pub fn input_otp(
  id~ : String,
  max_length? : Int = 6,
  default_value? : String = "",
  separator_after? : Int = -1,
  disabled? : Bool = false,
  invalid? : Bool = false,
  aria_label? : String = "One-time password",
  inputmode? : String = "numeric",
  pattern? : String,
  on_value_change? : @cmd.Emit[String],
  on_complete? : @cmd.Emit[String],
  class? : String,
  title? : String,
  attrs? : @html.Attrs,
  input_attrs? : @html.Attrs,
  style? : Array[String] = [],
  render? : (InputOtpScope) -> @html.Html,
) -> @rabbita.Val[@html.Html] {
  ignore((on_value_change, on_complete))
  let model = { value: otp_normalize(default_value, max_length) }
  let emit = @cmd.Emit(_ => @cmd.none)
  @rabbita.Val::constant(
    render_input_otp(
      { model, max_length, invalid, disabled, input_id: id, emit },
      separator_after,
      aria_label,
      inputmode,
      pattern,
      class,
      title,
      attrs,
      input_attrs,
      style,
      render,
    ),
  )
}