///|
/// Local CANopen CAN frame and medium skeleton for W1B.

///|
pub(all) enum CanopenFrameKind {
  NmtFrame
  HeartbeatFrame
  SdoDownloadFrame
  SdoUploadFrame
  Rpdo1Frame
  Tpdo1Frame
  ErrorStateFrame
} derive(Eq, Debug)

///|
pub fn CanopenFrameKind::label(self : CanopenFrameKind) -> String {
  match self {
    NmtFrame => "nmt"
    HeartbeatFrame => "heartbeat"
    SdoDownloadFrame => "sdo-download"
    SdoUploadFrame => "sdo-upload"
    Rpdo1Frame => "rpdo1"
    Tpdo1Frame => "tpdo1"
    ErrorStateFrame => "error-state"
  }
}

///|
pub(all) struct CanopenFrameEnvelope {
  cob_id : Int
  node_id : Int
  dlc : Int
  rtr : Bool
  extended : Bool
  kind : CanopenFrameKind
  payload_digest : Int
} derive(Eq, Debug)

///|
pub fn CanopenFrameEnvelope::make(
  cob_id~ : Int,
  node_id~ : Int,
  dlc~ : Int,
  kind~ : CanopenFrameKind,
  payload_digest~ : Int,
) -> CanopenFrameEnvelope {
  { cob_id, node_id, dlc, rtr: false, extended: false, kind, payload_digest }
}

///|
pub fn CanopenFrameEnvelope::valid_standard_data_frame(
  self : CanopenFrameEnvelope,
) -> Bool {
  self.cob_id >= 0 &&
  self.cob_id <= 0x7ff &&
  self.dlc >= 0 &&
  self.dlc <= 8 &&
  !self.rtr &&
  !self.extended
}

///|
pub(all) struct CanopenFrame {
  envelope : CanopenFrameEnvelope
  object_index : UInt?
  subindex : Byte?
  label : String
} derive(Eq, Debug)

///|
pub fn CanopenFrame::valid(self : CanopenFrame) -> Bool {
  self.envelope.valid_standard_data_frame() && self.label != ""
}

///|
pub(all) struct CanopenSdoExpeditedFrame {
  frame : CanopenFrame
  command_specifier : Int
  expedited : Bool
  size_indicated : Bool
  byte_count : Int
} derive(Eq, Debug)

///|
pub fn CanopenSdoExpeditedFrame::valid(self : CanopenSdoExpeditedFrame) -> Bool {
  self.frame.valid() &&
  self.expedited &&
  self.size_indicated &&
  self.byte_count > 0 &&
  self.byte_count <= 4
}

///|
pub(all) struct CanopenPdoFrame {
  frame : CanopenFrame
  channel : String
  mapped_objects : Array[String]
  direction : @trace.TraceDirection
} derive(Eq, Debug)

///|
pub fn CanopenPdoFrame::valid(self : CanopenPdoFrame) -> Bool {
  self.frame.valid() &&
  self.mapped_objects.length() > 0 &&
  (self.direction == @trace.Tx || self.direction == @trace.Rx)
}

///|
pub(all) struct CanopenArbitrationSample {
  left : CanopenFrame
  right : CanopenFrame
  winner : CanopenFrame
  loser : CanopenFrame
  deterministic : Bool
} derive(Eq, Debug)

///|
pub fn CanopenArbitrationSample::from_pair(
  left~ : CanopenFrame,
  right~ : CanopenFrame,
) -> CanopenArbitrationSample {
  if left.envelope.cob_id <= right.envelope.cob_id {
    { left, right, winner: left, loser: right, deterministic: true }
  } else {
    { left, right, winner: right, loser: left, deterministic: true }
  }
}

///|
pub fn CanopenArbitrationSample::passes(
  self : CanopenArbitrationSample,
) -> Bool {
  self.deterministic &&
  self.winner.envelope.cob_id <= self.loser.envelope.cob_id
}

///|
pub(all) enum CanopenErrorState {
  ErrorActive
  ErrorPassive
  BusOff
} derive(Eq, Debug)

///|
pub fn CanopenErrorState::label(self : CanopenErrorState) -> String {
  match self {
    ErrorActive => "error_active"
    ErrorPassive => "error_passive"
    BusOff => "bus_off"
  }
}

///|
pub(all) struct CanopenErrorCounters {
  tx_error_count : Int
  rx_error_count : Int
} derive(Eq, Debug)

///|
pub fn CanopenErrorCounters::state(
  self : CanopenErrorCounters,
) -> CanopenErrorState {
  if self.tx_error_count >= 256 {
    BusOff
  } else if self.tx_error_count >= 128 || self.rx_error_count >= 128 {
    ErrorPassive
  } else {
    ErrorActive
  }
}

///|
pub(all) struct CanopenErrorStateSample {
  counters : CanopenErrorCounters
  state : CanopenErrorState
  trace_direction : @trace.TraceDirection
  live_bus_evidence : Bool
} derive(Eq, Debug)

///|
pub fn CanopenErrorStateSample::passes(self : CanopenErrorStateSample) -> Bool {
  self.state == self.counters.state() &&
  self.trace_direction == @trace.Probe &&
  !self.live_bus_evidence
}

///|
pub(all) struct CanopenFrameMediumReport {
  sdo_expedited_valid : Bool
  pdo_frames_valid : Bool
  arbitration_valid : Bool
  error_state_valid : Bool
  trace_event_count : Int
  replay_count : Int
  trace_digest : @core.SimDigest
  observer_passes : Bool
  observer_checked_events : Int
  protocol_matched_steps : Int
  live_can_evidence : Bool
} derive(Eq, Debug)

///|
pub fn CanopenFrameMediumReport::passes(
  self : CanopenFrameMediumReport,
) -> Bool {
  self.sdo_expedited_valid &&
  self.pdo_frames_valid &&
  self.arbitration_valid &&
  self.error_state_valid &&
  self.replay_count == self.trace_event_count &&
  self.observer_passes &&
  self.observer_checked_events == 8 &&
  self.protocol_matched_steps == 8 &&
  !self.live_can_evidence
}

///|
pub fn canopen_sdo_expedited_download_fixture(
  node_id? : Int = 1,
  index? : UInt = @profile_cia402.cia402_mode_of_operation_index,
  subindex? : Byte = b'\x00',
  value_digest? : Int = 0x08,
) -> CanopenSdoExpeditedFrame {
  let frame = CanopenFrame::make(
    cob_id=canopen_sdo_download_cob_id(node_id~),
    node_id~,
    dlc=8,
    kind=SdoDownloadFrame,
    payload_digest=sdo_payload_digest(
      command_specifier=0x23,
      index~,
      subindex~,
      value_digest~,
    ),
    label="canopen.frame.sdo.download.expedited",
    object_index=Some(index),
    subindex=Some(subindex),
  )
  {
    frame,
    command_specifier: 0x23,
    expedited: true,
    size_indicated: true,
    byte_count: 4,
  }
}

///|
pub fn canopen_sdo_expedited_upload_fixture(
  node_id? : Int = 1,
  index? : UInt = @profile_cia402.cia402_fault_index,
  subindex? : Byte = b'\x00',
  value_digest? : Int = 0x0000,
) -> CanopenSdoExpeditedFrame {
  let frame = CanopenFrame::make(
    cob_id=canopen_sdo_upload_cob_id(node_id~),
    node_id~,
    dlc=8,
    kind=SdoUploadFrame,
    payload_digest=sdo_payload_digest(
      command_specifier=0x43,
      index~,
      subindex~,
      value_digest~,
    ),
    label="canopen.frame.sdo.upload.expedited",
    object_index=Some(index),
    subindex=Some(subindex),
  )
  {
    frame,
    command_specifier: 0x43,
    expedited: true,
    size_indicated: true,
    byte_count: 4,
  }
}

///|
pub fn canopen_rpdo1_frame_fixture(node_id? : Int = 1) -> CanopenPdoFrame {
  {
    frame: CanopenFrame::make(
      cob_id=canopen_rpdo1_cob_id(node_id~),
      node_id~,
      dlc=8,
      kind=Rpdo1Frame,
      payload_digest=0x6040 * 131 + 0x607a,
      label="canopen.frame.rpdo1.tx",
      object_index=None,
      subindex=None,
    ),
    channel: "rpdo1",
    mapped_objects: ["axis.controlword", "axis.target_position"],
    direction: @trace.Tx,
  }
}

///|
pub fn canopen_tpdo1_frame_fixture(node_id? : Int = 1) -> CanopenPdoFrame {
  {
    frame: CanopenFrame::make(
      cob_id=canopen_tpdo1_cob_id(node_id~),
      node_id~,
      dlc=8,
      kind=Tpdo1Frame,
      payload_digest=0x6041 * 131 + 0x6064,
      label="canopen.frame.tpdo1.rx",
      object_index=None,
      subindex=None,
    ),
    channel: "tpdo1",
    mapped_objects: ["axis.statusword", "axis.actual_position"],
    direction: @trace.Rx,
  }
}

///|
pub fn canopen_arbitration_sample_fixture(
  node_id? : Int = 1,
) -> CanopenArbitrationSample {
  CanopenArbitrationSample::from_pair(
    left=canopen_rpdo1_frame_fixture(node_id~).frame,
    right=canopen_tpdo1_frame_fixture(node_id~).frame,
  )
}

///|
pub fn canopen_error_state_sample_fixture() -> CanopenErrorStateSample {
  let counters = { tx_error_count: 130, rx_error_count: 0 }
  {
    counters,
    state: counters.state(),
    trace_direction: @trace.Probe,
    live_bus_evidence: false,
  }
}

///|
pub fn canopen_frame_medium_trace_fixture(
  node_id? : Int = 1,
  seed? : Int = 401,
) -> @trace.TraceLog {
  let log = canopen_cia402_trace_fixture(node_id~)
  let sdo_download = canopen_sdo_expedited_download_fixture(node_id~)
  let sdo_upload = canopen_sdo_expedited_upload_fixture(node_id~)
  let rpdo1 = canopen_rpdo1_frame_fixture(node_id~)
  let tpdo1 = canopen_tpdo1_frame_fixture(node_id~)
  let arbitration = canopen_arbitration_sample_fixture(node_id~)
  let error_state = canopen_error_state_sample_fixture()
  append_can_frame_event(
    log,
    11,
    sdo_download.frame,
    @trace.Tx,
    100L,
    seed~,
    node_text="controller",
  )
  append_can_frame_event(
    log,
    12,
    sdo_upload.frame,
    @trace.Rx,
    110L,
    seed~,
    node_text="drive",
  )
  append_can_frame_event(
    log,
    13,
    rpdo1.frame,
    rpdo1.direction,
    120L,
    seed~,
    node_text="controller",
  )
  append_can_frame_event(
    log,
    14,
    tpdo1.frame,
    tpdo1.direction,
    130L,
    seed~,
    node_text="drive",
  )
  append_can_probe_event(
    log,
    15,
    label="canopen.arbitration.winner." +
      arbitration.winner.envelope.kind.label(),
    payload_digest=arbitration.winner.envelope.cob_id,
    time_ns=140L,
    seed~,
  )
  append_can_probe_event(
    log,
    16,
    label="canopen.error_state." + error_state.state.label(),
    payload_digest=error_state.counters.tx_error_count,
    time_ns=150L,
    seed~,
  )
  log
}

///|
pub fn canopen_frame_medium_report(
  node_id? : Int = 1,
  seed? : Int = 401,
) -> CanopenFrameMediumReport {
  let trace = canopen_frame_medium_trace_fixture(node_id~, seed~)
  let observer = canopen_cia402_observer_backend(node_id~).check_trace(trace)
  let sdo_download = canopen_sdo_expedited_download_fixture(node_id~)
  let sdo_upload = canopen_sdo_expedited_upload_fixture(node_id~)
  let rpdo1 = canopen_rpdo1_frame_fixture(node_id~)
  let tpdo1 = canopen_tpdo1_frame_fixture(node_id~)
  {
    sdo_expedited_valid: sdo_download.valid() && sdo_upload.valid(),
    pdo_frames_valid: rpdo1.valid() && tpdo1.valid(),
    arbitration_valid: canopen_arbitration_sample_fixture(node_id~).passes(),
    error_state_valid: canopen_error_state_sample_fixture().passes(),
    trace_event_count: trace.len(),
    replay_count: replay_count(trace),
    trace_digest: trace.portable_digest(seed~),
    observer_passes: observer.passes(),
    observer_checked_events: observer.checked_events,
    protocol_matched_steps: observer.matched_steps,
    live_can_evidence: false,
  }
}

///|
fn CanopenFrame::make(
  cob_id~ : Int,
  node_id~ : Int,
  dlc~ : Int,
  kind~ : CanopenFrameKind,
  payload_digest~ : Int,
  label~ : String,
  object_index~ : UInt?,
  subindex~ : Byte?,
) -> CanopenFrame {
  {
    envelope: CanopenFrameEnvelope::make(
      cob_id~,
      node_id~,
      dlc~,
      kind~,
      payload_digest~,
    ),
    object_index,
    subindex,
    label,
  }
}

///|
fn append_can_frame_event(
  log : @trace.TraceLog,
  event_id : Int,
  frame : CanopenFrame,
  direction : @trace.TraceDirection,
  time_ns : Int64,
  seed~ : Int,
  node_text~ : String,
) -> 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=node_text,
      medium_id=canopen_medium_id(),
      channel_id=Some(@core.ChannelId(frame.envelope.cob_id)),
      direction~,
      payload_digest=Some(frame.envelope.payload_digest),
      rng_step=event_id,
      seed~,
      backend=@core.SimNative,
      label=frame.label,
    ),
  )
}

///|
fn append_can_probe_event(
  log : @trace.TraceLog,
  event_id : Int,
  label~ : String,
  payload_digest~ : Int,
  time_ns~ : Int64,
  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="can-bus",
      medium_id=canopen_medium_id(),
      channel_id=None,
      direction=@trace.Probe,
      payload_digest=Some(payload_digest),
      rng_step=event_id,
      seed~,
      backend=@core.SimNative,
      label~,
    ),
  )
}

///|
fn sdo_payload_digest(
  command_specifier~ : Int,
  index~ : UInt,
  subindex~ : Byte,
  value_digest~ : Int,
) -> Int {
  (
    (command_specifier * 131 + index.reinterpret_as_int()) * 131 +
    subindex.to_int()
  ) *
  131 +
  value_digest
}

///|
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
}