// Extended XPath 1.0 function library.
//
// The core functions that location paths and the XSLT engine depend on live in
// `eval.mbt`; this module holds the remaining string, number and node-set
// functions of the XPath 1.0 core library.

///|
/// Dispatch to the extended function library.
fn call_library_function(
  name : String,
  arguments : Array[Expr],
  context : EvalContext,
) -> Value raise EvaluationError {
  match name {
    "concat" => {
      if arguments.length() < 2 {
        raise EvaluationError::Message(
          "concat() expects at least two arguments",
        )
      }
      let sb = StringBuilder::StringBuilder()
      for argument in arguments {
        sb.write_string(evaluate(argument, context).string_value(context.doc))
      }
      String(sb.to_string())
    }
    "contains" => {
      expect_arity(name, arguments, 2)
      let haystack = string_argument(name, arguments, 0, context)
      let needle = string_argument(name, arguments, 1, context)
      Boolean(contains_text(haystack, needle))
    }
    "starts-with" => {
      expect_arity(name, arguments, 2)
      let text = string_argument(name, arguments, 0, context)
      let prefix = string_argument(name, arguments, 1, context)
      Boolean(text.has_prefix(prefix))
    }
    "substring" => {
      if arguments.length() < 2 || arguments.length() > 3 {
        raise EvaluationError::Message(
          "substring() expects two or three arguments",
        )
      }
      let text = string_argument(name, arguments, 0, context)
      let start = number_argument(name, arguments, 1, context)
      let length = if arguments.length() == 3 {
        Some(number_argument(name, arguments, 2, context))
      } else {
        None
      }
      String(substring_text(text, start, length))
    }
    "substring-before" => {
      expect_arity(name, arguments, 2)
      let text = string_argument(name, arguments, 0, context)
      let search = string_argument(name, arguments, 1, context)
      String(substring_before(text, search))
    }
    "substring-after" => {
      expect_arity(name, arguments, 2)
      let text = string_argument(name, arguments, 0, context)
      let search = string_argument(name, arguments, 1, context)
      String(substring_after(text, search))
    }
    "string-length" => {
      let text = if arguments.length() == 0 {
        string_of_node(context.doc, context.node)
      } else {
        string_argument(name, arguments, 0, context)
      }
      Number(text.to_array().length().to_double())
    }
    "normalize-space" => {
      let text = if arguments.length() == 0 {
        string_of_node(context.doc, context.node)
      } else {
        string_argument(name, arguments, 0, context)
      }
      String(normalize_space(text))
    }
    "translate" => {
      expect_arity(name, arguments, 3)
      let text = string_argument(name, arguments, 0, context)
      let from = string_argument(name, arguments, 1, context)
      let to = string_argument(name, arguments, 2, context)
      String(translate_text(text, from, to))
    }
    "sum" => {
      expect_arity(name, arguments, 1)
      match evaluate(arguments[0], context) {
        Nodes(set) => {
          let mut total = 0.0
          for node in set.nodes() {
            total = total + parse_number(string_of_node(context.doc, node))
          }
          Number(total)
        }
        _ => raise EvaluationError::Message("sum() requires a node-set")
      }
    }
    "floor" => Number(number_argument(name, arguments, 0, context).floor())
    "ceiling" => Number(number_argument(name, arguments, 0, context).ceil())
    "round" =>
      Number(round_number(number_argument(name, arguments, 0, context)))
    "id" => {
      expect_arity(name, arguments, 1)
      let ids = string_argument(name, arguments, 0, context)
      Nodes(NodeSet::new(context.doc, elements_by_id(context.doc, ids)))
    }
    "lang" => {
      expect_arity(name, arguments, 1)
      let language = string_argument(name, arguments, 0, context).to_lower()
      Boolean(matches_language(context.doc, context.node, language))
    }
    _ => raise EvaluationError::Message("unknown function " + name + "()")
  }
}

///|
fn string_argument(
  name : String,
  arguments : Array[Expr],
  index : Int32,
  context : EvalContext,
) -> String raise EvaluationError {
  if index >= arguments.length() {
    raise EvaluationError::Message(
      name + "() expects an argument at position " + (index + 1).to_string(),
    )
  }
  evaluate(arguments[index], context).string_value(context.doc)
}

///|
fn number_argument(
  name : String,
  arguments : Array[Expr],
  index : Int32,
  context : EvalContext,
) -> Double raise EvaluationError {
  if index >= arguments.length() {
    raise EvaluationError::Message(
      name + "() expects an argument at position " + (index + 1).to_string(),
    )
  }
  evaluate(arguments[index], context).number_value(context.doc)
}

///|
fn is_space_char(c : Char) -> Bool {
  c == ' ' || c == '\t' || c == '\r' || c == '\n'
}

///|
fn contains_text(haystack : String, needle : String) -> Bool {
  find_substring(haystack.to_array(), needle.to_array()) >= 0
}

///|
fn find_substring(haystack : Array[Char], needle : Array[Char]) -> Int32 {
  if needle.length() == 0 {
    return 0
  }
  let mut i = 0
  while i + needle.length() <= haystack.length() {
    let mut matched = true
    for j in 0.. String {
  let haystack = text.to_array()
  let index = find_substring(haystack, search.to_array())
  if index < 0 {
    ""
  } else {
    String::from_array(haystack[0:index])
  }
}

///|
fn substring_after(text : String, search : String) -> String {
  let haystack = text.to_array()
  let needle = search.to_array()
  let index = find_substring(haystack, needle)
  if index < 0 {
    ""
  } else {
    String::from_array(haystack[index + needle.length():])
  }
}

///|
/// XPath `substring()`: positions are one based and are rounded before use, and
/// a negative length yields the empty string.
fn substring_text(text : String, start : Double, length : Double?) -> String {
  let chars = text.to_array()
  let first = round_number(start)
  let limit = match length {
    Some(value) => first + round_number(value)
    None => 1.0e300
  }
  let sb = StringBuilder::StringBuilder()
  for i in 0..= first && position < limit {
      sb.write_char(chars[i])
    }
  }
  sb.to_string()
}

///|
/// XPath `round()`: round half up, preserving NaN and infinities.
fn round_number(value : Double) -> Double {
  if value.is_nan() || value.is_inf() {
    return value
  }
  if value >= -0.5 && value < 0.0 {
    return -0.0
  }
  (value + 0.5).floor()
}

///|
/// XPath `normalize-space()`: trim and collapse whitespace runs.
fn normalize_space(text : String) -> String {
  let sb = StringBuilder::StringBuilder()
  let mut pending_space = false
  let mut started = false
  for c in text.to_array() {
    if is_space_char(c) {
      if started {
        pending_space = true
      }
    } else {
      if pending_space {
        sb.write_char(' ')
        pending_space = false
      }
      sb.write_char(c)
      started = true
    }
  }
  sb.to_string()
}

///|
/// XPath `translate()`: map characters from `from` to `to` and delete the rest.
fn translate_text(text : String, from : String, to : String) -> String {
  let source = text.to_array()
  let from_chars = from.to_array()
  let to_chars = to.to_array()
  let sb = StringBuilder::StringBuilder()
  for c in source {
    let mut index = -1
    let mut i = 0
    while i < from_chars.length() {
      if from_chars[i] == c {
        index = i
        break
      }
      i = i + 1
    }
    if index < 0 {
      sb.write_char(c)
    } else if index < to_chars.length() {
      sb.write_char(to_chars[index])
    }
  }
  sb.to_string()
}

///|
/// Split a string on XPath whitespace.
fn split_whitespace(text : String) -> Array[String] {
  let result : Array[String] = []
  let sb = StringBuilder::StringBuilder()
  let mut started = false
  for c in text.to_array() {
    if is_space_char(c) {
      if started {
        result.push(sb.to_string())
        sb.reset()
        started = false
      }
    } else {
      sb.write_char(c)
      started = true
    }
  }
  if started {
    result.push(sb.to_string())
  }
  result
}

///|
/// The XPath `id()` function: select elements whose `id` attribute matches one
/// of the whitespace separated identifiers.
fn elements_by_id(doc : @xml.Document, ids : String) -> Array[Int32] {
  let wanted = split_whitespace(ids)
  let result : Array[Int32] = []
  for node in doc.nodes_in_document_order() {
    if doc.is_element(node) {
      match doc.attribute_value(node, "id") {
        Some(value) =>
          for id in wanted {
            if value == id {
              result.push(node)
            }
          }
        None => ()
      }
    }
  }
  result
}

///|
/// The XPath `lang()` function: match the `xml:lang` attribute of the node or of
/// its nearest ancestor that carries one.
fn matches_language(
  doc : @xml.Document,
  node : Int32,
  language : String,
) -> Bool {
  let mut current : Int32? = Some(node)
  while current is Some(id) {
    match
      doc.attribute_by_expanded(
        id, "http://www.w3.org/XML/1998/namespace", "lang",
      ) {
      Some(attribute) => {
        let declared = doc.value(attribute).to_lower()
        if declared == language {
          return true
        }
        if declared.has_prefix(language + "-") {
          return true
        }
        return false
      }
      None => current = doc.parent(id)
    }
  }
  false
}