///|
pub fn button(
  on_tap~ : Cmd,
  event_key? : String = "",
  id? : String = "",
  class? : String = "",
  style? : String = "",
  data_section? : String = "",
  semantics? : Semantics,
  disabled? : Bool = false,
  label : String,
) -> Node {
  let binding = event_binding(
    @renderer.page_event_kind("tap"),
    _ => Ok(on_tap),
    explicit_key=event_key,
  )
  let attrs = raw_element_attrs(style, data_section, semantics?)
  raw_bool_attr(attrs, "disabled", disabled)
  wrap_node(@ui.button(id~, class~, on_click=binding.key(), attrs~, label), [
    DirectBinding(binding),
  ])
}

///|
/// Native MiniApp focus payload shared by `input` and `textarea`.
pub struct FocusDetail {
  value : String
  height : Int
}

///|
/// Native MiniApp `input` blur payload.
pub struct InputBlurDetail {
  value : String
}

///|
/// Native MiniApp `textarea` blur payload.
pub struct TextareaBlurDetail {
  value : String
  cursor : Int
}

///|
fn detail_focus(payload : Json) -> Result[FocusDetail, DecodeError] {
  match detail_string(payload) {
    Err(error) => Err(error)
    Ok(value) =>
      match event_detail(payload) {
        Ok(detail) =>
          match detail_int(detail, "height") {
            Ok(height) => Ok({ value, height, })
            Err(error) => Err(error)
          }
        Err(error) => Err(error)
      }
  }
}

///|
fn detail_input_blur(payload : Json) -> Result[InputBlurDetail, DecodeError] {
  detail_string(payload).map(value => { value, })
}

///|
fn detail_textarea_blur(
  payload : Json,
) -> Result[TextareaBlurDetail, DecodeError] {
  match detail_string(payload) {
    Err(error) => Err(error)
    Ok(value) =>
      match event_detail(payload) {
        Ok(detail) =>
          match detail_int(detail, "cursor") {
            Ok(cursor) => Ok({ value, cursor, })
            Err(error) => Err(error)
          }
        Err(error) => Err(error)
      }
  }
}

///|
pub fn input(
  value~ : String,
  on_input~ : Emit[String],
  input_type? : InputType,
  maxlength? : Int,
  password? : Bool,
  selection_start? : Int,
  selection_end? : Int,
  name? : String = "",
  event_key? : String = "",
  disabled? : Bool = false,
  focus? : Bool = false,
  on_confirm? : Emit[String],
  confirm_key? : String = "",
  on_focus? : Emit[FocusDetail],
  focus_key? : String = "",
  on_blur? : Emit[InputBlurDetail],
  blur_key? : String = "",
  id? : String = "",
  class? : String = "",
  style? : String = "",
  data_section? : String = "",
  placeholder? : String = "",
  semantics? : Semantics,
) -> Node {
  if maxlength is Some(limit) {
    guard limit >= -1 else {
      abort("input maxlength must be -1 or nonnegative")
    }
  }
  for selection in [selection_start, selection_end] {
    if selection is Some(position) {
      guard position >= -1 else {
        abort("input selection positions must be -1 or nonnegative")
      }
    }
  }
  if selection_start is Some(start) &&
    selection_end is Some(end) &&
    start >= 0 &&
    end >= 0 {
    guard start <= end else {
      abort("input selection start must not exceed its end")
    }
  }
  let binding = event_binding(
    @renderer.page_event_kind("input"),
    payload => {
      match detail_string(payload) {
        Ok(value) => Ok(on_input(value))
        Err(error) => Err(error)
      }
    },
    explicit_key=event_key,
  )
  let confirm_binding = on_confirm.map(send => {
    event_binding(
      @renderer.page_event_kind("confirm"),
      payload => detail_string(payload).map(value => send(value)),
      explicit_key=confirm_key,
    )
  })
  let focus_binding = on_focus.map(send => {
    event_binding(
      @renderer.page_event_kind("focus"),
      payload => detail_focus(payload).map(value => send(value)),
      explicit_key=focus_key,
    )
  })
  let blur_binding = on_blur.map(send => {
    event_binding(
      @renderer.page_event_kind("blur"),
      payload => detail_input_blur(payload).map(value => send(value)),
      explicit_key=blur_key,
    )
  })
  let bindings = [DirectBinding(binding)]
  for optional_binding in [confirm_binding, focus_binding, blur_binding] {
    match optional_binding {
      Some(value) => append_direct_binding(bindings, value)
      None => ()
    }
  }
  let attrs = raw_element_attrs(style, data_section, semantics?)
  attrs.push(@ui.value_attr(value))
  if input_type is Some(value) {
    raw_string_attr(attrs, "type", value.host_name())
  }
  if maxlength is Some(value) {
    raw_int_attr(attrs, "maxlength", value)
  }
  if password is Some(value) {
    raw_bool_attr(attrs, "password", value)
  }
  if selection_start is Some(value) {
    raw_int_attr(attrs, "selection-start", value)
  }
  if selection_end is Some(value) {
    raw_int_attr(attrs, "selection-end", value)
  }
  raw_string_attr(attrs, "name", name)
  raw_bool_attr(attrs, "disabled", disabled)
  raw_bool_attr(attrs, "focus", focus)
  raw_event_attr(attrs, "on_input", "input", Some(binding))
  raw_event_attr(attrs, "on_confirm", "confirm", confirm_binding)
  raw_event_attr(attrs, "on_focus", "focus", focus_binding)
  raw_event_attr(attrs, "on_blur", "blur", blur_binding)
  raw_string_attr(attrs, "placeholder", placeholder)
  wrap_node(@ui.input(id~, class~, attrs~), bindings)
}

///|
pub fn switch(
  name? : String = "",
  checked~ : Bool,
  on_change~ : Emit[Bool],
  event_key? : String = "",
  disabled? : Bool = false,
  color? : String = "",
  id? : String = "",
  class? : String = "",
  style? : String = "",
  data_section? : String = "",
  semantics? : Semantics,
) -> Node {
  let binding = event_binding(
    @renderer.page_event_kind("change"),
    payload => {
      match detail_bool(payload) {
        Ok(value) => Ok(on_change(value))
        Err(error) => Err(error)
      }
    },
    explicit_key=event_key,
  )
  let attrs = raw_element_attrs(style, data_section, semantics?)
  raw_string_attr(attrs, "name", name)
  raw_bool_attr(attrs, "checked", checked)
  raw_bool_attr(attrs, "disabled", disabled)
  raw_string_attr(attrs, "color", color)
  raw_event_attr(attrs, "on_change", "change", Some(binding))
  wrap_node(@ui.element("switch", id~, class~, attrs~), [DirectBinding(binding)])
}

///|
pub fn[C : IsChildren] checkbox_group(
  name? : String = "",
  on_change~ : Emit[Array[String]],
  event_key? : String = "",
  id? : String = "",
  class? : String = "",
  style? : String = "",
  data_section? : String = "",
  children : C,
) -> Node {
  let binding = event_binding(
    @renderer.page_event_kind("change"),
    payload => {
      match detail_strings(payload) {
        Ok(value) => Ok(on_change(value))
        Err(error) => Err(error)
      }
    },
    explicit_key=event_key,
  )
  let children = children.to_nodes()
  let bindings = child_bindings(children)
  append_direct_binding(bindings, binding)
  let attrs = raw_element_attrs(style, data_section)
  raw_string_attr(attrs, "name", name)
  raw_event_attr(attrs, "on_change", "change", Some(binding))
  wrap_node(
    @ui.element(
      "checkbox-group",
      id~,
      class~,
      attrs~,
      children=child_raw(children),
    ),
    bindings,
  )
}

///|
pub fn[C : IsChildren] checkbox(
  value~ : String,
  checked? : Bool = false,
  disabled? : Bool = false,
  color? : String = "",
  id? : String = "",
  class? : String = "",
  style? : String = "",
  children : C,
) -> Node {
  let children = children.to_nodes()
  let attrs = raw_element_attrs(style, "")
  attrs.push(@ui.value_attr(value))
  raw_bool_attr(attrs, "checked", checked)
  raw_bool_attr(attrs, "disabled", disabled)
  raw_string_attr(attrs, "color", color)
  wrap_node(
    @ui.element("checkbox", id~, class~, attrs~, children=child_raw(children)),
    child_bindings(children),
  )
}

///|
pub fn[C : IsChildren] radio_group(
  name? : String = "",
  on_change~ : Emit[String],
  event_key? : String = "",
  id? : String = "",
  class? : String = "",
  style? : String = "",
  data_section? : String = "",
  children : C,
) -> Node {
  let binding = event_binding(
    @renderer.page_event_kind("change"),
    payload => {
      match detail_string(payload) {
        Ok(value) => Ok(on_change(value))
        Err(error) => Err(error)
      }
    },
    explicit_key=event_key,
  )
  let children = children.to_nodes()
  let bindings = child_bindings(children)
  append_direct_binding(bindings, binding)
  let attrs = raw_element_attrs(style, data_section)
  raw_string_attr(attrs, "name", name)
  raw_event_attr(attrs, "on_change", "change", Some(binding))
  wrap_node(
    @ui.element(
      "radio-group",
      id~,
      class~,
      attrs~,
      children=child_raw(children),
    ),
    bindings,
  )
}

///|
pub fn[C : IsChildren] radio(
  value~ : String,
  checked? : Bool = false,
  disabled? : Bool = false,
  color? : String = "",
  id? : String = "",
  class? : String = "",
  style? : String = "",
  children : C,
) -> Node {
  let children = children.to_nodes()
  let attrs = raw_element_attrs(style, "")
  attrs.push(@ui.value_attr(value))
  raw_bool_attr(attrs, "checked", checked)
  raw_bool_attr(attrs, "disabled", disabled)
  raw_string_attr(attrs, "color", color)
  wrap_node(
    @ui.element("radio", id~, class~, attrs~, children=child_raw(children)),
    child_bindings(children),
  )
}

///|
pub struct ScrollDetail {
  scroll_left : Double
  scroll_top : Double
  scroll_height : Double
  scroll_width : Double
  delta_x : Double
  delta_y : Double
}

///|
fn detail_int(
  detail : Map[String, Json],
  name : String,
) -> Result[Int, DecodeError] {
  match detail.get(name) {
    Some(Number(value, ..)) =>
      match event_int32(value) {
        Some(integer) => Ok(integer)
        None =>
          Err(
            decode_error(
              "event detail." +
              name +
              " must be an integer from 0 to 2147483647",
            ),
          )
      }
    _ =>
      Err(
        decode_error(
          "event detail." + name + " must be an integer from 0 to 2147483647",
        ),
      )
  }
}

///|
fn detail_number(
  detail : Map[String, Json],
  name : String,
) -> Result[Double, DecodeError] {
  match detail.get(name) {
    Some(Number(value, ..)) => Ok(value)
    _ => Err(decode_error("event detail." + name + " must be a number"))
  }
}

///|
fn detail_scroll(payload : Json) -> Result[ScrollDetail, DecodeError] {
  match event_detail(payload) {
    Ok(detail) => {
      let scroll_left = match detail_number(detail, "scrollLeft") {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      let scroll_top = match detail_number(detail, "scrollTop") {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      let scroll_height = match detail_number(detail, "scrollHeight") {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      let scroll_width = match detail_number(detail, "scrollWidth") {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      let delta_x = match detail_number(detail, "deltaX") {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      let delta_y = match detail_number(detail, "deltaY") {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      Ok({
        scroll_left,
        scroll_top,
        scroll_height,
        scroll_width,
        delta_x,
        delta_y,
      })
    }
    Err(error) => Err(error)
  }
}

///|
pub fn[C : IsChildren] scroll_view(
  scroll_x? : Bool = false,
  scroll_y? : Bool = false,
  scroll_top? : Int = 0,
  scroll_left? : Int = 0,
  scroll_top_px? : Double,
  scroll_left_px? : Double,
  scroll_into_view? : String = "",
  upper_threshold? : Int = 50,
  lower_threshold? : Int = 50,
  on_scroll? : Emit[ScrollDetail],
  scroll_key? : String = "",
  on_upper? : Cmd,
  upper_key? : String = "",
  on_lower? : Cmd,
  lower_key? : String = "",
  id? : String = "",
  class? : String = "",
  style? : String = "",
  data_section? : String = "",
  children : C,
) -> Node {
  let scroll_binding = on_scroll.map(send => {
    event_binding(
      @renderer.page_event_kind("scroll"),
      payload => {
        match detail_scroll(payload) {
          Ok(value) => Ok(send(value))
          Err(error) => Err(error)
        }
      },
      explicit_key=scroll_key,
    )
  })
  let upper_binding = on_upper.map(command => {
    event_binding(
      @renderer.page_event_kind("scrolltoupper"),
      _ => Ok(command),
      explicit_key=upper_key,
    )
  })
  let lower_binding = on_lower.map(command => {
    event_binding(
      @renderer.page_event_kind("scrolltolower"),
      _ => Ok(command),
      explicit_key=lower_key,
    )
  })
  let children = children.to_nodes()
  let bindings = child_bindings(children)
  for binding in [scroll_binding, upper_binding, lower_binding] {
    match binding {
      Some(value) => append_direct_binding(bindings, value)
      None => ()
    }
  }
  let attrs = raw_element_attrs(style, data_section)
  raw_bool_attr(attrs, "scroll-x", scroll_x)
  raw_bool_attr(attrs, "scroll-y", scroll_y)
  match scroll_top_px {
    Some(value) => {
      guard finite_number(value) && value >= 0.0 else {
        abort("scroll_top_px must be finite and nonnegative")
      }
      attrs.push(@ui.attr("scroll-top-px", value.to_string()))
    }
    None => raw_int_attr(attrs, "scroll-top", scroll_top)
  }
  match scroll_left_px {
    Some(value) => {
      guard finite_number(value) && value >= 0.0 else {
        abort("scroll_left_px must be finite and nonnegative")
      }
      attrs.push(@ui.attr("scroll-left-px", value.to_string()))
    }
    None => raw_int_attr(attrs, "scroll-left", scroll_left)
  }
  raw_string_attr(attrs, "scroll-into-view", scroll_into_view)
  raw_int_attr(attrs, "upper-threshold", upper_threshold)
  raw_int_attr(attrs, "lower-threshold", lower_threshold)
  raw_event_attr(attrs, "on_scroll", "scroll", scroll_binding)
  raw_event_attr(attrs, "on_scrolltoupper", "scrolltoupper", upper_binding)
  raw_event_attr(attrs, "on_scrolltolower", "scrolltolower", lower_binding)
  wrap_node(
    @ui.element(
      "scroll-view",
      id~,
      class~,
      attrs~,
      children=child_raw(children),
    ),
    bindings,
  )
}

///|
pub fn textarea(
  name? : String = "",
  value~ : String,
  on_input~ : Emit[String],
  input_key? : String = "",
  placeholder? : String = "",
  maxlength? : Int = 140,
  disabled? : Bool = false,
  focus? : Bool = false,
  on_confirm? : Emit[String],
  confirm_key? : String = "",
  on_focus? : Emit[FocusDetail],
  focus_key? : String = "",
  on_blur? : Emit[TextareaBlurDetail],
  blur_key? : String = "",
  id? : String = "",
  class? : String = "",
  style? : String = "",
  data_section? : String = "",
  semantics? : Semantics,
) -> Node {
  let input_binding = event_binding(
    @renderer.page_event_kind("input"),
    payload => {
      match detail_string(payload) {
        Ok(value) => Ok(on_input(value))
        Err(error) => Err(error)
      }
    },
    explicit_key=input_key,
  )
  let confirm_binding = on_confirm.map(send => {
    event_binding(
      @renderer.page_event_kind("confirm"),
      payload => {
        match detail_string(payload) {
          Ok(value) => Ok(send(value))
          Err(error) => Err(error)
        }
      },
      explicit_key=confirm_key,
    )
  })
  let focus_binding = on_focus.map(send => {
    event_binding(
      @renderer.page_event_kind("focus"),
      payload => detail_focus(payload).map(value => send(value)),
      explicit_key=focus_key,
    )
  })
  let blur_binding = on_blur.map(send => {
    event_binding(
      @renderer.page_event_kind("blur"),
      payload => detail_textarea_blur(payload).map(value => send(value)),
      explicit_key=blur_key,
    )
  })
  let bindings = [DirectBinding(input_binding)]
  for optional_binding in [confirm_binding, focus_binding, blur_binding] {
    match optional_binding {
      Some(value) => append_direct_binding(bindings, value)
      None => ()
    }
  }
  let attrs = raw_element_attrs(style, data_section, semantics?)
  raw_string_attr(attrs, "name", name)
  attrs.push(@ui.value_attr(value))
  raw_string_attr(attrs, "placeholder", placeholder)
  raw_int_attr(attrs, "maxlength", maxlength)
  raw_bool_attr(attrs, "disabled", disabled)
  raw_bool_attr(attrs, "focus", focus)
  raw_event_attr(attrs, "on_input", "input", Some(input_binding))
  raw_event_attr(attrs, "on_confirm", "confirm", confirm_binding)
  raw_event_attr(attrs, "on_focus", "focus", focus_binding)
  raw_event_attr(attrs, "on_blur", "blur", blur_binding)
  wrap_node(@ui.element("textarea", id~, class~, attrs~), bindings)
}

///|
pub fn[C : IsChildren] picker_selector(
  name? : String = "",
  options~ : Array[String],
  selected~ : Int,
  on_change~ : Emit[Int],
  event_key? : String = "",
  disabled? : Bool = false,
  id? : String = "",
  class? : String = "",
  style? : String = "",
  children : C,
) -> Node {
  let binding = event_binding(
    @renderer.page_event_kind("change"),
    payload => {
      match detail_index(payload) {
        Ok(value) => Ok(on_change(value))
        Err(error) => Err(error)
      }
    },
    explicit_key=event_key,
  )
  let children = children.to_nodes()
  let bindings = child_bindings(children)
  append_direct_binding(bindings, binding)
  let attrs = raw_element_attrs(style, "")
  raw_string_attr(attrs, "name", name)
  attrs.push(@ui.attr("mode", "selector"))
  attrs.push(
    @ui.attr(
      "range-json",
      Json::array(options.map(value => Json::string(value))).stringify(),
    ),
  )
  raw_int_attr(attrs, "value-index", selected)
  raw_bool_attr(attrs, "disabled", disabled)
  raw_event_attr(attrs, "on_change", "change", Some(binding))
  wrap_node(
    @ui.element("picker", id~, class~, attrs~, children=child_raw(children)),
    bindings,
  )
}

///|
pub fn[C : IsChildren] picker_date(
  name? : String = "",
  value~ : String,
  on_change~ : Emit[String],
  disabled? : Bool = false,
  event_key? : String = "",
  id? : String = "",
  class? : String = "",
  style? : String = "",
  children : C,
) -> Node {
  let binding = event_binding(
    @renderer.page_event_kind("change"),
    payload => {
      match detail_string(payload) {
        Ok(value) => Ok(on_change(value))
        Err(error) => Err(error)
      }
    },
    explicit_key=event_key,
  )
  let children = children.to_nodes()
  let bindings = child_bindings(children)
  append_direct_binding(bindings, binding)
  let attrs = raw_element_attrs(style, "")
  raw_string_attr(attrs, "name", name)
  attrs.push(@ui.attr("mode", "date"))
  attrs.push(@ui.attr("value", value))
  raw_bool_attr(attrs, "disabled", disabled)
  raw_event_attr(attrs, "on_change", "change", Some(binding))
  wrap_node(
    @ui.element("picker", id~, class~, attrs~, children=child_raw(children)),
    bindings,
  )
}

///|
pub fn[C : IsChildren] picker_time(
  name? : String = "",
  value~ : String,
  on_change~ : Emit[String],
  disabled? : Bool = false,
  event_key? : String = "",
  id? : String = "",
  class? : String = "",
  style? : String = "",
  children : C,
) -> Node {
  let binding = event_binding(
    @renderer.page_event_kind("change"),
    payload => {
      match detail_string(payload) {
        Ok(value) => Ok(on_change(value))
        Err(error) => Err(error)
      }
    },
    explicit_key=event_key,
  )
  let children = children.to_nodes()
  let bindings = child_bindings(children)
  append_direct_binding(bindings, binding)
  let attrs = raw_element_attrs(style, "")
  raw_string_attr(attrs, "name", name)
  attrs.push(@ui.attr("mode", "time"))
  attrs.push(@ui.attr("value", value))
  raw_bool_attr(attrs, "disabled", disabled)
  raw_event_attr(attrs, "on_change", "change", Some(binding))
  wrap_node(
    @ui.element("picker", id~, class~, attrs~, children=child_raw(children)),
    bindings,
  )
}

///|
pub fn[C : IsChildren] picker_region(
  name? : String = "",
  value~ : Array[String],
  on_change~ : Emit[Array[String]],
  disabled? : Bool = false,
  event_key? : String = "",
  id? : String = "",
  class? : String = "",
  style? : String = "",
  children : C,
) -> Node {
  let binding = event_binding(
    @renderer.page_event_kind("change"),
    payload => {
      match detail_strings(payload) {
        Ok(value) => Ok(on_change(value))
        Err(error) => Err(error)
      }
    },
    explicit_key=event_key,
  )
  let children = children.to_nodes()
  let bindings = child_bindings(children)
  append_direct_binding(bindings, binding)
  let attrs = raw_element_attrs(style, "")
  raw_string_attr(attrs, "name", name)
  attrs.push(@ui.attr("mode", "region"))
  attrs.push(
    @ui.attr(
      "value",
      Json::array(value.map(item => Json::string(item))).stringify(),
    ),
  )
  raw_bool_attr(attrs, "disabled", disabled)
  raw_event_attr(attrs, "on_change", "change", Some(binding))
  wrap_node(
    @ui.element("picker", id~, class~, attrs~, children=child_raw(children)),
    bindings,
  )
}

///|
pub fn[C : IsChildren] swiper(
  current? : Int = 0,
  vertical? : Bool = false,
  autoplay? : Bool = false,
  circular? : Bool = false,
  interval? : Int = 5000,
  duration? : Int = 500,
  on_change? : Emit[Int],
  event_key? : String = "",
  id? : String = "",
  class? : String = "",
  style? : String = "",
  children : C,
) -> Node {
  let change_binding = on_change.map(send => {
    event_binding(
      @renderer.page_event_kind("change"),
      payload => {
        match detail_current(payload) {
          Ok(value) => Ok(send(value))
          Err(error) => Err(error)
        }
      },
      explicit_key=event_key,
    )
  })
  let children = children.to_nodes()
  let bindings = child_bindings(children)
  match change_binding {
    Some(binding) => append_direct_binding(bindings, binding)
    None => ()
  }
  let attrs = raw_element_attrs(style, "")
  raw_int_attr(attrs, "current", current)
  raw_bool_attr(attrs, "vertical", vertical)
  raw_bool_attr(attrs, "autoplay", autoplay)
  raw_bool_attr(attrs, "circular", circular)
  raw_int_attr(attrs, "interval", interval)
  raw_int_attr(attrs, "duration", duration)
  raw_event_attr(attrs, "on_change", "change", change_binding)
  wrap_node(
    @ui.element("swiper", id~, class~, attrs~, children=child_raw(children)),
    bindings,
  )
}

///|
pub fn[C : IsChildren] swiper_item(
  item_id? : String = "",
  id? : String = "",
  class? : String = "",
  style? : String = "",
  children : C,
) -> Node {
  let children = children.to_nodes()
  let attrs = raw_element_attrs(style, "")
  raw_string_attr(attrs, "item-id", item_id)
  wrap_node(
    @ui.element(
      "swiper-item",
      id~,
      class~,
      attrs~,
      children=child_raw(children),
    ),
    child_bindings(children),
  )
}