///|
/// 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()
    match tokenize_dsl_line(raw_line, opts.allow_comments, line_no + 1) {
      Ok(tokens) =>
        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)
          }
        }
      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() {
    match tokenize_dsl_line(lines[i].to_owned(), true, i + 1) {
      Ok(tokens) =>
        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 if tokens.length() > 0 {
          return Err(
            ParseError("blackboard line " + (i + 1).to_string() + " is invalid"),
          )
        }
      Err(err) => return Err(err)
    }
    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 + " " + serialize_value(snapshot[i].1),
    )
    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_nonnegative_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 = match parse_status_list_checked(tokens[3]) {
    Ok(value) => value
    Err(invalid) =>
      return Err(
        ParseError(
          "line " + line_no.to_string() + ": invalid action status " + invalid,
        ),
      )
  }
  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_checked(raw : String) -> Result[Array[BtStatus], String] {
  let out = Array::new()
  let pieces = raw.split(",").collect()
  let mut i = 0
  while i < pieces.length() {
    let token = trim_ascii(pieces[i].to_owned())
    match parse_status(token) {
      Some(status) => out.push(status)
      None => return Err(if token == "" { "" } else { token })
    }
    i = i + 1
  }
  if out.length() == 0 {
    Err("")
  } else {
    Ok(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 parse_nonnegative_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() +
      " " +
      serialize_value(value)
    SetValue(key, value) =>
      "set " + n.id + " " + key + " " + serialize_value(value)
    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 + "=" + serialize_value(writes[i].1))
    i = i + 1
  }
  " " + join_strings(out, " ")
}

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

///|
fn serialize_value(value : BtValue) -> String {
  match value {
    BoolValue(value) => if value { "true" } else { "false" }
    IntValue(value) => value.to_string()
    TextValue(value) => "text:" + quote_string(value)
    EmptyValue => ""
  }
}

///|
fn quote_if_needed(raw : String) -> String {
  if raw == "" {
    quote_string(raw)
  } else if contains_space(raw) ||
    raw.contains("#") ||
    raw.contains("\"") ||
    raw.contains("\\") {
    quote_string(raw)
  } else {
    raw
  }
}

///|
fn quote_string(raw : String) -> String {
  let chars = Array::new()
  chars.push('"')
  let mut i = 0
  while i < raw.length() {
    match raw[i] {
      34 => {
        chars.push('\\')
        chars.push('"')
      }
      92 => {
        chars.push('\\')
        chars.push('\\')
      }
      10 => {
        chars.push('\\')
        chars.push('n')
      }
      13 => {
        chars.push('\\')
        chars.push('r')
      }
      9 => {
        chars.push('\\')
        chars.push('t')
      }
      code => chars.push(code.unsafe_to_char())
    }
    i = i + 1
  }
  chars.push('"')
  String::from_array(chars)
}

///|
fn tokenize_dsl_line(
  raw : String,
  allow_comments : Bool,
  line_no : Int,
) -> Result[Array[String], BtError] {
  let content = if allow_comments { strip_comment(raw) } else { raw }
  match validate_quoted_line(content, line_no) {
    Ok(_) => Ok(split_ascii_words(content))
    Err(err) => Err(err)
  }
}

///|
fn validate_quoted_line(raw : String, line_no : Int) -> Result[Unit, BtError] {
  let mut quoted = false
  let mut escaped = false
  let mut i = 0
  while i < raw.length() {
    let code = raw[i]
    if escaped {
      escaped = false
    } else if code == 92 && quoted {
      escaped = true
    } else if code == 34 {
      quoted = !quoted
    }
    i = i + 1
  }
  if escaped {
    Err(
      ParseError(
        "line " + line_no.to_string() + ": dangling escape in quoted value",
      ),
    )
  } else if quoted {
    Err(
      ParseError("line " + line_no.to_string() + ": unterminated quoted value"),
    )
  } else {
    Ok(())
  }
}

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

///|
fn append_escaped_char(chars : Array[Char], code : UInt16) -> Unit {
  match code {
    110 => chars.push('\n')
    114 => chars.push('\r')
    116 => chars.push('\t')
    other => chars.push(other.unsafe_to_char())
  }
}

///|
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
}