// Query parameters, and the query string they become.
//
// Both queries and procedures carry them -- a procedure's input is its body,
// but its `parameters` still go in the URL, which is a thing that surprises
// people writing an XRPC client for the first time.
//
// Arrays serialize as REPEATED KEYS (`?uris=a&uris=b`), not as a comma-joined
// value and not as `uris[]`. And arrays are homogeneous by construction here,
// because the Lexicon `params` grammar only admits an array of one primitive
// type -- so a mixed list is not something a caller should be able to build.

///|
pub(all) enum ParamValue {
  Str(String)
  Int(Int64)
  Bool(Bool)
  Strs(Array[String])
  Ints(Array[Int64])
  Bools(Array[Bool])
} derive(Eq, Debug)

///|
/// An ordered list rather than a map, because two entries may share a key --
/// that is how arrays are spelled -- and because a stable order makes a request
/// comparable in a test.
pub(all) struct Params {
  entries : Array[(String, ParamValue)]
} derive(Eq, Debug)

///|
pub fn Params::new() -> Params {
  { entries: [] }
}

///|
pub fn Params::of(entries : Array[(String, ParamValue)]) -> Params {
  { entries, }
}

///|
pub fn Params::length(self : Self) -> Int {
  self.entries.length()
}

///|
pub fn Params::is_empty(self : Self) -> Bool {
  self.entries.length() == 0
}

///|
pub fn Params::put(self : Self, key : String, value : ParamValue) -> Unit {
  self.entries.push((key, value))
}

///|
/// The whole reason this type exists rather than a `Map`: an absent optional
/// argument must vanish, not become an empty string. Every generated call is a
/// column of these.
pub fn Params::put_opt(self : Self, key : String, value : ParamValue?) -> Unit {
  if value is Some(v) {
    self.entries.push((key, v))
  }
}

///|
pub fn Params::put_string(self : Self, key : String, value : String?) -> Unit {
  self.put_opt(key, value.map(v => ParamValue::Str(v)))
}

///|
pub fn Params::put_int(self : Self, key : String, value : Int64?) -> Unit {
  self.put_opt(key, value.map(v => ParamValue::Int(v)))
}

///|
pub fn Params::put_bool(self : Self, key : String, value : Bool?) -> Unit {
  self.put_opt(key, value.map(v => ParamValue::Bool(v)))
}

///|
pub fn Params::put_strings(
  self : Self,
  key : String,
  value : Array[String]?,
) -> Unit {
  self.put_opt(key, value.map(v => ParamValue::Strs(v)))
}

///|
/// The query string, without a leading `?`, or `""` when there is nothing to
/// send. An empty array contributes nothing at all, which matches the reference
/// implementation -- `?tags=` would mean a tag that is the empty string.
pub fn Params::encode(self : Self) -> String {
  let out = StringBuilder::new()
  let mut first = true
  fn append(key : String, value : String) -> Unit {
    if !first {
      out.write_char('&')
    }
    first = false
    out.write_string(percent_encode(key))
    out.write_char('=')
    out.write_string(percent_encode(value))
  }

  for entry in self.entries {
    let (key, value) = entry
    match value {
      Str(v) => append(key, v)
      Int(v) => append(key, v.to_string())
      Bool(v) => append(key, bool_to_wire(v))
      Strs(items) =>
        for item in items {
          append(key, item)
        }
      Ints(items) =>
        for item in items {
          append(key, item.to_string())
        }
      Bools(items) =>
        for item in items {
          append(key, bool_to_wire(item))
        }
    }
  }
  out.to_string()
}

///|
/// `true` / `false`, never `1` / `0`. Slack accepts both and this protocol does
/// not, so unlike `@slack/api` there is no style to choose.
fn bool_to_wire(value : Bool) -> String {
  if value {
    "true"
  } else {
    "false"
  }
}

///|
/// RFC 3986 percent-encoding of everything outside the unreserved set.
///
/// Encoding goes through the UTF-8 bytes, not the string's UTF-16 code units.
/// Getting that wrong is invisible until someone searches for a word with an
/// accent in it, and then the query silently matches nothing.
pub fn percent_encode(text : String) -> String {
  let bytes = @utf8.encode(text)
  let out = StringBuilder::new(size_hint=bytes.length())
  for i = 0; i < bytes.length(); i = i + 1 {
    let byte = bytes[i].to_int()
    if is_unreserved(byte) {
      out.write_char(hex_char(byte))
    } else {
      out.write_char('%')
      out.write_char(hex_digit(byte >> 4))
      out.write_char(hex_digit(byte & 0x0F))
    }
  }
  out.to_string()
}

///|
/// `A-Za-z0-9-._~`, RFC 3986's unreserved set. Note `~` is in it: encoding it
/// is legal but needless, and leaving it alone matches what servers echo back.
fn is_unreserved(byte : Int) -> Bool {
  (byte >= 'a'.to_int() && byte <= 'z'.to_int()) ||
  (byte >= 'A'.to_int() && byte <= 'Z'.to_int()) ||
  (byte >= '0'.to_int() && byte <= '9'.to_int()) ||
  byte == '-'.to_int() ||
  byte == '.'.to_int() ||
  byte == '_'.to_int() ||
  byte == '~'.to_int()
}

///|
/// Safe only for the unreserved bytes, which are all ASCII.
fn hex_char(byte : Int) -> Char {
  byte.unsafe_to_char()
}

///|
fn hex_digit(value : Int) -> Char {
  if value < 10 {
    ('0'.to_int() + value).unsafe_to_char()
  } else {
    ('A'.to_int() + value - 10).unsafe_to_char()
  }
}