// Copyright 2026 moonbit-toml contributors
//
// SPDX-License-Identifier: MIT

///|
/// Serializes a table back into a TOML document. Sub-tables and arrays of
/// tables are emitted as `[a.b]` / `[[a.b]]` sections; every other value is
/// written inline. Key order follows first-definition order (scalars before
/// nested tables), and the output round-trips: parsing it again yields an
/// equal `Table`.
///
/// # Example
///
/// ```mbt check
/// test {
///   let doc = @toml.parse("[server]\nhost = \"localhost\"\nport = 80")
///   @test.assert_eq(
///     @toml.encode(doc),
///     "[server]\nhost = \"localhost\"\nport = 80\n",
///   )
/// }
/// ```
pub fn encode(table : Table) -> String {
  let sb = StringBuilder::StringBuilder()
  encode_table_into(sb, table, [])
  let s = sb.to_string()
  // Section headers always carry a leading newline for separation; a
  // document that starts with a table should not begin with a blank line.
  if s.has_prefix("\n") {
    s[1:].to_owned()
  } else {
    s
  }
}

///|
/// Serializes a single value in inline form (used for arrays and inline
/// tables, and handy for fragments).
pub fn encode_value(value : Value) -> String {
  let sb = StringBuilder::StringBuilder()
  encode_value_into(sb, value)
  sb.to_string()
}

///|
fn encode_table_into(
  sb : StringBuilder,
  table : Table,
  path : Array[String],
) -> Unit {
  // Simple (non-table) values first.
  for k in table.keys() {
    let v = match table.get(k) {
      Some(v) => v
      None => abort("unreachable")
    }
    let defer_value = match v {
      Value::Table(_) => true
      Value::Array(items) => is_array_of_tables(items)
      _ => false
    }
    if !defer_value {
      write_key_eq_value(sb, k, v)
    }
  }
  // Then nested tables and arrays of tables.
  for k in table.keys() {
    let v = match table.get(k) {
      Some(v) => v
      None => abort("unreachable")
    }
    match v {
      Value::Table(sub) => {
        write_table_header(sb, path, k, "[", "]")
        encode_table_into(sb, sub, extend_path(path, k))
      }
      Value::Array(items) =>
        if is_array_of_tables(items) {
          for item in items {
            match item {
              Value::Table(elem) => {
                write_table_header(sb, path, k, "[[", "]]")
                encode_table_into(sb, elem, extend_path(path, k))
              }
              _ => abort("unreachable")
            }
          }
        }
      _ => ()
    }
  }
}

///|
fn extend_path(path : Array[String], k : String) -> Array[String] {
  let next : Array[String] = []
  for p in path {
    next.push(p)
  }
  next.push(k)
  next
}

///|
fn write_table_header(
  sb : StringBuilder,
  path : Array[String],
  k : String,
  open : String,
  close : String,
) -> Unit {
  sb.write_char('\n')
  sb.write_string(open)
  for part in path {
    sb.write_string(encode_key(part))
    sb.write_char('.')
  }
  sb.write_string(encode_key(k))
  sb.write_string(close)
  sb.write_char('\n')
}

///|
fn write_key_eq_value(sb : StringBuilder, k : String, v : Value) -> Unit {
  sb.write_string(encode_key(k))
  sb.write_string(" = ")
  encode_value_into(sb, v)
  sb.write_char('\n')
}

///|
fn encode_value_into(sb : StringBuilder, v : Value) -> Unit {
  match v {
    Str(s) => sb.write_string(encode_basic_string(s))
    Int(i) => sb.write_object(i)
    Float(f) => sb.write_string(format_float(f))
    Bool(b) => sb.write_string(if b { "true" } else { "false" })
    Datetime(dt) => sb.write_string(encode_datetime(dt))
    Array(items) => {
      sb.write_char('[')
      for i, item in items {
        if i > 0 {
          sb.write_string(", ")
        }
        encode_value_into(sb, item)
      }
      sb.write_char(']')
    }
    Table(t) => {
      sb.write_char('{')
      let mut i = 0
      for k in t.keys() {
        if i > 0 {
          sb.write_string(", ")
        }
        let inner = match t.get(k) {
          Some(inner) => inner
          None => abort("unreachable")
        }
        sb.write_string(encode_key(k))
        sb.write_string(" = ")
        encode_value_into(sb, inner)
        i += 1
      }
      sb.write_char('}')
    }
  }
}

///|
/// Quotes a key when it is not expressible as a bare key.
fn encode_key(k : String) -> String {
  let mut bare = k.length() > 0
  for c in k {
    if !is_bare_key_char(c) {
      bare = false
      break
    }
  }
  if bare {
    k
  } else {
    encode_basic_string(k)
  }
}

///|
/// Renders a string as a TOML basic string with escapes.
fn encode_basic_string(s : String) -> String {
  let sb = StringBuilder::StringBuilder()
  sb.write_char('"')
  for c in s {
    let code = c.to_int()
    match c {
      '"' => sb.write_string("\\\"")
      '\\' => sb.write_string("\\\\")
      '\u{0008}' => sb.write_string("\\b")
      '\t' => sb.write_string("\\t")
      '\n' => sb.write_string("\\n")
      '\u{000C}' => sb.write_string("\\f")
      '\r' => sb.write_string("\\r")
      _ =>
        if code < 0x20 || code == 0x7F {
          sb.write_string(escape_unicode(code))
        } else {
          sb.write_char(c)
        }
    }
  }
  sb.write_char('"')
  sb.to_string()
}

///|
fn escape_unicode(code : Int) -> String {
  let sb = StringBuilder::StringBuilder()
  sb.write_string("\\u")
  for shift in [12, 8, 4, 0] {
    let d = (code >> shift) & 0xF
    sb.write_char(
      if d < 10 {
        (0x30 + d).to_char().unwrap_or('0')
      } else {
        (0x41 + d - 10).to_char().unwrap_or('A')
      },
    )
  }
  sb.to_string()
}

///|
/// Renders a float as a valid TOML float literal (always with a fractional
/// part or an exponent; specials as `inf` / `-inf` / `nan`).
fn format_float(f : Double) -> String {
  if f.is_nan() {
    return "nan"
  }
  if f.is_inf() {
    return if f > 0.0 { "inf" } else { "-inf" }
  }
  let s = f.to_string()
  let mut needs_frac = true
  for c in s {
    if c == '.' || c == 'e' || c == 'E' {
      needs_frac = false
      break
    }
  }
  if needs_frac {
    s + ".0"
  } else {
    s
  }
}

///|
/// Renders a datetime in its canonical form (uppercase `T`/`Z`, two-digit
/// components, trailing zeros of the fractional part removed).
pub fn encode_datetime(dt : Datetime) -> String {
  let sb = StringBuilder::StringBuilder()
  match dt.date {
    Some(d) => {
      write_padded_int(sb, d.year, 4)
      sb.write_char('-')
      write_padded_int(sb, d.month, 2)
      sb.write_char('-')
      write_padded_int(sb, d.day, 2)
    }
    None => ()
  }
  match dt.time {
    Some(t) => {
      if dt.date is Some(_) {
        sb.write_char('T')
      }
      write_padded_int(sb, t.hour, 2)
      sb.write_char(':')
      write_padded_int(sb, t.minute, 2)
      sb.write_char(':')
      write_padded_int(sb, t.second, 2)
      if t.nanos > 0 {
        sb.write_char('.')
        // 9 digits, trailing zeros trimmed.
        let digits = write_padded_to_string(t.nanos, 9)
        let mut end = digits.length()
        while end > 1 {
          match digits.get_char(end - 1) {
            Some('0') => end -= 1
            _ => break
          }
        }
        sb.write_string(digits[0:end].to_owned())
      }
    }
    None => ()
  }
  if dt.has_offset {
    if dt.offset == 0 && dt.date is Some(_) {
      sb.write_char('Z')
    } else if dt.offset == 0 && dt.date is None {
      // A local time never carries an offset; the parser never produces
      // this shape, but keep the output valid anyway.
      sb.write_char('Z')
    } else {
      let minutes = dt.offset
      sb.write_char(if minutes < 0 { '-' } else { '+' })
      let abs = if minutes < 0 { -minutes } else { minutes }
      write_padded_int(sb, abs / 60, 2)
      sb.write_char(':')
      write_padded_int(sb, abs % 60, 2)
    }
  }
  sb.to_string()
}

///|
fn write_padded_int(sb : StringBuilder, v : Int, width : Int) -> Unit {
  sb.write_string(write_padded_to_string(v, width))
}

///|
fn write_padded_to_string(v : Int, width : Int) -> String {
  let mut s = v.to_string()
  while s.length() < width {
    s = "0" + s
  }
  s
}