// Value data model for moon_tera.
//
// A JSON-like dynamic value used as the rendering context and the data flowing
// through the engine. Ported from Tera's `Value` (which itself mirrors
// serde_json::Value). We use MoonBit's native enum instead of serde.
//
// `Object` keeps insertion order (an array of key/value pairs) to match Tera's
// use of an index map — important for deterministic template rendering.

///|
/// A dynamic value used as the template rendering context and intermediate data.
pub(all) enum Value {
  Null
  Bool(Bool)
  Int(Int)
  Float(Double)
  Str(String)
  Array(Array[Value])
  // Insertion-ordered key/value pairs (avoids depending on a hash map and
  // matches Tera's ordered context).
  Object(Array[(String, Value)])
} derive(Debug)

///|
/// Truthiness used by `{% if %}` and filters like `default`.
///
/// Following Tera/Jinja: `Null`, `Bool(false)`, `Int(0)`, `Float(0.0)`,
/// empty `Str`/`Array`/`Object` are falsy; everything else is truthy.
pub fn Value::is_truthy(self : Value) -> Bool {
  match self {
    Null => false
    Bool(b) => b
    Int(i) => i != 0
    Float(f) => f != 0.0
    Str(s) => s.length() > 0
    Array(a) => a.length() > 0
    Object(o) => o.length() > 0
  }
}

///|
/// Construct a null value.
pub fn null_value() -> Value {
  Null
}

///|
/// Construct a boolean value.
pub fn bool_value(b : Bool) -> Value {
  Bool(b)
}

///|
/// Construct an integer value.
pub fn int_value(i : Int) -> Value {
  Int(i)
}

///|
/// Construct a float value.
pub fn float_value(f : Double) -> Value {
  Float(f)
}

///|
/// Construct a string value.
pub fn str_value(s : String) -> Value {
  Str(s)
}

///|
/// Construct an array value.
pub fn array_value(a : Array[Value]) -> Value {
  Array(a)
}

///|
/// Construct an object value from insertion-ordered key/value pairs.
pub fn object_value(o : Array[(String, Value)]) -> Value {
  Object(o)
}

///|
/// Return a stable, human-readable description for diagnostics and tests.
pub fn Value::describe(self : Value) -> String {
  match self {
    Null => "Null"
    Bool(value) => "Bool(" + value.to_string() + ")"
    Int(value) => "Int(" + value.to_string() + ")"
    Float(value) => "Float(" + value.to_string() + ")"
    Str(value) => "Str(" + value + ")"
    Array(_) => "Array(" + self.to_json_string() + ")"
    Object(_) => "Object(" + self.to_json_string() + ")"
  }
}

///|
fn json_escape(value : String) -> String {
  let out = StringBuilder::new()
  for char in value {
    match char {
      '"' => out.write_string("\\\"")
      '\\' => out.write_string("\\\\")
      '\n' => out.write_string("\\n")
      '\r' => out.write_string("\\r")
      '\t' => out.write_string("\\t")
      _ => out.write_char(char)
    }
  }
  out.to_string()
}

///|
fn join_json_parts(parts : Array[String]) -> String {
  let out = StringBuilder::new()
  for index, part in parts {
    if index > 0 {
      out.write_string(",")
    }
    out.write_string(part)
  }
  out.to_string()
}

///|
/// Encode a value as deterministic JSON while preserving object insertion order.
pub fn Value::to_json_string(self : Value) -> String {
  match self {
    Null => "null"
    Bool(value) => value.to_string()
    Int(value) => value.to_string()
    Float(value) => value.to_string()
    Str(value) => "\"" + json_escape(value) + "\""
    Array(values) => {
      let parts : Array[String] = []
      for value in values {
        parts.push(value.to_json_string())
      }
      "[" + join_json_parts(parts) + "]"
    }
    Object(entries) => {
      let parts : Array[String] = []
      for entry in entries {
        let (key, value) = entry
        parts.push("\"" + json_escape(key) + "\":" + value.to_json_string())
      }
      "{" + join_json_parts(parts) + "}"
    }
  }
}