///|
/// A response returned by a virtual or embedded device service.
pub(all) enum DeviceResponse {
  Reply(Frame)
  NoReply
}

///|
/// A protocol-level Modbus device backed by four bounded data tables.
pub struct Device {
  unit_id : Byte
  memory : DeviceMemory
  file_store : FileRecordStore
  mut server_id : Array[Byte]
  mut event_count : UInt16
  mut busy : Bool
  mut exception_count : Int
}

///|
/// Create a device with configurable table capacities.
pub fn Device::new(
  unit_id : Byte,
  coil_capacity? : Int = 2000,
  register_capacity? : Int = 125,
) -> Result[Device, ModbusError] {
  if !is_valid_unit_id(unit_id, broadcast=false) {
    return Err(InvalidUnitId)
  }
  match
    (
      DeviceMemory::new(coil_capacity~, register_capacity~),
      FileRecordStore::new(),
    ) {
    (Ok(memory), Ok(file_store)) =>
      Ok({
        unit_id,
        memory,
        file_store,
        server_id: [1, 0, 0],
        event_count: 0,
        busy: false,
        exception_count: 0,
      })
    (Err(error), _) => Err(error)
    (_, Err(error)) => Err(error)
  }
}

///|
pub fn Device::unit_id(self : Device) -> Byte {
  self.unit_id
}

///|
pub fn Device::memory(self : Device) -> DeviceMemory {
  self.memory
}

///|
pub fn Device::file_store(self : Device) -> FileRecordStore {
  self.file_store
}

///|
pub fn Device::event_count(self : Device) -> UInt16 {
  self.event_count
}

///|
pub fn Device::exception_count(self : Device) -> Int {
  self.exception_count
}

///|
pub fn Device::set_busy(self : Device, busy : Bool) -> Unit {
  self.busy = busy
}

///|
pub fn Device::is_busy(self : Device) -> Bool {
  self.busy
}

///|
pub fn Device::set_server_id(
  self : Device,
  values : Array[Byte],
) -> Result[Unit, ModbusError] {
  if values.length() < 1 || values.length() > 245 {
    Err(InvalidLength)
  } else {
    self.server_id = copy_bytes(values)
    Ok(())
  }
}

///|
pub fn Device::server_id(self : Device) -> Array[Byte] {
  copy_bytes(self.server_id)
}

///|
/// Handle a request and return a normal or exception response.
pub fn Device::handle(
  self : Device,
  request : Frame,
) -> Result[Frame, ModbusError] {
  if request.unit_id != self.unit_id && request.unit_id != 0 {
    return Err(UnitMismatch)
  }
  if request.unit_id == 0 && is_read_request(request.pdu.function) {
    return Err(NoResponse)
  }
  match validate_pdu(request.pdu) {
    Err(error) => {
      self.exception_count += 1
      Ok(error_response(request, error))
    }
    Ok(_) => {
      if self.busy && !is_write_request(request.pdu.function) {
        self.exception_count += 1
        return Ok(
          typed_exception_response(
            request.unit_id,
            request.pdu.function,
            ServerDeviceBusy,
          ),
        )
      }
      match self.handle_valid(request) {
        Ok(response) => {
          self.event_count += 1
          if request.unit_id == 0 {
            Err(NoResponse)
          } else {
            Ok(response)
          }
        }
        Err(error) => {
          self.exception_count += 1
          Ok(error_response(request, error))
        }
      }
    }
  }
}

///|
/// Handle a request while making broadcast no-response semantics explicit.
pub fn Device::serve(
  self : Device,
  request : Frame,
) -> Result[DeviceResponse, ModbusError] {
  match self.handle(request) {
    Ok(response) => Ok(Reply(response))
    Err(NoResponse) => Ok(NoReply)
    Err(error) => Err(error)
  }
}

///|
fn is_read_request(function : Byte) -> Bool {
  function == 1 ||
  function == 2 ||
  function == 3 ||
  function == 4 ||
  function == 7 ||
  function == 8 ||
  function == 11 ||
  function == 12 ||
  function == 17 ||
  function == 20 ||
  function == 24 ||
  function == 43
}

///|
fn is_write_request(function : Byte) -> Bool {
  function == 5 ||
  function == 6 ||
  function == 15 ||
  function == 16 ||
  function == 21 ||
  function == 22 ||
  function == 23
}

///|
fn Device::handle_valid(
  self : Device,
  request : Frame,
) -> Result[Frame, ModbusError] {
  match function_code(request.pdu.function) {
    ReadCoils => self.handle_read_coils(request, false)
    ReadDiscreteInputs => self.handle_read_coils(request, true)
    ReadHoldingRegisters => self.handle_read_registers(request, false)
    ReadInputRegisters => self.handle_read_registers(request, true)
    WriteSingleCoil => self.handle_write_single_coil(request)
    WriteSingleRegister => self.handle_write_single_register(request)
    WriteMultipleCoils => self.handle_write_multiple_coils(request)
    WriteMultipleRegisters => self.handle_write_multiple_registers(request)
    MaskWriteRegister => self.handle_mask_write(request)
    ReadWriteMultipleRegisters => self.handle_read_write(request)
    ReadFileRecord => self.handle_read_file_record(request)
    WriteFileRecord => self.handle_write_file_record(request)
    ReadExceptionStatus => Ok(response_frame(request, [0]))
    Diagnostics => Ok(response_frame(request, copy_bytes(request.pdu.data)))
    GetCommEventCounter =>
      Ok({
        unit_id: request.unit_id,
        pdu: {
          function: 11,
          data: [
            (self.event_count >> 8).to_byte(),
            self.event_count.to_byte(),
            (self.event_count >> 8).to_byte(),
            self.event_count.to_byte(),
          ],
        },
      })
    GetCommEventLog => Ok(response_frame(request, [0, 0, 0, 0, 0, 0, 0]))
    ReportServerId => Ok(response_frame(request, self.server_payload()))
    ReadFifoQueue => Ok(response_frame(request, [0, 2, 0, 0]))
    _ => Err(Unsupported)
  }
}

///|
fn Device::handle_read_coils(
  self : Device,
  request : Frame,
  discrete : Bool,
) -> Result[Frame, ModbusError] {
  let address = (request.pdu.data[0].to_uint16() << 8) |
    request.pdu.data[1].to_uint16()
  let quantity = ((request.pdu.data[2].to_uint16() << 8) |
  request.pdu.data[3].to_uint16()).to_int()
  let values = if discrete {
    self.memory.discrete_inputs().read(address, quantity)
  } else {
    self.memory.coils().read(address, quantity)
  }
  match values {
    Err(error) => Err(error)
    Ok(bits) => {
      let packed = pack_bits(bits)
      let data : Array[Byte] = [packed.length().to_byte()]
      for byte in packed {
        data.push(byte)
      }
      Ok(response_frame(request, data))
    }
  }
}

///|
fn Device::handle_read_registers(
  self : Device,
  request : Frame,
  input : Bool,
) -> Result[Frame, ModbusError] {
  let address = (request.pdu.data[0].to_uint16() << 8) |
    request.pdu.data[1].to_uint16()
  let quantity = ((request.pdu.data[2].to_uint16() << 8) |
  request.pdu.data[3].to_uint16()).to_int()
  let values = if input {
    self.memory.input_registers().read(address, quantity)
  } else {
    self.memory.holding_registers().read(address, quantity)
  }
  match values {
    Err(error) => Err(error)
    Ok(registers) => {
      let bytes = registers_to_bytes(registers, BigEndian)
      let data : Array[Byte] = [bytes.length().to_byte()]
      for byte in bytes {
        data.push(byte)
      }
      Ok(response_frame(request, data))
    }
  }
}

///|
fn Device::handle_write_single_coil(
  self : Device,
  request : Frame,
) -> Result[Frame, ModbusError] {
  let address = (request.pdu.data[0].to_uint16() << 8) |
    request.pdu.data[1].to_uint16()
  let value = request.pdu.data[2] == 0xFF && request.pdu.data[3] == 0
  match self.memory.coils().set(address, value) {
    Ok(_) => Ok(response_frame(request, copy_bytes(request.pdu.data)))
    Err(error) => Err(error)
  }
}

///|
fn Device::handle_write_single_register(
  self : Device,
  request : Frame,
) -> Result[Frame, ModbusError] {
  let address = (request.pdu.data[0].to_uint16() << 8) |
    request.pdu.data[1].to_uint16()
  let value = (request.pdu.data[2].to_uint16() << 8) |
    request.pdu.data[3].to_uint16()
  match self.memory.holding_registers().set(address, value) {
    Ok(_) => Ok(response_frame(request, copy_bytes(request.pdu.data)))
    Err(error) => Err(error)
  }
}

///|
fn Device::handle_write_multiple_coils(
  self : Device,
  request : Frame,
) -> Result[Frame, ModbusError] {
  let address = (request.pdu.data[0].to_uint16() << 8) |
    request.pdu.data[1].to_uint16()
  let quantity = ((request.pdu.data[2].to_uint16() << 8) |
  request.pdu.data[3].to_uint16()).to_int()
  let bytes : Array[Byte] = []
  for index in 5.. value
    Err(error) => return Err(error)
  }
  match self.memory.coils().write(address, values) {
    Ok(_) =>
      Ok({
        unit_id: request.unit_id,
        pdu: {
          function: 15,
          data: [
            request.pdu.data[0],
            request.pdu.data[1],
            request.pdu.data[2],
            request.pdu.data[3],
          ],
        },
      })
    Err(error) => Err(error)
  }
}

///|
fn Device::handle_write_multiple_registers(
  self : Device,
  request : Frame,
) -> Result[Frame, ModbusError] {
  let address = (request.pdu.data[0].to_uint16() << 8) |
    request.pdu.data[1].to_uint16()
  let bytes : Array[Byte] = []
  for index in 5.. value
    Err(error) => return Err(error)
  }
  match self.memory.holding_registers().write(address, values) {
    Ok(_) =>
      Ok({
        unit_id: request.unit_id,
        pdu: {
          function: 16,
          data: [
            request.pdu.data[0],
            request.pdu.data[1],
            request.pdu.data[2],
            request.pdu.data[3],
          ],
        },
      })
    Err(error) => Err(error)
  }
}

///|
fn Device::handle_mask_write(
  self : Device,
  request : Frame,
) -> Result[Frame, ModbusError] {
  let address = (request.pdu.data[0].to_uint16() << 8) |
    request.pdu.data[1].to_uint16()
  let and_mask = (request.pdu.data[2].to_uint16() << 8) |
    request.pdu.data[3].to_uint16()
  let or_mask = (request.pdu.data[4].to_uint16() << 8) |
    request.pdu.data[5].to_uint16()
  match self.memory.holding_registers().write_mask(address, and_mask, or_mask) {
    Ok(_) => Ok(response_frame(request, copy_bytes(request.pdu.data)))
    Err(error) => Err(error)
  }
}

///|
fn Device::handle_read_write(
  self : Device,
  request : Frame,
) -> Result[Frame, ModbusError] {
  let read_address = (request.pdu.data[0].to_uint16() << 8) |
    request.pdu.data[1].to_uint16()
  let read_quantity = ((request.pdu.data[2].to_uint16() << 8) |
  request.pdu.data[3].to_uint16()).to_int()
  let write_address = (request.pdu.data[4].to_uint16() << 8) |
    request.pdu.data[5].to_uint16()
  let bytes : Array[Byte] = []
  for index in 9.. value
    Err(error) => return Err(error)
  }
  match self.memory.holding_registers().write(write_address, values) {
    Err(error) => Err(error)
    Ok(_) =>
      match self.memory.holding_registers().read(read_address, read_quantity) {
        Err(error) => Err(error)
        Ok(result) => {
          let payload = registers_to_bytes(result, BigEndian)
          let data : Array[Byte] = [payload.length().to_byte()]
          for byte in payload {
            data.push(byte)
          }
          Ok(response_frame(request, data))
        }
      }
  }
}

///|
fn Device::server_payload(self : Device) -> Array[Byte] {
  let data : Array[Byte] = []
  for byte in self.server_id {
    data.push(byte)
  }
  data.push(0xFF)
  data
}

///|
/// A compact device health snapshot for dashboards and polling logs.
pub(all) struct DeviceHealth {
  unit_id : Byte
  event_count : UInt16
  exception_count : Int
  busy : Bool
}

///|
pub fn Device::health(self : Device) -> DeviceHealth {
  {
    unit_id: self.unit_id,
    event_count: self.event_count,
    exception_count: self.exception_count,
    busy: self.busy,
  }
}