///|
/// One native input owns IME, paste and focus; visible slots do not invent key events.
pub struct OtpScope {
  value : String
  length : Int
  active_index : Int
  disabled : Bool
  invalid : Bool
} derive(Eq, Debug)

///|
pub fn input_otp_slot(
  value? : String = "",
  active? : Bool = false,
  id? : String = "",
) -> @minimoon.Node {
  ui_container(
    "input-otp-slot",
    [@minimoon.text(value)],
    id~,
    class=if active { "mmui-active" } else { "" },
  )
}

///|
pub fn input_otp_controlled(
  id~ : String,
  value~ : @minimoon.Val[String],
  on_change~ : @minimoon.Emit[String],
  length? : Int = 6,
  disabled? : Bool = false,
  numeric? : Bool = true,
  separator_after? : Int = -1,
  invalid? : Bool = false,
  label? : String = "Verification code",
  accept? : (Char) -> Bool,
  render? : (OtpScope) -> @minimoon.Node,
  on_complete? : @minimoon.Emit[String],
) -> @minimoon.Val[@minimoon.Node] {
  guard length > 0 && length <= 64 else {
    abort("OTP length must be between 1 and 64")
  }
  let send = form_dispatch(value, (previous, text : String) => {
    if disabled {
      return @minimoon.none
    }
    let normalized = normalize_otp(text, length, numeric, accept)
    let complete = if normalized.iter().count() == length &&
      normalize_otp(previous, length, numeric, accept).iter().count() != length {
      on_complete.map(emit => emit(normalized)).unwrap_or(@minimoon.none)
    } else {
      @minimoon.none
    }
    @minimoon.batch([on_change(normalized), complete])
  })
  value.map(previous => {
    let value = normalize_otp(previous, length, numeric, accept)
    let slots : Array[@minimoon.Node] = []
    let chars = value.iter().collect()
    for index = 0; index < length; index = index + 1 {
      if index == separator_after && index > 0 {
        slots.push(input_otp_separator())
      }
      slots.push(
        input_otp_slot(
          value=if index < chars.length() {
            chars[index].to_string()
          } else {
            ""
          },
          active=index == chars.length(),
        ),
      )
    }
    ui_container(
      "input-otp",
      [
        match render {
          Some(render) =>
            render({
              value,
              length,
              active_index: chars.length(),
              disabled,
              invalid,
            })
          None => input_otp_group(slots)
        },
        @minimoon.input(
          id=id + "-input",
          event_key=id + "/input",
          value~,
          on_input=send,
          disabled~,
          maxlength=length,
          input_type=if numeric {
            @minimoon.NumberInput
          } else {
            @minimoon.TextInput
          },
          class="mmui-input-otp-native",
          semantics=@minimoon.semantics(
            role=@minimoon.TextboxRole,
            label~,
            disabled~,
            invalid~,
          ),
        ),
      ],
      id~,
    )
  })
}

///|
pub fn input_otp(
  id~ : String,
  initial_value? : String = "",
  length? : Int = 6,
  disabled? : Bool = false,
  numeric? : Bool = true,
  separator_after? : Int = -1,
  invalid? : Bool = false,
  label? : String = "Verification code",
  accept? : (Char) -> Bool,
  render? : (OtpScope) -> @minimoon.Node,
  on_complete? : @minimoon.Emit[String],
) -> @minimoon.Val[@minimoon.Node] {
  let (value, emit) = @minimoon.create_variable(initial_value)
  input_otp_controlled(
    id~,
    value~,
    on_change=emit.map(value => _ => value),
    length~,
    disabled~,
    numeric~,
    separator_after~,
    invalid~,
    label~,
    accept?,
    render?,
    on_complete?,
  )
}

///|
fn normalize_otp(
  value : String,
  length : Int,
  numeric : Bool,
  accept : ((Char) -> Bool)?,
) -> String {
  String::from_iter(
    value
    .iter()
    .filter(character => {
      (!numeric || (character >= '0' && character <= '9')) &&
      accept.map(predicate => predicate(character)).unwrap_or(true)
    })
    .take(length),
  )
}