# TOON implementation in mq

def _toon_is_numeric_like(s):
  let ss = to_string(s)
  | is_regex_match(ss, "^-?[0-9]+(\\.[0-9]+)?([eE][+-]?[0-9]+)?$") || is_regex_match(ss, "^0[0-9]+$")
end

def _toon_escape_string(s):
  replace(s, "\\", "\\\\") | replace("\"", "\\\"") | replace("\n", "\\n") | replace("\r", "\\r") | replace("\t", "\\t")
end

def _toon_needs_quote(v, delim):
  let s = to_string(v)
  | if (s == ""): true
    elif (starts_with(s, " ") || ends_with(s, " ")): true
    elif (s == "true" || s == "false" || s == "null"): true
    elif (_toon_is_numeric_like(s)): true
    elif (contains(s, ":") || contains(s, "\"") || contains(s, "\\")): true
    elif (contains(s, "[") || contains(s, "]") || contains(s, "{") || contains(s, "}")): true
    elif (contains(s, "\n") || contains(s, "\r") || contains(s, "\t")): true
    elif (contains(s, delim)): true
    elif (starts_with(s, "-")): true
    else: false
end

def _toon_stringify_primitive(v, delim):
  if (is_string(v)):
    if (_toon_needs_quote(v, delim)): "\"" + _toon_escape_string(v) + "\""
    else: v
  elif (is_number(v)):
    do
      let s = to_string(v)
      | if (s == "-0"): "0" else: s
    end
  elif (is_bool(v)): if (v): "true" else: "false"
  elif (is_none(v)): "null"
  else: to_string(v)
end

def _toon_is_primitive(v):
  is_string(v) || is_number(v) || is_bool(v) || is_none(v)
end

def _toon_indent(n): repeat("  ", n);

# --- Stringify ---

def _toon_stringify_dict(data, level, delim):
  let ind = _toon_indent(level)
  | let next_ind = _toon_indent(level + 1)
  | let lines = map(entries(data), fn(e):
      let k = e[0] | let v = e[1]
      | let ks = if (is_regex_match(k, "^[A-Za-z_][A-Za-z0-9_.]*$")): k else: "\"" + _toon_escape_string(k) + "\""
      | if (_toon_is_primitive(v)): ks + ": " + _toon_stringify_primitive(v, delim)
        else:
          do
            let value = _toon_stringify_recursive(v, level + 1, delim)
            | if (starts_with(value, "[")): ks + ind + value else: ks + ":\n" + next_ind + value
          end
    end)
  | join(lines, "\n" + ind)
end

def _toon_stringify_tabular(data, level, delim):
  let next_ind = _toon_indent(level + 1)
  | let fk = keys(first(data))
  | let header = "[" + to_string(len(data)) + "]{" + join(fk, delim) + "}:"
  | let rows = map(data, fn(item):
      join(map(fk, fn(k): _toon_stringify_primitive(item[k], delim);), delim);)
  | header + "\n" + next_ind + join(rows, "\n" + next_ind)
end

def _toon_stringify_expanded_item(v, level, delim):
  if (is_dict(v) && !is_empty(v)):
    do
      let ks = keys(v) | let fk = ks[0] | let fv = v[fk]
      | let fks = if (is_regex_match(fk, "^[A-Za-z_][A-Za-z0-9_.]*$")): fk else: "\"" + _toon_escape_string(fk) + "\""
      | let start = if (_toon_is_primitive(fv)): "- " + fks + ": " + _toon_stringify_primitive(fv, delim)
            else: "- " + fks + ":\n" + _toon_indent(level + 2) + _toon_stringify_recursive(fv, level + 2, delim)
      | let other_ks = slice(ks, 1, len(ks))
      | if (is_empty(other_ks)): start
        else:
          do
            let sub_ind = _toon_indent(level + 2)
            | let lines = map(other_ks, fn(k):
                          let ks_sub = if (is_regex_match(k, "^[A-Za-z_][A-Za-z0-9_.]*$")): k else: "\"" + _toon_escape_string(k) + "\""
                          | let val = v[k]
                          | if (_toon_is_primitive(val)): ks_sub + ": " + _toon_stringify_primitive(val, delim)
                            else: ks_sub + ":\n" + _toon_indent(level + 3) + _toon_stringify_recursive(val, level + 3, delim)
                        end)
            | start + "\n" + sub_ind + join(lines, "\n" + sub_ind)
          end
    end
  else: "- " + _toon_stringify_recursive(v, level + 1, delim)
end

def _toon_stringify_array(data, level, delim):
  if (all(data, _toon_is_primitive)):
    do
      let vals = map(data, fn(v): _toon_stringify_primitive(v, delim);)
      | "[" + to_string(len(data)) + "]: " + join(vals, delim)
    end
  else:
    do
      let fk = if (is_dict(first(data))): keys(first(data)) else: []
      | let is_tabular = if (is_empty(fk)): false
            else: all(data, fn(item):
              let ks = keys(item)
              | len(ks) == len(fk) && all(ks, fn(k): contains(fk, k);) && all(values(item), _toon_is_primitive);)
      | if (is_tabular): _toon_stringify_tabular(data, level, delim)
        else:
          do
            let nind = _toon_indent(level + 1)
            | let items = map(data, fn(v): _toon_stringify_expanded_item(v, level, delim);)
            | "[" + to_string(len(data)) + "]:\n" + nind + join(items, "\n" + nind)
          end
    end
end

def _toon_stringify_recursive(data, level, delim):
  if (_toon_is_primitive(data)): _toon_stringify_primitive(data, delim)
  elif (is_dict(data)):
    if (is_empty(data)): "{}" else: _toon_stringify_dict(data, level, delim)
  elif (is_array(data)):
    if (is_empty(data)): "[0]:" else: _toon_stringify_array(data, level, delim)
  else: to_string(data)
end

# To convert a data structure into a TOON string
def toon_stringify(data): _toon_stringify_recursive(data, 0, ",");

# To parse a TOON string into a data structure
def toon_parse(input):
  _toon_parse(to_string(input))
end
