// Values of the XPath 1.0 data model and the conversions between them.
//
// XPath has exactly four types: node-set, boolean, number and string. Every
// operation is defined in terms of the conversions implemented here, so this
// module is the semantic foundation of both the XPath and the XSLT engine.

///|
/// The four XPath value types.
pub(all) enum Value {
  Nodes(NodeSet)
  Boolean(Bool)
  Number(Double)
  String(String)
}

///|
/// An unordered set of nodes, held in document order.
///
/// Nodes are identified by their id in the arena; the document is supplied by
/// the evaluation context so that node-sets do not need to carry it around.
pub struct NodeSet {
  nodes : Array[Int32]
}

///|
/// Create a node-set from arbitrary ids, sorting them into document order and
/// removing duplicates.
pub fn NodeSet::new(doc : @xml.Document, ids : Array[Int32]) -> NodeSet {
  { nodes: doc.sort_document_order(ids), }
}

///|
/// The empty node-set.
pub fn NodeSet::empty() -> NodeSet {
  { nodes: [], }
}

///|
/// Number of nodes in the set.
pub fn NodeSet::length(self : NodeSet) -> Int32 {
  self.nodes.length()
}

///|
/// Whether the set is empty.
pub fn NodeSet::is_empty(self : NodeSet) -> Bool {
  self.nodes.length() == 0
}

///|
/// The nodes, in document order.
pub fn NodeSet::nodes(self : NodeSet) -> ArrayView[Int32] {
  self.nodes[:]
}

///|
/// The first node in document order, if any.
pub fn NodeSet::first(self : NodeSet) -> Int32? {
  if self.nodes.length() == 0 {
    None
  } else {
    Some(self.nodes[0])
  }
}

///|
/// Whether a node belongs to the set.
pub fn NodeSet::contains(self : NodeSet, id : Int32) -> Bool {
  for node in self.nodes {
    if node == id {
      return true
    }
  }
  false
}

///|
/// Union of two node-sets.
pub fn NodeSet::union(
  self : NodeSet,
  doc : @xml.Document,
  other : NodeSet,
) -> NodeSet {
  let merged : Array[Int32] = []
  for node in self.nodes {
    merged.push(node)
  }
  for node in other.nodes {
    merged.push(node)
  }
  NodeSet::new(doc, merged)
}

///|
/// The string-value of a single node.
///
/// Elements and the document node yield the concatenation of their descendant
/// text, attributes and text nodes yield their character data, and comments and
/// processing instructions yield their content.
pub fn string_of_node(doc : @xml.Document, id : Int32) -> String {
  if doc.is_element(id) || doc.is_document_node(id) {
    doc.text_content(id)
  } else {
    doc.value(id)
  }
}

///|
/// Wrap a boolean as an XPath value.
pub fn of_boolean(value : Bool) -> Value {
  Boolean(value)
}

///|
/// Wrap a number as an XPath value.
pub fn of_number(value : Double) -> Value {
  Number(value)
}

///|
/// Wrap a string as an XPath value.
pub fn of_string(value : String) -> Value {
  String(value)
}

///|
/// XPath boolean conversion.
pub fn Value::boolean_value(self : Value) -> Bool {
  match self {
    Boolean(value) => value
    Number(value) => !(value.is_nan() || value == 0.0)
    String(value) => value != ""
    Nodes(set) => !set.is_empty()
  }
}

///|
/// XPath string conversion.
pub fn Value::string_value(self : Value, doc : @xml.Document) -> String {
  match self {
    Boolean(value) => if value { "true" } else { "false" }
    Number(value) => format_number(value)
    String(value) => value
    Nodes(set) =>
      match set.first() {
        Some(node) => string_of_node(doc, node)
        None => ""
      }
  }
}

///|
/// XPath number conversion.
pub fn Value::number_value(self : Value, doc : @xml.Document) -> Double {
  match self {
    Boolean(value) => if value { 1.0 } else { 0.0 }
    Number(value) => value
    String(value) => parse_number(value)
    Nodes(set) =>
      match set.first() {
        Some(node) => parse_number(string_of_node(doc, node))
        None => nan()
      }
  }
}

///|
/// The IEEE NaN value, which XPath uses for numbers that cannot be represented.
pub fn nan() -> Double {
  @double.infinity - @double.infinity
}

///|
/// Format a number the way XPath's `string()` does.
///
/// Whole numbers are printed without a fractional part and infinities and NaN
/// use their XPath spellings.
pub fn format_number(value : Double) -> String {
  if value.is_nan() {
    "NaN"
  } else if value.is_pos_inf() {
    "Infinity"
  } else if value.is_neg_inf() {
    "-Infinity"
  } else if value == 0.0 {
    "0"
  } else if value == value.floor() && value.abs() < 1.0e15 {
    value.to_int().to_string()
  } else {
    trim_trailing_zeros(value.to_string())
  }
}

///|
/// Remove a trailing `.0` or trailing zeros from a decimal representation.
fn trim_trailing_zeros(text : String) -> String {
  if !text.contains(".") {
    return text
  }
  let chars = text.to_array()
  let mut end = chars.length()
  while end > 0 && chars[end - 1] == '0' {
    end = end - 1
  }
  if end > 0 && chars[end - 1] == '.' {
    end = end - 1
  }
  String::from_array(chars[0:end])
}

///|
/// Parse an XPath number: optional surrounding whitespace, an optional minus
/// sign, digits with an optional fractional part. Anything else is NaN.
pub fn parse_number(text : String) -> Double {
  let chars = text.trim().to_array()
  if chars.length() == 0 {
    return nan()
  }
  let mut index = 0
  let mut negative = false
  if chars[0] == '-' {
    negative = true
    index = 1
  }
  let start = index
  let mut value = 0.0
  let mut digits = 0
  while index < chars.length() && chars[index] >= '0' && chars[index] <= '9' {
    value = value * 10.0 + (chars[index].to_int() - '0'.to_int()).to_double()
    index = index + 1
    digits = digits + 1
  }
  if index < chars.length() && chars[index] == '.' {
    index = index + 1
    let mut scale = 0.1
    while index < chars.length() && chars[index] >= '0' && chars[index] <= '9' {
      value = value + (chars[index].to_int() - '0'.to_int()).to_double() * scale
      scale = scale / 10.0
      index = index + 1
      digits = digits + 1
    }
  }
  if digits == 0 || index != chars.length() {
    return nan()
  }
  let _ = start
  if negative {
    -value
  } else {
    value
  }
}

///|
/// Wrap a string as an XPath value.
pub fn Value::of_string(text : String) -> Value {
  String(text)
}

///|
/// Wrap a number as an XPath value.
pub fn Value::of_number(number : Double) -> Value {
  Number(number)
}

///|
/// Wrap a boolean as an XPath value.
pub fn Value::of_boolean(flag : Bool) -> Value {
  Boolean(flag)
}

///|
/// The node-set behind a value, if the value is a node-set.
pub fn Value::node_set(self : Value) -> NodeSet? {
  match self {
    Nodes(set) => Some(set)
    _ => None
  }
}

///|
/// Wrap a node-set as an XPath value.
pub fn Value::of_nodes(set : NodeSet) -> Value {
  Nodes(set)
}