///|
pub(all) struct TreeStats {
  nodes : Int
  leaves : Int
  composites : Int
  decorators : Int
  conditions : Int
  actions : Int
  waits : Int
  max_children : Int
  max_depth : Int
} derive(Debug, Eq)

///|
pub fn TreeStats::to_line(self : TreeStats) -> String {
  "nodes=" +
  self.nodes.to_string() +
  ", leaves=" +
  self.leaves.to_string() +
  ", composites=" +
  self.composites.to_string() +
  ", decorators=" +
  self.decorators.to_string() +
  ", conditions=" +
  self.conditions.to_string() +
  ", actions=" +
  self.actions.to_string() +
  ", waits=" +
  self.waits.to_string() +
  ", max_children=" +
  self.max_children.to_string() +
  ", max_depth=" +
  self.max_depth.to_string()
}

///|
pub fn analyze_tree(tree : BehaviorTree) -> TreeStats {
  let mut leaves = 0
  let mut composites = 0
  let mut decorators = 0
  let mut conditions = 0
  let mut actions = 0
  let mut waits = 0
  let mut max_children = 0
  let mut i = 0
  while i < tree.nodes.length() {
    let n = tree.nodes[i]
    if n.children.length() == 0 {
      leaves = leaves + 1
    }
    if n.children.length() > max_children {
      max_children = n.children.length()
    }
    match n.kind {
      Sequence | Selector | ParallelAll | ParallelAny => composites = composites + 1
      Inverter | Succeeder | Failer | Repeat(_) | Retry(_) => decorators = decorators + 1
      Condition(_, _, _) => conditions = conditions + 1
      ActionPlan(_, _, _) => actions = actions + 1
      Wait(_) => waits = waits + 1
      _ => ()
    }
    i = i + 1
  }
  {
    nodes: tree.nodes.length(),
    leaves,
    composites,
    decorators,
    conditions,
    actions,
    waits,
    max_children,
    max_depth: tree_depth(tree),
  }
}

///|
pub(all) struct LintIssue {
  severity : String
  node_id : String
  message : String
} derive(Debug, Eq)

///|
pub fn LintIssue::to_line(self : LintIssue) -> String {
  self.severity + " " + self.node_id + ": " + self.message
}

///|
pub(all) struct LintReport {
  ok : Bool
  issues : Array[LintIssue]
} derive(Debug, Eq)

///|
pub fn LintReport::summary(self : LintReport) -> String {
  if self.ok {
    "lint ok"
  } else {
    "lint issues=" + self.issues.length().to_string()
  }
}

///|
pub fn LintReport::lines(self : LintReport) -> Array[String] {
  let lines = Array::new()
  if self.ok {
    lines.push("lint ok")
  } else {
    let mut i = 0
    while i < self.issues.length() {
      lines.push(self.issues[i].to_line())
      i = i + 1
    }
  }
  lines
}

///|
pub fn lint_tree(tree : BehaviorTree) -> LintReport {
  let issues = Array::new()
  let validation = tree.validate()
  let mut v = 0
  while v < validation.issues.length() {
    issues.push({ severity: "error", node_id: tree.root, message: validation.issues[v] })
    v = v + 1
  }
  let mut i = 0
  while i < tree.nodes.length() {
    let n = tree.nodes[i]
    if n.name == "" {
      issues.push({ severity: "warning", node_id: n.id, message: "node name is empty" })
    }
    if n.children.length() > 8 {
      issues.push({
        severity: "info",
        node_id: n.id,
        message: "node has many children; consider grouping for readable traces",
      })
    }
    match n.kind {
      Wait(ticks) =>
        if ticks > 120 {
          issues.push({
            severity: "info",
            node_id: n.id,
            message: "long wait may hide intent in frame-based games",
          })
        }
      ActionPlan(_, statuses, _) => {
        if statuses.length() > 20 {
          issues.push({
            severity: "info",
            node_id: n.id,
            message: "scripted action has a long status sequence",
          })
        }
        if action_never_finishes(statuses) {
          issues.push({
            severity: "warning",
            node_id: n.id,
            message: "action status script never reaches success or failure",
          })
        }
      }
      Repeat(count) | Retry(count) =>
        if count > 20 {
          issues.push({
            severity: "info",
            node_id: n.id,
            message: "large decorator count can make tests slow",
          })
        }
      _ => ()
    }
    i = i + 1
  }
  { ok: issues.length() == 0, issues }
}

///|
pub(all) struct RunSummary {
  name : String
  final_status : BtStatus
  ticks : Int
  trace_events : Int
  digest : String
  ok : Bool
  detail : String
} derive(Debug, Eq)

///|
pub fn RunSummary::to_line(self : RunSummary) -> String {
  self.name +
  ": status=" +
  self.final_status.to_text() +
  ", ticks=" +
  self.ticks.to_string() +
  ", events=" +
  self.trace_events.to_string() +
  ", digest=" +
  self.digest +
  ", ok=" +
  (if self.ok { "true" } else { "false" }) +
  ", detail=" +
  self.detail
}

///|
pub fn smoke_run(
  name : String,
  tree : BehaviorTree,
  board : Blackboard,
  expected : BtStatus,
  max_ticks? : Int,
) -> RunSummary {
  let engine = new_engine(tree, blackboard=board, config=default_tick_config())
  match engine.run_until_done(max_ticks=max_ticks.unwrap_or(32)) {
    Ok(result) => {
      let ok = result.status == expected
      {
        name,
        final_status: result.status,
        ticks: result.tick,
        trace_events: engine.trace.length(),
        digest: engine.trace_digest(),
        ok,
        detail: if ok { "matched expected status" } else { "unexpected final status" },
      }
    }
    Err(err) => {
      {
        name,
        final_status: Failure,
        ticks: engine.tick_count,
        trace_events: engine.trace.length(),
        digest: engine.trace_digest(),
        ok: false,
        detail: err.message(),
      }
    }
  }
}

///|
pub(all) struct MarkdownReport {
  title : String
  lines : Array[String]
} derive(Debug, Eq)

///|
pub fn MarkdownReport::text(self : MarkdownReport) -> String {
  join_strings(self.lines, "\n")
}

///|
pub fn tree_markdown_report(
  title : String,
  tree : BehaviorTree,
  board : Blackboard,
) -> MarkdownReport {
  let stats = analyze_tree(tree)
  let lint = lint_tree(tree)
  let lines = Array::new()
  lines.push("# " + title)
  lines.push("")
  lines.push("## Summary")
  lines.push("")
  lines.push("- Root: `" + tree.root + "`")
  lines.push("- Stats: " + stats.to_line())
  lines.push("- Blackboard entries: " + board.len().to_string())
  lines.push("- Lint: " + lint.summary())
  lines.push("")
  lines.push("## Nodes")
  lines.push("")
  let mut i = 0
  while i < tree.nodes.length() {
    let n = tree.nodes[i]
    lines.push(
      "- `" +
      n.id +
      "` " +
      n.kind.to_text() +
      " children=[" +
      join_strings(n.children, ", ") +
      "]",
    )
    i = i + 1
  }
  if board.len() > 0 {
    lines.push("")
    lines.push("## Blackboard")
    lines.push("")
    let board_lines = board.to_lines()
    let mut b = 0
    while b < board_lines.length() {
      lines.push("- " + board_lines[b])
      b = b + 1
    }
  }
  if !lint.ok {
    lines.push("")
    lines.push("## Lint")
    lines.push("")
    let lint_lines = lint.lines()
    let mut l = 0
    while l < lint_lines.length() {
      lines.push("- " + lint_lines[l])
      l = l + 1
    }
  }
  { title, lines }
}

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

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

///|
pub fn trace_contains(events : Array[TickEvent], node_id : String, status : BtStatus) -> Bool {
  let mut i = 0
  while i < events.length() {
    if events[i].node_id == node_id && events[i].status == status {
      return true
    }
    i = i + 1
  }
  false
}

///|
pub fn board_contains(board : Array[(String, BtValue)], key : String, value : BtValue) -> Bool {
  let mut i = 0
  while i < board.length() {
    if board[i].0 == key && board[i].1 == value {
      return true
    }
    i = i + 1
  }
  false
}

///|
fn action_never_finishes(statuses : Array[BtStatus]) -> Bool {
  if statuses.length() == 0 {
    return true
  }
  let mut i = 0
  while i < statuses.length() {
    if statuses[i] != Running {
      return false
    }
    i = i + 1
  }
  true
}

///|
fn tree_depth(tree : BehaviorTree) -> Int {
  if !tree.has_node(tree.root) {
    0
  } else {
    depth_at(tree, tree.root, 1)
  }
}

///|
fn depth_at(tree : BehaviorTree, id : String, depth : Int) -> Int {
  match tree.node(id) {
    Some(n) => {
      let mut best = depth
      let mut i = 0
      while i < n.children.length() {
        let child_depth = depth_at(tree, n.children[i], depth + 1)
        if child_depth > best {
          best = child_depth
        }
        i = i + 1
      }
      best
    }
    None => depth
  }
}