///|
/// Serial-line parameters independent of any concrete serial-port library.
pub(all) struct SerialConfig {
  path : String
  baud : Int
  data_bits : Int
  stop_bits : Int
  parity : Byte
  mode : Mode
}

///|
pub fn SerialConfig::new(
  path : String,
  baud : Int,
  mode : Mode,
  data_bits? : Int = 8,
  stop_bits? : Int = 1,
  parity? : Byte = 0,
) -> Result[SerialConfig, ModbusError] {
  if path.length() == 0 ||
    baud < 300 ||
    data_bits < 5 ||
    data_bits > 8 ||
    stop_bits < 1 ||
    stop_bits > 2 ||
    parity > 2 ||
    !transport_is_serial(mode) {
    Err(InvalidData)
  } else {
    Ok({ path, baud, data_bits, stop_bits, parity, mode })
  }
}

///|
pub fn SerialConfig::parity_name(self : SerialConfig) -> String {
  match self.parity {
    0 => "none"
    1 => "odd"
    _ => "even"
  }
}

///|
/// TCP endpoint settings.
pub(all) struct TcpConfig {
  host : String
  port : Int
  connect_timeout : Int
  keep_alive : Bool
}

///|
pub fn TcpConfig::new(
  host : String,
  port : Int,
  connect_timeout? : Int = 5000,
  keep_alive? : Bool = true,
) -> Result[TcpConfig, ModbusError] {
  if host.length() == 0 || port < 1 || port > 65535 || connect_timeout < 1 {
    Err(InvalidData)
  } else {
    Ok({ host, port, connect_timeout, keep_alive })
  }
}

///|
pub fn TcpConfig::address(self : TcpConfig) -> String {
  self.host + ":" + self.port.to_string()
}

///|
/// A transport endpoint configuration.
pub(all) enum EndpointConfig {
  Serial(SerialConfig)
  Tcp(TcpConfig)
}

///|
pub fn EndpointConfig::mode(config : EndpointConfig) -> Mode {
  match config {
    Serial(value) => value.mode
    Tcp(_) => Tcp
  }
}

///|
pub fn EndpointConfig::name(config : EndpointConfig) -> String {
  match config {
    Serial(value) => value.path
    Tcp(value) => value.address()
  }
}

///|
pub fn validate_endpoint(config : EndpointConfig) -> Result[Unit, ModbusError] {
  match config {
    Serial(value) =>
      if value.path.length() > 0 {
        Ok(())
      } else {
        Err(InvalidData)
      }
    Tcp(value) =>
      if value.port > 0 && value.port <= 65535 {
        Ok(())
      } else {
        Err(InvalidData)
      }
  }
}

///|
/// Lifecycle state of a transport endpoint.
pub(all) enum EndpointState {
  Closed
  Connecting
  Ready
  Backoff
  Faulted
} derive(Debug, Eq)

///|
/// Runtime state tracked independently from operating-system handles.
pub(all) struct EndpointRuntime {
  config : EndpointConfig
  mut state : EndpointState
  mut opened_at : Int
  mut last_error : ModbusError?
  mut reconnects : Int
  mut frames_in : Int
  mut frames_out : Int
}

///|
pub fn EndpointRuntime::new(
  config : EndpointConfig,
) -> Result[EndpointRuntime, ModbusError] {
  match validate_endpoint(config) {
    Err(error) => Err(error)
    Ok(_) =>
      Ok({
        config,
        state: Closed,
        opened_at: 0,
        last_error: None,
        reconnects: 0,
        frames_in: 0,
        frames_out: 0,
      })
  }
}

///|
pub fn EndpointRuntime::open(
  self : EndpointRuntime,
  now : Int,
) -> Result[Unit, ModbusError] {
  if now < 0 || self.state == Ready || self.state == Connecting {
    return Err(Busy)
  }
  self.state = Connecting
  self.opened_at = now
  self.state = Ready
  self.reconnects += 1
  self.last_error = None
  Ok(())
}

///|
pub fn EndpointRuntime::close(self : EndpointRuntime) -> Unit {
  self.state = Closed
}

///|
pub fn EndpointRuntime::fail(
  self : EndpointRuntime,
  error : ModbusError,
) -> Unit {
  self.state = Faulted
  self.last_error = Some(error)
}

///|
pub fn EndpointRuntime::backoff(self : EndpointRuntime) -> Unit {
  self.state = Backoff
}

///|
pub fn EndpointRuntime::state(self : EndpointRuntime) -> EndpointState {
  self.state
}

///|
pub fn EndpointRuntime::config(self : EndpointRuntime) -> EndpointConfig {
  self.config
}

///|
pub fn EndpointRuntime::record_in(
  self : EndpointRuntime,
) -> Result[Unit, ModbusError] {
  if self.state != Ready {
    Err(NoResponse)
  } else {
    self.frames_in += 1
    Ok(())
  }
}

///|
pub fn EndpointRuntime::record_out(
  self : EndpointRuntime,
) -> Result[Unit, ModbusError] {
  if self.state != Ready {
    Err(NoResponse)
  } else {
    self.frames_out += 1
    Ok(())
  }
}

///|
pub fn EndpointRuntime::uptime(self : EndpointRuntime, now : Int) -> Int {
  if self.state == Ready && now >= self.opened_at {
    now - self.opened_at
  } else {
    0
  }
}

///|
pub fn EndpointRuntime::reconnects(self : EndpointRuntime) -> Int {
  self.reconnects
}

///|
pub fn EndpointRuntime::frames_in(self : EndpointRuntime) -> Int {
  self.frames_in
}

///|
pub fn EndpointRuntime::frames_out(self : EndpointRuntime) -> Int {
  self.frames_out
}

///|
pub fn EndpointRuntime::last_error(self : EndpointRuntime) -> ModbusError? {
  self.last_error
}