/// Percent-encoding and decoding utilities based on RFC 3986

///|
/// Check if a string contains valid percent-encoding
/// pct-encoded = "%" HEXDIG HEXDIG
pub fn is_valid_percent_encoding(s : String) -> Bool {
  let chars = s.to_array()
  let len = chars.length()
  fn check_from(i : Int) -> Bool {
    if i >= len {
      true
    } else if chars[i] == '%' {
      if i + 2 < len && is_hexdig(chars[i + 1]) && is_hexdig(chars[i + 2]) {
        check_from(i + 3)
      } else {
        false
      }
    } else {
      check_from(i + 1)
    }
  }

  check_from(0)
}

///|
/// Convert a single byte to its uppercase percent-encoded form (e.g. 0xA9 -> "%A9")
fn byte_to_percent(b : Byte) -> String {
  fn hex_digit(n : Int) -> String {
    (if n < 10 {
      (Char::to_int('0') + n).unsafe_to_char()
    } else {
      (Char::to_int('A') + n - 10).unsafe_to_char()
    }).to_string()
  }

  let v = Byte::to_int(b)
  "%" + hex_digit(v / 16) + hex_digit(v % 16)
}

///|
/// Encode a string with percent-encoding for characters that need it
/// The predicate function determines which characters should be encoded
/// Characters are encoded as their UTF-8 byte sequences, per RFC 3986
/// A '%' that begins a valid pct-encoded triplet ("%HH") is preserved
/// as-is so already-encoded strings pass through unchanged; any other
/// '%' is encoded as "%25" (RFC 3986 section 2.4)
pub fn percent_encode(s : String, should_encode : (Char) -> Bool) -> String {
  let chars = s.to_array()
  let len = chars.length()
  let buf = StringBuilder()
  for i = 0; i < len; i = i + 1 {
    let c = chars[i]
    if c == '%' &&
      i + 2 < len &&
      is_hexdig(chars[i + 1]) &&
      is_hexdig(chars[i + 2]) {
      buf.write_char(c) // already-encoded triplet: do not double-encode
    } else if should_encode(c) {
      @encoding.to_utf8_bytes(c)
      .iter()
      .each(fn(b) { buf.write_string(byte_to_percent(b)) })
    } else {
      buf.write_char(c)
    }
  }
  buf.to_string()
}

///|
/// Decode a percent-encoded string
/// Percent-escape sequences are collected as raw bytes; literal characters
/// contribute their UTF-8 bytes. The byte sequence is then decoded as UTF-8,
/// so multi-byte sequences produced by `percent_encode` round-trip correctly.
pub fn percent_decode(s : String) -> URIResult[String] {
  let chars = s.to_array()
  let len = chars.length()
  let bytes : Array[Byte] = []
  fn collect_from(i : Int) -> URIResult[Unit] {
    if i >= len {
      Ok(())
    } else if chars[i] == '%' {
      if i + 2 < len {
        match (hex_to_int(chars[i + 1]), hex_to_int(chars[i + 2])) {
          (Some(h1), Some(h2)) => {
            bytes.push((h1 * 16 + h2).to_byte())
            collect_from(i + 3)
          }
          _ =>
            Err(InvalidPercentEncoding("Invalid hex digits at position \{i}"))
        }
      } else {
        Err(InvalidPercentEncoding("Incomplete percent encoding at end"))
      }
    } else {
      @encoding.to_utf8_bytes(chars[i]).iter().each(fn(b) { bytes.push(b) })
      collect_from(i + 1)
    }
  }

  match collect_from(0) {
    Ok(_) =>
      Ok(decode_utf8(Bytes::from_array(bytes.view()))) catch {
        _ => Err(InvalidPercentEncoding("Invalid UTF-8 byte sequence"))
      }
    Err(e) => Err(e)
  }
}

///|
/// Decode a UTF-8 byte sequence, raising on malformed input
fn decode_utf8(bytes : Bytes) -> String raise {
  @encoding.decoder(@encoding.Encoding::UTF8).decode(bytes)
}

///|
/// Encode a string for use in URI path segments
pub fn encode_path_segment(s : String) -> String {
  percent_encode(s, fn(c) { !is_pchar(c) })
}

///|
/// Encode a string for use in URI query
pub fn encode_query(s : String) -> String {
  percent_encode(s, fn(c) { !is_query_char(c) })
}

///|
/// Encode a string for use in URI fragment
pub fn encode_fragment(s : String) -> String {
  percent_encode(s, fn(c) { !is_fragment_char(c) })
}

///|
/// Encode a string for use in URI userinfo
pub fn encode_userinfo(s : String) -> String {
  percent_encode(s, fn(c) { !is_userinfo_char(c) })
}

///|
/// Encode a string for use in URI reg-name (domain names)
pub fn encode_reg_name(s : String) -> String {
  percent_encode(s, fn(c) { !is_reg_name_char(c) })
}

///|
/// Check if a string is already properly percent-encoded and contains only valid characters
pub fn is_valid_encoded_string(
  s : String,
  char_validator : (Char) -> Bool,
) -> Bool {
  let chars = s.to_array()
  let len = chars.length()
  fn check_from(i : Int) -> Bool {
    if i >= len {
      true
    } else if chars[i] == '%' {
      if i + 2 < len && is_hexdig(chars[i + 1]) && is_hexdig(chars[i + 2]) {
        check_from(i + 3)
      } else {
        false
      }
    } else if char_validator(chars[i]) {
      check_from(i + 1)
    } else {
      false
    }
  }

  check_from(0)
}