///|
/// Byte-level RESP2 framing shared by the socket reader and `parse_redis_response`.
/// A RESP frame counts bytes, never characters, so all of this walks `Bytes`.

///|
fn decode_span(data : Bytes, start : Int, end : Int) -> String {
  @encoding/utf8.decode_lossy(data[start:end])
}

///|
fn parse_int_opt(text : String) -> Int? {
  Some(@string.parse_int(text)) catch {
    _ => None
  }
}

///|
fn find_crlf(data : Bytes, start : Int) -> Int? {
  let n = data.length()
  let mut i = start
  while i + 1 < n {
    if data[i] == b'\r' && data[i + 1] == b'\n' {
      return Some(i)
    }
    i = i + 1
  }
  None
}

///|
/// One CRLF-terminated line: its text without the terminator, and the offset just
/// past the terminator.
fn line_at(data : Bytes, start : Int) -> (String, Int)? {
  match find_crlf(data, start) {
    None => None
    Some(i) => Some((decode_span(data, start, i), i + 2))
  }
}

///|
fn parse_bulk_at(
  data : Bytes,
  len_text : String,
  after : Int,
) -> (RedisValue, Int)? {
  match parse_int_opt(len_text) {
    None => Some((Error("Invalid bulk length: " + len_text), after))
    Some(len) =>
      if len < 0 {
        Some((BulkString(None), after))
      } else if data.length() < after + len + 2 {
        None
      } else {
        Some(
          (
            BulkString(Some(decode_span(data, after, after + len))),
            after + len + 2,
          ),
        )
      }
  }
}

///|
fn parse_array_at(
  data : Bytes,
  count_text : String,
  after : Int,
) -> (RedisValue, Int)? {
  match parse_int_opt(count_text) {
    None => Some((Error("Invalid array count: " + count_text), after))
    Some(count) =>
      if count < 0 {
        Some((Array([]), after))
      } else {
        let items : Array[RedisValue] = []
        let mut pos = after
        let mut i = 0
        while i < count {
          match parse_reply(data, pos) {
            None => return None
            Some((value, next)) => {
              items.push(value)
              pos = next
            }
          }
          i = i + 1
        }
        Some((Array(items), pos))
      }
  }
}

///|
/// Parse the single reply that starts at `pos`; `None` means the buffer does not
/// hold all of it yet.
fn parse_reply(data : Bytes, pos : Int) -> (RedisValue, Int)? {
  if pos >= data.length() {
    return None
  }
  let tag = data[pos]
  if !(tag == b'+' || tag == b'-' || tag == b':' || tag == b'$' || tag == b'*') {
    return Some((Error("Unknown response type"), pos))
  }
  match line_at(data, pos + 1) {
    None => None
    Some((line, after)) =>
      if tag == b'+' {
        Some((SimpleString(line), after))
      } else if tag == b'-' {
        Some((Error(line), after))
      } else if tag == b':' {
        match parse_int_opt(line) {
          Some(number) => Some((Integer(number), after))
          None => Some((Error("Invalid integer: " + line), after))
        }
      } else if tag == b'$' {
        parse_bulk_at(data, line, after)
      } else {
        parse_array_at(data, line, after)
      }
  }
}

///|
/// Parse a complete RESP2 reply held in `response`.
///
/// Frame lengths are byte counts, so the text is re-encoded to UTF-8 before being
/// walked. An input that stops mid-frame yields `Error("Incomplete RESP reply")`
/// rather than a silently shortened value.
pub fn parse_redis_response(response : String) -> RedisValue {
  if response.length() == 0 {
    return Error("Empty response")
  }
  let data = @encoding/utf8.encode(response)
  match parse_reply(data, 0) {
    None => Error("Incomplete RESP reply")
    Some((value, _)) => value
  }
}