///|
/// Read a big-endian unsigned 16-bit value from a byte array.
pub fn read_u16_be(
  bytes : Array[Byte],
  offset : Int,
) -> Result[UInt16, ModbusError] {
  if offset < 0 || offset + 2 > bytes.length() {
    Err(Incomplete)
  } else {
    Ok(bytes[offset].to_uint16().shl(8) + bytes[offset + 1].to_uint16())
  }
}

///|
/// Read a big-endian unsigned 32-bit value using MoonBit's platform unsigned type.
pub fn read_u32_be(
  bytes : Array[Byte],
  offset : Int,
) -> Result[UInt, ModbusError] {
  if offset < 0 || offset + 4 > bytes.length() {
    return Err(Incomplete)
  }
  Ok(
    (bytes[offset].to_uint() << 24) |
    (bytes[offset + 1].to_uint() << 16) |
    (bytes[offset + 2].to_uint() << 8) |
    bytes[offset + 3].to_uint(),
  )
}

///|
/// Append a big-endian unsigned 16-bit value.
pub fn append_u16_be(out : Array[Byte], value : UInt16) -> Unit {
  out.push(value.shr(8).to_byte())
  out.push(value.to_byte())
}

///|
/// Append a big-endian unsigned 32-bit value.
pub fn append_u32_be(out : Array[Byte], value : UInt) -> Unit {
  out.push((value >> 24).to_byte())
  out.push((value >> 16).to_byte())
  out.push((value >> 8).to_byte())
  out.push(value.to_byte())
}

///|
/// Make an owned byte-array copy. Protocol parsers use this to avoid retaining views.
pub fn copy_bytes(bytes : Array[Byte]) -> Array[Byte] {
  let out : Array[Byte] = []
  for byte in bytes {
    out.push(byte)
  }
  out
}

///|
/// Copy a half-open range and return a protocol error when the range is invalid.
pub fn copy_range(
  bytes : Array[Byte],
  start : Int,
  end : Int,
) -> Result[Array[Byte], ModbusError] {
  if start < 0 || end < start || end > bytes.length() {
    return Err(InvalidLength)
  }
  let out : Array[Byte] = []
  for i in start.. Bool {
  quantity > 0 && address.to_int() + quantity <= 65536
}

///|
/// Validate a unit identifier for a request.
pub fn validate_unit(
  unit_id : Byte,
  allow_broadcast : Bool,
) -> Result[Unit, ModbusError] {
  if is_valid_unit_id(unit_id, broadcast=allow_broadcast) {
    Ok(())
  } else {
    Err(InvalidUnitId)
  }
}

///|
/// Return the ordinary function code for an exception response.
pub fn ordinary_function(function : Byte) -> Byte {
  function & 0x7F
}

///|
/// Check that a function code is in the supported public subset.
pub fn supported_function(function : Byte) -> Bool {
  match function_code(function) {
    Unknown(_) => false
    _ => true
  }
}

///|
/// Validate a request PDU's byte-level shape and protocol limits.
pub fn validate_pdu(pdu : Pdu) -> Result[Unit, ModbusError] {
  if pdu.function == 0 || pdu.is_exception() {
    return Err(InvalidFunction)
  }
  let size = pdu.data.length()
  match function_code(pdu.function) {
    ReadCoils
    | ReadDiscreteInputs
    | ReadHoldingRegisters
    | ReadInputRegisters =>
      if size != 4 {
        Err(InvalidLength)
      } else {
        let quantity = pdu.data[2].to_uint16().shl(8) + pdu.data[3].to_uint16()
        let limit = if pdu.function == 3 || pdu.function == 4 {
          125
        } else {
          2000
        }
        if quantity < 1 || quantity.to_int() > limit {
          Err(InvalidQuantity)
        } else if !valid_address_range(
            pdu.data[0].to_uint16().shl(8) + pdu.data[1].to_uint16(),
            quantity.to_int(),
          ) {
          Err(InvalidAddress)
        } else {
          Ok(())
        }
      }
    WriteSingleCoil | WriteSingleRegister =>
      if size != 4 {
        Err(InvalidLength)
      } else if pdu.function == 5 &&
        pdu.data[2].to_uint16().shl(8) + pdu.data[3].to_uint16() != 0 &&
        pdu.data[2].to_uint16().shl(8) + pdu.data[3].to_uint16() != 0xFF00 {
        Err(InvalidData)
      } else {
        Ok(())
      }
    WriteMultipleCoils => validate_write_multiple_coils(pdu.data)
    WriteMultipleRegisters => validate_write_multiple_registers(pdu.data)
    MaskWriteRegister => if size == 6 { Ok(()) } else { Err(InvalidLength) }
    ReadWriteMultipleRegisters => validate_read_write_multiple(pdu.data)
    ReadExceptionStatus => if size == 0 { Ok(()) } else { Err(InvalidLength) }
    Diagnostics =>
      if size >= 2 && size % 2 == 0 {
        Ok(())
      } else {
        Err(InvalidLength)
      }
    GetCommEventCounter => if size == 0 { Ok(()) } else { Err(InvalidLength) }
    GetCommEventLog => if size == 0 { Ok(()) } else { Err(InvalidLength) }
    ReportServerId => if size == 0 { Ok(()) } else { Err(InvalidLength) }
    ReadFifoQueue => if size == 2 { Ok(()) } else { Err(InvalidLength) }
    EncapsulatedInterface => if size >= 1 { Ok(()) } else { Err(InvalidLength) }
    ReadFileRecord => if size >= 1 { Ok(()) } else { Err(InvalidLength) }
    WriteFileRecord => if size >= 1 { Ok(()) } else { Err(InvalidLength) }
    ReportServerIdExtended => Ok(())
    Unknown(_) => Err(Unsupported)
  }
}

///|
fn validate_write_multiple_coils(
  data : Array[Byte],
) -> Result[Unit, ModbusError] {
  if data.length() < 6 {
    return Err(InvalidLength)
  }
  let quantity = data[2].to_uint16().shl(8) + data[3].to_uint16()
  let byte_count = data[4].to_int()
  if quantity < 1 || quantity > 1968 {
    Err(InvalidQuantity)
  } else if !valid_address_range(
      data[0].to_uint16().shl(8) + data[1].to_uint16(),
      quantity.to_int(),
    ) {
    Err(InvalidAddress)
  } else if byte_count != (quantity.to_int() + 7) / 8 {
    Err(InvalidByteCount)
  } else if data.length() != 5 + byte_count {
    Err(InvalidLength)
  } else {
    Ok(())
  }
}

///|
fn validate_write_multiple_registers(
  data : Array[Byte],
) -> Result[Unit, ModbusError] {
  if data.length() < 6 {
    return Err(InvalidLength)
  }
  let quantity = data[2].to_uint16().shl(8) + data[3].to_uint16()
  let byte_count = data[4].to_int()
  if quantity < 1 || quantity > 123 {
    Err(InvalidQuantity)
  } else if !valid_address_range(
      data[0].to_uint16().shl(8) + data[1].to_uint16(),
      quantity.to_int(),
    ) {
    Err(InvalidAddress)
  } else if byte_count != quantity.to_int() * 2 {
    Err(InvalidByteCount)
  } else if data.length() != 5 + byte_count {
    Err(InvalidLength)
  } else {
    Ok(())
  }
}

///|
fn validate_read_write_multiple(
  data : Array[Byte],
) -> Result[Unit, ModbusError] {
  if data.length() != 9 {
    return Err(InvalidLength)
  }
  let read_quantity = data[2].to_uint16().shl(8) + data[3].to_uint16()
  let write_quantity = data[6].to_uint16().shl(8) + data[7].to_uint16()
  if read_quantity < 1 ||
    read_quantity > 125 ||
    write_quantity < 1 ||
    write_quantity > 121 {
    Err(InvalidQuantity)
  } else if !valid_address_range(
      data[0].to_uint16().shl(8) + data[1].to_uint16(),
      read_quantity.to_int(),
    ) ||
    !valid_address_range(
      data[4].to_uint16().shl(8) + data[5].to_uint16(),
      write_quantity.to_int(),
    ) {
    Err(InvalidAddress)
  } else {
    Ok(())
  }
}

///|
/// Validate a complete logical frame before encoding it on any transport.
pub fn validate_frame(
  frame : Frame,
  allow_broadcast : Bool,
) -> Result[Unit, ModbusError] {
  match validate_unit(frame.unit_id, allow_broadcast) {
    Ok(_) => validate_pdu(frame.pdu)
    Err(error) => Err(error)
  }
}

///|
/// Validate a frame for wire encoding without assuming request-only payload shapes.
pub fn validate_wire_frame(
  frame : Frame,
  allow_broadcast : Bool,
) -> Result[Unit, ModbusError] {
  match validate_unit(frame.unit_id, allow_broadcast) {
    Err(error) => Err(error)
    Ok(_) =>
      if frame.pdu.function == 0 || frame.pdu.data.length() > 253 {
        Err(InvalidLength)
      } else {
        Ok(())
      }
  }
}

///|
/// Return the expected response function byte for a request.
pub fn expected_response_function(request : Frame) -> Byte {
  request.pdu.function
}

///|
/// Validate the common unit/function relationship of a response.
pub fn validate_response_header(
  request : Frame,
  response : Frame,
) -> Result[Unit, ModbusError] {
  if request.unit_id != response.unit_id {
    return Err(UnitMismatch)
  }
  if response.pdu.function == (request.pdu.function | 0x80) {
    if response.pdu.data.length() == 1 {
      Ok(())
    } else {
      Err(InvalidLength)
    }
  } else if response.pdu.function != expected_response_function(request) {
    Err(InvalidFunction)
  } else {
    Ok(())
  }
}

///|
/// Human-readable function-code name used by diagnostic output.
pub fn function_name(function : Byte) -> String {
  match function_code(ordinary_function(function)) {
    ReadCoils => "read-coils"
    ReadDiscreteInputs => "read-discrete-inputs"
    ReadHoldingRegisters => "read-holding-registers"
    ReadInputRegisters => "read-input-registers"
    WriteSingleCoil => "write-single-coil"
    WriteSingleRegister => "write-single-register"
    ReadExceptionStatus => "read-exception-status"
    Diagnostics => "diagnostics"
    GetCommEventCounter => "get-comm-event-counter"
    GetCommEventLog => "get-comm-event-log"
    ReportServerId | ReportServerIdExtended => "report-server-id"
    WriteMultipleCoils => "write-multiple-coils"
    WriteMultipleRegisters => "write-multiple-registers"
    ReadFileRecord => "read-file-record"
    WriteFileRecord => "write-file-record"
    MaskWriteRegister => "mask-write-register"
    ReadWriteMultipleRegisters => "read-write-multiple-registers"
    ReadFifoQueue => "read-fifo-queue"
    EncapsulatedInterface => "encapsulated-interface"
    Unknown(_) => "unknown"
  }
}

///|
/// Return the maximum legal request quantity for a read function.
pub fn read_quantity_limit(function : Byte) -> Int {
  if function == 1 || function == 2 {
    2000
  } else if function == 3 || function == 4 {
    125
  } else {
    0
  }
}

///|
/// Return the maximum legal write quantity for a write-multiple function.
pub fn write_quantity_limit(function : Byte) -> Int {
  if function == 15 {
    1968
  } else if function == 16 {
    123
  } else {
    0
  }
}

///|
/// Make the two-byte address representation used by request builders.
pub fn address_bytes(address : UInt16) -> Array[Byte] {
  [address.shr(8).to_byte(), address.to_byte()]
}

///|
/// Make a quantity representation used by request builders.
pub fn quantity_bytes_checked(quantity : UInt16) -> Array[Byte] {
  [quantity.shr(8).to_byte(), quantity.to_byte()]
}