///|
/// Aggregated execution data for one node.
pub(all) struct NodeProfile {
  node_id : String
  mut kind : String
  mut total : Int
  mut success : Int
  mut failure : Int
  mut running : Int
  mut first_tick : Int
  mut last_tick : Int
} derive(Debug, Eq)

///|
pub fn NodeProfile::dominant_status(self : NodeProfile) -> BtStatus {
  if self.failure >= self.success && self.failure >= self.running {
    Failure
  } else if self.running >= self.success && self.running >= self.failure {
    Running
  } else {
    Success
  }
}

///|
pub fn NodeProfile::to_line(self : NodeProfile) -> String {
  self.node_id +
  ": total=" +
  self.total.to_string() +
  ", success=" +
  self.success.to_string() +
  ", failure=" +
  self.failure.to_string() +
  ", running=" +
  self.running.to_string() +
  ", ticks=" +
  self.first_tick.to_string() +
  ".." +
  self.last_tick.to_string() +
  ", dominant=" +
  self.dominant_status().to_text()
}

///|
pub(all) struct ProfileReport {
  name : String
  profiles : Array[NodeProfile]
  total_events : Int
  total_ticks : Int
} derive(Debug, Eq)

///|
pub fn ProfileReport::summary(self : ProfileReport) -> String {
  self.name +
  ": nodes=" +
  self.profiles.length().to_string() +
  ", events=" +
  self.total_events.to_string() +
  ", ticks=" +
  self.total_ticks.to_string()
}

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

///|
pub fn ProfileReport::hotspots(self : ProfileReport, limit : Int) -> Array[NodeProfile] {
  let sorted = clone_profiles(self.profiles)
  sort_profiles_by_total(sorted)
  let out = Array::new()
  let mut i = 0
  while i < sorted.length() && i < limit {
    out.push(sorted[i])
    i = i + 1
  }
  out
}

///|
pub fn ProfileReport::hotspot_lines(self : ProfileReport, limit : Int) -> Array[String] {
  let hot = self.hotspots(limit)
  let out = Array::new()
  let mut i = 0
  while i < hot.length() {
    out.push((i + 1).to_string() + ". " + hot[i].to_line())
    i = i + 1
  }
  out
}

///|
pub fn profile_trace(name : String, events : Array[TickEvent]) -> ProfileReport {
  let profiles = Array::new()
  let mut max_tick = 0
  let mut i = 0
  while i < events.length() {
    let event = events[i]
    if event.tick > max_tick {
      max_tick = event.tick
    }
    match profile_index(profiles, event.node_id) {
      Some(idx) => update_profile(profiles[idx], event)
      None => profiles.push(new_profile(event))
    }
    i = i + 1
  }
  { name, profiles, total_events: events.length(), total_ticks: max_tick }
}

///|
pub fn profile_fixture(name : String) -> Result[ProfileReport, BtError] {
  match load_fixture(name) {
    Ok(doc) => {
      let engine = new_engine(doc.tree, blackboard=doc.blackboard)
      match engine.run_until_done(max_ticks=32) {
        Ok(_) => Ok(profile_trace(name, engine.trace))
        Err(err) => Err(InvalidTree(err.message()))
      }
    }
    Err(err) => Err(err)
  }
}

///|
pub fn fixture_profile_report() -> Array[String] {
  let specs = fixture_catalog()
  let out = Array::new()
  out.push("MoonBTKit fixture profile")
  let mut i = 0
  while i < specs.length() {
    match profile_fixture(specs[i].name) {
      Ok(report) => out.push(report.summary())
      Err(err) => out.push(specs[i].name + ": error " + err.message())
    }
    i = i + 1
  }
  out
}

///|
pub fn profile_markdown(report : ProfileReport, hotspot_limit? : Int) -> String {
  let limit = hotspot_limit.unwrap_or(5)
  let lines = Array::new()
  lines.push("## Profile: " + report.name)
  lines.push("")
  lines.push("- " + report.summary())
  lines.push("")
  lines.push("### Hotspots")
  lines.push("")
  let hot = report.hotspot_lines(limit)
  let mut i = 0
  while i < hot.length() {
    lines.push("- " + hot[i])
    i = i + 1
  }
  join_strings(lines, "\n")
}

///|
fn new_profile(event : TickEvent) -> NodeProfile {
  let p = {
    node_id: event.node_id,
    kind: event.kind,
    total: 0,
    success: 0,
    failure: 0,
    running: 0,
    first_tick: event.tick,
    last_tick: event.tick,
  }
  update_profile(p, event)
  p
}

///|
fn update_profile(profile : NodeProfile, event : TickEvent) -> Unit {
  profile.kind = event.kind
  profile.total = profile.total + 1
  if event.tick < profile.first_tick {
    profile.first_tick = event.tick
  }
  if event.tick > profile.last_tick {
    profile.last_tick = event.tick
  }
  match event.status {
    Success => profile.success = profile.success + 1
    Failure => profile.failure = profile.failure + 1
    Running => profile.running = profile.running + 1
  }
}

///|
fn profile_index(profiles : Array[NodeProfile], node_id : String) -> Int? {
  let mut i = 0
  while i < profiles.length() {
    if profiles[i].node_id == node_id {
      return Some(i)
    }
    i = i + 1
  }
  None
}

///|
fn clone_profiles(values : Array[NodeProfile]) -> Array[NodeProfile] {
  let out = Array::new()
  let mut i = 0
  while i < values.length() {
    out.push(values[i])
    i = i + 1
  }
  out
}

///|
fn sort_profiles_by_total(values : Array[NodeProfile]) -> Unit {
  let mut i = 0
  while i < values.length() {
    let mut best = i
    let mut j = i + 1
    while j < values.length() {
      if values[j].total > values[best].total {
        best = j
      }
      j = j + 1
    }
    if best != i {
      let tmp = values[i]
      values[i] = values[best]
      values[best] = tmp
    }
    i = i + 1
  }
}