///|
/// Return the PDU bytes following the unit identifier.
pub fn pdu_bytes(frame : Frame) -> Array[Byte] {
  let out : Array[Byte] = [frame.pdu.function]
  for byte in frame.pdu.data {
    out.push(byte)
  }
  out
}

///|
/// Return the RTU payload before its two-byte CRC.
pub fn rtu_payload(frame : Frame) -> Array[Byte] {
  let out : Array[Byte] = [frame.unit_id, frame.pdu.function]
  for byte in frame.pdu.data {
    out.push(byte)
  }
  out
}

///|
/// Encode a frame as Modbus RTU.
pub fn encode_rtu(frame : Frame) -> Array[Byte] {
  let body = rtu_payload(frame)
  let out : Array[Byte] = []
  for byte in body {
    out.push(byte)
  }
  let checksum = crc_bytes(body)
  out.push(checksum[0])
  out.push(checksum[1])
  out
}

///|
/// Checked RTU encoder for code that wants validation errors instead of a raw frame.
pub fn try_encode_rtu(frame : Frame) -> Result[Array[Byte], ModbusError] {
  match validate_wire_frame(frame, true) {
    Ok(_) => Ok(encode_rtu(frame))
    Err(error) => Err(error)
  }
}

///|
/// Decode one complete Modbus RTU frame.
pub fn decode_rtu(bytes : Array[Byte]) -> Result[Frame, ModbusError] {
  if bytes.length() < 4 {
    return Err(Incomplete)
  }
  if bytes.length() > 260 {
    return Err(InvalidLength)
  }
  match validate_unit(bytes[0], true) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let payload = copy_range(bytes, 0, bytes.length() - 2)
  let payload = match payload {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let expected = crc16(payload)
  let received = bytes[bytes.length() - 2].to_uint16() +
    (bytes[bytes.length() - 1].to_uint16() << 8)
  if expected != received {
    return Err(InvalidChecksum)
  }
  let data : Array[Byte] = []
  for i in 2..<(bytes.length() - 2) {
    data.push(bytes[i])
  }
  if data.length() == 0 {
    return Err(InvalidLength)
  }
  Ok({ unit_id: bytes[0], pdu: { function: bytes[1], data } })
}

///|
/// Encode a frame as Modbus TCP, using a caller-provided transaction id.
pub fn encode_tcp(transaction_id : UInt16, frame : Frame) -> Array[Byte] {
  let length = 2 + frame.pdu.data.length()
  let out : Array[Byte] = [
    (transaction_id >> 8).to_byte(),
    transaction_id.to_byte(),
    0,
    0,
    (length >> 8).to_byte(),
    length.to_byte(),
    frame.unit_id,
    frame.pdu.function,
  ]
  for byte in frame.pdu.data {
    out.push(byte)
  }
  out
}

///|
/// Checked TCP encoder.
pub fn try_encode_tcp(
  transaction_id : UInt16,
  frame : Frame,
) -> Result[Array[Byte], ModbusError] {
  if frame.pdu.data.length() > 251 {
    return Err(InvalidLength)
  }
  match validate_wire_frame(frame, true) {
    Ok(_) => Ok(encode_tcp(transaction_id, frame))
    Err(error) => Err(error)
  }
}

///|
/// Decode one complete Modbus TCP ADU and return its transaction id.
pub fn decode_tcp(bytes : Array[Byte]) -> Result[(UInt16, Frame), ModbusError] {
  if bytes.length() < 8 {
    return Err(Incomplete)
  }
  if bytes[2] != 0 || bytes[3] != 0 {
    return Err(InvalidMbap)
  }
  let length = bytes[4].to_uint16().shl(8) + bytes[5].to_uint16()
  if length < 2 || length > 253 {
    return Err(InvalidLength)
  }
  if length.to_int() != bytes.length() - 6 {
    return Err(InvalidLength)
  }
  match validate_unit(bytes[6], true) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let data : Array[Byte] = []
  for i in 8.. Array[Byte] {
  let body = rtu_payload(frame)
  let with_lrc : Array[Byte] = []
  for byte in body {
    with_lrc.push(byte)
  }
  with_lrc.push(lrc(body))
  let out : Array[Byte] = [58]
  for byte in with_lrc {
    out.push(hex_nibble(byte >> 4))
    out.push(hex_nibble(byte & 15))
  }
  out.push(13)
  out.push(10)
  out
}

///|
/// Checked ASCII encoder.
pub fn try_encode_ascii(frame : Frame) -> Result[Array[Byte], ModbusError] {
  if frame.pdu.data.length() > 251 {
    return Err(InvalidLength)
  }
  match validate_wire_frame(frame, true) {
    Ok(_) => Ok(encode_ascii(frame))
    Err(error) => Err(error)
  }
}

///|
/// Decode one complete Modbus ASCII frame.
pub fn decode_ascii(bytes : Array[Byte]) -> Result[Frame, ModbusError] {
  if bytes.length() < 7 {
    return Err(Incomplete)
  }
  if bytes[0] != 58 {
    return Err(InvalidAscii)
  }
  if bytes[bytes.length() - 2] != 13 || bytes[bytes.length() - 1] != 10 {
    return Err(Incomplete)
  }
  let hex_length = bytes.length() - 3
  if hex_length % 2 != 0 {
    return Err(InvalidAscii)
  }
  let decoded : Array[Byte] = []
  let mut i = 1
  while i < bytes.length() - 2 {
    match parse_hex_byte(bytes[i], bytes[i + 1]) {
      Ok(byte) => decoded.push(byte)
      Err(error) => return Err(error)
    }
    i += 2
  }
  if decoded.length() < 3 {
    return Err(InvalidLength)
  }
  match validate_unit(decoded[0], true) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let payload = copy_range(decoded, 0, decoded.length() - 1)
  let payload = match payload {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  if lrc(payload) != decoded[decoded.length() - 1] {
    return Err(InvalidChecksum)
  }
  let data : Array[Byte] = []
  for index in 2..<(decoded.length() - 1) {
    data.push(decoded[index])
  }
  Ok({ unit_id: decoded[0], pdu: { function: decoded[1], data } })
}

///|
/// Return the encoded size of a frame on a transport.
pub fn encoded_length(mode : Mode, frame : Frame) -> Int {
  match mode {
    Rtu => 4 + frame.pdu.data.length()
    Ascii => 7 + (frame.pdu.data.length() + 2) * 2
    Tcp => 8 + frame.pdu.data.length()
  }
}

///|
/// Return the expected TCP ADU size from a partial header.
pub fn expected_tcp_length(bytes : Array[Byte]) -> Result[Int, ModbusError] {
  if bytes.length() < 6 {
    return Err(Incomplete)
  }
  if bytes[2] != 0 || bytes[3] != 0 {
    return Err(InvalidMbap)
  }
  let length = bytes[4].to_int() * 256 + bytes[5].to_int()
  if length < 2 || length > 253 {
    Err(InvalidLength)
  } else {
    Ok(length + 6)
  }
}

///|
/// Return a complete ASCII frame boundary if CRLF is present.
pub fn ascii_boundary(bytes : Array[Byte]) -> Int? {
  if bytes.length() >= 2 &&
    bytes[bytes.length() - 2] == 13 &&
    bytes[bytes.length() - 1] == 10 {
    Some(bytes.length())
  } else {
    None
  }
}

///|
/// Decode a complete frame when the transport does not carry a TCP transaction id.
pub fn decode_mode(
  mode : Mode,
  bytes : Array[Byte],
) -> Result[Frame, ModbusError] {
  match mode {
    Rtu => decode_rtu(bytes)
    Ascii => decode_ascii(bytes)
    Tcp =>
      match decode_tcp(bytes) {
        Ok((_, frame)) => Ok(frame)
        Err(error) => Err(error)
      }
  }
}

///|
/// Encode a frame for a mode; TCP uses transaction id zero for this convenience API.
pub fn encode_mode(mode : Mode, frame : Frame) -> Array[Byte] {
  match mode {
    Rtu => encode_rtu(frame)
    Ascii => encode_ascii(frame)
    Tcp => encode_tcp(0, frame)
  }
}

///|
/// Checked transport encoder with an explicit TCP transaction id.
pub fn try_encode_mode(
  mode : Mode,
  transaction_id : UInt16,
  frame : Frame,
) -> Result[Array[Byte], ModbusError] {
  match mode {
    Rtu => try_encode_rtu(frame)
    Ascii => try_encode_ascii(frame)
    Tcp => try_encode_tcp(transaction_id, frame)
  }
}

///|
/// Decode an encoded frame and preserve a TCP transaction identifier when present.
pub fn decode_transaction(
  mode : Mode,
  bytes : Array[Byte],
) -> Result[(UInt16, Frame), ModbusError] {
  match mode {
    Tcp => decode_tcp(bytes)
    Rtu =>
      match decode_rtu(bytes) {
        Ok(frame) => Ok((0, frame))
        Err(error) => Err(error)
      }
    Ascii =>
      match decode_ascii(bytes) {
        Ok(frame) => Ok((0, frame))
        Err(error) => Err(error)
      }
  }
}

///|
/// Calculate how many bytes are needed for the shortest frame of a mode.
pub fn minimum_frame_length(mode : Mode) -> Int {
  match mode {
    Rtu => 4
    Ascii => 7
    Tcp => 8
  }
}