///|
/// Parser options for the line-based MoonBTKit DSL.
pub(all) struct DslOptions {
  allow_comments : Bool
  validate_after_parse : Bool
  default_root : String
} derive(Debug, Eq)

///|
pub fn default_dsl_options() -> DslOptions {
  { allow_comments: true, validate_after_parse: true, default_root: "root" }
}

///|
pub(all) struct DslDocument {
  tree : BehaviorTree
  blackboard : Blackboard
  warnings : Array[String]
} derive(Debug, Eq)

///|
pub fn DslDocument::summary(self : DslDocument) -> String {
  "root=" +
  self.tree.root +
  ", nodes=" +
  self.tree.node_count().to_string() +
  ", blackboard=" +
  self.blackboard.len().to_string() +
  ", warnings=" +
  self.warnings.length().to_string()
}

///|
/// Parse a behavior tree and initial blackboard from a compact line DSL.
///
/// Supported examples:
///
/// * `root root`
/// * `selector root engage patrol`
/// * `condition see_enemy enemy_visible eq true`
/// * `action chase chase_enemy running,success mode=chasing`
/// * `blackboard ammo 3`
pub fn parse_dsl(
  source : String,
  options? : DslOptions,
) -> Result[DslDocument, BtError] {
  let opts = options.unwrap_or(default_dsl_options())
  let mut root = opts.default_root
  let nodes = Array::new()
  let board = new_blackboard()
  let warnings = Array::new()
  let lines = source.replace_all(old="\r\n", new="\n").split("\n").collect()
  let mut line_no = 0
  while line_no < lines.length() {
    let raw_line = lines[line_no].to_owned()
    let cleaned = if opts.allow_comments {
      trim_ascii(strip_comment(raw_line))
    } else {
      trim_ascii(raw_line)
    }
    if cleaned != "" {
      let tokens = split_ascii_words(cleaned)
      if tokens.length() > 0 {
        match parse_line(tokens, line_no + 1, board, warnings) {
          Ok(LineRoot(id)) => root = id
          Ok(LineNode(n)) => nodes.push(n)
          Ok(LineNone) => ()
          Err(err) => return Err(err)
        }
      }
    }
    line_no = line_no + 1
  }
  let tree = tree_from_nodes(root, nodes)
  if opts.validate_after_parse {
    let report = tree.validate()
    if !report.ok {
      return Err(InvalidTree(report.message()))
    }
  }
  Ok({ tree, blackboard: board, warnings })
}

///|
pub fn parse_tree_dsl(source : String) -> Result[BehaviorTree, BtError] {
  match parse_dsl(source) {
    Ok(doc) => Ok(doc.tree)
    Err(err) => Err(err)
  }
}

///|
pub fn parse_blackboard_dsl(source : String) -> Result[Blackboard, BtError] {
  let board = new_blackboard()
  let lines = source.replace_all(old="\r\n", new="\n").split("\n").collect()
  let mut i = 0
  while i < lines.length() {
    let line = trim_ascii(strip_comment(lines[i].to_owned()))
    if line != "" {
      let tokens = split_ascii_words(line)
      if tokens.length() == 2 {
        board.set(tokens[0], parse_value(tokens[1]))
      } else if tokens.length() == 3 && tokens[0] == "blackboard" {
        board.set(tokens[1], parse_value(tokens[2]))
      } else {
        return Err(ParseError("blackboard line " + (i + 1).to_string() + " is invalid"))
      }
    }
    i = i + 1
  }
  Ok(board)
}

///|
pub fn serialize_tree(tree : BehaviorTree) -> Array[String] {
  let lines = Array::new()
  lines.push("root " + tree.root)
  let mut i = 0
  while i < tree.nodes.length() {
    lines.push(serialize_node(tree.nodes[i]))
    i = i + 1
  }
  lines
}

///|
pub fn serialize_blackboard(board : Blackboard) -> Array[String] {
  let lines = Array::new()
  let snapshot = board.snapshot()
  let mut i = 0
  while i < snapshot.length() {
    lines.push("blackboard " + snapshot[i].0 + " " + quote_if_needed(snapshot[i].1.to_text()))
    i = i + 1
  }
  lines
}

///|
enum ParsedLine {
  LineRoot(String)
  LineNode(BtNode)
  LineNone
} derive(Debug, Eq)

///|
fn parse_line(
  tokens : Array[String],
  line_no : Int,
  board : Blackboard,
  warnings : Array[String],
) -> Result[ParsedLine, BtError] {
  match tokens[0] {
    "root" => {
      if tokens.length() != 2 {
        return Err(ParseError("line " + line_no.to_string() + ": root expects one id"))
      }
      Ok(LineRoot(tokens[1]))
    }
    "blackboard" => {
      if tokens.length() != 3 {
        return Err(ParseError("line " + line_no.to_string() + ": blackboard expects key value"))
      }
      board.set(tokens[1], parse_value(tokens[2]))
      Ok(LineNone)
    }
    "sequence" => parse_composite(tokens, line_no, Sequence)
    "selector" => parse_composite(tokens, line_no, Selector)
    "parallel_all" => parse_composite(tokens, line_no, ParallelAll)
    "parallel_any" => parse_composite(tokens, line_no, ParallelAny)
    "inverter" => parse_decorator(tokens, line_no, Inverter)
    "succeeder" => parse_decorator(tokens, line_no, Succeeder)
    "failer" => parse_decorator(tokens, line_no, Failer)
    "repeat" => parse_count_decorator(tokens, line_no, true)
    "retry" => parse_count_decorator(tokens, line_no, false)
    "condition" => parse_condition(tokens, line_no)
    "set" => parse_set(tokens, line_no)
    "wait" => parse_wait(tokens, line_no)
    "emit" => parse_emit(tokens, line_no)
    "action" => parse_action(tokens, line_no, warnings)
    _ => Err(ParseError("line " + line_no.to_string() + ": unknown command " + tokens[0]))
  }
}

///|
fn parse_composite(
  tokens : Array[String],
  line_no : Int,
  kind : NodeKind,
) -> Result[ParsedLine, BtError] {
  if tokens.length() < 3 {
    return Err(
      ParseError("line " + line_no.to_string() + ": composite expects id and children"),
    )
  }
  let children = Array::new()
  let mut i = 2
  while i < tokens.length() {
    children.push(tokens[i])
    i = i + 1
  }
  Ok(LineNode(node(tokens[1], kind, children~)))
}

///|
fn parse_decorator(
  tokens : Array[String],
  line_no : Int,
  kind : NodeKind,
) -> Result[ParsedLine, BtError] {
  if tokens.length() != 3 {
    return Err(
      ParseError("line " + line_no.to_string() + ": decorator expects id and child"),
    )
  }
  Ok(LineNode(node(tokens[1], kind, children=[tokens[2]])))
}

///|
fn parse_count_decorator(
  tokens : Array[String],
  line_no : Int,
  repeat_kind : Bool,
) -> Result[ParsedLine, BtError] {
  if tokens.length() != 4 {
    return Err(
      ParseError("line " + line_no.to_string() + ": count decorator expects id count child"),
    )
  }
  match parse_positive_int(tokens[2]) {
    Some(count) => {
      let kind = if repeat_kind { Repeat(count) } else { Retry(count) }
      Ok(LineNode(node(tokens[1], kind, children=[tokens[3]])))
    }
    None => Err(ParseError("line " + line_no.to_string() + ": invalid count " + tokens[2]))
  }
}

///|
fn parse_condition(tokens : Array[String], line_no : Int) -> Result[ParsedLine, BtError] {
  if tokens.length() != 5 {
    return Err(
      ParseError("line " + line_no.to_string() + ": condition expects id key op value"),
    )
  }
  match parse_compare_op(tokens[3]) {
    Some(op) => Ok(LineNode(condition(tokens[1], tokens[2], op, parse_value(tokens[4]))))
    None => Err(ParseError("line " + line_no.to_string() + ": invalid compare op " + tokens[3]))
  }
}

///|
fn parse_set(tokens : Array[String], line_no : Int) -> Result[ParsedLine, BtError] {
  if tokens.length() != 4 {
    return Err(ParseError("line " + line_no.to_string() + ": set expects id key value"))
  }
  Ok(LineNode(set_value(tokens[1], tokens[2], parse_value(tokens[3]))))
}

///|
fn parse_wait(tokens : Array[String], line_no : Int) -> Result[ParsedLine, BtError] {
  if tokens.length() != 3 {
    return Err(ParseError("line " + line_no.to_string() + ": wait expects id ticks"))
  }
  match parse_positive_int(tokens[2]) {
    Some(ticks) => Ok(LineNode(wait(tokens[1], ticks)))
    None => Err(ParseError("line " + line_no.to_string() + ": invalid wait ticks " + tokens[2]))
  }
}

///|
fn parse_emit(tokens : Array[String], line_no : Int) -> Result[ParsedLine, BtError] {
  if tokens.length() != 3 {
    return Err(ParseError("line " + line_no.to_string() + ": emit expects id label"))
  }
  Ok(LineNode(node(tokens[1], Emit(unquote(tokens[2])))))
}

///|
fn parse_action(
  tokens : Array[String],
  line_no : Int,
  warnings : Array[String],
) -> Result[ParsedLine, BtError] {
  if tokens.length() < 4 {
    return Err(
      ParseError("line " + line_no.to_string() + ": action expects id name status-list"),
    )
  }
  let statuses = parse_status_list(tokens[3])
  if statuses.length() == 0 {
    return Err(ParseError("line " + line_no.to_string() + ": action has empty status-list"))
  }
  let writes = Array::new()
  let mut i = 4
  while i < tokens.length() {
    match parse_write_token(tokens[i]) {
      Some(pair) => writes.push(pair)
      None => warnings.push("line " + line_no.to_string() + ": ignored action token " + tokens[i])
    }
    i = i + 1
  }
  Ok(LineNode(node(tokens[1], ActionPlan(unquote(tokens[2]), statuses, writes))))
}

///|
fn parse_status_list(raw : String) -> Array[BtStatus] {
  let out = Array::new()
  let pieces = raw.split(",").collect()
  let mut i = 0
  while i < pieces.length() {
    match parse_status(trim_ascii(pieces[i].to_owned())) {
      Some(status) => out.push(status)
      None => ()
    }
    i = i + 1
  }
  out
}

///|
fn parse_status(raw : String) -> BtStatus? {
  match raw {
    "success" | "ok" => Some(Success)
    "failure" | "fail" => Some(Failure)
    "running" | "run" => Some(Running)
    _ => None
  }
}

///|
fn parse_write_token(raw : String) -> (String, BtValue)? {
  match raw.split_once("=") {
    Some((left, right)) => {
      let key = trim_ascii(left.to_owned())
      if key == "" {
        None
      } else {
        Some((key, parse_value(trim_ascii(right.to_owned()))))
      }
    }
    None => None
  }
}

///|
fn parse_positive_int(raw : String) -> Int? {
  match parse_decimal_int(raw) {
    Some(value) =>
      if value >= 0 {
        Some(value)
      } else {
        None
      }
    None => None
  }
}

///|
fn serialize_node(n : BtNode) -> String {
  match n.kind {
    Sequence => "sequence " + n.id + " " + join_strings(n.children, " ")
    Selector => "selector " + n.id + " " + join_strings(n.children, " ")
    ParallelAll => "parallel_all " + n.id + " " + join_strings(n.children, " ")
    ParallelAny => "parallel_any " + n.id + " " + join_strings(n.children, " ")
    Inverter => "inverter " + n.id + " " + first_child(n)
    Succeeder => "succeeder " + n.id + " " + first_child(n)
    Failer => "failer " + n.id + " " + first_child(n)
    Repeat(count) => "repeat " + n.id + " " + count.to_string() + " " + first_child(n)
    Retry(count) => "retry " + n.id + " " + count.to_string() + " " + first_child(n)
    Condition(key, op, value) =>
      "condition " +
      n.id +
      " " +
      key +
      " " +
      op.to_text() +
      " " +
      quote_if_needed(value.to_text())
    SetValue(key, value) =>
      "set " + n.id + " " + key + " " + quote_if_needed(value.to_text())
    ActionPlan(name, statuses, writes) =>
      "action " +
      n.id +
      " " +
      quote_if_needed(name) +
      " " +
      serialize_statuses(statuses) +
      serialize_writes(writes)
    Wait(ticks) => "wait " + n.id + " " + ticks.to_string()
    Emit(label) => "emit " + n.id + " " + quote_if_needed(label)
  }
}

///|
fn serialize_statuses(statuses : Array[BtStatus]) -> String {
  let out = Array::new()
  let mut i = 0
  while i < statuses.length() {
    out.push(statuses[i].to_text())
    i = i + 1
  }
  join_strings(out, ",")
}

///|
fn serialize_writes(writes : Array[(String, BtValue)]) -> String {
  if writes.length() == 0 {
    return ""
  }
  let out = Array::new()
  let mut i = 0
  while i < writes.length() {
    out.push(writes[i].0 + "=" + quote_if_needed(writes[i].1.to_text()))
    i = i + 1
  }
  " " + join_strings(out, " ")
}

///|
fn first_child(n : BtNode) -> String {
  if n.children.length() == 0 {
    ""
  } else {
    n.children[0]
  }
}

///|
fn quote_if_needed(raw : String) -> String {
  if raw == "" {
    "\"\""
  } else if contains_space(raw) || raw.contains("#") {
    "\"" + raw.replace_all(old="\"", new="\\\"") + "\""
  } else {
    raw
  }
}

///|
fn strip_comment(raw : String) -> String {
  let mut i = 0
  let mut quoted = false
  while i < raw.length() {
    if raw[i] == 34 {
      quoted = !quoted
    } else if raw[i] == 35 && !quoted {
      return raw.unsafe_substring(start=0, end=i)
    }
    i = i + 1
  }
  raw
}

///|
fn split_ascii_words(raw : String) -> Array[String] {
  let out = Array::new()
  let mut chars = Array::new()
  let mut quoted = false
  let mut escaped = false
  let mut i = 0
  while i < raw.length() {
    let code = raw[i]
    if escaped {
      chars.push(code.unsafe_to_char())
      escaped = false
    } else if code == 92 && quoted {
      escaped = true
    } else if code == 34 {
      quoted = !quoted
    } else if is_ascii_space(code) && !quoted {
      if chars.length() > 0 {
        out.push(String::from_array(chars))
        chars = Array::new()
      }
    } else {
      chars.push(code.unsafe_to_char())
    }
    i = i + 1
  }
  if chars.length() > 0 || quoted {
    out.push(String::from_array(chars))
  }
  out
}

///|
fn contains_space(raw : String) -> Bool {
  let mut i = 0
  while i < raw.length() {
    if is_ascii_space(raw[i]) {
      return true
    }
    i = i + 1
  }
  false
}