// Portions adapted from google/go-jsonnet v0.21.0 (Apache-2.0).
// https://github.com/google/go-jsonnet/tree/67968688d9952f50662e979fab1bef889d0d3e57
// Source: builtins.go
// Source: interpreter.go
// Copyright 2017 Google Inc. All rights reserved.
// Copyright 2016 Google Inc. All rights reserved.
// Modifications: Ported native text-rendering and quoting paths to MoonBit values with output budgets.
// License and retained attribution: LICENSE and NOTICE at the module root.

///|
// Adapted from go-jsonnet v0.21.0 native TOML rendering. See NOTICE and LICENSE.
fn toml_key(key : String) -> String {
  if key.is_empty() {
    return "''"
  }
  if key
    .iter()
    .all(c => {
      (c >= 'a' && c <= 'z') ||
      (c >= 'A' && c <= 'Z') ||
      (c >= '0' && c <= '9') ||
      c == '_' ||
      c == '-'
    }) {
    key
  } else {
    manifest_text_quote(key)
  }
}

///|
fn Vm::toml_section(self : Vm, value : Value, loc : Location) -> Bool raise {
  match value {
    ObjectValue(_, _) => true
    ArrayValue(items) => {
      if items.is_empty() {
        return false
      }
      for t in items {
        if !(self.force(t, loc) is ObjectValue(_, _)) {
          return false
        }
      }
      true
    }
    _ => false
  }
}

///|
fn Vm::toml_value(
  self : Vm,
  value : Value,
  indent : String,
  inline : Bool,
  depth : Int,
  loc : Location,
  indexed_path? : Array[String] = [],
) -> String raise {
  self.tick(loc)
  if depth >= self.limits.depth {
    raise problem(Limit, loc, "TOML depth limit exceeded")
  }
  let out = ManifestText::new(self.limits.output_bytes, loc)
  match value {
    Nil =>
      raise problem(
        Runtime,
        loc,
        "Tried to manifest \"null\" at [" + indexed_path.join(" ") + "]",
      )
    Closure(_, _, _) | Builtin(_) | Native(_) =>
      raise problem(
        Runtime,
        loc,
        "Tried to manifest function at [" + indexed_path.join(" ") + "]",
      )
    Bool(v) => out.write(if v { "true" } else { "false" })
    Num(v) =>
      out.write(
        if v.is_inf() || v.is_nan() {
          manifest_number(v)
        } else {
          @number.format(v)
        },
      )
    Str(v) => out.write(manifest_text_quote(v))
    ArrayValue(items) => {
      if items.is_empty() {
        return "[]"
      }
      let separator = if inline { " " } else { "\n" }
      out.write("[")
      out.write(separator)
      for i, t in items {
        if i > 0 {
          out.write(",")
          out.write(separator)
        }
        if !inline {
          out.write(indent)
        }
        out.write(
          self.toml_value(
            self.force(t, loc),
            indent,
            true,
            depth + 1,
            loc,
            indexed_path=indexed_path + [i.to_string()],
          ),
        )
      }
      out.write(separator)
      out.write("]")
    }
    ObjectValue(id, top) => {
      out.write("{ ")
      for i, key in self.keys(id, top, false) {
        if i > 0 {
          out.write(", ")
        }
        let value = self.field(id, top, key, loc)
        out.write(toml_key(key))
        out.write(" = ")
        out.write(
          self.toml_value(
            value,
            indent,
            true,
            depth + 1,
            loc,
            indexed_path=indexed_path + [key],
          ),
        )
      }
      out.write(" }")
    }
  }
  out.text()
}

///|
fn Vm::toml_table(
  self : Vm,
  value : Value,
  indent : String,
  path : Array[String],
  current_indent : String,
  depth : Int,
  loc : Location,
  indexed_path? : Array[String] = [],
) -> String raise {
  self.tick(loc)
  if depth >= self.limits.depth {
    raise problem(Limit, loc, "TOML depth limit exceeded")
  }
  guard value is ObjectValue(id, top) else {
    raise problem(
      Type,
      loc,
      "TOML body must be an object. Got " + value_type(value),
    )
  }
  let fields = ManifestText::new(self.limits.output_bytes, loc)
  let sections = ManifestText::new(self.limits.output_bytes, loc)
  let mut first = true
  // Render each key in order, even though scalar fields are ultimately emitted
  // before sections. Classification and rendering may evaluate lazy values.
  for key in self.keys(id, top, false) {
    let child = self.field(id, top, key, loc)
    if self.toml_section(child, loc) {
      let child_path = path + [key]
      let header = child_path.map(toml_key).join(".")
      let tables = match child {
        ArrayValue(items) => items
        _ => [self.store(Ready(child))]
      }
      let array_table = child is ArrayValue(_)
      for index, t in tables {
        let obj = self.force(t, loc)
        guard obj is ObjectValue(oid, otop) else {
          raise problem(Type, loc, "expected TOML table")
        }
        sections.write("\n\n")
        sections.write(current_indent)
        sections.write(if array_table { "[[" } else { "[" })
        sections.write(header)
        sections.write(if array_table { "]]" } else { "]" })
        if !self.keys(oid, otop, false).is_empty() {
          sections.write("\n")
        }
        sections.write(
          self.toml_table(
            obj,
            indent,
            child_path,
            current_indent + indent,
            depth + 1,
            loc,
            indexed_path=indexed_path +
              [key] +
              (if array_table { [index.to_string()] } else { [] }),
          ),
        )
      }
    } else {
      let rendered = self.toml_value(
        child,
        indent,
        false,
        depth + 1,
        loc,
        indexed_path=indexed_path + [key],
      )
      let line = toml_key(key) + " = " + rendered
      for part in line.split("\n") {
        if !first {
          fields.write("\n")
        }
        first = false
        fields.write(current_indent)
        fields.write(part.to_owned())
      }
    }
  }
  fields.write(sections.text())
  fields.text()
}