///|
/// Serializes name/value pairs, in order, as an
/// `application/x-www-form-urlencoded` body with the WHATWG URL Standard's
/// urlencoded serializer, as browsers and `URLSearchParams` do: a space
/// becomes `+`, and every UTF-8 byte other than an ASCII alphanumeric or one
/// of `*-._` is percent-encoded in uppercase hex. This differs from
/// `percent_encode`, which keeps `~`, encodes `*` and writes a space as
/// `%20`. A lone surrogate is encoded as U+FFFD.
///
/// ```mbt check
/// test {
///   assert_eq(
///     @http.form_urlencode([("q", "a b&c"), ("lang", "日本")]),
///     "q=a+b%26c&lang=%E6%97%A5%E6%9C%AC",
///   )
/// }
/// ```
pub fn form_urlencode(pairs : ArrayView[(String, String)]) -> String {
  let builder = StringBuilder()
  for i, pair in pairs {
    if i > 0 {
      builder.write_char('&')
    }
    let (name, value) = pair
    form_write(builder, name)
    builder.write_char('=')
    form_write(builder, value)
  }
  builder.to_string()
}

///|
/// Encodes `pairs` with `form_urlencode` as the body and supplies
/// `application/x-www-form-urlencoded` only if content-type is absent.
pub fn Request::form_body(
  self : Request,
  pairs : ArrayView[(String, String)],
) -> Request {
  let request = self.body_text(form_urlencode(pairs))
  if !request.headers.contains("content-type") {
    request.headers.set("content-type", "application/x-www-form-urlencoded")
  }
  request
}

///|
/// Appends one name or value under the urlencoded serializer's rules.
fn form_write(builder : StringBuilder, text : String) -> Unit {
  for byte in encode_utf8(text) {
    form_byte(builder, byte.to_int())
  }
}

///|
/// Appends one UTF-8 byte: kept, `+` for a space, or percent-encoded.
fn form_byte(builder : StringBuilder, byte : Int32) -> Unit {
  match byte {
    0x20 => builder.write_char('+')
    '0'..<='9' | 'A'..<='Z' | 'a'..<='z' | '*' | '-' | '.' | '_' =>
      builder.write_char(byte.unsafe_to_char())
    _ => {
      builder.write_char('%')
      builder.write_char(form_hex(byte >> 4))
      builder.write_char(form_hex(byte & 15))
    }
  }
}

///|
/// The uppercase hex digit for a nibble.
fn form_hex(nibble : Int32) -> Char {
  if nibble < 10 {
    ('0'.to_int() + nibble).unsafe_to_char()
  } else {
    ('A'.to_int() + nibble - 10).unsafe_to_char()
  }
}