///|
/// Evaluate a top-level object into filename/content pairs without filesystem
/// writes. Values are all manifested before checking types. JSON files end in
/// one newline; with string_output=true, raw strings have no added newline.
/// The payload byte budget is shared by all files, excluding JSON final newlines.
///
/// ```mbt check
/// test {
///   let files = @moonjsonnet.evaluate_multi("{'a.json':42,'b.json':[]}")
///   assert_eq(files["a.json"], "42\n")
///   assert_eq(files["b.json"], "[ ]\n")
/// }
/// ```
pub fn evaluate_multi(
  source : String,
  string_output? : Bool = false,
  filename? : String = "",
  anonymous? : Bool = false,
  ext_vars? : Map[String, Json] = Map([]),
  ext_code? : Map[String, String] = Map([]),
  ext_programs? : Map[String, Program] = Map([]),
  tla_vars? : Map[String, Json] = Map([]),
  tla_code? : Map[String, String] = Map([]),
  tla_programs? : Map[String, Program] = Map([]),
  native_functions? : Map[String, NativeFunction] = Map([]),
  trace? : (Location, String) -> Unit = default_trace,
  resolver? : (String, String) -> Source raise,
  binary_resolver? : (String, String) -> BinarySource raise,
  limits? : Limits = Limits::default(),
) -> Map[String, String] raise JsonnetError {
  Program::parse(source, filename~, anonymous~, limits~).evaluate_multi(
    string_output~,
    ext_vars~,
    ext_code~,
    ext_programs~,
    tla_vars~,
    tla_code~,
    tla_programs~,
    native_functions~,
    trace~,
    resolver?,
    binary_resolver?,
    limits~,
  )
}

///|
/// Execute this validated program with fresh evaluation state. Options and output
/// match `evaluate_multi`; source filename and anonymous mode were fixed when parsed.
pub fn Program::evaluate_multi(
  self : Program,
  string_output? : Bool = false,
  ext_vars? : Map[String, Json] = Map([]),
  ext_code? : Map[String, String] = Map([]),
  ext_programs? : Map[String, Program] = Map([]),
  tla_vars? : Map[String, Json] = Map([]),
  tla_code? : Map[String, String] = Map([]),
  tla_programs? : Map[String, Program] = Map([]),
  native_functions? : Map[String, NativeFunction] = Map([]),
  trace? : (Location, String) -> Unit = default_trace,
  resolver? : (String, String) -> Source raise,
  binary_resolver? : (String, String) -> BinarySource raise,
  limits? : Limits = Limits::default(),
) -> Map[String, String] raise JsonnetError {
  evaluate_with_output(
    self,
    (vm, value, loc) => vm.render_multi(value, loc, string_output),
    ext_vars~,
    ext_code~,
    ext_programs~,
    tla_vars~,
    tla_code~,
    tla_programs~,
    native_functions~,
    trace~,
    resolver?,
    binary_resolver?,
    limits~,
  )
}

///|
fn Vm::render_multi(
  vm : Vm,
  value : Value,
  loc : Location,
  string_output : Bool,
) -> Map[String, String] raise {
  let json = vm.manifest(value, loc, 0)
  guard json is Object(fields) else {
    raise problem(
      Type,
      loc,
      "multi mode: top-level object was a " +
      value_type(value) +
      ", should be an object whose keys are filenames and values hold the JSON for that file.",
    )
  }
  vm.output_bytes = 0
  let files : Map[String, String] = Map([])
  let keys = fields.keys().collect()
  keys.sort_by(compare_unicode)
  for key in keys {
    let text = if string_output {
      guard fields[key] is String(text) else {
        let kind = match fields[key] {
          Number(_, ..) => "float64"
          True | False => "bool"
          Null => ""
          Array(_) => "[]interface {}"
          Object(_) => "map[string]interface {}"
          _ => abort("string excluded by guard")
        }
        raise problem(
          Type,
          loc,
          "multi mode: top-level object's key " +
          key +
          " has a value of type " +
          kind +
          ", should be a string",
        )
      }
      vm.account_output(@utf8.encode(text).length(), loc)
      text
    } else {
      let text = cli_json(
        fields[key],
        vm.limits.output_bytes - vm.output_bytes,
        loc,
      )
      vm.account_output(@utf8.encode(text).length(), loc)
      text + "\n"
    }
    files[key] = text
  }
  files
}