///|
priv enum WriterNode {
  /// Nil node, which is used to indicate no previous node
  Nil
  /// Leaf node, which is a string
  Leaf
  /// SimpleExpr node, which is a simple expression with no nested lists
  SimpleExpr
  /// Simple expression, which is a list with no nested lists
  BoxedExpr
  /// Expr node, which can be complex
  Expr
} derive(Eq)

///|
/// `)`, end of a list
let char_close : Char = ')'

///|
/// `(`, start of a list)
let char_open : Char = '('

///|
/// Allowed characters in a leaf node, which can be used to generate a string safely
let allowed_chars : String = "$-:<>[]{}*=+.,\\/!?~_@#&%^|;'"

///|
fn is_a_digit(c : Char) -> Bool {
  let n = c.to_int()
  // ascii table https://tool.oschina.net/commons?type=4
  n >= 48 && n <= 57
}

///|
fn is_a_letter(c : Char) -> Bool {
  match c {
    'A'..='Z' => true
    'a'..='z' => true
    _ => false
  }
}

///|
/// Check if the expression is simple, which means it has no nested lists
fn is_simple_expr(ys : Array[Cirru]) -> Bool {
  for y in ys {
    match y {
      List(_) => return false
      Leaf(_) => ()
    }
  }
  true
}

///|
/// Check if the expression is boxed, which means it has no nested lists
fn is_boxed(ys : Array[Cirru]) -> Bool {
  for y in ys {
    match y {
      Leaf(_) => return false
      List(_) => ()
    }
  }
  true
}

///|
/// Check if the character is a simple character, which means it is a letter or a digit
fn is_simple_char(x : Char) -> Bool {
  is_a_letter(x) || is_a_digit(x)
}

///|
fn is_char_allowed(x : Char) -> Bool {
  if is_simple_char(x) {
    return true
  }
  allowed_chars.contains_char(x)
}

///|
/// Generate a leaf node, which is a string that may contain special characters
fn generate_leaf(s : String) -> String {
  let mut all_allowed = true
  for x in s {
    if !is_char_allowed(x) {
      all_allowed = false
      break
    }
  }
  if all_allowed {
    s.to_string()
  } else {
    let mut ret = ""
    ret += "\""
    for c in s {
      let content = match c {
        '\n' => "\\n"
        '\t' => "\\t"
        '\"' => "\\\""
        '\\' => "\\\\"
        '\'' => "\\'"
        _ => "\{c}"
      }
      ret += content
    }
    ret += "\""
    ret
  }
}

///|
fn generate_empty_expr() -> String {
  "()"
}

///|
fn generate_inline_expr(xs : Array[Cirru]) -> String {
  let mut result = char_open.to_string()
  for idx, x in xs {
    if idx > 0 {
      result += " "
    }
    let piece = match x {
      Leaf(s) => generate_leaf(s)
      List(ys) => generate_inline_expr(ys)
    }
    result += piece
  }
  result += char_close.to_string()
  result
}

///|
fn generate_statement_one_liner(xs : Array[Cirru]) -> String {
  let mut ret = ""
  let size = xs.length()
  for idx, cursor in xs {
    if idx > 0 {
      ret += " "
    }
    let at_tail = idx > 0 && idx == size - 1
    match cursor {
      Leaf(s) => ret += generate_leaf(s)
      List(ys) =>
        if at_tail {
          if ys.is_empty() {
            ret += "$"
          } else {
            ret += "$ "
            ret += generate_statement_one_liner(ys)
          }
        } else {
          ret += generate_inline_expr(ys)
        }
    }
  }
  ret
}

///|
/// by 2 spaces
fn push_spaces(buf : String, n : Int) -> String {
  let mut ret = buf
  for i = 0; i < n; i = i + 1 {
    ret += "  "
  }
  return ret
}

///|
fn render_newline(n : Int) -> String {
  let mut ret = ""
  ret += "\n"
  ret = push_spaces(ret, n)
  ret
}

///|
/// kind for writer nodes
fn Cirru::get_node_kind(self : Cirru) -> WriterNode {
  match self {
    Leaf(_) => Leaf
    List(xs) =>
      if xs.is_empty() {
        Leaf
      } else if is_simple_expr(xs) {
        SimpleExpr
      } else if is_boxed(xs) {
        BoxedExpr
      } else {
        Expr
      }
  }
}

///|
pub(all) suberror FormatCirruError {
  FormatCirruError(String)
} derive(Debug)

///|
fn generate_tree(
  xs : Array[Cirru],
  insist_head : Bool,
  use_inline? : Bool = false,
  base_level : Int,
  in_tail : Bool,
) -> String raise FormatCirruError {
  let mut prev_kind : WriterNode = Nil
  let mut level = base_level
  let mut result = ""
  for idx, cursor in xs {
    let kind = cursor.get_node_kind()
    let next_level = level + 1
    let child_insist_head = prev_kind == BoxedExpr || prev_kind == Expr
    let at_tail = idx != 0 &&
      !in_tail &&
      prev_kind == Leaf &&
      idx == xs.length() - 1

    // println!("\nloop {:?} {:?}", prev_kind, kind);
    // println!("cursor {:?} {} {}", cursor, idx, insist_head);
    // println!("{:?}", result);

    let child : String = match cursor {
      Leaf(s) => generate_leaf(s)
      List(ys) =>
        if at_tail {
          if ys.is_empty() {
            "$"
          } else {
            let mut ret = "$ "
            ret = ret + generate_tree(ys, false, use_inline~, level, at_tail)
            ret
          }
        } else if idx == 0 && insist_head {
          generate_inline_expr(ys)
        } else if kind == Leaf {
          if idx == 0 {
            let mut ret = render_newline(level)
            ret = ret + generate_empty_expr()
            ret
          } else {
            generate_empty_expr() // special since empty expr is treated as leaf
          }
        } else if kind == SimpleExpr {
          if prev_kind == Leaf {
            generate_inline_expr(ys)
          } else if use_inline && prev_kind == SimpleExpr {
            let mut ret = " "
            ret = ret + generate_inline_expr(ys)
            ret
          } else {
            let mut ret = render_newline(next_level)
            ret = ret +
              generate_tree(
                ys,
                child_insist_head,
                use_inline~,
                next_level,
                false,
              )
            ret
          }
        } else if kind == Expr {
          let content = generate_tree(
            ys,
            child_insist_head,
            use_inline~,
            next_level,
            false,
          )
          if content.has_prefix("\n") {
            content
          } else {
            let mut ret = render_newline(next_level)
            ret = ret + content
            ret
          }
        } else if kind == BoxedExpr {
          let content = generate_tree(
            ys,
            child_insist_head,
            use_inline~,
            next_level,
            false,
          )
          if prev_kind == Nil || prev_kind == Leaf || prev_kind == SimpleExpr {
            content
          } else {
            let mut ret = render_newline(next_level)
            ret = ret + content
            ret
          }
        } else {
          raise FormatCirruError("Unpected condition")
        }
    }
    let bended = kind == Leaf && (prev_kind == BoxedExpr || prev_kind == Expr)
    let chunk = if at_tail ||
      (prev_kind == Leaf && kind == Leaf) ||
      (prev_kind == Leaf && kind == SimpleExpr) ||
      (prev_kind == SimpleExpr && kind == Leaf) {
      let mut ret = " "
      ret = ret + child
      ret
    } else if bended {
      let mut ret = render_newline(next_level)
      ret = ret + ", " + child
      ret
    } else {
      child
    }
    result = result + chunk

    // update writer states

    if kind == SimpleExpr {
      if idx == 0 && insist_head {
        prev_kind = SimpleExpr
      } else if use_inline {
        if prev_kind == Leaf || prev_kind == SimpleExpr {
          prev_kind = SimpleExpr
        } else {
          prev_kind = Expr
        }
      } else if prev_kind == Leaf {
        prev_kind = SimpleExpr
      } else {
        prev_kind = Expr
      }
    } else {
      prev_kind = kind
    }
    if bended {
      level += 1
    }
  }

  // console.log("chunk", JSON.stringify(chunk));
  // console.log("And result", JSON.stringify(result));
  result
}

///|
fn generate_statements(
  ys : Array[Cirru],
  use_inline? : Bool = false,
) -> String raise FormatCirruError {
  let mut zs = ""
  for y in ys {
    match y {
      Leaf(_) => raise FormatCirruError("expected an exprs at top level")
      List(cs) => {
        zs += "\n"
        zs += generate_tree(cs, true, use_inline~, 0, false)
        zs += "\n"
      }
    }
  }
  zs
}

///|
/// format Cirru code, use options to control `use_inline` option
pub fn Cirru::format(
  xs : Array[Cirru],
  use_inline? : Bool = false,
) -> String raise FormatCirruError {
  generate_statements(xs, use_inline~)
}

///|
/// format a single Cirru expression as a single line
pub fn Cirru::format_expr_one_liner(
  expr : Cirru,
) -> String raise FormatCirruError {
  match expr {
    Leaf(_) =>
      raise FormatCirruError("format_expr_one_liner expects an expr (list)")
    List(xs) => generate_statement_one_liner(xs)
  }
}