///|
pub fn decode_percent_path(path : String) -> String {
  let chars = string_chars(path)
  let mut out = ""
  let mut index = 0
  while index < chars.length() {
    let ch = chars[index]
    if ch == '%' && index + 2 < chars.length() {
      let hi = hex_value(chars[index + 1])
      let lo = hex_value(chars[index + 2])
      match (hi, lo) {
        (Some(a), Some(b)) => {
          out = out + [Int::unsafe_to_char(a * 16 + b)]
          index += 3
          continue
        }
        _ => ()
      }
    }
    out = out + [ch]
    index += 1
  }
  out
}

///|
pub fn encode_path_spaces(path : String) -> String {
  let mut out = ""
  for _, ch in path {
    if ch == ' ' {
      out = out + "%20"
    } else {
      out = out + [ch]
    }
  }
  out
}

///|
pub fn hex_value(ch : Char) -> Int? {
  if ch >= '0' && ch <= '9' {
    Some(ch.to_int() - '0'.to_int())
  } else if ch >= 'a' && ch <= 'f' {
    Some(ch.to_int() - 'a'.to_int() + 10)
  } else if ch >= 'A' && ch <= 'F' {
    Some(ch.to_int() - 'A'.to_int() + 10)
  } else {
    None
  }
}

///|
pub fn has_percent_escape(text : String) -> Bool {
  let chars = string_chars(text)
  for index = 0; index + 2 < chars.length(); index = index + 1 {
    if chars[index] == '%' &&
      hex_value(chars[index + 1]) is Some(_) &&
      hex_value(chars[index + 2]) is Some(_) {
      return true
    }
  }
  false
}

///|
pub fn invalid_percent_offsets(text : String) -> Array[Int] {
  let chars = string_chars(text)
  let offsets : Array[Int] = []
  let mut index = 0
  while index < chars.length() {
    if chars[index] == '%' {
      if index + 2 >= chars.length() ||
        hex_value(chars[index + 1]) is None ||
        hex_value(chars[index + 2]) is None {
        offsets.push(index)
        index += 1
      } else {
        index += 3
      }
    } else {
      index += 1
    }
  }
  offsets
}

///|
pub fn percent_report(text : String) -> String {
  let bad = invalid_percent_offsets(text)
  if bad.is_empty() {
    "ok"
  } else {
    "invalid percent escapes at " + join_ints(bad, ",")
  }
}

///|
pub fn join_ints(values : Array[Int], sep : String) -> String {
  let parts : Array[String] = []
  for value in values {
    parts.push(value.to_string())
  }
  join_with(parts, sep)
}

///|
pub fn canonical_match_path(path : String) -> String {
  clean_path(encode_path_spaces(decode_percent_path(url_path(path))))
}

///|
pub fn string_chars(text : String) -> Array[Char] {
  let chars : Array[Char] = []
  for _, ch in text {
    chars.push(ch)
  }
  chars
}