///|
/// A contiguous, bounds-checked register bank.
pub(all) struct RegisterBank {
  start : UInt16
  values : Array[UInt16]
  writable : Bool
}

///|
/// Create a register bank in the 16-bit address space.
pub fn RegisterBank::new(
  start : UInt16,
  length : Int,
  writable? : Bool = true,
) -> Result[RegisterBank, ModbusError] {
  if length < 0 || start.to_int() + length > 65536 {
    return Err(CapacityExceeded)
  }
  let values : Array[UInt16] = []
  for _ in 0.. UInt16 {
  self.start
}

///|
pub fn RegisterBank::length(self : RegisterBank) -> Int {
  self.values.length()
}

///|
pub fn RegisterBank::end_exclusive(self : RegisterBank) -> Int {
  self.start.to_int() + self.values.length()
}

///|
pub fn RegisterBank::is_writable(self : RegisterBank) -> Bool {
  self.writable
}

///|
pub fn RegisterBank::contains(
  self : RegisterBank,
  address : UInt16,
  quantity : Int,
) -> Bool {
  quantity >= 0 &&
  address.to_int() >= self.start.to_int() &&
  address.to_int() + quantity <= self.end_exclusive()
}

///|
fn RegisterBank::index(
  self : RegisterBank,
  address : UInt16,
) -> Result[Int, ModbusError] {
  if !self.contains(address, 1) {
    Err(InvalidAddress)
  } else {
    Ok(address.to_int() - self.start.to_int())
  }
}

///|
pub fn RegisterBank::get(
  self : RegisterBank,
  address : UInt16,
) -> Result[UInt16, ModbusError] {
  match self.index(address) {
    Ok(index) => Ok(self.values[index])
    Err(error) => Err(error)
  }
}

///|
pub fn RegisterBank::set(
  self : RegisterBank,
  address : UInt16,
  value : UInt16,
) -> Result[Unit, ModbusError] {
  if !self.writable {
    return Err(Unsupported)
  }
  match self.index(address) {
    Ok(index) => {
      self.values[index] = value
      Ok(())
    }
    Err(error) => Err(error)
  }
}

///|
pub fn RegisterBank::read(
  self : RegisterBank,
  address : UInt16,
  quantity : Int,
) -> Result[Array[UInt16], ModbusError] {
  if quantity < 1 || quantity > 125 || !self.contains(address, quantity) {
    return Err(InvalidAddress)
  }
  let out : Array[UInt16] = []
  let start = address.to_int() - self.start.to_int()
  for index in start..<(start + quantity) {
    out.push(self.values[index])
  }
  Ok(out)
}

///|
pub fn RegisterBank::write(
  self : RegisterBank,
  address : UInt16,
  values : Array[UInt16],
) -> Result[Unit, ModbusError] {
  if !self.writable {
    return Err(Unsupported)
  }
  if values.length() < 1 ||
    values.length() > 123 ||
    !self.contains(address, values.length()) {
    return Err(InvalidAddress)
  }
  let start = address.to_int() - self.start.to_int()
  for index, value in values {
    self.values[start + index] = value
  }
  Ok(())
}

///|
pub fn RegisterBank::fill(self : RegisterBank, value : UInt16) -> Unit {
  for index in 0.. Unit {
  self.fill(0)
}

///|
pub fn RegisterBank::snapshot(self : RegisterBank) -> Array[UInt16] {
  copy_registers(self.values)
}

///|
pub fn RegisterBank::write_mask(
  self : RegisterBank,
  address : UInt16,
  and_mask : UInt16,
  or_mask : UInt16,
) -> Result[UInt16, ModbusError] {
  let current = match self.get(address) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let next = (current & and_mask) | (or_mask & (and_mask ^ 0xFFFF))
  match self.set(address, next) {
    Ok(_) => Ok(next)
    Err(error) => Err(error)
  }
}

///|
/// A bounded coil/discrete-input bank backed by a bit vector.
pub(all) struct CoilBank {
  start : UInt16
  bits : BitVector
  writable : Bool
}

///|
pub fn CoilBank::new(
  start : UInt16,
  length : Int,
  writable? : Bool = true,
) -> Result[CoilBank, ModbusError] {
  match BitVector::new(length) {
    Ok(bits) => Ok({ start, bits, writable })
    Err(error) => Err(error)
  }
}

///|
pub fn CoilBank::start(self : CoilBank) -> UInt16 {
  self.start
}

///|
pub fn CoilBank::length(self : CoilBank) -> Int {
  self.bits.length()
}

///|
pub fn CoilBank::is_writable(self : CoilBank) -> Bool {
  self.writable
}

///|
pub fn CoilBank::contains(
  self : CoilBank,
  address : UInt16,
  quantity : Int,
) -> Bool {
  quantity >= 0 &&
  address.to_int() >= self.start.to_int() &&
  address.to_int() + quantity <= self.start.to_int() + self.bits.length()
}

///|
pub fn CoilBank::get(
  self : CoilBank,
  address : UInt16,
) -> Result[Bool, ModbusError] {
  if !self.contains(address, 1) {
    Err(InvalidAddress)
  } else {
    self.bits.get(address.to_int() - self.start.to_int())
  }
}

///|
pub fn CoilBank::set(
  self : CoilBank,
  address : UInt16,
  value : Bool,
) -> Result[Unit, ModbusError] {
  if !self.writable {
    return Err(Unsupported)
  }
  if !self.contains(address, 1) {
    Err(InvalidAddress)
  } else {
    self.bits.set(address.to_int() - self.start.to_int(), value)
  }
}

///|
pub fn CoilBank::read(
  self : CoilBank,
  address : UInt16,
  quantity : Int,
) -> Result[Array[Bool], ModbusError] {
  if quantity < 1 || quantity > 2000 || !self.contains(address, quantity) {
    return Err(InvalidAddress)
  }
  let out : Array[Bool] = []
  let start = address.to_int() - self.start.to_int()
  for index in start..<(start + quantity) {
    out.push(self.bits.get(index).unwrap())
  }
  Ok(out)
}

///|
pub fn CoilBank::write(
  self : CoilBank,
  address : UInt16,
  values : Array[Bool],
) -> Result[Unit, ModbusError] {
  if !self.writable {
    return Err(Unsupported)
  }
  if values.length() < 1 ||
    values.length() > 1968 ||
    !self.contains(address, values.length()) {
    return Err(InvalidAddress)
  }
  let start = address.to_int() - self.start.to_int()
  for index, value in values {
    let _ = self.bits.set(start + index, value)
  }
  Ok(())
}

///|
pub fn CoilBank::fill(self : CoilBank, value : Bool) -> Unit {
  self.bits.fill(value)
}

///|
pub fn CoilBank::clear(self : CoilBank) -> Unit {
  self.fill(false)
}

///|
pub fn CoilBank::snapshot(self : CoilBank) -> Array[Bool] {
  self.bits.to_bits()
}

///|
pub fn CoilBank::snapshot_bytes(self : CoilBank) -> Array[Byte] {
  self.bits.to_bytes()
}

///|
/// The four standard Modbus data tables exposed by a device.
pub(all) struct DeviceMemory {
  coils : CoilBank
  discrete_inputs : CoilBank
  input_registers : RegisterBank
  holding_registers : RegisterBank
}

///|
/// Create a device memory map with all tables starting at address zero.
pub fn DeviceMemory::new(
  coil_capacity? : Int = 2000,
  register_capacity? : Int = 125,
) -> Result[DeviceMemory, ModbusError] {
  let coils = match CoilBank::new(0, coil_capacity, writable=true) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let discrete_inputs = match CoilBank::new(0, coil_capacity, writable=false) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let input_registers = match
    RegisterBank::new(0, register_capacity, writable=false) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let holding_registers = match
    RegisterBank::new(0, register_capacity, writable=true) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  Ok({ coils, discrete_inputs, input_registers, holding_registers })
}

///|
pub fn DeviceMemory::coils(self : DeviceMemory) -> CoilBank {
  self.coils
}

///|
pub fn DeviceMemory::discrete_inputs(self : DeviceMemory) -> CoilBank {
  self.discrete_inputs
}

///|
pub fn DeviceMemory::input_registers(self : DeviceMemory) -> RegisterBank {
  self.input_registers
}

///|
pub fn DeviceMemory::holding_registers(self : DeviceMemory) -> RegisterBank {
  self.holding_registers
}

///|
pub fn DeviceMemory::clear(self : DeviceMemory) -> Unit {
  self.coils.clear()
  self.discrete_inputs.clear()
  self.input_registers.clear()
  self.holding_registers.clear()
}

///|
pub fn DeviceMemory::load_holding(
  self : DeviceMemory,
  address : UInt16,
  values : Array[UInt16],
) -> Result[Unit, ModbusError] {
  self.holding_registers.write(address, values)
}

///|
pub fn DeviceMemory::load_input(
  self : DeviceMemory,
  address : UInt16,
  values : Array[UInt16],
) -> Result[Unit, ModbusError] {
  if !self.input_registers.contains(address, values.length()) {
    return Err(InvalidAddress)
  }
  let start = address.to_int() - self.input_registers.start().to_int()
  for index, value in values {
    self.input_registers.values[start + index] = value
  }
  Ok(())
}

///|
pub fn DeviceMemory::load_discrete(
  self : DeviceMemory,
  address : UInt16,
  values : Array[Bool],
) -> Result[Unit, ModbusError] {
  if !self.discrete_inputs.contains(address, values.length()) {
    return Err(InvalidAddress)
  }
  let start = address.to_int() - self.discrete_inputs.start().to_int()
  for index, value in values {
    let _ = self.discrete_inputs.bits.set(start + index, value)
  }
  Ok(())
}