///|
/// Build the smallest practical JSON object satisfying required source fields.
fn minimal_object_json(contract : Contract, object : ObjectType) -> String {
  object_json(contract, object, None, None, None, [])
}

///|
/// Build a source-valid payload while omitting one field.
fn object_without_field(
  contract : Contract,
  object : ObjectType,
  omitted : String,
) -> String {
  object_json(contract, object, Some(omitted), None, None, [])
}

///|
/// Build a source-valid payload containing a selected field and value.
fn object_with_field(
  contract : Contract,
  object : ObjectType,
  field : Field,
  value : String,
) -> String {
  object_json(contract, object, None, Some(field.name), Some(value), [])
}

///|
/// Build a source object with an additional property not declared by the contract.
fn object_with_extra_field(contract : Contract, object : ObjectType) -> String {
  let base = object_json(contract, object, None, None, None, [])
  if base == "{}" {
    "{\"__extra\":null}"
  } else {
    base[:base.length() - 1].to_owned() + ",\"__extra\":null}"
  }
}

///|
fn object_json(
  contract : Contract,
  object : ObjectType,
  omitted : String?,
  forced_field : String?,
  forced_value : String?,
  visited : Array[String],
) -> String {
  if visited.contains(object.name) || visited.length() >= 8 {
    return "{}"
  }
  let next_visited = visited.copy()
  next_visited.push(object.name)
  let members : Array[String] = []
  for field in object.fields {
    if omitted is Some(name) && field.name == name {
      continue
    }
    let forced = forced_field is Some(name) && field.name == name
    if field.required || forced {
      let value = if forced {
        forced_value.unwrap_or("")
      } else {
        sample_value(contract, field, next_visited)
      }
      members.push("\"" + json_escape(field.name) + "\":" + value)
    }
  }
  "{" + members.join(",") + "}"
}

///|
fn sample_value(
  contract : Contract,
  field : Field,
  visited : Array[String],
) -> String {
  match field.type_expr {
    StringType => {
      let length = field.constraints.min_len.unwrap_or(0)
      "\"" + "a".repeat(length) + "\""
    }
    IntType | NumberType => field.constraints.min_int.unwrap_or(0).to_string()
    BoolType => "false"
    EnumType(values) =>
      if values.is_empty() {
        "\"\""
      } else {
        "\"" + json_escape(values[0]) + "\""
      }
    RefType(name) =>
      match contract.find_object(name) {
        Some(object) => object_json(contract, object, None, None, None, visited)
        None => "{}"
      }
    ListType(_) => {
      let length = field.constraints.min_len.unwrap_or(0)
      if length <= 0 {
        "[]"
      } else {
        let values = Array::make(
          length,
          list_item_value(field.type_expr, contract, visited),
        )
        "[" + values.join(",") + "]"
      }
    }
  }
}

///|
fn list_item_value(
  type_expr : TypeExpr,
  contract : Contract,
  visited : Array[String],
) -> String {
  guard type_expr is ListType(item) else { return "null" }
  match item {
    "string" => "\"\""
    "int" | "number" => "0"
    "bool" => "false"
    _ =>
      if item.has_prefix("ref:") {
        let name = item[4:].to_owned()
        match contract.find_object(name) {
          Some(object) =>
            object_json(contract, object, None, None, None, visited)
          None => "{}"
        }
      } else {
        "null"
      }
  }
}

///|
fn representative_value(contract : Contract, field : Field) -> String {
  sample_value(contract, field, [])
}

///|
fn value_for_type_break(
  contract : Contract,
  source : Field,
  target : Field,
) -> String {
  match (source.type_expr, target.type_expr) {
    (NumberType, IntType) => "0.5"
    (EnumType(source_values), EnumType(target_values)) => {
      for value in source_values {
        if !target_values.contains(value) {
          return "\"" + json_escape(value) + "\""
        }
      }
      representative_value(contract, source)
    }
    _ => representative_value(contract, source)
  }
}

///|
fn string_value(length : Int) -> String {
  "\"" + "a".repeat(length) + "\""
}

///|
fn json_escape(value : String) -> String {
  let builder = StringBuilder(size_hint=value.length() + 8)
  for char in value {
    match char {
      '"' => builder.write_string("\\\"")
      '\\' => builder.write_string("\\\\")
      '\n' => builder.write_string("\\n")
      '\r' => builder.write_string("\\r")
      '\t' => builder.write_string("\\t")
      _ => builder.write_char(char)
    }
  }
  builder.to_string()
}

///|
fn make_witness(
  payload : String,
  source : Contract,
  target : Contract,
  reason : String,
) -> Witness {
  {
    payload,
    accepted_by: source.name + "@" + source.version,
    rejected_by: target.name + "@" + target.version,
    reason,
  }
}