///|
/// A Modbus wire mode.
pub(all) enum Mode {
  Rtu
  Ascii
  Tcp
} derive(Debug, Eq)

///|
/// Return the stable display name of a transport mode.
pub fn mode_name(mode : Mode) -> String {
  match mode {
    Rtu => "rtu"
    Ascii => "ascii"
    Tcp => "tcp"
  }
}

///|
/// Standard constructors kept as named entry points for configuration files.
pub fn rtu_mode() -> Mode {
  Rtu
}

///|
pub fn ascii_mode() -> Mode {
  Ascii
}

///|
pub fn tcp_mode() -> Mode {
  Tcp
}

///|
/// The function codes defined by the Modbus application protocol.
pub(all) enum FunctionCode {
  ReadCoils
  ReadDiscreteInputs
  ReadHoldingRegisters
  ReadInputRegisters
  WriteSingleCoil
  WriteSingleRegister
  ReadExceptionStatus
  Diagnostics
  GetCommEventCounter
  GetCommEventLog
  ReportServerId
  WriteMultipleCoils
  WriteMultipleRegisters
  ReportServerIdExtended
  ReadFileRecord
  WriteFileRecord
  MaskWriteRegister
  ReadWriteMultipleRegisters
  ReadFifoQueue
  EncapsulatedInterface
  Unknown(Byte)
} derive(Debug, Eq)

///|
/// Convert a function code byte into the typed function-code vocabulary.
pub fn function_code(value : Byte) -> FunctionCode {
  match value {
    1 => ReadCoils
    2 => ReadDiscreteInputs
    3 => ReadHoldingRegisters
    4 => ReadInputRegisters
    5 => WriteSingleCoil
    6 => WriteSingleRegister
    7 => ReadExceptionStatus
    8 => Diagnostics
    11 => GetCommEventCounter
    12 => GetCommEventLog
    17 => ReportServerId
    15 => WriteMultipleCoils
    16 => WriteMultipleRegisters
    20 => ReadFileRecord
    21 => WriteFileRecord
    22 => MaskWriteRegister
    23 => ReadWriteMultipleRegisters
    24 => ReadFifoQueue
    43 => EncapsulatedInterface
    n => Unknown(n)
  }
}

///|
/// Return the numeric representation of a typed function code.
pub fn FunctionCode::to_byte(code : FunctionCode) -> Byte {
  match code {
    ReadCoils => 1
    ReadDiscreteInputs => 2
    ReadHoldingRegisters => 3
    ReadInputRegisters => 4
    WriteSingleCoil => 5
    WriteSingleRegister => 6
    ReadExceptionStatus => 7
    Diagnostics => 8
    GetCommEventCounter => 11
    GetCommEventLog => 12
    ReportServerId => 17
    WriteMultipleCoils => 15
    WriteMultipleRegisters => 16
    ReportServerIdExtended => 17
    ReadFileRecord => 20
    WriteFileRecord => 21
    MaskWriteRegister => 22
    ReadWriteMultipleRegisters => 23
    ReadFifoQueue => 24
    EncapsulatedInterface => 43
    Unknown(value) => value
  }
}

///|
/// Standard exception codes returned by a Modbus server.
pub(all) enum ExceptionCode {
  IllegalFunction
  IllegalDataAddress
  IllegalDataValue
  ServerDeviceFailure
  Acknowledge
  ServerDeviceBusy
  NegativeAcknowledge
  MemoryParityError
  GatewayPathUnavailable
  GatewayTargetFailedToRespond
  UnknownException(Byte)
} derive(Debug, Eq)

///|
pub fn exception_code(value : Byte) -> ExceptionCode {
  match value {
    1 => IllegalFunction
    2 => IllegalDataAddress
    3 => IllegalDataValue
    4 => ServerDeviceFailure
    5 => Acknowledge
    6 => ServerDeviceBusy
    7 => NegativeAcknowledge
    8 => MemoryParityError
    10 => GatewayPathUnavailable
    11 => GatewayTargetFailedToRespond
    n => UnknownException(n)
  }
}

///|
pub fn ExceptionCode::to_byte(code : ExceptionCode) -> Byte {
  match code {
    IllegalFunction => 1
    IllegalDataAddress => 2
    IllegalDataValue => 3
    ServerDeviceFailure => 4
    Acknowledge => 5
    ServerDeviceBusy => 6
    NegativeAcknowledge => 7
    MemoryParityError => 8
    GatewayPathUnavailable => 10
    GatewayTargetFailedToRespond => 11
    UnknownException(value) => value
  }
}

///|
/// Errors produced by the protocol core and its in-memory services.
pub(all) enum ModbusError {
  Incomplete
  InvalidLength
  InvalidChecksum
  InvalidAscii
  InvalidMbap
  InvalidUnitId
  InvalidFunction
  InvalidAddress
  InvalidQuantity
  InvalidByteCount
  InvalidData
  InvalidTransaction
  Unsupported
  UnitMismatch
  CapacityExceeded
  NoResponse
  Busy
} derive(Debug, Eq)

///|
/// Convert a protocol error to a stable diagnostic name.
pub fn error_name(error : ModbusError) -> String {
  match error {
    Incomplete => "incomplete"
    InvalidLength => "invalid-length"
    InvalidChecksum => "invalid-checksum"
    InvalidAscii => "invalid-ascii"
    InvalidMbap => "invalid-mbap"
    InvalidUnitId => "invalid-unit-id"
    InvalidFunction => "invalid-function"
    InvalidAddress => "invalid-address"
    InvalidQuantity => "invalid-quantity"
    InvalidByteCount => "invalid-byte-count"
    InvalidData => "invalid-data"
    InvalidTransaction => "invalid-transaction"
    Unsupported => "unsupported"
    UnitMismatch => "unit-mismatch"
    CapacityExceeded => "capacity-exceeded"
    NoResponse => "no-response"
    Busy => "busy"
  }
}

///|
/// A Modbus application data unit without transport framing.
pub(all) struct Pdu {
  function : Byte
  data : Array[Byte]
}

///|
pub fn Pdu::new(function : Byte, data : Array[Byte]) -> Pdu {
  { function, data }
}

///|
pub fn Pdu::function_code(self : Pdu) -> FunctionCode {
  function_code(self.function)
}

///|
pub fn Pdu::data_length(self : Pdu) -> Int {
  self.data.length()
}

///|
pub fn Pdu::is_exception(self : Pdu) -> Bool {
  (self.function & 0x80) != 0
}

///|
/// A decoded frame, including the unit identifier.
pub(all) struct Frame {
  unit_id : Byte
  pdu : Pdu
}

///|
pub fn Frame::new(unit_id : Byte, function : Byte, data : Array[Byte]) -> Frame {
  { unit_id, pdu: { function, data } }
}

///|
pub fn Frame::function_code(self : Frame) -> FunctionCode {
  self.pdu.function_code()
}

///|
pub fn Frame::is_exception(self : Frame) -> Bool {
  self.pdu.is_exception()
}

///|
pub fn Frame::data_length(self : Frame) -> Int {
  self.pdu.data_length()
}

///|
/// True when the frame uses the serial-line broadcast address.
pub fn is_broadcast(unit_id : Byte) -> Bool {
  unit_id == 0
}

///|
/// True for a legal serial-line unit identifier, including broadcast.
pub fn is_valid_unit_id(unit_id : Byte, broadcast? : Bool = true) -> Bool {
  (broadcast && unit_id == 0) || (unit_id >= 1 && unit_id <= 247)
}

///|
/// A request/response pair associated with a TCP transaction identifier.
pub(all) struct TransactionFrame {
  transaction_id : UInt16
  request : Frame
  response : Frame?
}

///|
pub fn TransactionFrame::new(
  transaction_id : UInt16,
  request : Frame,
) -> TransactionFrame {
  { transaction_id, request, response: None }
}

///|
pub fn TransactionFrame::complete(
  self : TransactionFrame,
  response : Frame,
) -> TransactionFrame {
  { ..self, response: Some(response) }
}

///|
pub fn TransactionFrame::is_complete(self : TransactionFrame) -> Bool {
  self.response is Some(_)
}

///|
/// A compact description useful for logs and metrics.
pub(all) struct FrameSummary {
  unit_id : Byte
  function : Byte
  data_length : Int
  exception : Bool
}

///|
pub fn summarize(frame : Frame) -> FrameSummary {
  {
    unit_id: frame.unit_id,
    function: frame.pdu.function,
    data_length: frame.pdu.data.length(),
    exception: frame.pdu.is_exception(),
  }
}

///|
/// A raw PDU result that retains whether a response was an exception.
pub(all) enum ResponseValue {
  Normal(Frame)
  Exception(Frame, ExceptionCode)
}

///|
pub fn response_value(frame : Frame) -> ResponseValue {
  if frame.is_exception() && frame.pdu.data.length() == 1 {
    Exception(frame, exception_code(frame.pdu.data[0]))
  } else {
    Normal(frame)
  }
}

///|
/// A small immutable configuration snapshot used by clients and servers.
pub(all) struct ProtocolLimits {
  max_adu : Int
  max_pdu : Int
  max_registers : Int
  max_coils : Int
  max_buffer : Int
}

///|
pub fn default_limits() -> ProtocolLimits {
  {
    max_adu: 260,
    max_pdu: 253,
    max_registers: 125,
    max_coils: 2000,
    max_buffer: 4096,
  }
}

///|
pub fn limits_for(mode : Mode) -> ProtocolLimits {
  match mode {
    Rtu => default_limits()
    Ascii => { ..default_limits(), max_adu: 513 }
    Tcp => { ..default_limits(), max_adu: 260 }
  }
}

///|
/// A request for reading holding registers (function 03).
pub fn read_holding(
  unit_id : Byte,
  address : UInt16,
  quantity : UInt16,
) -> Frame {
  {
    unit_id,
    pdu: {
      function: 3,
      data: [
        address.shr(8).to_byte(),
        address.to_byte(),
        quantity.shr(8).to_byte(),
        quantity.to_byte(),
      ],
    },
  }
}

///|
/// Checked variant of `read_holding` for application code.
pub fn read_holding_checked(
  unit_id : Byte,
  address : UInt16,
  quantity : UInt16,
) -> Result[Frame, ModbusError] {
  if !is_valid_unit_id(unit_id) {
    Err(InvalidUnitId)
  } else if quantity < 1 || quantity > 125 {
    Err(InvalidQuantity)
  } else if !valid_address_range(address, quantity.to_int()) {
    Err(InvalidAddress)
  } else {
    Ok(read_holding(unit_id, address, quantity))
  }
}

///|
/// A request for writing one holding register (function 06).
pub fn write_single_register(
  unit_id : Byte,
  address : UInt16,
  value : UInt16,
) -> Frame {
  {
    unit_id,
    pdu: {
      function: 6,
      data: [
        address.shr(8).to_byte(),
        address.to_byte(),
        value.shr(8).to_byte(),
        value.to_byte(),
      ],
    },
  }
}

///|
/// Decode a signed 16-bit register using two's-complement representation.
pub fn signed_register(value : UInt16) -> Int {
  let n = value.to_int()
  if n >= 32768 {
    n - 65536
  } else {
    n
  }
}

///|
/// Decode two registers as the raw IEEE-754 single precision words.
pub fn float_words(high : UInt16, low : UInt16) -> UInt64 {
  (high.to_uint64() << 16) + low.to_uint64()
}

///|
/// Decode the data portion of a register-oriented frame without a byte count.
pub fn registers(frame : Frame) -> Result[Array[UInt16], ModbusError] {
  if frame.pdu.data.length() % 2 != 0 {
    return Err(InvalidLength)
  }
  let out : Array[UInt16] = []
  for i in 0..<(frame.pdu.data.length() / 2) {
    out.push(
      frame.pdu.data[i * 2].to_uint16().shl(8) +
      frame.pdu.data[i * 2 + 1].to_uint16(),
    )
  }
  Ok(out)
}