///|
/// A dynamically typed value produced by a PEG.js-style parser.
///
/// Mirrors the JavaScript values PEG.js parsers produce: literals, classes and
/// `.` yield `Str`, sequences and repetitions yield `Arr`, `?` yields `Null`
/// on failure and predicates yield `Undefined`. `Custom` carries application
/// payloads produced by actions.
pub(all) enum Value[T] {
  Undefined
  Null
  Bool(Bool)
  Num(Double)
  Str(String)
  Arr(Array[Value[T]])
  Obj(Map[String, Value[T]])
  Custom(T)
} derive(Eq, Debug)

///|
/// JavaScript truthiness, used for semantic predicates.
pub fn[T] Value::truthy(self : Value[T]) -> Bool {
  match self {
    Undefined | Null => false
    Bool(b) => b
    Num(n) => !(n == 0.0 || n.is_nan())
    Str(s) => s != ""
    Arr(_) | Obj(_) | Custom(_) => true
  }
}

///|
/// Returns the string payload, if any.
pub fn[T] Value::as_str(self : Value[T]) -> String? {
  match self {
    Str(s) => Some(s)
    _ => None
  }
}

///|
/// Returns the array payload, if any.
pub fn[T] Value::as_arr(self : Value[T]) -> Array[Value[T]]? {
  match self {
    Arr(a) => Some(a)
    _ => None
  }
}

///|
/// Converts to JSON. `Undefined` becomes `{"$": "undefined"}` and custom
/// payloads are rendered with `custom`, matching the fixture encoding of
/// tools/harvest.js.
pub fn[T] Value::to_json_with(self : Value[T], custom : (T) -> Json) -> Json {
  match self {
    Undefined => { "$": "undefined" }
    Null => Json::null()
    Bool(b) => b.to_json()
    Num(n) =>
      if n.is_nan() {
        { "$": "number", "value": "NaN" }
      } else if n.is_inf() {
        {
          "$": "number",
          "value": if n > 0.0 {
            "Infinity"
          } else {
            "-Infinity"
          },
        }
      } else {
        Json::number(n)
      }
    Str(s) => s.to_json()
    Arr(xs) => Json::array(xs.map(x => x.to_json_with(custom)))
    Obj(m) => {
      let obj : Map[String, Json] = Map([])
      for k, v in m {
        obj[k] = v.to_json_with(custom)
      }
      Json::object(obj)
    }
    Custom(t) => custom(t)
  }
}

///|
pub impl[T : ToJson] ToJson for Value[T] with fn to_json(self) {
  self.to_json_with(t => t.to_json())
}

///|
/// JavaScript `String(value)`. Objects and custom payloads render as
/// `[object Object]`.
pub fn[T] Value::js_string(self : Value[T]) -> String {
  match self {
    Undefined => "undefined"
    Null => "null"
    Bool(b) => b.to_string()
    Num(n) => n.to_string()
    Str(s) => s
    Arr(xs) =>
      xs
      .map(x => {
        match x {
          Undefined | Null => ""
          _ => x.js_string()
        }
      })
      .join(",")
    Obj(_) | Custom(_) => "[object Object]"
  }
}