///|
/// UTF-8 validation helpers used for PostgreSQL protocol strings and text
/// payloads.
///
/// Primary references:
/// - https://www.postgresql.org/docs/current/protocol-overview.html#PROTOCOL-OVERVIEW
/// - https://www.postgresql.org/docs/current/protocol-message-formats.html#PROTOCOL-MESSAGE-FORMATS
fn[T] invalid_utf8() -> T raise ProtocolError {
  raise InvalidInput("invalid UTF-8")
}

///|
fn byte_in_range(b : Byte, lower : Int, upper : Int) -> Bool {
  let value = b.to_int()
  value >= lower && value <= upper
}

///|
fn is_continuation_byte(b : Byte) -> Bool {
  byte_in_range(b, 0x80, 0xBF)
}

///|
fn consume_two_byte_scalar(bytes : BytesView) -> BytesView raise ProtocolError {
  match bytes {
    [_, b1, .. tail] =>
      if is_continuation_byte(b1) {
        tail
      } else {
        invalid_utf8()
      }
    _ => invalid_utf8()
  }
}

///|
fn consume_three_byte_scalar(
  bytes : BytesView,
  second_lower : Int,
  second_upper : Int,
) -> BytesView raise ProtocolError {
  match bytes {
    [_, b1, b2, .. tail] =>
      if byte_in_range(b1, second_lower, second_upper) &&
        is_continuation_byte(b2) {
        tail
      } else {
        invalid_utf8()
      }
    _ => invalid_utf8()
  }
}

///|
fn consume_four_byte_scalar(
  bytes : BytesView,
  second_lower : Int,
  second_upper : Int,
) -> BytesView raise ProtocolError {
  match bytes {
    [_, b1, b2, b3, .. tail] =>
      if byte_in_range(b1, second_lower, second_upper) &&
        is_continuation_byte(b2) &&
        is_continuation_byte(b3) {
        tail
      } else {
        invalid_utf8()
      }
    _ => invalid_utf8()
  }
}

///|
fn consume_utf8_scalar(bytes : BytesView) -> BytesView raise ProtocolError {
  match bytes {
    [b0, .. tail] => {
      let lead = b0.to_int()
      if lead <= 0x7F {
        tail
      } else if lead <= 0xC1 {
        invalid_utf8()
      } else if lead <= 0xDF {
        consume_two_byte_scalar(bytes)
      } else if lead == 0xE0 {
        consume_three_byte_scalar(bytes, 0xA0, 0xBF)
      } else if lead == 0xED {
        consume_three_byte_scalar(bytes, 0x80, 0x9F)
      } else if lead <= 0xEF {
        consume_three_byte_scalar(bytes, 0x80, 0xBF)
      } else if lead == 0xF0 {
        consume_four_byte_scalar(bytes, 0x90, 0xBF)
      } else if lead <= 0xF3 {
        consume_four_byte_scalar(bytes, 0x80, 0xBF)
      } else if lead == 0xF4 {
        consume_four_byte_scalar(bytes, 0x80, 0x8F)
      } else {
        invalid_utf8()
      }
    }
    [] => bytes
  }
}

///|
/// Validate that bytes are valid UTF-8.
///
/// The higher-level client requests `client_encoding=UTF8`, so protocol strings
/// and textual values handled by this package are expected to pass this check.
pub fn validate_utf8(bytes : BytesView) -> Unit raise ProtocolError {
  let mut rest = bytes
  while !rest.is_empty() {
    rest = consume_utf8_scalar(rest)
  }
}

///|
/// Encode a String (UTF-16) as UTF-8 bytes.
pub fn utf8_encode(s : String) -> Bytes {
  @utf8.encode(s)
}