///|
fn json_escape(text : String) -> String {
  let output = StringBuilder()
  for character in text {
    match character {
      '"' => output <+ "\\\""
      '\\' => output <+ "\\\\"
      '\n' => output <+ "\\n"
      '\r' => output <+ "\\r"
      '\t' => output <+ "\\t"
      character if character.to_int() < 0x20 => output <+ "?"
      character => output.write_char(character)
    }
  }
  output.to_string()
}

///|
fn json_string(text : String) -> String {
  "\"" + json_escape(text) + "\""
}

///|
fn join_json(values : Array[String]) -> String {
  values.join(",")
}

///|
fn value_json(value : Value) -> String {
  match value {
    Null => "null"
    Omitted => "\"*\""
    Integer(number) => number.to_string()
    Real(number) => number.to_string()
    String(text) => json_string(text)
    Enumeration(text) => json_string("." + text + ".")
    Reference(id) => "{\"ref\":" + id.to_string() + "}"
    Binary(text) => json_string(text)
    Typed(name~, value~) =>
      "{\"type\":" + json_string(name) + ",\"value\":" + value_json(value) + "}"
    List(values) => "[" + join_json(values.map(value_json)) + "]"
  }
}

///|
pub fn value_to_json(value : Value) -> String {
  value_json(value)
}

///|
pub fn entity_to_json(entity : Entity) -> String {
  "{\"id\":" +
  entity.id.to_string() +
  ",\"type\":" +
  json_string(entity.name) +
  ",\"arguments\":[" +
  join_json(entity.arguments.map(value_json)) +
  "]}"
}

///|
fn optional_string_json(value : String?) -> String {
  match value {
    Some(text) => json_string(text)
    None => "null"
  }
}

///|
fn optional_int_json(value : Int?) -> String {
  match value {
    Some(number) => number.to_string()
    None => "null"
  }
}

///|
fn product_kind_json(kind : ProductKind) -> String {
  match kind {
    Project => "project"
    Site => "site"
    Building => "building"
    Storey => "storey"
    Space => "space"
    Wall => "wall"
    Door => "door"
    Window => "window"
    Beam => "beam"
    Column => "column"
    Slab => "slab"
    Roof => "roof"
    Stair => "stair"
    Ramp => "ramp"
    Opening => "opening"
    Generic => "generic"
  }
}

///|
pub fn product_to_json(product : Product) -> String {
  "{\"id\":" +
  product.id.to_string() +
  ",\"type\":" +
  json_string(product.ifc_type) +
  ",\"kind\":" +
  json_string(product_kind_json(product.kind)) +
  ",\"global_id\":" +
  optional_string_json(product.global_id) +
  ",\"name\":" +
  optional_string_json(product.name) +
  ",\"description\":" +
  optional_string_json(product.description) +
  ",\"object_type\":" +
  optional_string_json(product.object_type) +
  ",\"placement\":" +
  optional_int_json(product.placement) +
  ",\"representation\":" +
  optional_int_json(product.representation) +
  ",\"tag\":" +
  optional_string_json(product.tag) +
  ",\"property_sets\":[" +
  product.property_sets.map(value => value.to_string()).join(",") +
  "]" +
  ",\"container\":" +
  optional_int_json(product.container) +
  "}"
}

///|
pub fn property_to_json(property : Property) -> String {
  "{\"name\":" +
  json_string(property.name) +
  ",\"description\":" +
  optional_string_json(property.description) +
  ",\"value\":" +
  value_json(property.value) +
  ",\"unit\":" +
  optional_int_json(property.unit) +
  "}"
}

///|
pub fn property_set_to_json(property_set : PropertySet) -> String {
  "{\"id\":" +
  property_set.id.to_string() +
  ",\"name\":" +
  optional_string_json(property_set.name) +
  ",\"description\":" +
  optional_string_json(property_set.description) +
  ",\"properties\":[" +
  join_json(property_set.properties.map(property_to_json)) +
  "]}"
}

///|
fn point_json(point : Point3) -> String {
  "[" +
  point.x.to_string() +
  "," +
  point.y.to_string() +
  "," +
  point.z.to_string() +
  "]"
}

///|
pub fn geometry_to_json(geometry : Geometry) -> String {
  let kind = match geometry.kind {
    Extrusion => "extrusion"
    FacetedBrep => "faceted_brep"
    TriangulatedMesh => "triangulated_mesh"
    CsgSolid => "csg_solid"
    Profile => "profile"
    Polyline => "polyline"
    PointCloud => "point_cloud"
    Unknown => "unknown"
  }
  "{\"source_id\":" +
  geometry.source_id.to_string() +
  ",\"kind\":" +
  json_string(kind) +
  ",\"points\":[" +
  join_json(geometry.points.map(point_json)) +
  "]" +
  ",\"references\":[" +
  geometry.references.map(value => value.to_string()).join(",") +
  "]" +
  ",\"depth\":" +
  geometry.depth.map(Double::to_string).unwrap_or("null") +
  ",\"profile_width\":" +
  geometry.profile_width.map(Double::to_string).unwrap_or("null") +
  ",\"profile_height\":" +
  geometry.profile_height.map(Double::to_string).unwrap_or("null") +
  ",\"profile_radius\":" +
  geometry.profile_radius.map(Double::to_string).unwrap_or("null") +
  ",\"face_count\":" +
  geometry.face_count.to_string() +
  ",\"triangle_count\":" +
  geometry.triangle_count.to_string() +
  "}"
}

///|
fn spatial_to_json(node : SpatialNode) -> String {
  "{\"id\":" +
  node.id.to_string() +
  ",\"type\":" +
  json_string(node.ifc_type) +
  ",\"name\":" +
  optional_string_json(node.name) +
  ",\"products\":[" +
  node.products.map(value => value.to_string()).join(",") +
  "]" +
  ",\"children\":[" +
  join_json(node.children.map(spatial_to_json)) +
  "]}"
}

///|
pub fn model_to_json(model : Model) -> String raise ParseError {
  let index = model.index()
  let products = extract_products(index)
  let property_sets = all_property_sets(index)
  let geometries = geometry_entities(index).map(entity => {
    geometry_for_entity(index, entity)
  })
  let roots = spatial_roots(index)
  "{\"schema\":" +
  json_string(model.schema()) +
  ",\"header\":{\"file_name\":" +
  optional_string_json(model.header.file_name) +
  ",\"time_stamp\":" +
  optional_string_json(model.header.time_stamp) +
  ",\"description\":[" +
  model.header.description.map(json_string).join(",") +
  ",\"schemas\":[" +
  model.header.schema_identifiers.map(json_string).join(",") +
  "]}" +
  ",\"entities\":[" +
  join_json(model.entities.map(entity_to_json)) +
  "]" +
  ",\"products\":[" +
  join_json(products.map(product_to_json)) +
  "]" +
  ",\"property_sets\":[" +
  join_json(property_sets.map(property_set_to_json)) +
  "]" +
  ",\"geometry\":[" +
  join_json(geometries.map(geometry_to_json)) +
  "]" +
  ",\"spatial\":[" +
  join_json(roots.map(spatial_to_json)) +
  "]}"
}