///|
fn ast_object(fields : Array[(String, Json)]) -> Json {
  Json::object(Map::from_array(fields))
}

///|
fn span_json(span : SourceSpan) -> Json {
  ast_object([
    ("start", Json::number(span.start.to_double(), repr=span.start.to_string())),
    ("end", Json::number(span.end.to_double(), repr=span.end.to_string())),
    ("line", Json::number(span.line.to_double(), repr=span.line.to_string())),
    (
      "column",
      Json::number(span.column.to_double(), repr=span.column.to_string()),
    ),
  ])
}

///|
fn literal_json(literal : Literal) -> Json {
  match literal {
    Null =>
      ast_object([("type", Json::string("null")), ("value", Json::null())])
    Bool(value) =>
      ast_object([
        ("type", Json::string("bool")),
        ("value", Json::boolean(value)),
      ])
    Int(value) =>
      ast_object([
        ("type", Json::string("int")),
        ("value", Json::string(value.to_string())),
      ])
    UInt(value) =>
      ast_object([
        ("type", Json::string("uint")),
        ("value", Json::string(value.to_string())),
      ])
    Double(value) =>
      ast_object([
        ("type", Json::string("double")),
        ("value", Json::number(value)),
      ])
    String(value) =>
      ast_object([
        ("type", Json::string("string")),
        ("value", Json::string(value)),
      ])
    Bytes(value) =>
      ast_object([
        ("type", Json::string("bytes")),
        (
          "length",
          Json::number(
            value.length().to_double(),
            repr=value.length().to_string(),
          ),
        ),
      ])
  }
}

///|
fn expr_base(
  expr : Expr,
  kind : String,
  fields : Array[(String, Json)],
) -> Json {
  let result : Array[(String, Json)] = [
    ("id", Json::number(expr.id.to_double(), repr=expr.id.to_string())),
    ("kind", Json::string(kind)),
    ("span", span_json(expr.span)),
  ]
  result.append(fields)
  ast_object(result)
}

///|
/// Convert an expression and all descendants to a stable JSON AST.
pub fn Expr::to_json(self : Expr) -> Json {
  match self.kind {
    Literal(value) =>
      expr_base(self, "literal", [("literal", literal_json(value))])
    Identifier(name) =>
      expr_base(self, "identifier", [("name", Json::string(name))])
    List(items) =>
      expr_base(self, "list", [
        ("items", Json::array(items.map(item => item.to_json()))),
      ])
    Map(entries) =>
      expr_base(self, "map", [
        (
          "entries",
          Json::array(
            entries.map(entry => {
              ast_object([
                ("key", entry.key.to_json()),
                ("value", entry.value.to_json()),
                ("span", span_json(entry.span)),
              ])
            }),
          ),
        ),
      ])
    Unary(operator, operand) =>
      expr_base(self, "unary", [
        ("operator", Json::string(operator.symbol())),
        ("operand", operand.to_json()),
      ])
    Binary(operator, left, right) =>
      expr_base(self, "binary", [
        ("operator", Json::string(operator.symbol())),
        ("left", left.to_json()),
        ("right", right.to_json()),
      ])
    Conditional(condition, when_true, when_false) =>
      expr_base(self, "conditional", [
        ("condition", condition.to_json()),
        ("when_true", when_true.to_json()),
        ("when_false", when_false.to_json()),
      ])
    Select(target, field) =>
      expr_base(self, "select", [
        ("target", target.to_json()),
        ("field", Json::string(field)),
      ])
    Index(target, index) =>
      expr_base(self, "index", [
        ("target", target.to_json()),
        ("index", index.to_json()),
      ])
    Call(name, args) =>
      expr_base(self, "call", [
        ("name", Json::string(name)),
        ("arguments", Json::array(args.map(arg => arg.to_json()))),
      ])
    ReceiverCall(target, name, args) =>
      expr_base(self, "receiver_call", [
        ("target", target.to_json()),
        ("name", Json::string(name)),
        ("arguments", Json::array(args.map(arg => arg.to_json()))),
      ])
  }
}

///|
pub fn ParsedExpr::to_json(self : ParsedExpr) -> Json {
  ast_object([
    ("schema", Json::string("moon-cel.ast/1")),
    ("source", Json::string(self.source())),
    ("root", self.root().to_json()),
  ])
}

///|
pub fn ParsedExpr::to_json_string(
  self : ParsedExpr,
  indent? : Int = 2,
) -> String {
  self.to_json().stringify(indent~)
}