///|
/// Parameter serialization helpers for the SDK runtime.
///
/// Implements the V1 serialization profile:
/// - path: simple style (scalar, array)
/// - query: form style (scalar, array, explode/joined)
/// - header: simple style (scalar, array)
/// - percent encoding

///|
fn is_unreserved_byte(byte : Byte) -> Bool {
  let n = byte.to_int()
  (n >= 0x41 && n <= 0x5A) || // A-Z
  (n >= 0x61 && n <= 0x7A) || // a-z
  (n >= 0x30 && n <= 0x39) || // 0-9
  n == 0x2D || // -
  n == 0x2E || // .
  n == 0x5F || // _
  n == 0x7E // ~
}

///|
/// Percent-encode a string value according to RFC 3986.
/// Reserved characters and non-ASCII bytes are encoded as %XX.
/// Unreserved characters (A-Z, a-z, 0-9, -, ., _, ~) pass through.
pub fn percent_encode(value : String) -> String {
  let out = StringBuilder(size_hint=value.length() * 3)
  for byte in @utf8.encode(value) {
    if is_unreserved_byte(byte) {
      out.write_char(byte.to_char())
    } else {
      out.write_char('%')
      let hex = byte.to_hex().to_upper()
      if hex.length() == 1 {
        out.write_char('0')
      }
      out.write_string(hex)
    }
  }
  out.to_string()
}

///|
/// Encode a single value for path insertion (simple style).
pub fn encode_path_value(value : String) -> String {
  percent_encode(value)
}

///|
/// Encode multiple values for path insertion (simple style, array).
pub fn encode_path_array(values : Array[String]) -> String {
  let out = StringBuilder()
  let mut first = true
  for v in values {
    if first {
      first = false
    } else {
      out.write_char(',')
    }
    out.write_string(percent_encode(v))
  }
  out.to_string()
}

///|
/// Encode a query parameter key-value pair (form style).
/// Both key and value are percent-encoded.
pub fn encode_query_pair(name : String, value : String) -> (String, String) {
  (percent_encode(name), percent_encode(value))
}

///|
/// Encode a query array parameter with explode=true (form style).
pub fn encode_query_array_explode(
  name : String,
  values : Array[String],
) -> Array[(String, String)] {
  let out : Array[(String, String)] = []
  let encoded_name = percent_encode(name)
  for v in values {
    out.push((encoded_name, percent_encode(v)))
  }
  out
}

///|
/// Encode a query array parameter with explode=false (form style).
pub fn encode_query_array_joined(
  name : String,
  values : Array[String],
) -> (String, String) {
  let encoded_name = percent_encode(name)
  let encoded_values = StringBuilder()
  let mut first = true
  for v in values {
    if first {
      first = false
    } else {
      encoded_values.write_char(',')
    }
    encoded_values.write_string(percent_encode(v))
  }
  (encoded_name, encoded_values.to_string())
}

///|
/// Encode a header value (simple style).
/// Simple style passes through as-is.
pub fn encode_header_value(value : String) -> String {
  value
}

///|
/// Encode header array values (simple style).
/// Values are comma-separated.
pub fn encode_header_array(values : Array[String]) -> String {
  let out = StringBuilder()
  let mut first = true
  for v in values {
    if first {
      first = false
    } else {
      out.write_char(',')
    }
    out.write_string(v)
  }
  out.to_string()
}

///|
/// Build a query string from encoded key-value pairs.
/// Returns the query string without the leading ?.
pub fn build_query_string(params : Array[(String, String)]) -> String {
  let out = StringBuilder()
  let mut first = true
  for pair in params {
    let (key, value) = pair
    if first {
      first = false
    } else {
      out.write_char('&')
    }
    out.write_string(key)
    out.write_char('=')
    out.write_string(value)
  }
  out.to_string()
}

///|
/// Interpolate path parameters into a path template.
/// Replaces {paramName} placeholders with encoded values from the map.
pub fn interpolate_path(path : String, params : Map[String, String]) -> String {
  let out = StringBuilder()
  let mut i = 0
  let bytes = @utf8.encode(path)
  while i < bytes.length() {
    if bytes[i].to_int() == 0x7B { // '{'
      let mut end = i + 1
      while end < bytes.length() && bytes[end].to_int() != 0x7D {
        end = end + 1
      }
      if end < bytes.length() {
        let name_view = bytes[i + 1:end]
        let name = name_view.to_string()
        match params.get(name) {
          Some(value) => {
            out.write_string(value)
            i = end + 1
          }
          None => {
            out.write_char('{')
            out.write_string(name)
            out.write_char('}')
            i = end + 1
          }
        }
      } else {
        out.write_char('{')
        i = i + 1
      }
    } else {
      out.write_char(bytes[i].to_char())
      i = i + 1
    }
  }
  out.to_string()
}