///|
fn unreserved(c : Char) -> Bool {
  is_name_char(c) || c == '-' || c == '.' || c == '~'
}

///|
fn reserved(c : Char) -> Bool {
  match c {
    ':'
    | '/'
    | '?'
    | '#'
    | '['
    | ']'
    | '@'
    | '!'
    | '$'
    | '&'
    | '\''
    | '('
    | ')'
    | '*'
    | '+'
    | ','
    | ';'
    | '=' => true
    _ => false
  }
}

///|
fn hex_digit(n : Int32) -> Char {
  (if n < 10 { 48 + n } else { 55 + n }).unsafe_to_char()
}

///|
fn write_byte(out : StringBuilder, n : Int32) -> Unit {
  out.write_char('%')
  out.write_char(hex_digit((n >> 4) & 15))
  out.write_char(hex_digit(n & 15))
}

///|
fn write_encoded(out : StringBuilder, c : Char) -> Unit {
  let n = c.to_int()
  if n < 0x80 {
    write_byte(out, n)
  } else if n < 0x800 {
    write_byte(out, 0xC0 | (n >> 6))
    write_byte(out, 0x80 | (n & 63))
  } else if n < 0x10000 {
    write_byte(out, 0xE0 | (n >> 12))
    write_byte(out, 0x80 | ((n >> 6) & 63))
    write_byte(out, 0x80 | (n & 63))
  } else {
    write_byte(out, 0xF0 | (n >> 18))
    write_byte(out, 0x80 | ((n >> 12) & 63))
    write_byte(out, 0x80 | ((n >> 6) & 63))
    write_byte(out, 0x80 | (n & 63))
  }
}

///|
fn encode_chars(
  cs : Array[Char],
  start : Int32,
  end : Int32,
  allow_reserved : Bool,
) -> String {
  let out = StringBuilder()
  let mut i = start
  while i < end {
    if allow_reserved && pct_at(cs, i, end) {
      out.write_char(cs[i])
      out.write_char(cs[i + 1])
      out.write_char(cs[i + 2])
      i = i + 3
    } else {
      let c = cs[i]
      if unreserved(c) || (allow_reserved && reserved(c)) {
        out.write_char(c)
      } else {
        write_encoded(out, c)
      }
      i = i + 1
    }
  }
  out.to_string()
}

///|
/// First `length` code points of the raw value, before any encoding. This is
/// what the reference implementations do for every operator; a percent
/// triplet cut short by the limit is then encoded as a stray `%`.
fn prefix(value : String, length : Int32) -> String {
  let mut index = 0
  let mut count = 0
  while index < value.length() && count < length {
    let unit = value[index].to_int()
    if unit >= 0xD800 && unit <= 0xDBFF && index + 1 < value.length() {
      let next = value[index + 1].to_int()
      if next >= 0xDC00 && next <= 0xDFFF {
        index = index + 1
      }
    }
    index = index + 1
    count = count + 1
  }
  if index == value.length() {
    value
  } else {
    value.unsafe_substring(start=0, end=index)
  }
}