///|
// Keep all position fields independently of @basic.show_loc's display setting.
fn location_to_json(loc : Location) -> Json {
  { "start": ToJson::to_json(loc.start), "end": ToJson::to_json(loc.end) }
}

///|
fn field_to_json(field : Field) -> Json {
  {
    "key": Json::string(field.key),
    "key_loc": location_to_json(field.key_loc),
    "value": expr_to_json(field.value),
  }
}

///|
fn expr_to_json(expr : Expr) -> Json {
  match expr {
    Bool(value~, loc~) =>
      {
        "$tag": "Bool",
        "value": Json::boolean(value),
        "loc": location_to_json(loc),
      }
    Int(repr~, loc~) =>
      {
        "$tag": "Int",
        "repr": Json::string(repr),
        "loc": location_to_json(loc),
      }
    String(str~, loc~) =>
      {
        "$tag": "String",
        "str": Json::string(str),
        "loc": location_to_json(loc),
      }
    Array(items~, loc~) =>
      {
        "$tag": "Array",
        "items": Json::array(items.map(expr_to_json)),
        "loc": location_to_json(loc),
      }
    Map(fields~, loc~) =>
      {
        "$tag": "Map",
        "fields": Json::array(fields.map(field_to_json)),
        "loc": location_to_json(loc),
      }
    Apply(name~, name_loc~, args~, args_loc~, loc~) =>
      {
        "$tag": "Apply",
        "name": Json::string(name),
        "name_loc": location_to_json(name_loc),
        "args": Json::array(args.map(field_to_json)),
        "args_loc": location_to_json(args_loc),
        "loc": location_to_json(loc),
      }
    Hole(loc) => { "$tag": "Hole", "0": location_to_json(loc) }
  }
}

///|
fn import_item_to_json(item : ImportItem) -> Json {
  {
    "path": Json::string(item.path),
    "path_loc": location_to_json(item.path_loc),
    "alias_": if item.alias_ is Some((name, loc)) {
      Json::array([Json::string(name), location_to_json(loc)])
    } else {
      Json::null()
    },
    "loc": location_to_json(item.loc),
  }
}

///|
fn statement_to_json(statement : Statement) -> Json {
  match statement {
    Assign(field) => { "$tag": "Assign", "0": field_to_json(field) }
    Expr(expr) => { "$tag": "Expr", "0": expr_to_json(expr) }
    Import(items~, kind~, conditions~, keyword_loc~, list_loc~, loc~) => {
      let kind = match kind {
        Normal => "Normal"
        Test => "Test"
        Wbtest => "Wbtest"
      }
      {
        "$tag": "Import",
        "items": Json::array(items.map(import_item_to_json)),
        "kind": { "$tag": Json::string(kind) },
        "conditions": Json::array(
          conditions.map(condition => {
            "raw": Json::string(condition.raw),
            "parsed": ToJson::to_json(condition.parsed),
            "loc": location_to_json(condition.loc),
          }),
        ),
        "keyword_loc": location_to_json(keyword_loc),
        "list_loc": location_to_json(list_loc),
        "loc": location_to_json(loc),
      }
    }
  }
}

///|
pub impl ToJson for Ast with fn to_json(self) {
  let calls : Map[String, Array[Expr]] = Map([])
  for statement in self.0 {
    if statement is Expr(Apply(name~, args~, args_loc~, ..)) {
      calls.get_or_init(name, () => []).push(Map(fields=args, loc=args_loc))
    }
  }
  let statements : Array[Json] = []
  for statement in self.0 {
    if statement is Expr(Apply(name~, name_loc~, ..)) {
      // Emit each group at its first occurrence, retaining call order.
      if calls.remove_and_get(name) is Some(items) {
        if items.length() > 1 {
          let loc : Location = {
            start: items[0].loc().start,
            end: items[items.length() - 1].loc().end,
          }
          statements.push(
            statement_to_json(
              Assign({
                key: name,
                key_loc: name_loc,
                value: Array(items~, loc~),
              }),
            ),
          )
        } else {
          statements.push(statement_to_json(statement))
        }
      }
    } else {
      statements.push(statement_to_json(statement))
    }
  }
  Json::array(statements)
}

///|
pub extend Ast with ToJson::{to_json}