///|
/// Options for rendering behavior trees into graph descriptions.
pub(all) struct VisualOptions {
  include_node_names : Bool
  include_node_kinds : Bool
  show_leaf_shape : Bool
} derive(Debug, Eq)

///|
pub fn default_visual_options() -> VisualOptions {
  { include_node_names: true, include_node_kinds: true, show_leaf_shape: true }
}

///|
pub fn compact_visual_options() -> VisualOptions {
  { include_node_names: false, include_node_kinds: false, show_leaf_shape: false }
}

///|
/// Export a behavior tree to Graphviz DOT.
pub fn tree_to_dot(tree : BehaviorTree, options? : VisualOptions) -> String {
  let opts = options.unwrap_or(default_visual_options())
  let lines = Array::new()
  lines.push("digraph MoonBTKit {")
  lines.push("  rankdir=TB;")
  lines.push("  node [fontname=\"Arial\", fontsize=10];")
  let mut i = 0
  while i < tree.nodes.length() {
    let n = tree.nodes[i]
    lines.push(
      "  \"" +
      dot_escape(n.id) +
      "\" [label=\"" +
      dot_escape(node_visual_label(n, opts)) +
      "\", shape=" +
      dot_shape(n, opts) +
      "];",
    )
    i = i + 1
  }
  let mut p = 0
  while p < tree.nodes.length() {
    let parent = tree.nodes[p]
    let mut c = 0
    while c < parent.children.length() {
      lines.push(
        "  \"" +
        dot_escape(parent.id) +
        "\" -> \"" +
        dot_escape(parent.children[c]) +
        "\";",
      )
      c = c + 1
    }
    p = p + 1
  }
  lines.push("}")
  join_strings(lines, "\n")
}

///|
/// Export a behavior tree to Mermaid flowchart syntax.
pub fn tree_to_mermaid(tree : BehaviorTree, options? : VisualOptions) -> String {
  let opts = options.unwrap_or(default_visual_options())
  let lines = Array::new()
  lines.push("flowchart TD")
  let mut i = 0
  while i < tree.nodes.length() {
    let n = tree.nodes[i]
    lines.push(
      "  " +
      mermaid_id(n.id) +
      mermaid_node_open(n, opts) +
      mermaid_escape(node_visual_label(n, opts)) +
      mermaid_node_close(n, opts),
    )
    i = i + 1
  }
  let mut p = 0
  while p < tree.nodes.length() {
    let parent = tree.nodes[p]
    let mut c = 0
    while c < parent.children.length() {
      lines.push(
        "  " +
        mermaid_id(parent.id) +
        " --> " +
        mermaid_id(parent.children[c]),
      )
      c = c + 1
    }
    p = p + 1
  }
  join_strings(lines, "\n")
}

///|
/// Render trace events as a Mermaid sequence diagram.
pub fn trace_to_mermaid_sequence(events : Array[TickEvent]) -> String {
  let lines = Array::new()
  lines.push("sequenceDiagram")
  lines.push("  participant Engine")
  let participants = trace_participants(events)
  let mut p = 0
  while p < participants.length() {
    lines.push("  participant " + mermaid_id(participants[p]) + " as " + mermaid_escape(participants[p]))
    p = p + 1
  }
  let mut i = 0
  while i < events.length() {
    let event = events[i]
    lines.push(
      "  Engine->>" +
      mermaid_id(event.node_id) +
      ": tick " +
      event.tick.to_string() +
      " " +
      event.status.to_text(),
    )
    i = i + 1
  }
  join_strings(lines, "\n")
}

///|
/// Return a short visualization bundle suitable for docs and issue comments.
pub fn visualization_bundle(tree : BehaviorTree, events : Array[TickEvent]) -> Array[(String, String)] {
  [
    ("tree.dot", tree_to_dot(tree)),
    ("tree.mmd", tree_to_mermaid(tree)),
    ("trace.mmd", trace_to_mermaid_sequence(events)),
  ]
}

///|
fn node_visual_label(n : BtNode, opts : VisualOptions) -> String {
  let mut label = n.id
  if opts.include_node_names && n.name != n.id {
    label = label + "\\n" + n.name
  }
  if opts.include_node_kinds {
    label = label + "\\n" + n.kind.to_text()
  }
  label
}

///|
fn dot_shape(n : BtNode, opts : VisualOptions) -> String {
  if opts.show_leaf_shape && n.children.length() == 0 {
    match n.kind {
      Condition(_, _, _) => "diamond"
      ActionPlan(_, _, _) => "box"
      Wait(_) => "oval"
      SetValue(_, _) => "note"
      Emit(_) => "cds"
      _ => "ellipse"
    }
  } else {
    match n.kind {
      Sequence | Selector | ParallelAll | ParallelAny => "folder"
      Inverter | Succeeder | Failer | Repeat(_) | Retry(_) => "component"
      _ => "ellipse"
    }
  }
}

///|
fn mermaid_node_open(n : BtNode, opts : VisualOptions) -> String {
  if opts.show_leaf_shape && n.children.length() == 0 {
    match n.kind {
      Condition(_, _, _) => "{"
      ActionPlan(_, _, _) => "["
      Wait(_) => "(["
      _ => "["
    }
  } else {
    match n.kind {
      Sequence | Selector | ParallelAll | ParallelAny => "[["
      Inverter | Succeeder | Failer | Repeat(_) | Retry(_) => "[/"
      _ => "["
    }
  }
}

///|
fn mermaid_node_close(n : BtNode, opts : VisualOptions) -> String {
  if opts.show_leaf_shape && n.children.length() == 0 {
    match n.kind {
      Condition(_, _, _) => "}"
      Wait(_) => "])"
      _ => "]"
    }
  } else {
    match n.kind {
      Sequence | Selector | ParallelAll | ParallelAny => "]]"
      Inverter | Succeeder | Failer | Repeat(_) | Retry(_) => "/]"
      _ => "]"
    }
  }
}

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

///|
fn mermaid_id(raw : String) -> String {
  let chars = Array::new()
  let mut i = 0
  while i < raw.length() {
    let code = raw[i]
    if is_alpha_num_code(code) {
      chars.push(code.unsafe_to_char())
    } else {
      chars.push('_')
    }
    i = i + 1
  }
  let id = String::from_array(chars)
  if id == "" { "node" } else { "n_" + id }
}

///|
fn is_alpha_num_code(code : UInt16) -> Bool {
  (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || (code >= 48 && code <= 57)
}

///|
fn dot_escape(raw : String) -> String {
  raw.replace_all(old="\\", new="\\\\").replace_all(old="\"", new="\\\"")
}

///|
fn mermaid_escape(raw : String) -> String {
  raw.replace_all(old="\"", new="'").replace_all(old="\n", new="
").replace_all(old="\\n", new="
") }