// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// The name–value pairs of an `application/x-www-form-urlencoded` string: a URL's
/// query, or a form body (WHATWG URL Standard §5). Order and repeated names are
/// kept, as `URLSearchParams` and Go's `url.Values` keep them.
pub struct Query {
  priv pairs : Array[(String, String)]
} derive(Eq, Debug)

///|
pub extend Query with Eq::{equal, not_equal}

///|
pub extend Query with Debug::{to_repr}

///|
/// What a decode accepts. Past either bound the whole input is refused rather
/// than cut short, so a handler never acts on part of a form.
pub(all) struct Limits {
  /// Most pairs.
  pairs : Int32
  /// Most bytes in one pair as it arrives, still encoded.
  size : Int32
} derive(Eq, Debug)

///|
pub extend Limits with Eq::{equal, not_equal}

///|
pub extend Limits with Debug::{to_repr}

///|
/// A thousand pairs of up to 1 MiB each: Starlette's `max_fields` and
/// `max_part_size`, and the `parameterLimit` of Node's `qs`.
pub let limits : Limits = { pairs: 1000, size: 1 << 20, }

///|
/// Limits by name, each defaulting to the preset's.
pub fn Limits::new(
  pairs~ : Int32 = limits.pairs,
  size~ : Int32 = limits.size,
) -> Limits {
  { pairs, size, }
}

///|
/// A query holding `pairs`, in order.
pub fn Query::new(pairs? : Array[(String, String)]) -> Query {
  { pairs: pairs.map(p => p.copy()).unwrap_or([]), }
}

///|
/// Read an `application/x-www-form-urlencoded` string (WHATWG URL Standard §5.1).
///
/// `&` separates pairs and the first `=` separates a name from its value; `+` is
/// a space here and nowhere else in a URL. Escapes are undone to octets and the
/// octets read as UTF-8, a broken sequence becoming U+FFFD. An empty piece
/// between two `&` is no pair at all.
pub fn Query::decode(
  raw : BytesView,
  limits~ : Limits = limits,
) -> Query raise Refused {
  let pairs = []
  let n = raw.length()
  let mut start = 0
  for i in 0..<=n {
    guard i == n || raw[i] == b'&' else { continue }
    if i > start {
      if pairs.length() == limits.pairs {
        raise Pairs(limit=limits.pairs)
      }
      if i - start > limits.size {
        raise Pair(limit=limits.size, got=i - start)
      }
      let piece = raw[start:i]
      pairs.push(
        match piece.find(b"=") {
          Some(eq) => (form_text(piece[:eq]), form_text(piece[eq + 1:]))
          None => (form_text(piece), "")
        },
      )
    }
    start = i + 1
  }
  { pairs, }
}

///|
/// Write the pairs as `application/x-www-form-urlencoded` (WHATWG URL Standard
/// §5.2): letters, digits and `*-._` stand for themselves, a space is `+`, and
/// every other octet of the UTF-8 is `%XX`.
pub fn Query::encode(self : Query) -> String {
  let out = StringBuilder()
  for i, pair in self.pairs {
    if i > 0 {
      out.write_char('&')
    }
    form_escape(out, pair.0)
    out.write_char('=')
    form_escape(out, pair.1)
  }
  out.to_string()
}

///|
/// The first value under `name`.
pub fn Query::get(self : Query, name : StringView) -> String? {
  for pair in self.pairs {
    if pair.0[:] == name {
      return Some(pair.1)
    }
  }
  None
}

///|
/// Every value under `name`, in order.
pub fn Query::values(self : Query, name : StringView) -> Array[String] {
  self.pairs.filter(p => p.0[:] == name).map(p => p.1)
}

///|
/// Whether any pair is named `name`.
pub fn Query::has(self : Query, name : StringView) -> Bool {
  self.pairs.iter().any(p => p.0[:] == name)
}

///|
/// Add a pair at the end, keeping any already under `name`.
pub fn Query::add(self : Query, name : String, value : String) -> Unit {
  self.pairs.push((name, value))
}

///|
/// Leave exactly one pair under `name`, holding `value`: the first one's place
/// if there was one, the end if not.
pub fn Query::set(self : Query, name : String, value : String) -> Unit {
  match self.pairs.search_by(p => p.0 == name) {
    Some(at) => {
      self.pairs[at] = (name, value)
      let mut i = 0
      self.pairs.retain(p => {
        let keep = i == at || p.0 != name
        i += 1
        keep
      })
    }
    None => self.pairs.push((name, value))
  }
}

///|
/// Remove every pair under `name`.
pub fn Query::remove(self : Query, name : StringView) -> Unit {
  self.pairs.retain(p => p.0[:] != name)
}

///|
/// Every pair, in order.
pub fn Query::entries(self : Query) -> Array[(String, String)] {
  self.pairs.copy()
}

///|
/// One encoded name or value as text.
fn form_text(raw : BytesView) -> String {
  let spaced = Bytes::makei(raw.length(), i => {
    if raw[i] == b'+' {
      b' '
    } else {
      raw[i]
    }
  })
  @utf8.decode_lossy(unescape(spaced)[:])
}

///|
/// Append `text` in the form serialiser's escaping.
fn form_escape(out : StringBuilder, text : String) -> Unit {
  let digits = "0123456789ABCDEF"
  for b in @utf8.encode(text) {
    match b {
      b' ' => out.write_char('+')
      b'*' | b'-' | b'.' | b'_' | b'0'..<=b'9' | b'A'..<=b'Z' | b'a'..<=b'z' =>
        out.write_char(b.to_int().unsafe_to_char())
      _ => {
        out.write_char('%')
        out.write_char(digits[b.to_int() / 16].unsafe_to_char())
        out.write_char(digits[b.to_int() % 16].unsafe_to_char())
      }
    }
  }
}