///|
/// Simulation package for M1-CORE-P2P.

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

///|
pub(all) struct BenchmarkSample {
  events_per_run : Int
  nominal_events_per_second : Int
  copies_per_frame : Int
} derive(Eq, Debug)

///|
pub(all) struct SimRun {
  seed : Int
  events : Array[@core.RxEvent]
  digest : @core.SimDigest
  benchmark : BenchmarkSample
} derive(Eq, Debug)

///|
pub(all) struct P2pLink {
  a : @core.EndpointId
  b : @core.EndpointId
  delay : @core.Duration
} derive(Eq, Debug)

///|
pub fn P2pLink::new(
  a~ : @core.EndpointId,
  b~ : @core.EndpointId,
  delay~ : @core.Duration,
) -> P2pLink {
  { a, b, delay }
}

///|
fn P2pLink::deliver_bound(
  self : P2pLink,
  ev : @core.TxEvent,
) -> Array[@core.RxEvent] {
  let rx : @core.RxEvent = {
    id: ev.id + 1000,
    parent_id: ev.id,
    vtime: ev.vtime.add(self.delay),
    channel_id: ev.channel_id,
    seq: ev.seq,
    source: ev.source,
    target: ev.target,
    frame: ev.frame.sending().sent().receiving().received(),
  }
  [rx]
}

///|
pub impl @core.Medium for P2pLink with fn submit(self, ev) {
  if (ev.source == self.a && ev.target == self.b) ||
    (ev.source == self.b && ev.target == self.a) {
    self.deliver_bound(ev)
  } else {
    raise @core.TransportError::UnboundEndpoint(ev.target)
  }
}

///|
pub impl @core.Medium for P2pLink with fn lookahead(self) {
  self.delay
}

///|
pub impl @core.PointToPointMedium for P2pLink with fn bind(_self, _a, _b) {
  ()
}

///|
pub fn run_p2p_demo(seed~ : Int) -> SimRun {
  let left = @core.EndpointId(1)
  let right = @core.EndpointId(2)
  let link = P2pLink::new(a=left, b=right, delay=@core.Duration::from_ns(5L))
  let frame_a = @core.Frame::acquire(id=1).filled(payload=b"a")
  let frame_b = @core.Frame::acquire(id=2).filled(payload=b"b")
  let first = @core.TxEvent::make(
    id=1,
    vtime=@core.VTime::from_ns(0L),
    channel_id=@core.ChannelId(1),
    seq=1,
    source=left,
    target=right,
    frame=frame_a,
  )
  let second = @core.TxEvent::make(
    id=2,
    vtime=@core.VTime::from_ns(0L),
    channel_id=@core.ChannelId(2),
    seq=1,
    source=right,
    target=left,
    frame=frame_b,
  )
  let ordered = if seed % 2 == 0 { [first, second] } else { [second, first] }
  let mut digest = @core.SimDigest::empty(seed~).step_rng()
  let events : Array[@core.RxEvent] = []
  for tx in ordered {
    let rx_events = link.deliver_bound(tx)
    for rx in rx_events {
      events.push(rx)
      digest = digest
        .mix(rx.id)
        .mix(rx.parent_id)
        .mix(rx.channel_id.value())
        .mix(rx.seq)
    }
  }
  {
    seed,
    events,
    digest,
    benchmark: {
      events_per_run: events.length(),
      nominal_events_per_second: events.length(),
      copies_per_frame: 0,
    },
  }
}

///|
pub fn same_seed_digest_stable(seed~ : Int) -> Bool {
  run_p2p_demo(seed~).digest == run_p2p_demo(seed~).digest
}

///|
pub fn SimRun::to_trace_log(
  self : SimRun,
  backend~ : @core.BackendProfile,
) -> @trace.TraceLog {
  let log = @trace.TraceLog::new()
  for rx in self.events {
    log.append(
      @trace.rx_event_to_trace(
        rx,
        seed=self.seed,
        rng_step=self.digest.rng_steps,
        clock_domain="sim",
        node_id="p2p",
        medium_id="p2p",
        backend~,
      ),
    )
  }
  log
}

///|
pub fn SimRun::benchmark_report(
  self : SimRun,
  scenario~ : String,
  backend~ : @core.BackendProfile,
) -> @trace.RuntimeBenchmarkReport {
  self
  .to_trace_log(backend~)
  .benchmark_report(
    scenario~,
    seed=self.seed,
    backend~,
    copies_per_frame=self.benchmark.copies_per_frame,
  )
}

///|
pub fn p2p_benchmark_report(
  seed~ : Int,
  backend~ : @core.BackendProfile,
) -> @trace.RuntimeBenchmarkReport {
  run_p2p_demo(seed~).benchmark_report(scenario="p2p", backend~)
}

///|
pub(all) struct SegmentNode {
  id : @core.EndpointId
  name : String
  port_delay : @core.Duration
  window_offset : Int
  window_length : Int
  rewrite_value : Byte?
} derive(Eq, Debug)

///|
pub fn SegmentNode::new(
  id~ : @core.EndpointId,
  name~ : String,
  port_delay~ : @core.Duration,
) -> SegmentNode {
  {
    id,
    name,
    port_delay,
    window_offset: 0,
    window_length: 0,
    rewrite_value: None,
  }
}

///|
pub fn SegmentNode::with_window_write(
  self : SegmentNode,
  offset~ : Int,
  length~ : Int,
  value~ : Byte,
) -> SegmentNode {
  {
    id: self.id,
    name: self.name,
    port_delay: self.port_delay,
    window_offset: offset,
    window_length: length,
    rewrite_value: Some(value),
  }
}

///|
pub(all) struct FrameWindow {
  node_id : @core.EndpointId
  offset : Int
  length : Int
  before_digest : Int
  after_digest : Int
} derive(Eq, Debug)

///|
pub(all) struct SegmentHop {
  index : Int
  node_id : @core.EndpointId
  node_name : String
  arrive : @core.VTime
  depart : @core.VTime
  port_delay : @core.Duration
  window : FrameWindow
} derive(Eq, Debug)

///|
pub(all) struct SegmentRun {
  seed : Int
  line_name : String
  hops : Array[SegmentHop]
  final_payload : Bytes
  total_delay : @core.Duration
  digest : @core.SimDigest
} derive(Eq, Debug)

///|
pub(all) struct SegmentLine {
  name : String
  nodes : Array[SegmentNode]
} derive(Eq, Debug)

///|
pub fn SegmentLine::new(
  name~ : String,
  nodes~ : Array[SegmentNode],
) -> SegmentLine {
  { name, nodes }
}

///|
pub fn SegmentLine::run(
  self : SegmentLine,
  payload~ : Bytes,
  start~ : @core.VTime,
  seed~ : Int,
) -> SegmentRun {
  let mut now = start
  let mut frame = payload
  let hops : Array[SegmentHop] = []
  let mut digest = @core.SimDigest::empty(seed~).step_rng()
  for i, node in self.nodes {
    let arrive = now
    let before_digest = segment_payload_digest(frame)
    frame = apply_segment_window(node, frame)
    let after_digest = segment_payload_digest(frame)
    now = now.add(node.port_delay)
    let hop : SegmentHop = {
      index: i,
      node_id: node.id,
      node_name: node.name,
      arrive,
      depart: now,
      port_delay: node.port_delay,
      window: {
        node_id: node.id,
        offset: node.window_offset,
        length: node.window_length,
        before_digest,
        after_digest,
      },
    }
    hops.push(hop)
    digest = digest
      .mix(node.id.value())
      .mix(arrive.ns().to_int())
      .mix(now.ns().to_int())
      .mix(before_digest)
      .mix(after_digest)
  }
  {
    seed,
    line_name: self.name,
    hops,
    final_payload: frame,
    total_delay: @core.Duration::from_ns(now.ns() - start.ns()),
    digest,
  }
}

///|
pub fn SegmentRun::timeline_text(self : SegmentRun) -> String {
  let buf = StringBuilder::new()
  buf.write_string("segment=")
  buf.write_string(self.line_name)
  buf.write_string("|seed=")
  buf.write_string(self.seed.to_string())
  buf.write_string("|total_delay_ns=")
  buf.write_string(self.total_delay.ns().to_string())
  for hop in self.hops {
    buf.write_char('\n')
    buf.write_string("hop=")
    buf.write_string(hop.index.to_string())
    buf.write_string("|node=")
    buf.write_string(hop.node_name)
    buf.write_string("|arrive_ns=")
    buf.write_string(hop.arrive.ns().to_string())
    buf.write_string("|depart_ns=")
    buf.write_string(hop.depart.ns().to_string())
    buf.write_string("|port_delay_ns=")
    buf.write_string(hop.port_delay.ns().to_string())
    buf.write_string("|before=")
    buf.write_string(hop.window.before_digest.to_string())
    buf.write_string("|after=")
    buf.write_string(hop.window.after_digest.to_string())
  }
  buf.to_string()
}

///|
pub fn run_segment_toy(seed~ : Int) -> SegmentRun {
  let line = SegmentLine::new(name="line-3", nodes=[
    SegmentNode::new(
      id=@core.EndpointId(1),
      name="drive-1",
      port_delay=@core.Duration::from_ns(2L),
    ).with_window_write(offset=0, length=1, value=b'A'),
    SegmentNode::new(
      id=@core.EndpointId(2),
      name="drive-2",
      port_delay=@core.Duration::from_ns(3L),
    ).with_window_write(offset=1, length=1, value=b'B'),
    SegmentNode::new(
      id=@core.EndpointId(3),
      name="drive-3",
      port_delay=@core.Duration::from_ns(5L),
    ),
  ])
  line.run(payload=b"xyz", start=@core.VTime::from_ns(0L), seed~)
}

///|
fn apply_segment_window(node : SegmentNode, payload : Bytes) -> Bytes {
  guard node.rewrite_value is Some(value) else { return payload }
  if node.window_length <= 0 {
    return payload
  }
  let offset = node.window_offset
  if offset < 0 || offset >= payload.length() {
    return payload
  }
  let buf = FixedArray::make(payload.length(), b'\x00')
  for i in 0.. Int {
  let mut digest = 23
  for i in 0..