///|
priv enum WriteFrame {
  Array(Array[Json], mut i~ : Int) // (arr, index)
  Object(Iter[(String, Json)], mut first~ : Bool) // (kvs, first)
}

///|
fn indent_string(level : Int, indent : Int) -> String {
  if indent == 0 {
    ""
  } else {
    let spaces = indent * level
    match spaces {
      0 => "\n"
      1 => "\n "
      2 => "\n  "
      3 => "\n   "
      4 => "\n    "
      5 => "\n     "
      6 => "\n      "
      7 => "\n       "
      8 => "\n        "
      _ => "\n" + " ".repeat(spaces)
    }
  }
}

///|
fn escape(str : String, escape_slash~ : Bool) -> String {
  let buf = StringBuilder::new(size_hint=str.length())
  for c in str {
    match c {
      '"' => buf.write_string("\\\"")
      '\\' => buf.write_string("\\\\")
      '/' =>
        if escape_slash {
          buf.write_string("\\/")
        } else {
          buf.write_char(c)
        }
      '\n' => buf.write_string("\\n")
      '\r' => buf.write_string("\\r")
      '\b' => buf.write_string("\\b")
      '\t' => buf.write_string("\\t")
      _ => {
        let code = c.to_int()
        if code == 0x0C {
          buf.write_string("\\f")
        } else if code < ' ' {
          buf.write_string("\\u00")
          buf.write_string(code.to_byte().to_hex())
        } else {
          buf.write_char(c)
        }
      }
    }
  }
  buf.to_string()
}

///|
pub fn Json::stringify(
  self : Json,
  escape_slash? : Bool = false,
  indent? : Int = 0,
) -> String {
  let buf = StringBuilder::new(size_hint=0)

  // Explicit stack to replace recursive calls
  let stack : Array[WriteFrame] = []
  let mut depth = 0
  loop Some(self) {
    Some(value) => {
      match value {
        Object(members) =>
          if members.is_empty() {
            buf.write_string("{}")
          } else {
            depth += 1
            buf.write_char('{')
            buf.write_string(indent_string(depth, indent))
            // After child value printed, we resume from this frame
            stack.push(WriteFrame::Object(members.iter(), first=true))
          }
        Array(arr) =>
          if arr.is_empty() {
            buf.write_string("[]")
          } else {
            depth += 1
            buf.write_char('[')
            buf.write_string(indent_string(depth, indent))
            stack.push(WriteFrame::Array(arr, i=0))
          }
        String(s) =>
          buf
          ..write_char('\"')
          ..write_string(escape(s, escape_slash~))
          ..write_char('\"')
        Number(n) => buf.write_string(n.0)
        Bool(true) => buf.write_string("true")
        Bool(false) => buf.write_string("false")
        Null => buf.write_string("null")
      }
      continue None
    }
    None =>
      // No current node to write; try to resume a pending container
      match stack {
        [] => break
        [.., WriteFrame::Array(arr, i~) as frame] =>
          if i < arr.length() {
            let element = arr[i]
            frame.i = i + 1
            if i > 0 {
              buf.write_char(',')
              buf.write_string(indent_string(depth, indent))
            }
            continue Some(element)
          } else {
            depth -= 1
            ignore(stack.pop())
            buf.write_string(indent_string(depth, indent))
            buf.write_char(']')
            continue None
          }
        [.., WriteFrame::Object(iterator, first~) as frame] =>
          match iterator.next() {
            Some((k, v)) => {
              if !first {
                buf.write_char(',')
                buf.write_string(indent_string(depth, indent))
              }
              buf
              ..write_char('\"')
              ..write_string(escape(k, escape_slash~))
              ..write_char('\"')
              ..write_char(':')
              if indent > 0 {
                buf.write_char(' ')
              }
              frame.first = false
              continue Some(v)
            }
            None => {
              depth -= 1
              ignore(stack.pop())
              buf.write_string(indent_string(depth, indent))
              buf.write_char('}')
              continue None
            }
          }
      }
  }
  buf.to_string()
}