///|
/// Decoded query parameters from a request URL.
///
/// Query decoding follows `application/x-www-form-urlencoded`: `+` decodes to
/// a space, `%20` also decodes to a space, and a literal plus sign must be
/// encoded as `%2B`.
pub struct QueryParams {
  priv values : Map[String, ReadOnlyArray[String]]
  priv entries : Array[(String, String)]
}

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

///|
fn x_www_form_url_decode(input : StringView) -> String {
  let out : Array[Byte] = []
  let len = input.length()
  let mut i = 0
  while i < len {
    let ch = input.code_unit_at(i)
    if ch.to_int() == '+'.to_int() {
      out.push(' '.to_int().to_byte())
      i += 1
    } else if ch.to_int() == '%'.to_int() && i + 2 < len {
      match
        (
          hex_value(input.code_unit_at(i + 1)),
          hex_value(input.code_unit_at(i + 2)),
        ) {
        (Some(hi), Some(lo)) => {
          out.push((hi * 16 + lo).to_byte())
          i += 3
        }
        _ => {
          out.push(ch.to_byte())
          i += 1
        }
      }
    } else {
      let next = if i + 1 < len &&
        input.code_unit_at(i).is_leading_surrogate() &&
        input.code_unit_at(i + 1).is_trailing_surrogate() {
        i + 2
      } else {
        i + 1
      }
      out.push_iter(@utf8.encode(input[i:next]).iter())
      i = next
    }
  }
  @utf8.decode_lossy(Bytes::from_array(out))
}

///|
fn parse_query_pair(pair : StringView) -> (String, String) {
  match pair.split_once("=") {
    Some((name, value)) =>
      (x_www_form_url_decode(name), x_www_form_url_decode(value))
    None => (x_www_form_url_decode(pair), "")
  }
}

///|
fn parse_query_params(search : StringView?) -> QueryParams {
  let entries : Array[(String, String)] = []
  guard search is Some(search) else { return { values: {}, entries } }
  let values : Map[String, Array[String]] = Map([])
  for parsed = 0 {
    let pair_end = if search[parsed:].find("&") is Some(pair_end) {
      parsed + pair_end
    } else {
      search.length()
    }
    let pair = search[parsed:pair_end]
    if !pair.is_empty() {
      let (name, value) = parse_query_pair(pair)
      entries.push((name, value))
      match values.get(name) {
        Some(items) => items.push(value)
        None => values[name] = [value]
      }
    }

    if pair_end == search.length() {
      break
    } else {
      continue pair_end + 1
    }
  }
  { values: values.map((_k, v) => ReadOnlyArray::from_array(v)), entries }
}

///|
/// Returns the first value for a query parameter name, if present.
pub fn QueryParams::get(self : QueryParams, name : StringView) -> String? {
  match self.values.get_from_string(name) {
    Some(values) => values.get(0)
    None => None
  }
}

///|
/// Returns all values for a query parameter name in request order.
pub fn QueryParams::get_all(
  self : QueryParams,
  name : StringView,
) -> ReadOnlyArray[String] {
  match self.values.get_from_string(name) {
    Some(values) => values
    None => []
  }
}

///|
/// Returns whether a query parameter name is present.
pub fn QueryParams::has(self : QueryParams, name : StringView) -> Bool {
  self.values.get_from_string(name) is Some(_)
}

///|
/// Iterates over decoded query parameter pairs in request order.
pub fn QueryParams::iter(self : QueryParams) -> Iter[(String, String)] {
  self.entries.iter()
}