///|
/// Decodes percent-encoded bytes in a URL query component. `+` is treated as
/// a space, matching `application/x-www-form-urlencoded`.
pub fn percent_decode(input : String) -> String {
  percent_decode_with(input, true)
}

///|
/// Same as `percent_decode`, but leaves `+` untouched. Use this for path
/// segments, where `+` is a literal plus sign.
pub fn percent_decode_path(input : String) -> String {
  percent_decode_with(input, false)
}

///|
fn percent_decode_with(input : String, plus_as_space : Bool) -> String {
  let needs_work = input.contains("%") || (plus_as_space && input.contains("+"))
  if !needs_work {
    return input
  }
  let src = @utf8.encode(input)
  let out : Array[Byte] = []
  let mut i = 0
  let mut changed = false
  while i < src.length() {
    let b = src[i].to_int()
    if b == 37 && i + 2 < src.length() {
      let hi = hex_digit(src[i + 1].to_int())
      let lo = hex_digit(src[i + 2].to_int())
      if hi >= 0 && lo >= 0 {
        out.push((hi * 16 + lo).to_byte())
        i = i + 3
        changed = true
        continue
      }
    }
    if b == 43 && plus_as_space {
      out.push((32 : Int32).to_byte())
      changed = true
    } else {
      out.push(b.to_byte())
    }
    i = i + 1
  }
  if !changed {
    return input
  }
  @utf8.decode_lossy(Bytes::from_array(out))
}

///|
fn hex_digit(v : Int32) -> Int32 {
  if v >= 48 && v <= 57 {
    v - 48
  } else if v >= 65 && v <= 70 {
    v - 55
  } else if v >= 97 && v <= 102 {
    v - 87
  } else {
    -1
  }
}

///|
/// Percent-encodes a value for use in a query string or cookie, keeping the
/// unreserved characters from RFC 3986 untouched.
pub fn percent_encode(input : String) -> String {
  let bytes = @utf8.encode(input)
  let sb = StringBuilder()
  let mut i = 0
  while i < bytes.length() {
    let b = bytes[i].to_int()
    let safe = (b >= 48 && b <= 57) ||
      (b >= 65 && b <= 90) ||
      (b >= 97 && b <= 122) ||
      b == 45 ||
      b == 95 ||
      b == 46 ||
      b == 126
    if safe {
      sb.write_char(b.unsafe_to_char())
    } else {
      sb.write_string("%")
      sb.write_char(hex_char(b / 16))
      sb.write_char(hex_char(b % 16))
    }
    i = i + 1
  }
  sb.to_string()
}

///|
fn hex_char(value : Int32) -> Char {
  if value < 10 {
    (48 + value).unsafe_to_char()
  } else {
    (55 + value).unsafe_to_char()
  }
}

///|
/// Parses a `Cookie` request header (`a=1; b=hello%20world`) into a map.
pub fn parse_cookies(raw : String) -> Map[String, String] {
  let out : Map[String, String] = Map([])
  for part in raw.split(";") {
    let pair = part.to_owned().trim().to_owned()
    if pair == "" {
      continue
    }
    match pair.split_once("=") {
      Some((name, value)) =>
        out.set(name.to_owned(), percent_decode(value.to_owned()))
      None => out.set(pair, "")
    }
  }
  out
}

///|
/// Parses a raw query string (`a=1&b=hello+world`) into a map.
pub fn parse_query(raw : String) -> Map[String, String] {
  let out : Map[String, String] = Map([])
  for part in raw.split("&") {
    let pair = part.to_owned()
    if pair == "" {
      continue
    }
    match pair.split_once("=") {
      Some((key, value)) =>
        out.set(
          percent_decode(key.to_owned()),
          percent_decode(value.to_owned()),
        )
      None => out.set(percent_decode(pair), "")
    }
  }
  out
}

///|
/// Parses an `application/x-www-form-urlencoded` body. This is the same
/// grammar as a query string.
pub fn parse_urlencoded(raw : String) -> Map[String, String] {
  parse_query(raw)
}

///|
/// Node of the nested form tree built by `parse_urlencoded_nested`.
priv enum FormNode {
  FormLeaf(String)
  FormList(Array[FormNode])
  FormMap(Map[String, FormNode])
}

///|
/// Parses an `application/x-www-form-urlencoded` body with bracketed names
/// (`a[b]=1&a[]=2&a[0]=3`) into nested JSON, mirroring the `extended` mode of
/// `express.urlencoded`.
pub fn parse_urlencoded_nested(raw : String) -> Json {
  let root : Map[String, FormNode] = Map([])
  for part in raw.split("&") {
    let pair = part.to_owned()
    if pair == "" {
      continue
    }
    let (raw_key, raw_value) = match pair.split_once("=") {
      Some((key, value)) => (key.to_owned(), value.to_owned())
      None => (pair, "")
    }
    let segments = form_key_segments(percent_decode(raw_key))
    if segments.length() == 0 || segments[0] == "" {
      continue
    }
    let head = segments[0]
    if segments.length() == 1 {
      root.set(head, FormLeaf(percent_decode(raw_value)))
    } else {
      root.set(
        head,
        form_insert(root.get(head), segments, 1, percent_decode(raw_value)),
      )
    }
  }
  form_node_to_json(FormMap(root))
}

///|
/// Splits a bracketed form key: `a[b][c]` becomes `["a", "b", "c"]` and
/// `a[]` becomes `["a", ""]`.
fn form_key_segments(key : String) -> Array[String] {
  let segments : Array[String] = []
  let first = match key.find("[") {
    Some(index) => index
    None => key.length()
  }
  segments.push(key[:first].to_owned())
  let mut rest = key[first:].to_owned()
  while rest.has_prefix("[") {
    match rest.find("]") {
      Some(end) => {
        segments.push(rest[1:end].to_owned())
        rest = rest[end + 1:].to_owned()
      }
      None => break
    }
  }
  segments
}

///|
/// Inserts one value into the tree below `at`, creating lists for `[]` and
/// numeric keys and maps for named keys.
fn form_insert(
  current : FormNode?,
  segments : Array[String],
  at : Int32,
  value : String,
) -> FormNode {
  let key = segments[at]
  let last = at == segments.length() - 1
  if key == "" || is_numeric_key(key) {
    let items = match current {
      Some(FormList(items)) => items
      _ => []
    }
    if last {
      items.push(FormLeaf(value))
    } else {
      items.push(form_insert(None, segments, at + 1, value))
    }
    FormList(items)
  } else {
    let entries = match current {
      Some(FormMap(entries)) => entries
      _ => Map([])
    }
    if last {
      entries.set(key, FormLeaf(value))
    } else {
      entries.set(key, form_insert(entries.get(key), segments, at + 1, value))
    }
    FormMap(entries)
  }
}

///|
fn is_numeric_key(key : String) -> Bool {
  if key == "" {
    return false
  }
  for ch in key {
    let digit = ch.to_int() - 48
    if digit < 0 || digit > 9 {
      return false
    }
  }
  true
}

///|
fn form_node_to_json(node : FormNode) -> Json {
  match node {
    FormLeaf(value) => Json::string(value)
    FormList(items) => {
      let out : Array[Json] = []
      for item in items {
        out.push(form_node_to_json(item))
      }
      Json::array(out)
    }
    FormMap(entries) => {
      let out : Map[String, Json] = Map([])
      for pair in entries.iter() {
        out.set(pair.0, form_node_to_json(pair.1))
      }
      Json::object(out)
    }
  }
}

///|
/// A route pattern. Patterns are compiled once at registration time and
/// matched in MoonBit, while the actual HTTP transport stays in µWebSockets.
pub struct PathPattern {
  priv segments : Array[Segment]
  priv source : String
}

///|
priv enum Segment {
  Fixed(String)
  Slot(String)
  Splat(String)
}

///|
pub fn make_pattern(path : String) -> PathPattern {
  let segments : Array[Segment] = []
  for raw in path.split("/") {
    let segment = raw.to_owned()
    if segment == "" {
      continue
    }
    if segment.has_prefix(":") {
      segments.push(Slot(segment[1:].to_owned()))
    } else if segment.has_prefix("*") {
      let name = if segment.length() > 1 { segment[1:].to_owned() } else { "0" }
      segments.push(Splat(name))
    } else {
      segments.push(Fixed(segment))
    }
  }
  { segments, source: path, }
}

///|
/// The original pattern string, as used by `req.route.path`.
pub fn PathPattern::source(self : PathPattern) -> String {
  self.source
}

///|
pub fn PathPattern::is_any(self : PathPattern) -> Bool {
  self.segments.length() == 0
}

///|
/// Matches `path` against this pattern and returns captured parameters.
/// When `prefix` is true the pattern only has to match the beginning of the
/// path, which is how `app.use("/api", ...)` behaves.
pub fn PathPattern::match_path(
  self : PathPattern,
  path : String,
  prefix : Bool,
) -> Array[(String, String)]? {
  let parts = split_path(path)
  let params : Array[(String, String)] = []
  let mut pi = 0
  let mut si = 0
  while pi < self.segments.length() {
    match self.segments[pi] {
      Fixed(expected) => {
        if si >= parts.length() || parts[si].to_lower() != expected.to_lower() {
          return None
        }
        si = si + 1
        pi = pi + 1
      }
      Slot(name) => {
        if si >= parts.length() {
          return None
        }
        params.push((name, percent_decode_path(parts[si])))
        si = si + 1
        pi = pi + 1
      }
      Splat(name) => {
        params.push((name, percent_decode_path(join_parts(parts, si))))
        si = parts.length()
        pi = pi + 1
      }
    }
  }
  if !prefix && si < parts.length() {
    return None
  }
  Some(params)
}

///|
/// Splits a URL path into non-empty segments. Repeated slashes and a trailing
/// slash are ignored, so `/users/` and `/users` match the same route.
pub fn split_path(path : String) -> Array[String] {
  let parts : Array[String] = []
  for raw in path.split("/") {
    let segment = raw.to_owned()
    if segment != "" {
      parts.push(segment)
    }
  }
  parts
}

///|
fn join_parts(parts : Array[String], from : Int32) -> String {
  let sb = StringBuilder()
  let mut i = from
  while i < parts.length() {
    if i > from {
      sb.write_string("/")
    }
    sb.write_string(parts[i])
    i = i + 1
  }
  sb.to_string()
}

///|
/// Returns the parameter names of a route pattern, in order.
pub fn parse_param_names(path : String) -> Array[String] {
  let pattern = make_pattern(path)
  let names : Array[String] = []
  for segment in pattern.segments {
    match segment {
      Slot(name) | Splat(name) => names.push(name)
      Fixed(_) => ()
    }
  }
  names
}

///|
/// HTTP reason phrase for the status codes used by the framework.
pub fn status_text(code : Int32) -> String {
  match code {
    100 => "Continue"
    101 => "Switching Protocols"
    102 => "Processing"
    103 => "Early Hints"
    200 => "OK"
    201 => "Created"
    202 => "Accepted"
    203 => "Non-Authoritative Information"
    204 => "No Content"
    205 => "Reset Content"
    206 => "Partial Content"
    207 => "Multi-Status"
    208 => "Already Reported"
    226 => "IM Used"
    300 => "Multiple Choices"
    301 => "Moved Permanently"
    302 => "Found"
    303 => "See Other"
    304 => "Not Modified"
    305 => "Use Proxy"
    307 => "Temporary Redirect"
    308 => "Permanent Redirect"
    400 => "Bad Request"
    401 => "Unauthorized"
    402 => "Payment Required"
    403 => "Forbidden"
    404 => "Not Found"
    405 => "Method Not Allowed"
    406 => "Not Acceptable"
    407 => "Proxy Authentication Required"
    408 => "Request Timeout"
    409 => "Conflict"
    410 => "Gone"
    411 => "Length Required"
    412 => "Precondition Failed"
    413 => "Payload Too Large"
    414 => "URI Too Long"
    415 => "Unsupported Media Type"
    416 => "Range Not Satisfiable"
    417 => "Expectation Failed"
    418 => "I'm a Teapot"
    421 => "Misdirected Request"
    422 => "Unprocessable Entity"
    423 => "Locked"
    424 => "Failed Dependency"
    425 => "Too Early"
    426 => "Upgrade Required"
    428 => "Precondition Required"
    429 => "Too Many Requests"
    431 => "Request Header Fields Too Large"
    451 => "Unavailable For Legal Reasons"
    500 => "Internal Server Error"
    501 => "Not Implemented"
    502 => "Bad Gateway"
    503 => "Service Unavailable"
    504 => "Gateway Timeout"
    505 => "HTTP Version Not Supported"
    506 => "Variant Also Negotiates"
    507 => "Insufficient Storage"
    508 => "Loop Detected"
    510 => "Not Extended"
    511 => "Network Authentication Required"
    _ => "Unknown"
  }
}