///|
/// Local Modbus TCP/RTU framing simulation for W2B.

///|
pub(all) enum ModbusPduKind {
  ReadHoldingRegisters
  ReadInputRegisters
  WriteSingleHoldingRegister
  WriteMultipleHoldingRegisters
} derive(Eq, Debug)

///|
pub fn ModbusPduKind::function_code(self : ModbusPduKind) -> Int {
  match self {
    ReadHoldingRegisters => 3
    ReadInputRegisters => 4
    WriteSingleHoldingRegister => 6
    WriteMultipleHoldingRegisters => 16
  }
}

///|
pub(all) struct ModbusApplicationPdu {
  kind : ModbusPduKind
  address : UInt
  quantity : Int
  value_digest : Int
} derive(Eq, Debug)

///|
pub fn ModbusApplicationPdu::request_length(self : ModbusApplicationPdu) -> Int {
  match self.kind {
    ReadHoldingRegisters | ReadInputRegisters | WriteSingleHoldingRegister => 5
    WriteMultipleHoldingRegisters => 6 + self.quantity * 2
  }
}

///|
pub fn ModbusApplicationPdu::digest(self : ModbusApplicationPdu) -> Int {
  (
    (self.kind.function_code() * 131 + self.address.reinterpret_as_int()) * 131 +
    self.quantity
  ) *
  131 +
  self.value_digest
}

///|
pub(all) struct ModbusMbapEnvelope {
  transaction_id : Int
  protocol_id : Int
  length : Int
  unit_id : Int
} derive(Eq, Debug)

///|
pub fn ModbusMbapEnvelope::for_pdu(
  transaction_id~ : Int,
  unit_id~ : Int,
  pdu : ModbusApplicationPdu,
) -> ModbusMbapEnvelope {
  { transaction_id, protocol_id: 0, length: 1 + pdu.request_length(), unit_id }
}

///|
pub fn ModbusMbapEnvelope::length_matches(
  self : ModbusMbapEnvelope,
  pdu : ModbusApplicationPdu,
) -> Bool {
  self.protocol_id == 0 && self.length == 1 + pdu.request_length()
}

///|
pub(all) struct ModbusTcpFrame {
  mbap : ModbusMbapEnvelope
  pdu : ModbusApplicationPdu
  payload_digest : Int
} derive(Eq, Debug)

///|
pub fn ModbusTcpFrame::make(
  transaction_id~ : Int,
  unit_id~ : Int,
  pdu~ : ModbusApplicationPdu,
) -> ModbusTcpFrame {
  let mbap = ModbusMbapEnvelope::for_pdu(transaction_id~, unit_id~, pdu)
  { mbap, pdu, payload_digest: mbap.transaction_id * 257 + pdu.digest() }
}

///|
pub fn ModbusTcpFrame::mbap_valid(self : ModbusTcpFrame) -> Bool {
  self.mbap.length_matches(self.pdu)
}

///|
pub(all) struct ModbusRtuTiming {
  baud_rate : Int
  char_time_ns : Int64
  t35_ns : Int64
} derive(Eq, Debug)

///|
pub fn ModbusRtuTiming::for_baud(baud_rate? : Int = 19_200) -> ModbusRtuTiming {
  let baud_rate = if baud_rate <= 0 { 19_200 } else { baud_rate }
  let char_time_ns = 11_000_000_000L / Int64::from_int(baud_rate)
  { baud_rate, char_time_ns, t35_ns: char_time_ns * 35L / 10L }
}

///|
pub(all) struct ModbusRtuFrameBoundary {
  timing : ModbusRtuTiming
  silent_before_ns : Int64
  frame_time_ns : Int64
  silent_after_ns : Int64
} derive(Eq, Debug)

///|
pub fn ModbusRtuFrameBoundary::passes(self : ModbusRtuFrameBoundary) -> Bool {
  self.silent_before_ns >= self.timing.t35_ns &&
  self.silent_after_ns >= self.timing.t35_ns
}

///|
pub(all) struct ModbusRtuFrame {
  unit_id : Int
  pdu : ModbusApplicationPdu
  crc_placeholder : Int
  boundary : ModbusRtuFrameBoundary
} derive(Eq, Debug)

///|
pub fn ModbusRtuFrame::make(
  unit_id~ : Int,
  pdu~ : ModbusApplicationPdu,
  timing~ : ModbusRtuTiming,
  silent_before_ns~ : Int64,
  silent_after_ns~ : Int64,
) -> ModbusRtuFrame {
  {
    unit_id,
    pdu,
    crc_placeholder: (unit_id * 257 + pdu.digest()) & 0xffff,
    boundary: {
      timing,
      silent_before_ns,
      frame_time_ns: timing.char_time_ns *
      Int64::from_int(1 + pdu.request_length() + 2),
      silent_after_ns,
    },
  }
}

///|
pub fn ModbusRtuFrame::delimited(self : ModbusRtuFrame) -> Bool {
  self.boundary.passes()
}

///|
pub(all) struct ModbusFramingSimulationReport {
  tcp_mbap_valid : Bool
  rtu_t35_delimited : Bool
  p2p_event_count : Int
  fabric_event_count : Int
  p2p_replay_count : Int
  fabric_replay_count : Int
  p2p_digest : @core.SimDigest
  fabric_digest : @core.SimDigest
  live_device_evidence : Bool
} derive(Eq, Debug)

///|
pub fn ModbusFramingSimulationReport::passes(
  self : ModbusFramingSimulationReport,
) -> Bool {
  self.tcp_mbap_valid &&
  self.rtu_t35_delimited &&
  self.p2p_event_count == 6 &&
  self.fabric_event_count > self.p2p_event_count &&
  self.p2p_replay_count == self.p2p_event_count &&
  self.fabric_replay_count == self.fabric_event_count &&
  !self.live_device_evidence
}

///|
pub fn modbus_read_holding_pdu(
  address? : UInt = 0x0000U,
  quantity? : Int = 2,
) -> ModbusApplicationPdu {
  { kind: ReadHoldingRegisters, address, quantity, value_digest: 0 }
}

///|
pub fn modbus_read_input_pdu(
  address? : UInt = 0x0000U,
  quantity? : Int = 2,
) -> ModbusApplicationPdu {
  { kind: ReadInputRegisters, address, quantity, value_digest: 0 }
}

///|
pub fn modbus_tcp_read_holding_fixture(
  transaction_id? : Int = 1,
  unit_id? : Int = 1,
) -> ModbusTcpFrame {
  ModbusTcpFrame::make(transaction_id~, unit_id~, pdu=modbus_read_holding_pdu())
}

///|
pub fn modbus_rtu_read_input_fixture(
  unit_id? : Int = 1,
  baud_rate? : Int = 19_200,
) -> ModbusRtuFrame {
  let timing = ModbusRtuTiming::for_baud(baud_rate~)
  ModbusRtuFrame::make(
    unit_id~,
    pdu=modbus_read_input_pdu(),
    timing~,
    silent_before_ns=timing.t35_ns,
    silent_after_ns=timing.t35_ns,
  )
}

///|
pub fn modbus_rtu_boundary_fixture(
  baud_rate? : Int = 19_200,
) -> ModbusRtuFrameBoundary {
  let timing = ModbusRtuTiming::for_baud(baud_rate~)
  {
    timing,
    silent_before_ns: timing.t35_ns,
    frame_time_ns: timing.char_time_ns * 8L,
    silent_after_ns: timing.t35_ns,
  }
}

///|
pub fn modbus_p2p_framing_trace(seed? : Int = 52) -> @trace.TraceLog {
  let tcp = modbus_tcp_read_holding_fixture()
  let rtu = modbus_rtu_read_input_fixture()
  let log = @trace.TraceLog::new()
  append_modbus_frame_event(
    log,
    1,
    "modbus.tcp.mbap.tx",
    @trace.Tx,
    0L,
    tcp.payload_digest,
    "modbus.tcp.p2p",
    seed~,
  )
  append_modbus_frame_event(
    log,
    2,
    "modbus.tcp.mbap.rx",
    @trace.Rx,
    5L,
    tcp.payload_digest,
    "modbus.tcp.p2p",
    seed~,
  )
  append_modbus_frame_event(
    log,
    3,
    "modbus.rtu.t35.silent-before",
    @trace.Meta,
    rtu.boundary.timing.t35_ns,
    rtu.boundary.timing.t35_ns.to_int(),
    "modbus.rtu.p2p",
    seed~,
  )
  append_modbus_frame_event(
    log,
    4,
    "modbus.rtu.frame.tx",
    @trace.Tx,
    rtu.boundary.timing.t35_ns + 10L,
    rtu.crc_placeholder,
    "modbus.rtu.p2p",
    seed~,
  )
  append_modbus_frame_event(
    log,
    5,
    "modbus.rtu.t35.silent-after",
    @trace.Meta,
    rtu.boundary.timing.t35_ns + rtu.boundary.frame_time_ns,
    rtu.boundary.timing.t35_ns.to_int(),
    "modbus.rtu.p2p",
    seed~,
  )
  append_modbus_frame_event(
    log,
    6,
    "modbus.rtu.frame.rx",
    @trace.Rx,
    rtu.boundary.timing.t35_ns +
    rtu.boundary.frame_time_ns +
    rtu.boundary.timing.t35_ns,
    rtu.crc_placeholder,
    "modbus.rtu.p2p",
    seed~,
  )
  log
}

///|
pub fn modbus_fabric_framing_trace(seed? : Int = 52) -> @trace.TraceLog {
  let fabric = @sim.run_fabric_partition_heal(seed~)
  let tcp = modbus_tcp_read_holding_fixture(transaction_id=2, unit_id=1)
  let log = @trace.TraceLog::new()
  append_modbus_frame_event(
    log,
    1,
    "modbus.tcp.fabric.mbap.tx",
    @trace.Tx,
    0L,
    tcp.payload_digest,
    "modbus.tcp.fabric",
    seed~,
  )
  let mut event_id = 2
  for event in fabric.trace.events {
    append_fabric_envelope_event(log, event, event_id~, seed~)
    event_id += 1
  }
  append_modbus_frame_event(
    log,
    event_id,
    "modbus.tcp.fabric.mbap.rx",
    @trace.Rx,
    Int64::from_int(event_id) * 10L,
    tcp.payload_digest,
    "modbus.tcp.fabric",
    seed~,
  )
  log
}

///|
pub fn modbus_framing_simulation_report(
  seed? : Int = 52,
) -> ModbusFramingSimulationReport {
  let p2p = modbus_p2p_framing_trace(seed~)
  let fabric = modbus_fabric_framing_trace(seed~)
  {
    tcp_mbap_valid: modbus_tcp_read_holding_fixture().mbap_valid(),
    rtu_t35_delimited: modbus_rtu_read_input_fixture().delimited(),
    p2p_event_count: p2p.len(),
    fabric_event_count: fabric.len(),
    p2p_replay_count: replay_count(p2p),
    fabric_replay_count: replay_count(fabric),
    p2p_digest: p2p.portable_digest(seed~),
    fabric_digest: fabric.portable_digest(seed~),
    live_device_evidence: false,
  }
}

///|
fn append_modbus_frame_event(
  log : @trace.TraceLog,
  event_id : Int,
  label : String,
  direction : @trace.TraceDirection,
  time_ns : Int64,
  payload_digest : Int,
  medium_id : String,
  seed~ : Int,
) -> Unit {
  log.append(
    @trace.TraceEvent::make(
      event_id~,
      parent_id=None,
      vtime=@core.VTime::from_ns(time_ns),
      clock_domain="sim",
      raw_ns=time_ns,
      node_id="controller",
      medium_id~,
      channel_id=None,
      direction~,
      payload_digest=Some(payload_digest),
      rng_step=event_id,
      seed~,
      backend=@core.SimNative,
      label~,
    ),
  )
}

///|
fn append_fabric_envelope_event(
  log : @trace.TraceLog,
  event : @trace.TraceEvent,
  event_id~ : Int,
  seed~ : Int,
) -> Unit {
  let direction = match event.direction {
    @trace.Fault => @trace.Probe
    other => other
  }
  let time_ns = Int64::from_int(event_id) * 10L
  log.append(
    @trace.TraceEvent::make(
      event_id~,
      parent_id=event.parent_id,
      vtime=@core.VTime::from_ns(time_ns),
      clock_domain="sim",
      raw_ns=time_ns,
      node_id="fabric",
      medium_id="modbus.tcp.fabric",
      channel_id=event.channel_id,
      direction~,
      payload_digest=event.payload_digest,
      rng_step=event_id,
      seed~,
      backend=@core.SimNative,
      label="modbus.tcp.fabric." + event.label,
    ),
  )
}

///|
fn replay_count(log : @trace.TraceLog) -> Int {
  let replay = @trace.Replay::from_log(log)
  let mut count = 0
  while !replay.is_exhausted() {
    ignore(replay.next_event())
    count += 1
  }
  count
}