///|
/// Trace package for M3-TRACE-REPLAY.

///|
pub fn package_id() -> String {
  "lockwire/trace"
}

///|
pub(all) enum TraceDirection {
  Tx
  Rx
  Fault
  Tick
  Charge
  Process
  Resource
  Probe
  Meta
} derive(Eq, Debug)

///|
pub fn TraceDirection::code(self : TraceDirection) -> Int {
  match self {
    Tx => 1
    Rx => 2
    Fault => 3
    Tick => 4
    Charge => 5
    Process => 6
    Resource => 7
    Probe => 8
    Meta => 9
  }
}

///|
pub fn TraceDirection::label(self : TraceDirection) -> String {
  match self {
    Tx => "tx"
    Rx => "rx"
    Fault => "fault"
    Tick => "tick"
    Charge => "charge"
    Process => "process"
    Resource => "resource"
    Probe => "probe"
    Meta => "meta"
  }
}

///|
pub(all) struct TraceEvent {
  event_id : Int
  parent_id : Int?
  vtime : @core.VTime
  clock_domain : String
  raw_ns : Int64
  node_id : String
  medium_id : String
  channel_id : @core.ChannelId?
  direction : TraceDirection
  payload_digest : Int?
  rng_step : Int
  seed : Int
  backend : @core.BackendProfile
  label : String
} derive(Eq, Debug)

///|
pub fn TraceEvent::make(
  event_id~ : Int,
  parent_id~ : Int?,
  vtime~ : @core.VTime,
  clock_domain~ : String,
  raw_ns~ : Int64,
  node_id~ : String,
  medium_id~ : String,
  channel_id~ : @core.ChannelId?,
  direction~ : TraceDirection,
  payload_digest~ : Int?,
  rng_step~ : Int,
  seed~ : Int,
  backend~ : @core.BackendProfile,
  label~ : String,
) -> TraceEvent {
  {
    event_id,
    parent_id,
    vtime,
    clock_domain,
    raw_ns,
    node_id,
    medium_id,
    channel_id,
    direction,
    payload_digest,
    rng_step,
    seed,
    backend,
    label,
  }
}

///|
pub fn payload_digest_placeholder(payload : Bytes) -> Int {
  let mut digest = 17
  for i in 0.. TraceEvent {
  TraceEvent::make(
    event_id=ev.id,
    parent_id=ev.parent_id,
    vtime=ev.vtime,
    clock_domain~,
    raw_ns=ev.vtime.ns(),
    node_id~,
    medium_id~,
    channel_id=Some(ev.channel_id),
    direction=Tx,
    payload_digest=Some(payload_digest_placeholder(ev.frame.payload)),
    rng_step~,
    seed~,
    backend~,
    label="tx",
  )
}

///|
pub fn rx_event_to_trace(
  ev : @core.RxEvent,
  seed~ : Int,
  rng_step~ : Int,
  clock_domain~ : String,
  node_id~ : String,
  medium_id~ : String,
  backend~ : @core.BackendProfile,
) -> TraceEvent {
  TraceEvent::make(
    event_id=ev.id,
    parent_id=Some(ev.parent_id),
    vtime=ev.vtime,
    clock_domain~,
    raw_ns=ev.vtime.ns(),
    node_id~,
    medium_id~,
    channel_id=Some(ev.channel_id),
    direction=Rx,
    payload_digest=Some(payload_digest_placeholder(ev.frame.payload)),
    rng_step~,
    seed~,
    backend~,
    label="rx",
  )
}

///|
pub(all) struct TraceLog {
  events : Array[TraceEvent]
} derive(Debug)

///|
pub fn TraceLog::new() -> TraceLog {
  { events: [] }
}

///|
pub fn TraceLog::append(self : TraceLog, event : TraceEvent) -> Unit {
  self.events.push(event)
}

///|
pub fn TraceLog::len(self : TraceLog) -> Int {
  self.events.length()
}

///|
pub fn TraceLog::digest(self : TraceLog, seed~ : Int) -> @core.SimDigest {
  let mut digest = @core.SimDigest::empty(seed~)
  let mut max_rng_step = 0
  for event in self.events {
    digest = mix_event(digest, event)
    if event.rng_step > max_rng_step {
      max_rng_step = event.rng_step
    }
  }
  {
    state_digest: digest.state_digest,
    rng_steps: max_rng_step,
    event_count: self.events.length(),
  }
}

///|
pub fn TraceLog::portable_digest(
  self : TraceLog,
  seed~ : Int,
) -> @core.SimDigest {
  let mut digest = @core.SimDigest::empty(seed~)
  let mut max_rng_step = 0
  for event in self.events {
    digest = mix_portable_event(digest, event)
    if event.rng_step > max_rng_step {
      max_rng_step = event.rng_step
    }
  }
  {
    state_digest: digest.state_digest,
    rng_steps: max_rng_step,
    event_count: self.events.length(),
  }
}

///|
pub fn TraceLog::to_golden_text(self : TraceLog) -> String {
  let buf = StringBuilder::new()
  for i, event in self.events {
    if i > 0 {
      buf.write_char('\n')
    }
    buf.write_string(event.to_golden_line())
  }
  buf.to_string()
}

///|
fn TraceLog::to_portable_benchmark_text(self : TraceLog) -> String {
  let buf = StringBuilder::new()
  for i, event in self.events {
    if i > 0 {
      buf.write_char('\n')
    }
    buf.write_string(event.to_portable_benchmark_line())
  }
  buf.to_string()
}

///|
pub(all) enum BenchmarkMetricStatus {
  Measured
  Estimated
  Unsupported
  Missing
} derive(Eq, Debug)

///|
pub fn BenchmarkMetricStatus::label(self : BenchmarkMetricStatus) -> String {
  match self {
    Measured => "measured"
    Estimated => "estimated"
    Unsupported => "unsupported"
    Missing => "missing"
  }
}

///|
pub(all) struct BenchmarkMetric {
  name : String
  status : BenchmarkMetricStatus
  value : Int64?
  unit : String
  note : String
} derive(Eq, Debug)

///|
pub fn BenchmarkMetric::make(
  name~ : String,
  status~ : BenchmarkMetricStatus,
  value~ : Int64?,
  unit~ : String,
  note~ : String,
) -> BenchmarkMetric {
  { name, status, value, unit, note }
}

///|
pub fn BenchmarkMetric::measured(
  name~ : String,
  value~ : Int64,
  unit~ : String,
  note~ : String,
) -> BenchmarkMetric {
  BenchmarkMetric::make(name~, status=Measured, value=Some(value), unit~, note~)
}

///|
pub fn BenchmarkMetric::estimated(
  name~ : String,
  value~ : Int64,
  unit~ : String,
  note~ : String,
) -> BenchmarkMetric {
  BenchmarkMetric::make(
    name~,
    status=Estimated,
    value=Some(value),
    unit~,
    note~,
  )
}

///|
pub fn BenchmarkMetric::unsupported(
  name~ : String,
  unit~ : String,
  note~ : String,
) -> BenchmarkMetric {
  BenchmarkMetric::make(name~, status=Unsupported, value=None, unit~, note~)
}

///|
pub fn BenchmarkMetric::missing(
  name~ : String,
  unit~ : String,
  note~ : String,
) -> BenchmarkMetric {
  BenchmarkMetric::make(name~, status=Missing, value=None, unit~, note~)
}

///|
pub fn BenchmarkMetric::to_text(self : BenchmarkMetric) -> String {
  "metric=" +
  escape_text(self.name) +
  "|status=" +
  self.status.label() +
  "|value=" +
  int64_option_to_text(self.value) +
  "|unit=" +
  escape_text(self.unit) +
  "|note=" +
  escape_text(self.note)
}

///|
pub(all) struct RuntimeBenchmarkReport {
  scenario : String
  seed : Int
  backend : @core.BackendProfile
  digest : @core.SimDigest
  metrics : Array[BenchmarkMetric]
  hard_realtime_claim : Bool
  live_io_evidence : Bool
} derive(Eq, Debug)

///|
pub fn RuntimeBenchmarkReport::make(
  scenario~ : String,
  seed~ : Int,
  backend~ : @core.BackendProfile,
  digest~ : @core.SimDigest,
  metrics~ : Array[BenchmarkMetric],
  hard_realtime_claim~ : Bool,
  live_io_evidence~ : Bool,
) -> RuntimeBenchmarkReport {
  {
    scenario,
    seed,
    backend,
    digest,
    metrics,
    hard_realtime_claim,
    live_io_evidence,
  }
}

///|
pub fn RuntimeBenchmarkReport::metric_status(
  self : RuntimeBenchmarkReport,
  name : String,
) -> BenchmarkMetricStatus? {
  for metric in self.metrics {
    if metric.name == name {
      return Some(metric.status)
    }
  }
  None
}

///|
pub fn RuntimeBenchmarkReport::metric_value(
  self : RuntimeBenchmarkReport,
  name : String,
) -> Int64? {
  for metric in self.metrics {
    if metric.name == name {
      return metric.value
    }
  }
  None
}

///|
pub fn RuntimeBenchmarkReport::unsupported_count(
  self : RuntimeBenchmarkReport,
) -> Int {
  let mut count = 0
  for metric in self.metrics {
    if metric.status is Unsupported {
      count += 1
    }
  }
  count
}

///|
pub fn RuntimeBenchmarkReport::missing_count(
  self : RuntimeBenchmarkReport,
) -> Int {
  let mut count = 0
  for metric in self.metrics {
    if metric.status is Missing {
      count += 1
    }
  }
  count
}

///|
pub fn RuntimeBenchmarkReport::passes_offline_boundary(
  self : RuntimeBenchmarkReport,
) -> Bool {
  self.digest.event_count > 0 &&
  self.metrics.length() >= 6 &&
  !self.hard_realtime_claim &&
  !self.live_io_evidence
}

///|
pub fn RuntimeBenchmarkReport::to_text(self : RuntimeBenchmarkReport) -> String {
  let buf = StringBuilder::new()
  buf.write_string("benchmark=")
  buf.write_string(escape_text(self.scenario))
  buf.write_string("|seed=")
  buf.write_string(self.seed.to_string())
  buf.write_string("|backend=")
  buf.write_string(@core.backend_profile_label(self.backend))
  buf.write_string("|digest=")
  buf.write_string(self.digest.state_digest.to_string())
  buf.write_string("|rng_steps=")
  buf.write_string(self.digest.rng_steps.to_string())
  buf.write_string("|event_count=")
  buf.write_string(self.digest.event_count.to_string())
  buf.write_string("|hard_realtime_claim=")
  buf.write_string(bool_to_text(self.hard_realtime_claim))
  buf.write_string("|live_io_evidence=")
  buf.write_string(bool_to_text(self.live_io_evidence))
  for metric in self.metrics {
    buf.write_char('\n')
    buf.write_string(metric.to_text())
  }
  buf.to_string()
}

///|
pub fn TraceLog::benchmark_report(
  self : TraceLog,
  scenario~ : String,
  seed~ : Int,
  backend~ : @core.BackendProfile,
  copies_per_frame~ : Int,
) -> RuntimeBenchmarkReport {
  let trace_text = self.to_portable_benchmark_text()
  RuntimeBenchmarkReport::make(
    scenario~,
    seed~,
    backend~,
    digest=self.portable_digest(seed~),
    metrics=[
      BenchmarkMetric::measured(
        name="events/run",
        value=Int64::from_int(self.len()),
        unit="events",
        note="deterministic event count; not wall-clock throughput",
      ),
      BenchmarkMetric::unsupported(
        name="events/s",
        unit="events/s",
        note="wall-clock timer is not part of offline deterministic run",
      ),
      BenchmarkMetric::measured(
        name="trace-bytes/run",
        value=Int64::from_int(trace_text.length()),
        unit="bytes",
        note="portable trace text size excluding backend label",
      ),
      BenchmarkMetric::unsupported(
        name="trace-throughput",
        unit="bytes/s",
        note="wall-clock timer is not part of offline deterministic run",
      ),
      BenchmarkMetric::unsupported(
        name="alloc/event",
        unit="alloc/event",
        note="MoonBit runtime allocation counter is unavailable here",
      ),
      BenchmarkMetric::measured(
        name="copy/frame",
        value=Int64::from_int(copies_per_frame),
        unit="copies/frame",
        note="declared by the offline medium fixture",
      ),
    ],
    hard_realtime_claim=false,
    live_io_evidence=false,
  )
}

///|
pub struct Recording {
  priv log : TraceLog
} derive(Debug)

///|
pub fn Recording::new() -> Recording {
  { log: TraceLog::new() }
}

///|
pub fn Recording::record(self : Recording, event : TraceEvent) -> Unit {
  self.log.append(event)
}

///|
pub fn Recording::record_tx(
  self : Recording,
  ev : @core.TxEvent,
  seed~ : Int,
  rng_step~ : Int,
  clock_domain~ : String,
  node_id~ : String,
  medium_id~ : String,
  backend~ : @core.BackendProfile,
) -> TraceEvent {
  let event = tx_event_to_trace(
    ev,
    seed~,
    rng_step~,
    clock_domain~,
    node_id~,
    medium_id~,
    backend~,
  )
  self.record(event)
  event
}

///|
pub fn Recording::record_rx(
  self : Recording,
  ev : @core.RxEvent,
  seed~ : Int,
  rng_step~ : Int,
  clock_domain~ : String,
  node_id~ : String,
  medium_id~ : String,
  backend~ : @core.BackendProfile,
) -> TraceEvent {
  let event = rx_event_to_trace(
    ev,
    seed~,
    rng_step~,
    clock_domain~,
    node_id~,
    medium_id~,
    backend~,
  )
  self.record(event)
  event
}

///|
pub fn Recording::get_log(self : Recording) -> TraceLog {
  self.log
}

///|
pub fn Recording::digest(self : Recording, seed~ : Int) -> @core.SimDigest {
  self.log.digest(seed~)
}

///|
pub struct Replay {
  priv log : TraceLog
  priv mut cursor : Int
} derive(Debug)

///|
pub fn Replay::from_log(log : TraceLog) -> Replay {
  { log, cursor: 0 }
}

///|
pub fn Replay::remaining(self : Replay) -> Int {
  self.log.len() - self.cursor
}

///|
pub fn Replay::is_exhausted(self : Replay) -> Bool {
  self.cursor >= self.log.len()
}

///|
pub fn Replay::peek(self : Replay) -> TraceEvent? {
  self.log.events.get(self.cursor)
}

///|
pub fn Replay::next_event(self : Replay) -> TraceEvent? {
  match self.peek() {
    Some(event) => {
      self.cursor += 1
      Some(event)
    }
    None => None
  }
}

///|
pub fn Replay::digest(self : Replay, seed~ : Int) -> @core.SimDigest {
  self.log.digest(seed~)
}

///|
pub fn TraceEvent::to_golden_line(self : TraceEvent) -> String {
  "event_id=" +
  self.event_id.to_string() +
  "|parent_id=" +
  int_option_to_text(self.parent_id) +
  "|vtime_ns=" +
  self.vtime.ns().to_string() +
  "|clock_domain=" +
  escape_text(self.clock_domain) +
  "|raw_ns=" +
  self.raw_ns.to_string() +
  "|node_id=" +
  escape_text(self.node_id) +
  "|medium_id=" +
  escape_text(self.medium_id) +
  "|channel_id=" +
  channel_option_to_text(self.channel_id) +
  "|direction=" +
  self.direction.label() +
  "|payload_digest=" +
  int_option_to_text(self.payload_digest) +
  "|rng_step=" +
  self.rng_step.to_string() +
  "|seed=" +
  self.seed.to_string() +
  "|backend=" +
  @core.backend_profile_label(self.backend) +
  "|label=" +
  escape_text(self.label)
}

///|
fn TraceEvent::to_portable_benchmark_line(self : TraceEvent) -> String {
  "event_id=" +
  self.event_id.to_string() +
  "|parent_id=" +
  int_option_to_text(self.parent_id) +
  "|vtime_ns=" +
  self.vtime.ns().to_string() +
  "|clock_domain=" +
  escape_text(self.clock_domain) +
  "|raw_ns=" +
  self.raw_ns.to_string() +
  "|node_id=" +
  escape_text(self.node_id) +
  "|medium_id=" +
  escape_text(self.medium_id) +
  "|channel_id=" +
  channel_option_to_text(self.channel_id) +
  "|direction=" +
  self.direction.label() +
  "|payload_digest=" +
  int_option_to_text(self.payload_digest) +
  "|rng_step=" +
  self.rng_step.to_string() +
  "|seed=" +
  self.seed.to_string() +
  "|label=" +
  escape_text(self.label)
}

///|
fn mix_event(digest : @core.SimDigest, event : TraceEvent) -> @core.SimDigest {
  let digest = digest
    .mix(event.event_id)
    .mix(event.vtime.ns().to_int())
    .mix(event.raw_ns.to_int())
    .mix(event.direction.code())
    .mix(event.rng_step)
    .mix(event.seed)
    .mix(backend_code(event.backend))
  let digest = mix_int_option(digest, event.parent_id)
  let digest = mix_channel_option(digest, event.channel_id)
  let digest = mix_int_option(digest, event.payload_digest)
  let digest = mix_string(digest, event.clock_domain)
  let digest = mix_string(digest, event.node_id)
  let digest = mix_string(digest, event.medium_id)
  mix_string(digest, event.label)
}

///|
fn mix_portable_event(
  digest : @core.SimDigest,
  event : TraceEvent,
) -> @core.SimDigest {
  let digest = digest
    .mix(event.event_id)
    .mix(event.vtime.ns().to_int())
    .mix(event.raw_ns.to_int())
    .mix(event.direction.code())
    .mix(event.rng_step)
    .mix(event.seed)
  let digest = mix_int_option(digest, event.parent_id)
  let digest = mix_channel_option(digest, event.channel_id)
  let digest = mix_int_option(digest, event.payload_digest)
  let digest = mix_string(digest, event.clock_domain)
  let digest = mix_string(digest, event.node_id)
  let digest = mix_string(digest, event.medium_id)
  mix_string(digest, event.label)
}

///|
fn backend_code(profile : @core.BackendProfile) -> Int {
  match profile {
    SimNative => 1
    SimWasm => 2
    Replay => 3
    RealLinux => 4
    RealEmbedded => 5
  }
}

///|
fn mix_int_option(digest : @core.SimDigest, value : Int?) -> @core.SimDigest {
  match value {
    Some(v) => digest.mix(1).mix(v)
    None => digest.mix(0)
  }
}

///|
fn mix_channel_option(
  digest : @core.SimDigest,
  value : @core.ChannelId?,
) -> @core.SimDigest {
  match value {
    Some(v) => digest.mix(1).mix(v.value())
    None => digest.mix(0)
  }
}

///|
fn mix_string(digest : @core.SimDigest, value : String) -> @core.SimDigest {
  let mut out = digest.mix(value.length())
  for c in value {
    out = out.mix(c.to_int())
  }
  out
}

///|
fn int_option_to_text(value : Int?) -> String {
  match value {
    Some(v) => v.to_string()
    None => "none"
  }
}

///|
fn int64_option_to_text(value : Int64?) -> String {
  match value {
    Some(v) => v.to_string()
    None => "none"
  }
}

///|
fn bool_to_text(value : Bool) -> String {
  if value {
    "true"
  } else {
    "false"
  }
}

///|
fn channel_option_to_text(value : @core.ChannelId?) -> String {
  match value {
    Some(v) => v.value().to_string()
    None => "none"
  }
}

///|
fn escape_text(value : String) -> String {
  let buf = StringBuilder::new()
  for c in value {
    match c {
      '\n' => buf.write_string("\\n")
      '\r' => buf.write_string("\\r")
      '\\' => buf.write_string("\\\\")
      '|' => buf.write_string("\\|")
      _ => buf.write_char(c)
    }
  }
  buf.to_string()
}