///|
/// Minimal URL helpers used when building request paths with query strings.
///
/// These are intentionally small and dependency-free; they cover the cases
/// the SDK needs (joining a base URL with a path, appending query parameters,
/// and percent-encoding parameter values).

///|
/// Join a base URL and a path, collapsing a duplicated `/` at the boundary and
/// inserting one if neither side provides it.
pub fn join_url(base : String, path : String) -> String {
  let base_slash = base.has_suffix("/")
  let path_slash = path.has_prefix("/")
  if base_slash && path_slash {
    base + path[1:].to_owned()
  } else if base_slash || path_slash {
    base + path
  } else {
    base + "/" + path
  }
}

///|
/// Percent-encode a string for use in a URL query value. Unreserved
/// characters (`A-Z a-z 0-9 - _ . ~`) pass through; everything else is encoded
/// as `%XX` using UTF-8 bytes.
pub fn percent_encode(s : String) -> String {
  let out = StringBuilder::new()
  for byte in @utf8.encode(s) {
    let b = byte.to_int()
    if is_unreserved(b) {
      out.write_char(Int::unsafe_to_char(b))
    } else {
      out.write_char('%')
      out.write_char(hex_digit(b / 16))
      out.write_char(hex_digit(b % 16))
    }
  }
  out.to_string()
}

///|
/// Whether a byte is an RFC 3986 unreserved character.
fn is_unreserved(b : Int) -> Bool {
  (b >= 0x41 && b <= 0x5A) || // A-Z
  (b >= 0x61 && b <= 0x7A) || // a-z
  (b >= 0x30 && b <= 0x39) || // 0-9
  b == 0x2D ||
  b == 0x5F ||
  b == 0x2E ||
  b == 0x7E // - _ . ~
}

///|
/// The uppercase hex digit for a value 0..15.
fn hex_digit(n : Int) -> Char {
  if n < 10 {
    Int::unsafe_to_char(0x30 + n)
  } else {
    Int::unsafe_to_char(0x41 + (n - 10))
  }
}

///|
/// Build a query string (without the leading `?`) from key/value pairs,
/// percent-encoding both keys and values. Returns an empty string when there
/// are no parameters.
pub fn build_query(params : Array[(String, String)]) -> String {
  if params.length() == 0 {
    return ""
  }
  let out = StringBuilder::new()
  let mut first = true
  for pair in params {
    let (k, v) = pair
    if !first {
      out.write_char('&')
    }
    first = false
    out.write_string(percent_encode(k))
    out.write_char('=')
    out.write_string(percent_encode(v))
  }
  out.to_string()
}

///|
/// Append a query string to a path, choosing `?` or `&` appropriately.
pub fn with_query(path : String, params : Array[(String, String)]) -> String {
  let query = build_query(params)
  if query == "" {
    path
  } else if path.contains("?") {
    path + "&" + query
  } else {
    path + "?" + query
  }
}