///|
pub(all) struct TimelineFrame {
  tick : Int
  events : Array[TickEvent]
} derive(Debug, Eq)

///|
pub fn TimelineFrame::summary(self : TimelineFrame) -> String {
  "tick=" + self.tick.to_string() + ", events=" + self.events.length().to_string()
}

///|
pub fn TimelineFrame::lines(self : TimelineFrame) -> Array[String] {
  let lines = Array::new()
  lines.push(self.summary())
  let mut i = 0
  while i < self.events.length() {
    lines.push("  " + self.events[i].to_line())
    i = i + 1
  }
  lines
}

///|
pub fn timeline_from_trace(events : Array[TickEvent]) -> Array[TimelineFrame] {
  let frames = Array::new()
  let mut i = 0
  while i < events.length() {
    let tick = events[i].tick
    match frame_index(frames, tick) {
      Some(idx) => frames[idx].events.push(events[i])
      None => frames.push({ tick, events: [events[i]] })
    }
    i = i + 1
  }
  frames
}

///|
pub fn timeline_text(events : Array[TickEvent]) -> String {
  let frames = timeline_from_trace(events)
  let lines = Array::new()
  let mut i = 0
  while i < frames.length() {
    let frame_lines = frames[i].lines()
    let mut j = 0
    while j < frame_lines.length() {
      lines.push(frame_lines[j])
      j = j + 1
    }
    i = i + 1
  }
  join_strings(lines, "\n")
}

///|
pub fn filter_trace_by_node(
  events : Array[TickEvent],
  node_id : String,
) -> Array[TickEvent] {
  let out = Array::new()
  let mut i = 0
  while i < events.length() {
    if events[i].node_id == node_id {
      out.push(events[i])
    }
    i = i + 1
  }
  out
}

///|
pub fn filter_trace_by_status(
  events : Array[TickEvent],
  status : BtStatus,
) -> Array[TickEvent] {
  let out = Array::new()
  let mut i = 0
  while i < events.length() {
    if events[i].status == status {
      out.push(events[i])
    }
    i = i + 1
  }
  out
}

///|
pub(all) struct TraceSummary {
  total : Int
  success : Int
  failure : Int
  running : Int
  unique_nodes : Int
} derive(Debug, Eq)

///|
pub fn TraceSummary::to_line(self : TraceSummary) -> String {
  "total=" +
  self.total.to_string() +
  ", success=" +
  self.success.to_string() +
  ", failure=" +
  self.failure.to_string() +
  ", running=" +
  self.running.to_string() +
  ", unique_nodes=" +
  self.unique_nodes.to_string()
}

///|
pub fn summarize_trace(events : Array[TickEvent]) -> TraceSummary {
  let nodes = Array::new()
  let mut success = 0
  let mut failure = 0
  let mut running = 0
  let mut i = 0
  while i < events.length() {
    match events[i].status {
      Success => success = success + 1
      Failure => failure = failure + 1
      Running => running = running + 1
    }
    if !string_array_contains(nodes, events[i].node_id) {
      nodes.push(events[i].node_id)
    }
    i = i + 1
  }
  {
    total: events.length(),
    success,
    failure,
    running,
    unique_nodes: nodes.length(),
  }
}

///|
pub fn compact_trace(events : Array[TickEvent]) -> Array[String] {
  let out = Array::new()
  let mut i = 0
  while i < events.length() {
    let event = events[i]
    out.push(
      event.tick.to_string() +
      ":" +
      event.node_id +
      "=" +
      event.status.to_text(),
    )
    i = i + 1
  }
  out
}

///|
pub fn last_event_for_node(
  events : Array[TickEvent],
  node_id : String,
) -> TickEvent? {
  let mut i = events.length() - 1
  while i >= 0 {
    if events[i].node_id == node_id {
      return Some(events[i])
    }
    if i == 0 {
      return None
    }
    i = i - 1
  }
  None
}

///|
fn frame_index(frames : Array[TimelineFrame], tick : Int) -> Int? {
  let mut i = 0
  while i < frames.length() {
    if frames[i].tick == tick {
      return Some(i)
    }
    i = i + 1
  }
  None
}

///|
fn string_array_contains(values : Array[String], item : String) -> Bool {
  let mut i = 0
  while i < values.length() {
    if values[i] == item {
      return true
    }
    i = i + 1
  }
  false
}