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

///|
/// Converts a TOML value to JSON (`moonbitlang/core` `Json`).
///
/// Mapping:
/// - integers keep full 64-bit precision through their textual
///   representation;
/// - the four datetime forms become strings in the canonical TOML notation;
/// - JSON has no representation for non-finite floats, so `inf`, `-inf`
///   and `nan` become the strings `"inf"`, `"-inf"` and `"nan"`;
/// - tables become JSON objects, arrays become JSON arrays.
///
/// # Example
///
/// ```mbt check
/// test {
///   let doc = @toml.parse("port = 8080\nactive = true")
///   @test.assert_eq(
///     @toml.Value::Table(doc).to_json_string(),
///     "{\"port\":8080,\"active\":true}",
///   )
/// }
/// ```
pub fn Value::to_json(self : Value) -> Json {
  match self {
    Str(s) => Json::string(s)
    Int(i) => Json::number(i.to_double(), repr=i.to_string())
    Float(f) =>
      if f.is_nan() {
        Json::string("nan")
      } else if f.is_inf() {
        Json::string(if f > 0.0 { "inf" } else { "-inf" })
      } else {
        Json::number(f)
      }
    Bool(b) => Json::boolean(b)
    Datetime(dt) => Json::string(encode_datetime(dt))
    Array(items) => {
      let arr : Array[Json] = []
      for item in items {
        arr.push(item.to_json())
      }
      Json::array(arr)
    }
    Table(t) => {
      let obj : Map[String, Json] = Map([])
      for k in t.keys() {
        let v = match t.get(k) {
          Some(v) => v
          None => abort("unreachable")
        }
        obj[k] = v.to_json()
      }
      Json::object(obj)
    }
  }
}

///|
/// Implements the standard `ToJson` trait so TOML values work with
/// `@json.to_json`, `@json.json_inspect` and friends.
pub impl ToJson for Value with fn to_json(self) {
  self.to_json()
}

///|
/// Serializes a TOML value to a JSON string (compact form).
pub fn Value::to_json_string(self : Value) -> String {
  @json.to_json(self).stringify()
}

///|
/// Error raised by `Value::from_json` for JSON values that have no TOML
/// representation.
pub suberror FromJsonError {
  /// JSON `null` does not exist in TOML.
  JsonNull
} derive(Eq, Debug)

///|
/// Converts a core `Json` value into a TOML `Value`.
///
/// Conventions:
/// - JSON `null` raises `FromJsonError::JsonNull` (TOML has no null);
/// - numbers that are integral and fit in 64 bits become TOML integers,
///   everything else stays a float (so JSON `1.0` becomes the integer
///   `1`, and values beyond the double range fall back to `inf`-style
///   floats);
/// - JSON strings never become datetimes — a string that looks like an
///   RFC 3339 date stays a string;
/// - objects become tables, arrays become arrays.
///
/// # Example
///
/// ```mbt check
/// test {
///   let v = @toml.Value::from_json(@json.parse("{\"a\": [1, true]}")) catch {
///     _ => abort("unreachable")
///   }
///   @test.assert_eq(@toml.encode_value(v), "{a = [1, true]}")
/// }
/// ```
pub fn Value::from_json(j : Json) -> Value raise FromJsonError {
  match j {
    Null => raise JsonNull
    True => Bool(true)
    False => Bool(false)
    Number(d, repr~) =>
      match repr {
        // The textual representation is authoritative when present.
        Some(text) =>
          Value::Int(@string.parse_int64(text)) catch {
            _ => Value::Float(d)
          }
        None =>
          if d == d.trunc() &&
            d >= -9223372036854775808.0 &&
            d < 9223372036854775808.0 {
            Value::Int(d.to_int64())
          } else {
            Value::Float(d)
          }
      }
    String(s) => Str(s)
    Array(items) => {
      let arr : Array[Value] = []
      for item in items {
        arr.push(Value::from_json(item))
      }
      Array(arr)
    }
    Object(entries) => {
      let t = Table::new()
      for k, v in entries {
        t.set(k, Value::from_json(v))
      }
      Table(t)
    }
  }
}