///|
fn escape_ion_string(text : String) -> String {
  let out = StringBuilder()
  out.write_char('"')
  let mut i = 0
  while i < text.length() {
    let c = text[i].to_int()
    if c == 92 {
      out.write_string("\\\\")
    } else if c == 34 {
      out.write_string("\\\"")
    } else if c == 10 {
      out.write_string("\\n")
    } else if c == 13 {
      out.write_string("\\r")
    } else if c == 9 {
      out.write_string("\\t")
    } else if c < 32 {
      out.write_string("\\x")
      out.write_char(nibble_char((c >> 4) & 15, false))
      out.write_char(nibble_char(c & 15, false))
    } else {
      out.write_char(c.unsafe_to_char())
    }
    i += 1
  }
  out.write_char('"')
  out.to_string()
}

///|
fn write_annotations(anns : Array[String]) -> String {
  let out = StringBuilder()
  for a in anns {
    out.write_string(quote_symbol(a))
    out.write_string("::")
  }
  out.to_string()
}

///|
pub fn dumps_text(value : IonValue) -> String {
  write_annotations(value.annotations) + dumps_datum(value.datum)
}

///|
pub fn dumps_text_all(values : Array[IonValue]) -> String {
  let out = StringBuilder()
  out.write_string("$ion_1_0\n")
  for v in values {
    out.write_string(dumps_text(v))
    out.write_char('\n')
  }
  out.to_string()
}

///|
fn dumps_datum(datum : IonDatum) -> String {
  match datum {
    NullD(t) =>
      match t {
        NullT => "null"
        _ => "null." + t.name()
      }
    BoolD(v) => if v { "true" } else { "false" }
    IntD(v) => v.to_decimal_string()
    FloatD(v) => float_to_ion_text(v)
    DecimalD(v) => v.to_ion_text()
    TimestampD(v) => v.to_ion_text()
    SymbolD(v) => quote_symbol(v)
    StringD(v) => escape_ion_string(v)
    ClobD(v) => clob_to_text(v)
    BlobD(v) => "{{" + base64_encode(v) + "}}"
    ListD(v) => list_to_text("[", "]", v)
    SexpD(v) => list_to_text("(", ")", v)
    StructD(v) => struct_to_text(v)
  }
}

///|
fn float_to_ion_text(value : Double) -> String {
  if is_nan(value) {
    "nan"
  } else if is_inf(value) {
    if value < 0.0 {
      "-inf"
    } else {
      "+inf"
    }
  } else {
    let s = value.to_string()
    if str_has(s, 101) || str_has(s, 69) || str_has(s, 46) {
      s
    } else {
      s + "e0"
    }
  }
}

///|
fn clob_to_text(data : Bytes) -> String {
  let arr = bytes_to_array(data)
  let inner = StringBuilder()
  for b in arr {
    let c = b.to_int()
    if c == 92 {
      inner.write_string("\\\\")
    } else if c == 34 {
      inner.write_string("\\\"")
    } else if c >= 32 && c <= 126 {
      inner.write_char(c.unsafe_to_char())
    } else {
      inner.write_string("\\x")
      inner.write_char(nibble_char((c >> 4) & 15, false))
      inner.write_char(nibble_char(c & 15, false))
    }
  }
  "{{" + "\"" + inner.to_string() + "\"" + "}}"
}

///|
fn list_to_text(
  open : String,
  close : String,
  values : Array[IonValue],
) -> String {
  let out = StringBuilder()
  out.write_string(open)
  let mut i = 0
  while i < values.length() {
    if i > 0 {
      out.write_string(", ")
    }
    out.write_string(dumps_text(values[i]))
    i += 1
  }
  out.write_string(close)
  out.to_string()
}

///|
fn struct_to_text(fields : Array[IonField]) -> String {
  let out = StringBuilder()
  out.write_char('{')
  let mut i = 0
  while i < fields.length() {
    if i > 0 {
      out.write_string(", ")
    }
    out.write_string(quote_symbol(fields[i].name))
    out.write_string(": ")
    out.write_string(dumps_text(fields[i].value))
    i += 1
  }
  out.write_char('}')
  out.to_string()
}

///|
fn str_has(text : String, code : Int) -> Bool {
  let mut i = 0
  let mut found = false
  while i < text.length() {
    if text[i].to_int() == code {
      found = true
    }
    i += 1
  }
  found
}