///|
/// An exact JSON number. Equality compares numeric values, not Double
/// approximations or token spellings. No exponent is expanded into zeros.
pub struct NodeNumber {
  text : String
  coefficient : BigInt
  exponent : BigInt
  approximate : Double
} derive(Debug)

///|
pub impl Eq for NodeNumber with fn equal(self, other) {
  self.coefficient == other.coefficient &&
  (self.coefficient == BigInt::from_int(0) || self.exponent == other.exponent)
}

///|
fn NodeNumber::numeric_compare(self : NodeNumber, other : NodeNumber) -> Int32 {
  if self == other {
    return 0
  }
  let zero = BigInt::from_int(0)
  let left_negative = self.coefficient < zero
  let right_negative = other.coefficient < zero
  if left_negative != right_negative {
    return if left_negative { -1 } else { 1 }
  }
  if self.coefficient == zero {
    return if right_negative { 1 } else { -1 }
  }
  if other.coefficient == zero {
    return if left_negative { -1 } else { 1 }
  }
  let left = (if left_negative { -self.coefficient } else { self.coefficient }).to_string()
  let right = (if right_negative {
    -other.coefficient
  } else {
    other.coefficient
  }).to_string()
  let left_scale = self.exponent + BigInt::from_int(left.length())
  let right_scale = other.exponent + BigInt::from_int(right.length())
  let order = if left_scale < right_scale {
    -1
  } else if left_scale > right_scale {
    1
  } else {
    let width = if left.length() > right.length() {
      left.length()
    } else {
      right.length()
    }
    let a = BigInt::from_string(left + "0".repeat(width - left.length()))
    let b = BigInt::from_string(right + "0".repeat(width - right.length()))
    if a < b {
      -1
    } else if a > b {
      1
    } else {
      0
    }
  }
  if left_negative {
    -order
  } else {
    order
  }
}

///|
pub fn NodeNumber::parse(text : String) -> NodeNumber? {
  let value = @json.parse(text) catch { _ => return None }
  guard value is Number(approximate, ..) else { return None }
  // Reject surrounding whitespace so text is always exactly one token.
  guard text.trim().to_owned() == text else { return None }
  let parts = text.replace_all(old="E", new="e").split("e").to_array()
  let mantissa = parts[0].to_owned()
  let power = if parts.length() == 2 {
    parts[1].to_owned().trim_start(chars="+").to_owned()
  } else {
    "0"
  }
  let fractional = match mantissa.find(".") {
    Some(at) => mantissa.length() - at - 1
    None => 0
  }
  let digits = mantissa.replace_all(old=".", new="")
  let trimmed = digits.trim_end(chars="0").to_owned()
  let coefficient = if trimmed == "" || trimmed == "-" {
    BigInt::from_int(0)
  } else {
    BigInt::from_string(trimmed)
  }
  let exponent = BigInt::from_string(power) -
    BigInt::from_int(fractional) +
    BigInt::from_int(digits.length() - trimmed.length())
  Some({ text, coefficient, exponent, approximate, })
}

///|
pub impl @json.ToJson for NodeNumber with fn to_json(self) {
  Json::number(self.approximate, repr=self.text)
}

///|
pub(all) suberror NodeError {
  InvalidNode(String)
} derive(Eq, Debug)

///|
/// Source-independent values used for metadata and applied traits.
pub(all) enum NodeValue {
  Null
  Boolean(Bool)
  Number(NodeNumber)
  String(String)
  Array(Array[NodeValue])
  Object(Map[String, NodeValue])
} derive(Eq, Debug)

///|
pub fn NodeValue::from_json(value : Json) -> NodeValue raise NodeError {
  match value {
    Null => Null
    True => Boolean(true)
    False => Boolean(false)
    String(s) => String(s)
    Number(number, repr~) => {
      let text = repr.unwrap_or(number.to_string())
      match NodeNumber::parse(text) {
        Some(n) => Number(n)
        None =>
          raise InvalidNode(
            "I expected a finite JSON number or a valid numeric representation, but found \{text}.",
          )
      }
    }
    Array(values) => Array(values.map(NodeValue::from_json))
    Object(values) => {
      let out : Map[String, NodeValue] = Map([])
      for key, value in values {
        out[key] = NodeValue::from_json(value)
      }
      Object(out)
    }
  }
}

///|
pub impl @json.ToJson for NodeValue with fn to_json(self) {
  match self {
    Null => Json::null()
    Boolean(value) => Json::boolean(value)
    String(value) => Json::string(value)
    Number(value) => @json.to_json(value)
    Array(values) => Json::array(values.map(v => @json.to_json(v)))
    Object(values) => {
      let out : Map[String, Json] = Map([])
      for key, value in values {
        out[key] = @json.to_json(value)
      }
      Json::object(out)
    }
  }
}