///|
/// A Rego value.
///
/// Integers are kept as `Int64` and other numbers as `Double`; arbitrary
/// precision decimals arrive in a later increment. Objects and sets are flat
/// arrays with linear lookup, which lets any term act as a key without
/// imposing a hashable representation.
pub(all) enum Value {
  NullVal
  BoolVal(Bool)
  IntVal(Int64)
  FloatVal(Double)
  StringVal(String)
  ArrayVal(Array[Value])
  ObjectVal(Array[(Value, Value)])
  SetVal(Array[Value])
} derive(Eq, @debug.Debug)

///|
pub extend Value with Eq::{equal, not_equal}

///|
pub extend Value with @debug.Debug::{to_repr}

///|
pub extend Value with Show::{to_string, output}

///|
pub impl Show for Value with fn output(self, logger) {
  logger.write_string(Value::to_rego_text(self))
}

///|
pub fn Value::null() -> Value {
  NullVal
}

///|
pub fn Value::boolean(value : Bool) -> Value {
  BoolVal(value)
}

///|
pub fn Value::int(value : Int64) -> Value {
  IntVal(value)
}

///|
pub fn Value::float(value : Double) -> Value {
  FloatVal(value)
}

///|
pub fn Value::string(value : String) -> Value {
  StringVal(value)
}

///|
pub fn Value::array(items : Array[Value]) -> Value {
  ArrayVal(items)
}

///|
pub fn Value::object(pairs : Array[(Value, Value)]) -> Value {
  ObjectVal(pairs)
}

///|
/// A set with duplicate elements removed.
pub fn Value::set(items : Array[Value]) -> Value {
  let unique : Array[Value] = []
  for item in items {
    if !contains_value(unique, item) {
      unique.push(item)
    }
  }
  SetVal(unique)
}

///|
/// Builds a number from a Rego literal. A literal with a fraction or an
/// exponent becomes a `Double`; an integer that fits becomes an `Int64`.
pub fn Value::number_literal(text : String) -> Value {
  let mut fractional = false
  for c in text.to_array() {
    if c == '.' || c == 'e' || c == 'E' {
      fractional = true
    }
  }
  if fractional {
    FloatVal(@string.parse_double(text) catch { _error => 0.0 })
  } else {
    let parsed = Some(@string.parse_int64(text)) catch { _error => None }
    match parsed {
      Some(value) => IntVal(value)
      None => FloatVal(@string.parse_double(text) catch { _error => 0.0 })
    }
  }
}

///|
fn compare_double(left : Double, right : Double) -> Int32 {
  if left < right {
    -1
  } else if left > right {
    1
  } else {
    0
  }
}

///|
/// Rego equality: integers and floats compare by numeric value, sets compare
/// regardless of order, and objects compare by key.
pub fn Value::equals(self : Value, other : Value) -> Bool {
  match (self, other) {
    (NullVal, NullVal) => true
    (BoolVal(left), BoolVal(right)) => left == right
    (IntVal(left), IntVal(right)) => left == right
    (FloatVal(left), FloatVal(right)) => left == right
    (IntVal(left), FloatVal(right)) => left.to_double() == right
    (FloatVal(left), IntVal(right)) => left == right.to_double()
    (StringVal(left), StringVal(right)) => left == right
    (ArrayVal(left), ArrayVal(right)) => values_equal(left, right)
    (SetVal(left), SetVal(right)) => sets_equal(left, right)
    (ObjectVal(left), ObjectVal(right)) => objects_equal(left, right)
    _ => false
  }
}

///|
fn values_equal(left : Array[Value], right : Array[Value]) -> Bool {
  if left.length() != right.length() {
    return false
  }
  for i = 0; i < left.length(); i = i + 1 {
    if !left[i].equals(right[i]) {
      return false
    }
  }
  true
}

///|
fn contains_value(items : Array[Value], target : Value) -> Bool {
  for item in items {
    if item.equals(target) {
      return true
    }
  }
  false
}

///|
fn sets_equal(left : Array[Value], right : Array[Value]) -> Bool {
  if left.length() != right.length() {
    return false
  }
  for item in left {
    if !contains_value(right, item) {
      return false
    }
  }
  true
}

///|
fn objects_equal(
  left : Array[(Value, Value)],
  right : Array[(Value, Value)],
) -> Bool {
  if left.length() != right.length() {
    return false
  }
  for pair in left {
    match object_get(right, pair.0) {
      Some(value) => if !value.equals(pair.1) { return false }
      None => return false
    }
  }
  true
}

///|
fn object_get(pairs : Array[(Value, Value)], key : Value) -> Value? {
  for pair in pairs {
    if pair.0.equals(key) {
      return Some(pair.1)
    }
  }
  None
}

///|
/// Orders two values that Rego can compare, or returns `None` when the types
/// do not form an ordering.
pub fn Value::compare(self : Value, other : Value) -> Int32? {
  match (self, other) {
    (IntVal(left), IntVal(right)) =>
      Some(if left < right { -1 } else if left > right { 1 } else { 0 })
    (FloatVal(left), FloatVal(right)) => Some(compare_double(left, right))
    (IntVal(left), FloatVal(right)) =>
      Some(compare_double(left.to_double(), right))
    (FloatVal(left), IntVal(right)) =>
      Some(compare_double(left, right.to_double()))
    (StringVal(left), StringVal(right)) =>
      Some(if left < right { -1 } else if left > right { 1 } else { 0 })
    _ => None
  }
}

///|
/// Looks up a key in an object, or returns `None` for other values.
pub fn Value::lookup(self : Value, key : Value) -> Value? {
  match self {
    ObjectVal(pairs) => object_get(pairs, key)
    _ => None
  }
}

///|
/// Returns an object with the key set to the value, replacing an existing
/// entry.
pub fn Value::with_entry(self : Value, key : Value, value : Value) -> Value {
  match self {
    ObjectVal(pairs) => {
      let next : Array[(Value, Value)] = []
      let mut replaced = false
      for pair in pairs {
        if pair.0.equals(key) {
          next.push((key, value))
          replaced = true
        } else {
          next.push(pair)
        }
      }
      if !replaced {
        next.push((key, value))
      }
      ObjectVal(next)
    }
    _ => ObjectVal([(key, value)])
  }
}

///|
/// Whether a set contains an element.
pub fn Value::element(self : Value, target : Value) -> Bool {
  match self {
    SetVal(items) => contains_value(items, target)
    _ => false
  }
}

///|
/// Returns a set with the element added once.
pub fn Value::with_element(self : Value, item : Value) -> Value {
  match self {
    SetVal(items) =>
      if contains_value(items, item) {
        SetVal(items)
      } else {
        let next : Array[Value] = []
        for existing in items {
          next.push(existing)
        }
        next.push(item)
        SetVal(next)
      }
    _ => SetVal([item])
  }
}

///|
/// The number of elements: array and set items, object entries, or string
/// characters. Scalars have length zero.
pub fn Value::length(self : Value) -> Int32 {
  match self {
    ArrayVal(items) | SetVal(items) => items.length()
    ObjectVal(pairs) => pairs.length()
    StringVal(text) => text.length()
    _ => 0
  }
}

///|
fn escape_string(text : String) -> String {
  let buffer = StringBuilder()
  for c in text.to_array() {
    match c {
      '"' => buffer.write_string("\\\"")
      '\\' => buffer.write_string("\\\\")
      '\n' => buffer.write_string("\\n")
      '\r' => buffer.write_string("\\r")
      '\t' => buffer.write_string("\\t")
      _ => buffer.write_char(c)
    }
  }
  buffer.to_string()
}

///|
/// Renders the value the way Rego prints it: strings are quoted, sets use
/// braces, and objects keep their insertion order.
pub fn Value::to_rego_text(self : Value) -> String {
  match self {
    NullVal => "null"
    BoolVal(true) => "true"
    BoolVal(false) => "false"
    IntVal(value) => value.to_string()
    FloatVal(value) => value.to_string()
    StringVal(text) => "\"" + escape_string(text) + "\""
    ArrayVal(items) => {
      let buffer = StringBuilder()
      buffer.write_char('[')
      for i = 0; i < items.length(); i = i + 1 {
        if i > 0 {
          buffer.write_string(", ")
        }
        buffer.write_string(items[i].to_rego_text())
      }
      buffer.write_char(']')
      buffer.to_string()
    }
    SetVal(items) => {
      let buffer = StringBuilder()
      buffer.write_char('{')
      for i = 0; i < items.length(); i = i + 1 {
        if i > 0 {
          buffer.write_string(", ")
        }
        buffer.write_string(items[i].to_rego_text())
      }
      buffer.write_char('}')
      buffer.to_string()
    }
    ObjectVal(pairs) => {
      let buffer = StringBuilder()
      buffer.write_char('{')
      for i = 0; i < pairs.length(); i = i + 1 {
        if i > 0 {
          buffer.write_string(", ")
        }
        buffer.write_string(pairs[i].0.to_rego_text())
        buffer.write_string(": ")
        buffer.write_string(pairs[i].1.to_rego_text())
      }
      buffer.write_char('}')
      buffer.to_string()
    }
  }
}