// Dynamic cell values and the JavaScript type coercions that Vega applies to
// data (`toNumber`, `toDate`, `toBoolean`, `toString`, `isValid`, `ascending`).
//
// Dates travel through Vega datasets as epoch milliseconds (numbers) or Date
// objects, which compare and intern like numbers, so there is no separate date
// variant here.

///|
pub(all) enum Value {
  /// JavaScript `undefined`: a missing field
  Undef
  Null
  Bool(Bool)
  Num(Double)
  Str(String)
  Arr(Array[Value])
  Obj(Map[String, Value])
}

///|
fn is_js_space(c : Char) -> Bool {
  let n = c.to_int()
  n == 9 ||
  n == 10 ||
  n == 11 ||
  n == 12 ||
  n == 13 ||
  n == 32 ||
  n == 0xA0 ||
  n == 0x1680 ||
  (n >= 0x2000 && n <= 0x200A) ||
  n == 0x2028 ||
  n == 0x2029 ||
  n == 0x202F ||
  n == 0x205F ||
  n == 0x3000 ||
  n == 0xFEFF
}

///|
pub fn trim_js(s : String) -> String {
  let cs = s.to_array()
  let mut a = 0
  let mut b = cs.length()
  while a < b && is_js_space(cs[a]) {
    a = a + 1
  }
  while b > a && is_js_space(cs[b - 1]) {
    b = b - 1
  }
  String::from_array(cs[a:b])
}

///|
fn digit_value(c : Char) -> Int32 {
  if c >= '0' && c <= '9' {
    c.to_int() - '0'.to_int()
  } else if c >= 'a' && c <= 'z' {
    c.to_int() - 'a'.to_int() + 10
  } else if c >= 'A' && c <= 'Z' {
    c.to_int() - 'A'.to_int() + 10
  } else {
    99
  }
}

///|
/// JavaScript's `Number(string)` (`ToNumber` applied to a String).
pub fn string_to_number(s : String) -> Double {
  let t = trim_js(s)
  if t == "" {
    return 0.0
  }
  let cs = t.to_array()
  let n = cs.length()
  // 0x / 0o / 0b literals (no sign allowed)
  if n > 2 && cs[0] == '0' {
    let radix = match cs[1] {
      'x' | 'X' => 16
      'o' | 'O' => 8
      'b' | 'B' => 2
      _ => 0
    }
    if radix != 0 {
      let mut v = 0.0
      for i in 2..= radix {
          return @double.not_a_number
        }
        v = v * radix.to_double() + d.to_double()
      }
      return v
    }
  }
  let mut i = 0
  let mut negative = false
  if cs[0] == '+' || cs[0] == '-' {
    negative = cs[0] == '-'
    i = 1
  }
  if t.unsafe_substring(start=i, end=n) == "Infinity" {
    return if negative { @double.neg_infinity } else { @double.infinity }
  }
  // StrDecimalLiteral: digits [. digits] | . digits, optional exponent
  let int_start = i
  while i < n && cs[i] >= '0' && cs[i] <= '9' {
    i = i + 1
  }
  let int_digits = i - int_start
  let mut frac_digits = 0
  if i < n && cs[i] == '.' {
    i = i + 1
    let f0 = i
    while i < n && cs[i] >= '0' && cs[i] <= '9' {
      i = i + 1
    }
    frac_digits = i - f0
  }
  if int_digits == 0 && frac_digits == 0 {
    return @double.not_a_number
  }
  if i < n && (cs[i] == 'e' || cs[i] == 'E') {
    i = i + 1
    if i < n && (cs[i] == '+' || cs[i] == '-') {
      i = i + 1
    }
    let e0 = i
    while i < n && cs[i] >= '0' && cs[i] <= '9' {
      i = i + 1
    }
    if i == e0 {
      return @double.not_a_number
    }
  }
  if i != n {
    return @double.not_a_number
  }
  // normalise ".5" and "5." for the parser
  let body = t.unsafe_substring(start=int_start, end=n)
  let norm = if int_digits == 0 { "0" + body } else { body }
  let norm2 = norm.replace(old=".e", new=".0e").replace(old=".E", new=".0E")
  let norm3 = if norm2.has_suffix(".") { norm2 + "0" } else { norm2 }
  // out-of-range literals overflow to Infinity or underflow to 0
  let v : Double = @string.from_str(norm3) catch {
    _ => {
      let exp_negative = norm3.contains("e-") || norm3.contains("E-")
      if exp_negative {
        0.0
      } else {
        @double.infinity
      }
    }
  }
  if negative {
    -v
  } else {
    v
  }
}

///|
/// `+value`: JavaScript's unary plus on a cell value.
pub fn Value::to_number(self : Value) -> Double {
  match self {
    Undef => @double.not_a_number
    Null => 0.0
    Bool(b) => if b { 1.0 } else { 0.0 }
    Num(x) => x
    Str(s) => string_to_number(s)
    Arr(items) =>
      match items.length() {
        0 => 0.0
        1 => items[0].to_number()
        _ => @double.not_a_number
      }
    Obj(_) => @double.not_a_number
  }
}

///|
/// `String(value)`.
pub fn Value::to_js_string(self : Value) -> String {
  match self {
    Undef => "undefined"
    Null => "null"
    Bool(b) => if b { "true" } else { "false" }
    Num(x) => @util.js_number_to_string(x)
    Str(s) => s
    Arr(items) =>
      items
      .map(fn(v) {
        match v {
          Null | Undef => ""
          other => other.to_js_string()
        }
      })
      .join(",")
    Obj(_) => "[object Object]"
  }
}

///|
/// vega-lite / vega `isValid`: not null and not NaN.
pub fn Value::is_valid(self : Value) -> Bool {
  match self {
    Null | Undef => false
    Num(x) => !x.is_nan()
    _ => true
  }
}

///|
/// JavaScript truthiness.
pub fn Value::truthy(self : Value) -> Bool {
  match self {
    Null | Undef => false
    Bool(b) => b
    Num(x) => !(x == 0.0 || x.is_nan())
    Str(s) => s != ""
    _ => true
  }
}

///|
fn is_blank(v : Value) -> Bool {
  match v {
    Null | Undef => true
    Str("") => true
    _ => false
  }
}

///|
/// vega-util `toNumber`: null for null / "", else `+v`.
pub fn Value::coerce_number(self : Value) -> Value {
  if is_blank(self) {
    Null
  } else {
    Num(self.to_number())
  }
}

///|
/// vega-util `toString`: null for null / "", else `v + ""`.
pub fn Value::coerce_string(self : Value) -> Value {
  if is_blank(self) {
    Null
  } else {
    Str(self.to_js_string())
  }
}

///|
/// vega-util `toBoolean`.
pub fn Value::coerce_boolean(self : Value) -> Value {
  if is_blank(self) {
    return Null
  }
  match self {
    Str("false") | Str("0") => Bool(false)
    other => Bool(other.truthy())
  }
}

///|
/// vega-util `toDate` with the default parser: numbers pass through, strings
/// go through `Date.parse` (NaN when unparsable).
pub fn Value::coerce_date(self : Value) -> Value {
  if is_blank(self) {
    return Null
  }
  match self {
    Num(x) => Num(x)
    Str(s) => Num(parse_date(s))
    Bool(_) => Num(@double.not_a_number)
    other => Num(other.to_number())
  }
}

///|
/// vega-util `ascending`: nulls first, then natural order; NaN after numbers.
pub fn compare_values(u : Value, v : Value) -> Int32 {
  match (u, v) {
    (Null | Undef, Null | Undef) => 0
    (Null | Undef, _) => -1
    (_, Null | Undef) => 1
    (Num(a), Num(b)) =>
      if a < b {
        -1
      } else if a > b {
        1
      } else if a == b {
        0
      } else if a.is_nan() && !b.is_nan() {
        // vega: `v === v && u !== u` sorts NaN first
        -1
      } else if b.is_nan() && !a.is_nan() {
        1
      } else {
        0
      }
    (Str(a), Str(b)) => {
      let c = a.lexical_compare(b)
      if c < 0 {
        -1
      } else if c > 0 {
        1
      } else {
        0
      }
    }
    (Bool(a), Bool(b)) => if a == b { 0 } else if !a { -1 } else { 1 }
    _ => {
      // mixed types: compare as numbers when possible, like JavaScript
      let a = u.to_number()
      let b = v.to_number()
      if a < b {
        -1
      } else if a > b {
        1
      } else {
        0
      }
    }
  }
}