///|
/// Contract check result for a request/response pair.
pub(all) enum ContractResult {
  ContractPass
  ContractUnitMismatch
  ContractFunctionMismatch
  ContractLengthMismatch
  ContractDataMismatch
  ContractException
} derive(Debug, Eq)

///|
/// Expected shape of one application data unit.
pub(all) struct FrameContract {
  id : String
  function : Byte
  request_min : Int
  request_max : Int
  response_min : Int
  response_max : Int
  require_echo : Bool
}

///|
pub fn FrameContract::new(
  id : String,
  function : Byte,
  request_min : Int,
  request_max : Int,
  response_min : Int,
  response_max : Int,
  require_echo? : Bool = false,
) -> Result[FrameContract, ModbusError] {
  if id == "" ||
    function == 0 ||
    request_min < 0 ||
    response_min < 0 ||
    request_max < request_min ||
    response_max < response_min {
    return Err(InvalidData)
  }
  Ok({
    id,
    function,
    request_min,
    request_max,
    response_min,
    response_max,
    require_echo,
  })
}

///|
pub fn FrameContract::id(self : FrameContract) -> String {
  self.id
}

///|
pub fn FrameContract::function(self : FrameContract) -> Byte {
  self.function
}

///|
pub fn FrameContract::request_min(self : FrameContract) -> Int {
  self.request_min
}

///|
pub fn FrameContract::request_max(self : FrameContract) -> Int {
  self.request_max
}

///|
pub fn FrameContract::response_min(self : FrameContract) -> Int {
  self.response_min
}

///|
pub fn FrameContract::response_max(self : FrameContract) -> Int {
  self.response_max
}

///|
pub fn FrameContract::require_echo(self : FrameContract) -> Bool {
  self.require_echo
}

///|
/// Validate a response against this function-level contract.
pub fn FrameContract::check(
  self : FrameContract,
  request : Frame,
  response : Frame,
) -> ContractResult {
  if response.is_exception() {
    return ContractException
  }
  if request.unit_id != response.unit_id {
    return ContractUnitMismatch
  }
  if response.pdu.function != self.function {
    return ContractFunctionMismatch
  }
  if request.pdu.data.length() < self.request_min ||
    request.pdu.data.length() > self.request_max {
    return ContractLengthMismatch
  }
  if response.pdu.data.length() < self.response_min ||
    response.pdu.data.length() > self.response_max {
    return ContractLengthMismatch
  }
  if self.require_echo {
    let overlap = if request.pdu.data.length() < response.pdu.data.length() {
      request.pdu.data.length()
    } else {
      response.pdu.data.length()
    }
    for i in 0.. String {
  match result {
    ContractPass => "pass"
    ContractUnitMismatch => "unit-mismatch"
    ContractFunctionMismatch => "function-mismatch"
    ContractLengthMismatch => "length-mismatch"
    ContractDataMismatch => "data-mismatch"
    ContractException => "exception"
  }
}

///|
/// A catalog of frame contracts used by server and gateway adapters.
pub(all) struct FrameContractCatalog {
  contracts : Array[FrameContract]
  max_contracts : Int
}

///|
pub fn FrameContractCatalog::new(
  max_contracts? : Int = 64,
) -> Result[FrameContractCatalog, ModbusError] {
  if max_contracts < 1 {
    Err(InvalidData)
  } else {
    Ok({ contracts: [], max_contracts })
  }
}

///|
pub fn FrameContractCatalog::count(self : FrameContractCatalog) -> Int {
  self.contracts.length()
}

///|
pub fn FrameContractCatalog::add(
  self : FrameContractCatalog,
  contract : FrameContract,
) -> Result[Unit, ModbusError] {
  if self.contracts.length() >= self.max_contracts {
    return Err(CapacityExceeded)
  }
  for existing in self.contracts {
    if existing.id() == contract.id() ||
      existing.function() == contract.function() {
      return Err(InvalidData)
    }
  }
  self.contracts.push(contract)
  Ok(())
}

///|
pub fn FrameContractCatalog::find(
  self : FrameContractCatalog,
  function : Byte,
) -> FrameContract? {
  for contract in self.contracts {
    if contract.function() == function {
      return Some(contract)
    }
  }
  None
}

///|
pub fn FrameContractCatalog::check(
  self : FrameContractCatalog,
  request : Frame,
  response : Frame,
) -> Result[ContractResult, ModbusError] {
  match self.find(request.pdu.function) {
    Some(contract) => Ok(contract.check(request, response))
    None => Err(Unsupported)
  }
}

///|
/// Check all known standard functions and return the first failure.
pub fn FrameContractCatalog::check_all(
  self : FrameContractCatalog,
  pairs : Array[(Frame, Frame)],
) -> Array[ContractResult] {
  let result : Array[ContractResult] = []
  for pair in pairs {
    match self.check(pair.0, pair.1) {
      Ok(value) => result.push(value)
      Err(_) => result.push(ContractFunctionMismatch)
    }
  }
  result
}

///|
/// Build a conservative catalog for the common Modbus function codes.
pub fn standard_frame_contracts() -> FrameContractCatalog {
  let catalog = match FrameContractCatalog::new() {
    Ok(value) => value
    Err(_) => panic()
  }
  let definitions : Array[FrameContract] = [
    frame_contract_or_panic(FrameContract::new("read-coils", 1, 4, 4, 1, 250)),
    frame_contract_or_panic(
      FrameContract::new("read-discrete-inputs", 2, 4, 4, 1, 250),
    ),
    frame_contract_or_panic(FrameContract::new("read-holding", 3, 4, 4, 1, 250)),
    frame_contract_or_panic(FrameContract::new("read-input", 4, 4, 4, 1, 250)),
    frame_contract_or_panic(
      FrameContract::new("write-coil", 5, 4, 4, 4, 4, require_echo=true),
    ),
    frame_contract_or_panic(
      FrameContract::new("write-register", 6, 4, 4, 4, 4, require_echo=true),
    ),
    frame_contract_or_panic(FrameContract::new("write-coils", 15, 5, 255, 4, 4)),
    frame_contract_or_panic(
      FrameContract::new("write-registers", 16, 6, 251, 4, 4),
    ),
  ]
  for contract in definitions {
    ignore(catalog.add(contract))
  }
  catalog
}

///|
fn frame_contract_or_panic(
  result : Result[FrameContract, ModbusError],
) -> FrameContract {
  match result {
    Ok(value) => value
    Err(_) => panic()
  }
}