///|
fn supported_datatype(datatype : String) -> Bool {
  datatype == rdf + "langString" ||
  [
    "string", "boolean", "integer", "int", "long", "short", "byte", "unsignedLong",
    "unsignedInt", "unsignedShort", "unsignedByte", "positiveInteger", "negativeInteger",
    "nonNegativeInteger", "nonPositiveInteger", "decimal", "hexBinary", "duration",
  ].any(d => datatype == xsd + d)
}

///|
fn digits(s : String) -> Bool {
  !s.is_empty() && s.iter().all(c => c >= '0' && c <= '9')
}

///|
fn unsigned_decimal(s : String) -> Bool {
  let parts = s.split(".").to_array()
  if parts.length() == 1 {
    digits(s)
  } else if parts.length() == 2 {
    (parts[0].is_empty() || digits(parts[0].to_owned())) &&
    (parts[1].is_empty() || digits(parts[1].to_owned())) &&
    !(parts[0].is_empty() && parts[1].is_empty())
  } else {
    false
  }
}

///|
fn without_sign(s : String) -> String {
  if s.has_prefix("+") || s.has_prefix("-") {
    s[1:].to_owned()
  } else {
    s
  }
}

///|
fn valid_duration(s : String) -> Bool {
  let s = if s.has_prefix("-") { s[1:].to_owned() } else { s }
  if !s.has_prefix("P") {
    return false
  }
  let mut token = ""
  let mut time = false
  let mut last = -1
  let mut components = 0
  let mut time_components = 0
  for c in s[1:].iter() {
    if c == 'T' {
      if time || !token.is_empty() {
        return false
      }
      time = true
      last = -1
    } else if (c >= '0' && c <= '9') || c == '.' {
      token = token + c.to_string()
    } else {
      let position = if time {
        match c {
          'H' => 0
          'M' => 1
          'S' => 2
          _ => -1
        }
      } else {
        match c {
          'Y' => 0
          'M' => 1
          'D' => 2
          _ => -1
        }
      }
      if position <= last || position < 0 {
        return false
      }
      if !(if time && c == 'S' {
          let parts = token.split(".").to_array()
          (parts.length() == 1 && digits(token)) ||
          (
            parts.length() == 2 &&
            digits(parts[0].to_owned()) &&
            digits(parts[1].to_owned())
          )
        } else {
          digits(token)
        }) {
        return false
      }
      components = components + 1
      if time {
        time_components = time_components + 1
      }
      last = position
      token = ""
    }
  }
  token.is_empty() && components > 0 && (!time || time_components > 0)
}

///|
fn valid_lexical(value : String, datatype : String, language : String?) -> Bool {
  if datatype == rdf + "langString" {
    return language is Some(tag) && !tag.is_empty()
  }
  if language is Some(_) {
    return false
  }
  if datatype == xsd + "string" {
    return true
  }
  let s = value.trim(chars=" \t\r\n").to_owned()
  if datatype == xsd + "boolean" {
    return ["true", "false", "1", "0"].contains(s)
  }
  if datatype == xsd + "decimal" {
    return unsigned_decimal(without_sign(s))
  }
  if datatype == xsd + "hexBinary" {
    return s.length() % 2 == 0 &&
      s
      .iter()
      .all(c => {
        (c >= '0' && c <= '9') ||
        (c >= 'a' && c <= 'f') ||
        (c >= 'A' && c <= 'F')
      })
  }
  if datatype == xsd + "duration" {
    return valid_duration(s)
  }
  if !digits(without_sign(s)) {
    return false
  }
  let numeric = if s.has_prefix("+") { s[1:].to_owned() } else { s }
  let n = @string.parse_bigint(numeric) catch { _ => return false }
  let bounds : (String, String)? = match datatype[xsd.length():].to_owned() {
    "byte" => Some(("-128", "127"))
    "short" => Some(("-32768", "32767"))
    "int" => Some(("-2147483648", "2147483647"))
    "long" => Some(("-9223372036854775808", "9223372036854775807"))
    "unsignedByte" => Some(("0", "255"))
    "unsignedShort" => Some(("0", "65535"))
    "unsignedInt" => Some(("0", "4294967295"))
    "unsignedLong" => Some(("0", "18446744073709551615"))
    "positiveInteger" => return n.compare_int(0) > 0
    "negativeInteger" => return n.compare_int(0) < 0
    "nonNegativeInteger" => return n.compare_int(0) >= 0
    "nonPositiveInteger" => return n.compare_int(0) <= 0
    "integer" => return true
    _ => return false
  }
  match bounds {
    Some((low, high)) => {
      let lo = @string.parse_bigint(low) catch { _ => return false }
      let hi = @string.parse_bigint(high) catch { _ => return false }
      n.compare(lo) >= 0 && n.compare(hi) <= 0
    }
    None => false
  }
}