///|
/// Evaluate source to Go-style JSON text or an unescaped string. Both modes
/// append one newline. The byte budget excludes that final newline.
/// Options and fresh evaluation state match `evaluate`.
///
/// ```mbt check
/// test {
///   assert_eq(
///     @moonjsonnet.evaluate_text("'hello'", string_output=true),
///     "hello\n",
///   )
/// }
/// ```
pub fn evaluate_text(
  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(),
) -> String raise JsonnetError {
  Program::parse(source, filename~, anonymous~, limits~).evaluate_text(
    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_text`; source filename and anonymous mode were fixed when parsed.
pub fn Program::evaluate_text(
  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(),
) -> String raise JsonnetError {
  evaluate_with_output(
    self,
    (vm, value, loc) => vm.render_text(value, loc, string_output),
    ext_vars~,
    ext_code~,
    ext_programs~,
    tla_vars~,
    tla_code~,
    tla_programs~,
    native_functions~,
    trace~,
    resolver?,
    binary_resolver?,
    limits~,
  )
}

///|
/// Evaluate a top-level array to JSON documents, each ending in a newline.
/// All visible values are manifested before checking the top-level type.
/// The output byte budget is shared across document payloads; final newlines
/// are excluded. String output mode does not apply to streams.
///
/// ```mbt check
/// test {
///   assert_eq(@moonjsonnet.evaluate_stream("[1,'x']"), ["1\n", "\"x\"\n"])
/// }
/// ```
pub fn evaluate_stream(
  source : String,
  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(),
) -> Array[String] raise JsonnetError {
  Program::parse(source, filename~, anonymous~, limits~).evaluate_stream(
    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_stream`; source filename and anonymous mode were fixed when parsed.
pub fn Program::evaluate_stream(
  self : Program,
  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(),
) -> Array[String] raise JsonnetError {
  evaluate_with_output(
    self,
    (vm, value, loc) => vm.render_stream(value, loc),
    ext_vars~,
    ext_code~,
    ext_programs~,
    tla_vars~,
    tla_code~,
    tla_programs~,
    native_functions~,
    trace~,
    resolver?,
    binary_resolver?,
    limits~,
  )
}

///|
fn cli_json(json : Json, limit : Int, loc : Location) -> String raise {
  let output = ManifestText::new(limit, loc)
  write_cli_json(json, output, "")
  output.text()
}

///|
fn write_cli_json(
  json : Json,
  output : ManifestText,
  indent : String,
) -> Unit raise {
  let next = indent + "   "
  match json {
    Array(items) =>
      if items.is_empty() {
        output.write("[ ]")
      } else {
        output.write("[\n")
        for i, item in items {
          if i > 0 {
            output.write(",\n")
          }
          output.write(next)
          write_cli_json(item, output, next)
        }
        output.write("\n" + indent + "]")
      }
    Object(fields) => {
      let keys = fields.keys().collect()
      keys.sort_by(compare_unicode)
      if keys.is_empty() {
        output.write("{ }")
      } else {
        output.write("{\n")
        for i, key in keys {
          if i > 0 {
            output.write(",\n")
          }
          output.write(next + manifest_text_quote(key) + ": ")
          write_cli_json(fields[key], output, next)
        }
        output.write("\n" + indent + "}")
      }
    }
    Number(n, ..) => {
      let text = if n.is_inf() || n.is_nan() {
        manifest_number(n)
      } else {
        @number.format(n)
      }
      if text.is_empty() {
        raise problem(Runtime, output.loc, "cannot format output number")
      }
      output.write(text)
    }
    String(text) => output.write(manifest_text_quote(text))
    _ => output.write(json.stringify())
  }
}

///|
fn Vm::render_text(
  vm : Vm,
  value : Value,
  loc : Location,
  string_output : Bool,
) -> String raise {
  if string_output {
    guard value is Str(text) else {
      raise problem(
        Type,
        loc,
        "expected string result, got: " + value_type(value),
      )
    }
    vm.account_output(@utf8.encode(text).length(), loc)
    text + "\n"
  } else {
    let json = vm.manifest(value, loc, 0)
    cli_json(json, vm.limits.output_bytes, loc) + "\n"
  }
}

///|
fn Vm::render_stream(
  vm : Vm,
  value : Value,
  loc : Location,
) -> Array[String] raise {
  let json = vm.manifest(value, loc, 0)
  guard json is Array(documents) else {
    raise problem(
      Type,
      loc,
      "stream mode: top-level object was a " +
      value_type(value) +
      ", should be an array whose elements hold the JSON for each document in the stream.",
    )
  }
  vm.output_bytes = 0
  documents.map(document => {
    let text = cli_json(document, vm.limits.output_bytes - vm.output_bytes, loc)
    vm.account_output(@utf8.encode(text).length(), loc)
    text + "\n"
  })
}