///|
/// Build a read-coils request (function 01).
pub fn read_coils(
  unit_id : Byte,
  address : UInt16,
  quantity : UInt16,
) -> Result[Frame, ModbusError] {
  read_bits_request(unit_id, 1, address, quantity, 2000)
}

///|
/// Build a read-discrete-inputs request (function 02).
pub fn read_discrete_inputs(
  unit_id : Byte,
  address : UInt16,
  quantity : UInt16,
) -> Result[Frame, ModbusError] {
  read_bits_request(unit_id, 2, address, quantity, 2000)
}

///|
fn read_bits_request(
  unit_id : Byte,
  function : Byte,
  address : UInt16,
  quantity : UInt16,
  limit : Int,
) -> Result[Frame, ModbusError] {
  if !is_valid_unit_id(unit_id) {
    return Err(InvalidUnitId)
  }
  if quantity < 1 || quantity.to_int() > limit {
    return Err(InvalidQuantity)
  }
  if !valid_address_range(address, quantity.to_int()) {
    return Err(InvalidAddress)
  }
  Ok({
    unit_id,
    pdu: {
      function,
      data: address_bytes(address).add(quantity_bytes_checked(quantity)),
    },
  })
}

///|
/// Build a read-holding-registers request (function 03).
pub fn read_holding_registers(
  unit_id : Byte,
  address : UInt16,
  quantity : UInt16,
) -> Result[Frame, ModbusError] {
  read_registers_request(unit_id, 3, address, quantity)
}

///|
/// Build a read-input-registers request (function 04).
pub fn read_input_registers(
  unit_id : Byte,
  address : UInt16,
  quantity : UInt16,
) -> Result[Frame, ModbusError] {
  read_registers_request(unit_id, 4, address, quantity)
}

///|
fn read_registers_request(
  unit_id : Byte,
  function : Byte,
  address : UInt16,
  quantity : UInt16,
) -> Result[Frame, ModbusError] {
  if !is_valid_unit_id(unit_id) {
    return Err(InvalidUnitId)
  }
  if quantity < 1 || quantity.to_int() > 125 {
    return Err(InvalidQuantity)
  }
  if !valid_address_range(address, quantity.to_int()) {
    return Err(InvalidAddress)
  }
  Ok({
    unit_id,
    pdu: {
      function,
      data: address_bytes(address).add(quantity_bytes_checked(quantity)),
    },
  })
}

///|
/// Build a write-single-coil request (function 05).
pub fn write_single_coil(
  unit_id : Byte,
  address : UInt16,
  value : Bool,
) -> Result[Frame, ModbusError] {
  if !is_valid_unit_id(unit_id) {
    return Err(InvalidUnitId)
  }
  let encoded : UInt16 = if value { 0xFF00 } else { 0 }
  Ok({
    unit_id,
    pdu: {
      function: 5,
      data: address_bytes(address).add(quantity_bytes_checked(encoded)),
    },
  })
}

///|
/// Build a write-single-register request (function 06) with validation.
pub fn write_single_register_checked(
  unit_id : Byte,
  address : UInt16,
  value : UInt16,
) -> Result[Frame, ModbusError] {
  if !is_valid_unit_id(unit_id) {
    Err(InvalidUnitId)
  } else {
    Ok(write_single_register(unit_id, address, value))
  }
}

///|
/// Pack boolean values into Modbus least-significant-bit-first bytes.
fn pack_coils_for_request(values : Array[Bool]) -> Array[Byte] {
  let out : Array[Byte] = []
  let byte_count = (values.length() + 7) / 8
  for _ in 0.. Unit {
  for value in values {
    out.push(value)
  }
}

///|
/// Build a write-multiple-coils request (function 15).
pub fn write_multiple_coils(
  unit_id : Byte,
  address : UInt16,
  values : Array[Bool],
) -> Result[Frame, ModbusError] {
  if !is_valid_unit_id(unit_id) {
    return Err(InvalidUnitId)
  }
  if values.length() < 1 || values.length() > 1968 {
    return Err(InvalidQuantity)
  }
  if !valid_address_range(address, values.length()) {
    return Err(InvalidAddress)
  }
  let data : Array[Byte] = []
  append_bytes_to(data, address_bytes(address))
  append_bytes_to(data, quantity_bytes_checked(values.length().to_uint16()))
  data.push(((values.length() + 7) / 8).to_byte())
  append_bytes_to(data, pack_coils_for_request(values))
  Ok({ unit_id, pdu: { function: 15, data } })
}

///|
/// Build a write-multiple-registers request (function 16).
pub fn write_multiple_registers(
  unit_id : Byte,
  address : UInt16,
  values : Array[UInt16],
) -> Result[Frame, ModbusError] {
  if !is_valid_unit_id(unit_id) {
    return Err(InvalidUnitId)
  }
  if values.length() < 1 || values.length() > 123 {
    return Err(InvalidQuantity)
  }
  if !valid_address_range(address, values.length()) {
    return Err(InvalidAddress)
  }
  let data : Array[Byte] = []
  append_bytes_to(data, address_bytes(address))
  append_bytes_to(data, quantity_bytes_checked(values.length().to_uint16()))
  data.push((values.length() * 2).to_byte())
  for value in values {
    append_bytes_to(data, quantity_bytes_checked(value))
  }
  Ok({ unit_id, pdu: { function: 16, data } })
}

///|
/// Build a mask-write-register request (function 22).
pub fn mask_write_register(
  unit_id : Byte,
  address : UInt16,
  and_mask : UInt16,
  or_mask : UInt16,
) -> Result[Frame, ModbusError] {
  if !is_valid_unit_id(unit_id) {
    return Err(InvalidUnitId)
  }
  let data : Array[Byte] = []
  append_bytes_to(data, address_bytes(address))
  append_bytes_to(data, quantity_bytes_checked(and_mask))
  append_bytes_to(data, quantity_bytes_checked(or_mask))
  Ok({ unit_id, pdu: { function: 22, data } })
}

///|
/// Build a combined read/write-multiple-registers request (function 23).
pub fn read_write_multiple_registers(
  unit_id : Byte,
  read_address : UInt16,
  read_quantity : UInt16,
  write_address : UInt16,
  write_values : Array[UInt16],
) -> Result[Frame, ModbusError] {
  if !is_valid_unit_id(unit_id) {
    return Err(InvalidUnitId)
  }
  if read_quantity < 1 || read_quantity > 125 {
    return Err(InvalidQuantity)
  }
  if write_values.length() < 1 || write_values.length() > 121 {
    return Err(InvalidQuantity)
  }
  if !valid_address_range(read_address, read_quantity.to_int()) ||
    !valid_address_range(write_address, write_values.length()) {
    return Err(InvalidAddress)
  }
  let data : Array[Byte] = []
  append_bytes_to(data, address_bytes(read_address))
  append_bytes_to(data, quantity_bytes_checked(read_quantity))
  append_bytes_to(data, address_bytes(write_address))
  append_bytes_to(
    data,
    quantity_bytes_checked(write_values.length().to_uint16()),
  )
  data.push((write_values.length() * 2).to_byte())
  for value in write_values {
    append_bytes_to(data, quantity_bytes_checked(value))
  }
  Ok({ unit_id, pdu: { function: 23, data } })
}

///|
/// Build a diagnostics request (function 08).
pub fn diagnostics(
  unit_id : Byte,
  subfunction : UInt16,
  data : Array[Byte],
) -> Result[Frame, ModbusError] {
  if !is_valid_unit_id(unit_id) {
    return Err(InvalidUnitId)
  }
  if data.length() % 2 != 0 {
    return Err(InvalidLength)
  }
  let payload : Array[Byte] = []
  append_bytes_to(payload, quantity_bytes_checked(subfunction))
  append_bytes_to(payload, data)
  Ok({ unit_id, pdu: { function: 8, data: payload } })
}

///|
/// Build the common Return Query Data diagnostics request.
pub fn diagnostics_return_query_data(
  unit_id : Byte,
  data : Array[Byte],
) -> Result[Frame, ModbusError] {
  diagnostics(unit_id, 0, data)
}

///|
/// Build a request for the exception status byte (function 07).
pub fn read_exception_status(unit_id : Byte) -> Result[Frame, ModbusError] {
  if !is_valid_unit_id(unit_id) {
    Err(InvalidUnitId)
  } else {
    Ok({ unit_id, pdu: { function: 7, data: [] } })
  }
}

///|
/// Build a request for the communication event counter (function 11).
pub fn get_comm_event_counter(unit_id : Byte) -> Result[Frame, ModbusError] {
  if !is_valid_unit_id(unit_id) {
    Err(InvalidUnitId)
  } else {
    Ok({ unit_id, pdu: { function: 11, data: [] } })
  }
}

///|
/// Build a request for the communication event log (function 12).
pub fn get_comm_event_log(unit_id : Byte) -> Result[Frame, ModbusError] {
  if !is_valid_unit_id(unit_id) {
    Err(InvalidUnitId)
  } else {
    Ok({ unit_id, pdu: { function: 12, data: [] } })
  }
}

///|
/// Build a Report Server ID request (function 17).
pub fn report_server_id(unit_id : Byte) -> Result[Frame, ModbusError] {
  if !is_valid_unit_id(unit_id) {
    Err(InvalidUnitId)
  } else {
    Ok({ unit_id, pdu: { function: 17, data: [] } })
  }
}

///|
/// Build a Read FIFO Queue request (function 24).
pub fn read_fifo_queue(
  unit_id : Byte,
  address : UInt16,
) -> Result[Frame, ModbusError] {
  if !is_valid_unit_id(unit_id) {
    Err(InvalidUnitId)
  } else {
    Ok({ unit_id, pdu: { function: 24, data: address_bytes(address) } })
  }
}

///|
/// Build a generic encapsulated-interface request (function 43).
pub fn encapsulated_interface(
  unit_id : Byte,
  mei_type : Byte,
  payload : Array[Byte],
) -> Result[Frame, ModbusError] {
  if !is_valid_unit_id(unit_id) {
    return Err(InvalidUnitId)
  }
  Ok({ unit_id, pdu: { function: 43, data: [mei_type].add(payload) } })
}

///|
/// Build a standard Modbus exception response.
pub fn exception_response(
  unit_id : Byte,
  function : Byte,
  code : Byte,
) -> Frame {
  { unit_id, pdu: { function: function | 0x80, data: [code] } }
}

///|
/// Build a typed exception response.
pub fn typed_exception_response(
  unit_id : Byte,
  function : Byte,
  code : ExceptionCode,
) -> Frame {
  exception_response(unit_id, function, code.to_byte())
}

///|
/// Whether a PDU is an exception response.
pub fn is_exception(frame : Frame) -> Bool {
  frame.pdu.is_exception()
}

///|
/// Return the function number without the exception bit.
pub fn base_function(frame : Frame) -> Byte {
  frame.pdu.function & 0x7F
}

///|
/// Construct a response with an explicit function and owned data array.
pub fn response_frame(request : Frame, data : Array[Byte]) -> Frame {
  { unit_id: request.unit_id, pdu: { function: request.pdu.function, data } }
}

///|
/// Construct an exception from a protocol error.
pub fn error_response(request : Frame, error : ModbusError) -> Frame {
  let code = match error {
    InvalidFunction | Unsupported => IllegalFunction
    InvalidAddress | UnitMismatch => IllegalDataAddress
    InvalidQuantity | InvalidByteCount | InvalidData | InvalidLength =>
      IllegalDataValue
    Busy => ServerDeviceBusy
    _ => ServerDeviceFailure
  }
  typed_exception_response(request.unit_id, request.pdu.function, code)
}

///|
/// Return a copy of a frame with a different unit identifier.
pub fn with_unit(frame : Frame, unit_id : Byte) -> Frame {
  { ..frame, unit_id, }
}

///|
/// Return a copy of a frame with a different function code.
pub fn with_function(frame : Frame, function : Byte) -> Frame {
  { ..frame, pdu: { ..frame.pdu, function, } }
}

///|
/// Return a copy of a frame with a different data payload.
pub fn with_data(frame : Frame, data : Array[Byte]) -> Frame {
  { ..frame, pdu: { ..frame.pdu, data, } }
}