///|
/// Offline arbitrated-bus medium model for L5A.

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

///|
pub fn ArbitratedBusErrorState::label(self : ArbitratedBusErrorState) -> String {
  match self {
    ErrorActive => "error-active"
    ErrorPassive => "error-passive"
    BusOff => "bus-off"
    Recovering => "recovering"
  }
}

///|
pub fn ArbitratedBusErrorState::code(self : ArbitratedBusErrorState) -> Int {
  match self {
    ErrorActive => 1
    ErrorPassive => 2
    BusOff => 3
    Recovering => 4
  }
}

///|
pub(all) enum ArbitratedBusEventKind {
  ArbitrationWin
  ArbitrationLost
  FrameRx
  ErrorFrame
  AutoRetransmit
  BusOffEvent
  RecoveryEvent
  ReorderRejected
} derive(Eq, Debug)

///|
pub fn ArbitratedBusEventKind::label(self : ArbitratedBusEventKind) -> String {
  match self {
    ArbitrationWin => "arbitration-win"
    ArbitrationLost => "arbitration-lost"
    FrameRx => "rx"
    ErrorFrame => "error-frame"
    AutoRetransmit => "auto-retransmit"
    BusOffEvent => "bus-off"
    RecoveryEvent => "recovery"
    ReorderRejected => "reorder-rejected"
  }
}

///|
pub fn ArbitratedBusEventKind::direction(
  self : ArbitratedBusEventKind,
) -> @trace.TraceDirection {
  match self {
    ArbitrationWin | ArbitrationLost | FrameRx | AutoRetransmit => @trace.Rx
    ErrorFrame | BusOffEvent | ReorderRejected => @trace.Fault
    RecoveryEvent => @trace.Meta
  }
}

///|
pub fn ArbitratedBusEventKind::code(self : ArbitratedBusEventKind) -> Int {
  match self {
    ArbitrationWin => 1
    ArbitrationLost => 2
    FrameRx => 3
    ErrorFrame => 4
    AutoRetransmit => 5
    BusOffEvent => 6
    RecoveryEvent => 7
    ReorderRejected => 8
  }
}

///|
pub(all) struct ArbitratedBusFrame {
  frame_id : Int
  sender : @core.EndpointId
  target : @core.EndpointId
  arbitration_id : Int
  payload_len : Int
  submitted_at : @core.VTime
  label : String
} derive(Eq, Debug)

///|
pub fn ArbitratedBusFrame::make(
  frame_id~ : Int,
  sender~ : @core.EndpointId,
  target~ : @core.EndpointId,
  arbitration_id~ : Int,
  payload_len~ : Int,
  submitted_at~ : @core.VTime,
  label~ : String,
) -> ArbitratedBusFrame {
  {
    frame_id,
    sender,
    target,
    arbitration_id,
    payload_len: clamp_non_negative(payload_len),
    submitted_at,
    label,
  }
}

///|
pub(all) struct ArbitratedBusProfile {
  name : String
  bit_time_ns : Int64
  stuff_bit_divisor : Int
  tec_error_increment : Int
  bus_off_threshold : Int
  error_passive_threshold : Int
  supports_reorder : Bool
  frame_level_duration_estimate : Bool
  live_io_evidence : Bool
  socketcan_vcan_evidence : Bool
  protocol_semantics_present : Bool
} derive(Eq, Debug)

///|
pub fn ArbitratedBusProfile::offline_default() -> ArbitratedBusProfile {
  {
    name: "arbitrated-bus-offline",
    bit_time_ns: 100L,
    stuff_bit_divisor: 5,
    tec_error_increment: 8,
    bus_off_threshold: 16,
    error_passive_threshold: 8,
    supports_reorder: false,
    frame_level_duration_estimate: true,
    live_io_evidence: false,
    socketcan_vcan_evidence: false,
    protocol_semantics_present: false,
  }
}

///|
pub(all) struct ArbitratedBusFrameSlot {
  slot_id : Int
  start : @core.VTime
  end : @core.VTime
  winner_frame_id : Int
  winner_arbitration_id : Int
  loser_frame_ids : Array[Int]
  error_state_before : ArbitratedBusErrorState
  error_state_after : ArbitratedBusErrorState
  error_frame : Bool
  auto_retransmit : Bool
  bus_off : Bool
  recovered : Bool
  rejected_reorder_fault : Bool
} derive(Eq, Debug)

///|
pub(all) struct ArbitratedBusOfflineRun {
  seed : Int
  profile : ArbitratedBusProfile
  slots : Array[ArbitratedBusFrameSlot]
  trace : @trace.TraceLog
  digest : @core.SimDigest
  replay_count : Int
  arbitration_count : Int
  arbitration_lost_count : Int
  delivered_count : Int
  error_frame_count : Int
  retransmit_count : Int
  bus_off_count : Int
  recovery_count : Int
  reorder_rejected_count : Int
} derive(Debug)

///|
pub fn ArbitratedBusOfflineRun::passes(self : ArbitratedBusOfflineRun) -> Bool {
  self.profile.frame_level_duration_estimate &&
  !self.profile.live_io_evidence &&
  !self.profile.socketcan_vcan_evidence &&
  !self.profile.protocol_semantics_present &&
  self.slots.length() >= 4 &&
  self.arbitration_count >= 1 &&
  self.arbitration_lost_count >= 1 &&
  self.delivered_count >= 2 &&
  self.error_frame_count >= 1 &&
  self.retransmit_count >= 1 &&
  self.bus_off_count >= 1 &&
  self.recovery_count >= 1 &&
  self.reorder_rejected_count >= 1 &&
  self.replay_count == self.trace.len() &&
  self.digest.event_count == self.trace.len()
}

///|
pub(all) struct ArbitratedBusOfflineReport {
  profile_name : String
  seed : Int
  trace_event_count : Int
  replay_count : Int
  same_seed_stable : Bool
  digest : @core.SimDigest
  slot_count : Int
  arbitration_count : Int
  arbitration_lost_count : Int
  delivered_count : Int
  error_frame_count : Int
  retransmit_count : Int
  bus_off_count : Int
  recovery_count : Int
  reorder_rejected_count : Int
  frame_level_duration_estimate : Bool
  live_io_evidence : Bool
  socketcan_vcan_evidence : Bool
  protocol_semantics_present : Bool
} derive(Eq, Debug)

///|
pub fn ArbitratedBusOfflineReport::passes(
  self : ArbitratedBusOfflineReport,
) -> Bool {
  self.trace_event_count > 0 &&
  self.replay_count == self.trace_event_count &&
  self.same_seed_stable &&
  self.slot_count >= 4 &&
  self.arbitration_count >= 1 &&
  self.arbitration_lost_count >= 1 &&
  self.delivered_count >= 2 &&
  self.error_frame_count >= 1 &&
  self.retransmit_count >= 1 &&
  self.bus_off_count >= 1 &&
  self.recovery_count >= 1 &&
  self.reorder_rejected_count >= 1 &&
  self.frame_level_duration_estimate &&
  !self.live_io_evidence &&
  !self.socketcan_vcan_evidence &&
  !self.protocol_semantics_present
}

///|
pub struct ArbitratedBusMedium {
  priv profile : ArbitratedBusProfile
}

///|
pub fn ArbitratedBusMedium::new(profile~ : ArbitratedBusProfile) -> Self {
  { profile, }
}

///|
pub fn ArbitratedBusMedium::offline_default() -> Self {
  ArbitratedBusMedium::new(profile=ArbitratedBusProfile::offline_default())
}

///|
pub fn ArbitratedBusMedium::profile(self : Self) -> ArbitratedBusProfile {
  self.profile
}

///|
pub fn ArbitratedBusMedium::arbitrate(
  self : Self,
  candidates : Array[ArbitratedBusFrame],
) -> ArbitratedBusFrame? {
  ignore(self)
  let mut winner : ArbitratedBusFrame? = None
  for frame in candidates {
    match winner {
      None => winner = Some(frame)
      Some(current) =>
        if frame_precedes(frame, current) {
          winner = Some(frame)
        }
    }
  }
  winner
}

///|
pub fn ArbitratedBusMedium::frame_duration(
  self : Self,
  frame : ArbitratedBusFrame,
) -> @core.Duration {
  let base_bits = 47 + frame.payload_len * 8
  let divisor = if self.profile.stuff_bit_divisor <= 0 {
    5
  } else {
    self.profile.stuff_bit_divisor
  }
  let stuff_bits = (base_bits + divisor - 1) / divisor
  @core.Duration::from_ns(
    Int64::from_int(base_bits + stuff_bits) * self.profile.bit_time_ns,
  )
}

///|
pub fn run_arbitrated_bus_offline(seed? : Int = 764) -> ArbitratedBusOfflineRun {
  let medium = ArbitratedBusMedium::offline_default()
  let profile = medium.profile()
  let trace = @trace.TraceLog::new()
  let slots : Array[ArbitratedBusFrameSlot] = []
  let mut event_id = 1
  let mut now = @core.VTime::from_ns(0L)
  let mut state = ErrorActive
  let mut tec = 0
  let mut delivered_count = 0
  let mut error_frame_count = 0
  let mut retransmit_count = 0
  let mut bus_off_count = 0
  let mut recovery_count = 0
  let mut reorder_rejected_count = 0
  let high = ArbitratedBusFrame::make(
    frame_id=1,
    sender=@core.EndpointId(1),
    target=@core.EndpointId(3),
    arbitration_id=0x080,
    payload_len=8,
    submitted_at=now,
    label="high-priority-control",
  )
  let low = ArbitratedBusFrame::make(
    frame_id=2,
    sender=@core.EndpointId(2),
    target=@core.EndpointId(3),
    arbitration_id=0x180,
    payload_len=8,
    submitted_at=now,
    label="low-priority-telemetry",
  )
  let winner = medium.arbitrate([low, high]).unwrap()
  let duration = medium.frame_duration(winner)
  let slot_end = now.add(duration)
  slots.push({
    slot_id: 1,
    start: now,
    end: slot_end,
    winner_frame_id: winner.frame_id,
    winner_arbitration_id: winner.arbitration_id,
    loser_frame_ids: [low.frame_id],
    error_state_before: state,
    error_state_after: state,
    error_frame: false,
    auto_retransmit: false,
    bus_off: false,
    recovered: false,
    rejected_reorder_fault: false,
  })
  event_id = append_bus_event(
    trace,
    event_id~,
    seed~,
    frame=winner,
    at=now,
    kind=ArbitrationWin,
    state~,
    slot_id=1,
  )
  event_id = append_bus_event(
    trace,
    event_id~,
    seed~,
    frame=low,
    at=now,
    kind=ArbitrationLost,
    state~,
    slot_id=1,
  )
  event_id = append_bus_event(
    trace,
    event_id~,
    seed~,
    frame=winner,
    at=slot_end,
    kind=FrameRx,
    state~,
    slot_id=1,
  )
  delivered_count += 1
  now = slot_end

  let mut retry_frame = low
  for retry_slot in 2..<=3 {
    let state_before = state
    let retry_start = now
    let retry_end = retry_start.add(medium.frame_duration(retry_frame))
    tec += profile.tec_error_increment
    state = error_state_from_tec(tec, profile)
    slots.push({
      slot_id: retry_slot,
      start: retry_start,
      end: retry_end,
      winner_frame_id: retry_frame.frame_id,
      winner_arbitration_id: retry_frame.arbitration_id,
      loser_frame_ids: [],
      error_state_before: state_before,
      error_state_after: state,
      error_frame: true,
      auto_retransmit: true,
      bus_off: state == BusOff,
      recovered: false,
      rejected_reorder_fault: false,
    })
    event_id = append_bus_event(
      trace,
      event_id~,
      seed~,
      frame=retry_frame,
      at=retry_start,
      kind=ErrorFrame,
      state~,
      slot_id=retry_slot,
    )
    event_id = append_bus_event(
      trace,
      event_id~,
      seed~,
      frame=retry_frame,
      at=retry_end,
      kind=AutoRetransmit,
      state~,
      slot_id=retry_slot,
    )
    error_frame_count += 1
    retransmit_count += 1
    if state == BusOff {
      event_id = append_bus_event(
        trace,
        event_id~,
        seed~,
        frame=retry_frame,
        at=retry_end,
        kind=BusOffEvent,
        state~,
        slot_id=retry_slot,
      )
      bus_off_count += 1
    }
    now = retry_end
    retry_frame = { ..retry_frame, submitted_at: now }
  }

  let recovery_start = now
  state = Recovering
  tec = 0
  event_id = append_bus_event(
    trace,
    event_id~,
    seed~,
    frame=retry_frame,
    at=recovery_start,
    kind=RecoveryEvent,
    state~,
    slot_id=4,
  )
  recovery_count += 1
  state = ErrorActive
  let recovery_end = recovery_start.add(medium.frame_duration(retry_frame))
  slots.push({
    slot_id: 4,
    start: recovery_start,
    end: recovery_end,
    winner_frame_id: retry_frame.frame_id,
    winner_arbitration_id: retry_frame.arbitration_id,
    loser_frame_ids: [],
    error_state_before: Recovering,
    error_state_after: state,
    error_frame: false,
    auto_retransmit: false,
    bus_off: false,
    recovered: true,
    rejected_reorder_fault: false,
  })
  event_id = append_bus_event(
    trace,
    event_id~,
    seed~,
    frame=retry_frame,
    at=recovery_end,
    kind=FrameRx,
    state~,
    slot_id=4,
  )
  delivered_count += 1

  let rejected_at = recovery_end.add(@core.Duration::from_ns(1L))
  event_id = append_bus_event(
    trace,
    event_id~,
    seed~,
    frame=retry_frame,
    at=rejected_at,
    kind=ReorderRejected,
    state~,
    slot_id=5,
  )
  ignore(event_id)
  reorder_rejected_count += 1

  let arbitration_count = count_bus_events(trace, ArbitrationWin)
  let arbitration_lost_count = count_bus_events(trace, ArbitrationLost)
  let replay_count = replay_all(trace)
  {
    seed,
    profile,
    slots,
    trace,
    digest: trace.portable_digest(seed~),
    replay_count,
    arbitration_count,
    arbitration_lost_count,
    delivered_count,
    error_frame_count,
    retransmit_count,
    bus_off_count,
    recovery_count,
    reorder_rejected_count,
  }
}

///|
pub fn arbitrated_bus_offline_report(
  seed? : Int = 764,
) -> ArbitratedBusOfflineReport {
  let run = run_arbitrated_bus_offline(seed~)
  let same = run.digest == run_arbitrated_bus_offline(seed~).digest
  {
    profile_name: run.profile.name,
    seed,
    trace_event_count: run.trace.len(),
    replay_count: run.replay_count,
    same_seed_stable: same,
    digest: run.digest,
    slot_count: run.slots.length(),
    arbitration_count: run.arbitration_count,
    arbitration_lost_count: run.arbitration_lost_count,
    delivered_count: run.delivered_count,
    error_frame_count: run.error_frame_count,
    retransmit_count: run.retransmit_count,
    bus_off_count: run.bus_off_count,
    recovery_count: run.recovery_count,
    reorder_rejected_count: run.reorder_rejected_count,
    frame_level_duration_estimate: run.profile.frame_level_duration_estimate,
    live_io_evidence: run.profile.live_io_evidence,
    socketcan_vcan_evidence: run.profile.socketcan_vcan_evidence,
    protocol_semantics_present: run.profile.protocol_semantics_present,
  }
}

///|
pub fn ArbitratedBusOfflineRun::timeline_text(
  self : ArbitratedBusOfflineRun,
) -> String {
  let buf = StringBuilder::new()
  buf.write_string("arbitrated_bus=")
  buf.write_string(self.profile.name)
  buf.write_string("|seed=")
  buf.write_string(self.seed.to_string())
  buf.write_string("|events=")
  buf.write_string(self.trace.len().to_string())
  for slot in self.slots {
    buf.write_char('\n')
    buf.write_string("slot=")
    buf.write_string(slot.slot_id.to_string())
    buf.write_string("|winner=")
    buf.write_string(slot.winner_frame_id.to_string())
    buf.write_string("|arb_id=")
    buf.write_string(slot.winner_arbitration_id.to_string())
    buf.write_string("|start_ns=")
    buf.write_string(slot.start.ns().to_string())
    buf.write_string("|end_ns=")
    buf.write_string(slot.end.ns().to_string())
    buf.write_string("|state_before=")
    buf.write_string(slot.error_state_before.label())
    buf.write_string("|state_after=")
    buf.write_string(slot.error_state_after.label())
    buf.write_string("|error_frame=")
    buf.write_string(slot.error_frame.to_string())
    buf.write_string("|auto_retransmit=")
    buf.write_string(slot.auto_retransmit.to_string())
    buf.write_string("|bus_off=")
    buf.write_string(slot.bus_off.to_string())
    buf.write_string("|recovered=")
    buf.write_string(slot.recovered.to_string())
  }
  buf.to_string()
}

///|
fn append_bus_event(
  trace : @trace.TraceLog,
  event_id~ : Int,
  seed~ : Int,
  frame~ : ArbitratedBusFrame,
  at~ : @core.VTime,
  kind~ : ArbitratedBusEventKind,
  state~ : ArbitratedBusErrorState,
  slot_id~ : Int,
) -> Int {
  trace.append(
    @trace.TraceEvent::make(
      event_id~,
      parent_id=Some(frame.frame_id),
      vtime=at,
      clock_domain="sim",
      raw_ns=at.ns(),
      node_id="arbitrated-bus",
      medium_id="arbitrated-bus",
      channel_id=Some(@core.ChannelId(slot_id)),
      direction=kind.direction(),
      payload_digest=Some(
        frame.arbitration_id * 31 + kind.code() * 7 + state.code(),
      ),
      rng_step=event_id,
      seed~,
      backend=@core.SimNative,
      label="bus.arbitrated." + kind.label() + "." + frame.label,
    ),
  )
  event_id + 1
}

///|
fn replay_all(trace : @trace.TraceLog) -> Int {
  let replay = @trace.Replay::from_log(trace)
  let mut count = 0
  while !replay.is_exhausted() {
    match replay.next_event() {
      Some(_) => count += 1
      None => ()
    }
  }
  count
}

///|
fn count_bus_events(
  trace : @trace.TraceLog,
  kind : ArbitratedBusEventKind,
) -> Int {
  let needle = ".\{kind.label()}."
  let mut count = 0
  for event in trace.events {
    if event.label.contains(needle) {
      count += 1
    }
  }
  count
}

///|
fn frame_precedes(
  left : ArbitratedBusFrame,
  right : ArbitratedBusFrame,
) -> Bool {
  if left.arbitration_id < right.arbitration_id {
    true
  } else if left.arbitration_id == right.arbitration_id {
    left.frame_id < right.frame_id
  } else {
    false
  }
}

///|
fn error_state_from_tec(
  tec : Int,
  profile : ArbitratedBusProfile,
) -> ArbitratedBusErrorState {
  if tec >= profile.bus_off_threshold {
    BusOff
  } else if tec >= profile.error_passive_threshold {
    ErrorPassive
  } else {
    ErrorActive
  }
}

///|
fn clamp_non_negative(value : Int) -> Int {
  if value < 0 {
    0
  } else {
    value
  }
}