///|
fn ascii_lower(input : String) -> String {
  let out = StringBuilder()
  for i = 0; i < input.length(); i = i + 1 {
    let c = input[i].to_int()
    out.write_char(
      Int::unsafe_to_char(if c >= 65 && c <= 90 { c + 32 } else { c }),
    )
  }
  out.to_string()
}

///|
fn is_ascii_alnum(c : Int) -> Bool {
  (c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122)
}

///|
fn is_type_char(c : Int) -> Bool {
  is_ascii_alnum(c) || c == 46 || c == 43 || c == 45
}

///|
fn is_unreserved(c : Int) -> Bool {
  is_ascii_alnum(c) || c == 45 || c == 46 || c == 95 || c == 126
}

///|
fn hex_value(c : Int) -> Int {
  if c >= 48 && c <= 57 {
    c - 48
  } else if c >= 65 && c <= 70 {
    c - 55
  } else if c >= 97 && c <= 102 {
    c - 87
  } else {
    -1
  }
}

///|
fn upper_hex(value : Int) -> Char {
  "0123456789ABCDEF".to_array()[value]
}

///|
/// Decode percent triplets for ASCII bytes. Non-ASCII UTF-8 triplets are kept
/// in normalized uppercase form because this first release does not claim a
/// Unicode normalization profile.
fn decode_component(input : String, base : Int) -> String raise PurlError {
  let out = StringBuilder()
  let mut i = 0
  while i < input.length() {
    let c = input[i].to_int()
    if c != 37 {
      if c < 32 || c == 127 {
        raise Syntax("CONTROL_CHARACTER", base + i)
      }
      out.write_char(Int::unsafe_to_char(c))
      i += 1
      continue
    }
    if i + 2 >= input.length() {
      raise Syntax("BAD_PERCENT_ESCAPE", base + i)
    }
    let hi = hex_value(input[i + 1].to_int())
    let lo = hex_value(input[i + 2].to_int())
    if hi < 0 || lo < 0 {
      raise Syntax("BAD_PERCENT_ESCAPE", base + i)
    }
    let value = hi * 16 + lo
    if value < 128 {
      out.write_char(Int::unsafe_to_char(value))
    } else {
      out.write_char('%')
      out.write_char(upper_hex(hi))
      out.write_char(upper_hex(lo))
    }
    i += 3
  }
  out.to_string()
}

///|
fn encode_component(input : String, allow_colon? : Bool = false) -> String {
  let out = StringBuilder()
  for i = 0; i < input.length(); i = i + 1 {
    let c = input[i].to_int()
    if is_unreserved(c) || (allow_colon && c == 58) {
      out.write_char(Int::unsafe_to_char(c))
    } else if c < 128 {
      out.write_char('%')
      out.write_char(upper_hex((c >> 4) & 15))
      out.write_char(upper_hex(c & 15))
    } else {
      // Preserve already-normalized non-ASCII percent sequences or literal
      // Unicode. Full UTF-8 normalization is an explicit non-goal of 0.1.0.
      out.write_char(Int::unsafe_to_char(c))
    }
  }
  out.to_string()
}

///|
fn split_nonempty(
  input : String,
  separator : Int,
  base : Int,
  empty_code : String,
) -> Array[String] raise PurlError {
  let parts : Array[String] = []
  let mut start = 0
  for i = 0; i <= input.length(); i = i + 1 {
    if i == input.length() || input[i].to_int() == separator {
      if i == start {
        raise Syntax(empty_code, base + i)
      }
      parts.push(input[start:i].to_owned())
      start = i + 1
    }
  }
  parts
}

///|
fn normalize_pypi_name(name : String) -> String {
  let lower = ascii_lower(name)
  let out = StringBuilder()
  let mut separator = false
  for i = 0; i < lower.length(); i = i + 1 {
    let c = lower[i].to_int()
    if c == 45 || c == 46 || c == 95 {
      if !separator {
        out.write_char('-')
        separator = true
      }
    } else {
      out.write_char(Int::unsafe_to_char(c))
      separator = false
    }
  }
  out.to_string()
}