///|
/// Compatibility result for two versions of a behavior tree asset.
pub(all) enum CompatibilityLevel {
  Compatible
  ReviewRequired
  Breaking
} derive(Debug, Eq)

///|
pub fn CompatibilityLevel::to_text(self : CompatibilityLevel) -> String {
  match self {
    Compatible => "compatible"
    ReviewRequired => "review-required"
    Breaking => "breaking"
  }
}

///|
/// A structural change for one node that exists in both tree versions.
pub(all) struct NodeDelta {
  id : String
  before_kind : String
  after_kind : String
  kind_changed : Bool
  children_changed : Bool
  name_changed : Bool
} derive(Debug, Eq)

///|
pub fn NodeDelta::is_breaking(self : NodeDelta) -> Bool {
  self.kind_changed || self.children_changed
}

///|
pub fn NodeDelta::summary(self : NodeDelta) -> String {
  let changes = Array::new()
  if self.kind_changed {
    changes.push("kind " + self.before_kind + " -> " + self.after_kind)
  }
  if self.children_changed {
    changes.push("children changed")
  }
  if self.name_changed {
    changes.push("name changed")
  }
  self.id + ": " + join_strings(changes, ", ")
}

///|
/// Structural difference between two behavior tree assets.
pub(all) struct TreeDiff {
  before_root : String
  after_root : String
  added_nodes : Array[String]
  removed_nodes : Array[String]
  changed_nodes : Array[NodeDelta]
} derive(Debug, Eq)

///|
pub fn TreeDiff::root_changed(self : TreeDiff) -> Bool {
  self.before_root != self.after_root
}

///|
pub fn TreeDiff::change_count(self : TreeDiff) -> Int {
  self.added_nodes.length() +
  self.removed_nodes.length() +
  self.changed_nodes.length() +
  (if self.root_changed() { 1 } else { 0 })
}

///|
pub fn TreeDiff::is_empty(self : TreeDiff) -> Bool {
  self.change_count() == 0
}

///|
pub fn TreeDiff::compatibility(self : TreeDiff) -> CompatibilityLevel {
  if self.root_changed() || self.removed_nodes.length() > 0 {
    return Breaking
  }
  let mut i = 0
  while i < self.changed_nodes.length() {
    if self.changed_nodes[i].is_breaking() {
      return Breaking
    }
    i = i + 1
  }
  if self.changed_nodes.length() > 0 {
    ReviewRequired
  } else {
    Compatible
  }
}

///|
pub fn TreeDiff::summary(self : TreeDiff) -> String {
  "compatibility=" +
  self.compatibility().to_text() +
  ", changes=" +
  self.change_count().to_string() +
  ", added=" +
  self.added_nodes.length().to_string() +
  ", removed=" +
  self.removed_nodes.length().to_string() +
  ", changed=" +
  self.changed_nodes.length().to_string()
}

///|
pub fn TreeDiff::lines(self : TreeDiff) -> Array[String] {
  let lines = Array::new()
  lines.push(self.summary())
  if self.root_changed() {
    lines.push("root: " + self.before_root + " -> " + self.after_root)
  }
  if self.added_nodes.length() > 0 {
    lines.push("added: " + join_strings(self.added_nodes, ", "))
  }
  if self.removed_nodes.length() > 0 {
    lines.push("removed: " + join_strings(self.removed_nodes, ", "))
  }
  let mut i = 0
  while i < self.changed_nodes.length() {
    lines.push("changed: " + self.changed_nodes[i].summary())
    i = i + 1
  }
  lines
}

///|
pub fn TreeDiff::markdown(self : TreeDiff, title? : String) -> String {
  let lines = Array::new()
  lines.push("# " + title.unwrap_or("MoonBTKit Asset Diff"))
  lines.push("")
  lines.push("- " + self.summary())
  lines.push("")
  lines.push("## Changes")
  lines.push("")
  let details = self.lines()
  let mut i = 1
  if details.length() == 1 {
    lines.push("- no structural changes")
  }
  while i < details.length() {
    lines.push("- " + details[i])
    i = i + 1
  }
  join_strings(lines, "\n")
}

///|
/// Compare two validated behavior tree models by stable node identifiers.
pub fn diff_trees(before : BehaviorTree, after : BehaviorTree) -> TreeDiff {
  let added = Array::new()
  let removed = Array::new()
  let changed = Array::new()
  let mut i = 0
  while i < before.nodes.length() {
    let old_node = before.nodes[i]
    match after.node(old_node.id) {
      Some(new_node) => {
        let kind_changed = old_node.kind != new_node.kind
        let children_changed = !same_string_array(
          old_node.children,
          new_node.children,
        )
        let name_changed = old_node.name != new_node.name
        if kind_changed || children_changed || name_changed {
          changed.push({
            id: old_node.id,
            before_kind: old_node.kind.to_text(),
            after_kind: new_node.kind.to_text(),
            kind_changed,
            children_changed,
            name_changed,
          })
        }
      }
      None => removed.push(old_node.id)
    }
    i = i + 1
  }
  let mut j = 0
  while j < after.nodes.length() {
    if !before.has_node(after.nodes[j].id) {
      added.push(after.nodes[j].id)
    }
    j = j + 1
  }
  {
    before_root: before.root,
    after_root: after.root,
    added_nodes: added,
    removed_nodes: removed,
    changed_nodes: changed,
  }
}

///|
/// Parse and compare two DSL assets. Invalid assets return their parse error.
pub fn diff_dsl(
  before_source : String,
  after_source : String,
) -> Result[TreeDiff, BtError] {
  match parse_tree_dsl(before_source) {
    Ok(before) =>
      match parse_tree_dsl(after_source) {
        Ok(after) => Ok(diff_trees(before, after))
        Err(err) => Err(err)
      }
    Err(err) => Err(err)
  }
}

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