lib/sparql/functions/builtins.ex

defmodule SPARQL.Functions.Builtins do

  require Logger

  alias RDF.{IRI, BlankNode, Literal, XSD, NS}

  # Value equality
  # - <https://www.w3.org/TR/sparql11-query/#OperatorMapping>
  # - <https://www.w3.org/TR/sparql11-query/#func-RDFterm-equal>
  def call(:=, [left, right], _) do
    left |> RDF.Term.equal_value?(right) |> ebv()
  end

  # Value inequality
  # - <https://www.w3.org/TR/sparql11-query/#OperatorMapping>
  # - <https://www.w3.org/TR/sparql11-query/#func-RDFterm-equal>
  def call(:!=, [left, right], _) do
    left |> RDF.Term.equal_value?(right) |> fn_not()
  end

  # sameTerm equality
  # - <https://www.w3.org/TR/sparql11-query/#func-sameTerm>
  def call(:sameTerm, [left, right], _) do
    left |> RDF.Term.equal?(right) |> ebv()
  end

  # Less-than operator
  # - <https://www.w3.org/TR/sparql11-query/#OperatorMapping>
  def call(:<, [%Literal{} = left, %Literal{} = right], _) do
    case Literal.compare(left, right) do
      :lt -> true
      nil -> nil
      _ -> false
    end
    |> ebv()
  end

  def call(:<, _, _), do: :error

  # Greater-than operator
  # - <https://www.w3.org/TR/sparql11-query/#OperatorMapping>
  def call(:>, [%Literal{} = left, %Literal{} = right], _) do
    case Literal.compare(left, right) do
      :gt -> true
      nil -> nil
      _ -> false
    end
    |> ebv()
  end

  def call(:>, _, _), do: :error

  # Greater-or-equal operator
  # - <https://www.w3.org/TR/sparql11-query/#OperatorMapping>
  def call(:>=, [%Literal{} = left, %Literal{} = right], _) do
    case Literal.compare(left, right) do
      :gt -> XSD.true
      :eq -> XSD.true
      :lt -> XSD.false
      _   -> :error
    end
  end

  def call(:>=, _, _), do: :error

  # Less-or-equal operator
  # - <https://www.w3.org/TR/sparql11-query/#OperatorMapping>
  def call(:<=, [%Literal{} = left, %Literal{} = right], _) do
    case Literal.compare(left, right) do
      :lt -> XSD.true
      :eq -> XSD.true
      :gt -> XSD.false
      _   -> :error
    end
  end

  def call(:<=, _, _), do: :error

  # Logical `NOT`
  #
  # Returns `RDF.XSD.true` if the effective boolean value of the given argument is
  # `RDF.XSD.false`, or `RDF.XSD.false` if it is `RDF.XSD.true`. Otherwise it returns `error`.
  #
  # - <http://www.w3.org/TR/xpath-functions/#func-not>
  def call(:!, [argument], _) do
    fn_not(argument)
  end


  # Numeric unary plus
  # - <http://www.w3.org/TR/xpath-functions/#func-numeric-unary-plus>
  def call(:+, [number], _) do
    if XSD.Numeric.datatype?(number) do
      number
    else
      :error
    end
  end

  # Numeric unary minus
  # - <http://www.w3.org/TR/xpath-functions/#func-numeric-unary-minus>
  def call(:-, [number], _) do
    if XSD.Numeric.datatype?(number) do
      XSD.Numeric.multiply(number, -1)
    else
      :error
    end
  end

  # Numeric addition
  # - <http://www.w3.org/TR/xpath-functions/#func-numeric-add>
  def call(:+, [left, right], _) do
    XSD.Numeric.add(left, right) || :error
  end

  # Numeric subtraction
  # - <http://www.w3.org/TR/xpath-functions/#func-numeric-subtract>
  def call(:-, [left, right], _) do
    XSD.Numeric.subtract(left, right) || :error
  end

  # Numeric multiplication
  # - <http://www.w3.org/TR/xpath-functions/#func-numeric-multiply>
  def call(:*, [left, right], _) do
    XSD.Numeric.multiply(left, right) || :error
  end

  # Numeric division
  # - <http://www.w3.org/TR/xpath-functions/#func-numeric-divide>
  def call(:/, [left, right], _) do
    XSD.Numeric.divide(left, right) || :error
  end

  # Checks if the given argument is an IRI.
  # - <https://www.w3.org/TR/sparql11-query/#func-isIRI>
  def call(:isIRI, [%IRI{}], _), do: XSD.true
  def call(:isIRI, [:error], _), do: :error
  def call(:isIRI, _, _),        do: XSD.false

  # Checks if the given argument is an IRI.
  # - <https://www.w3.org/TR/sparql11-query/#func-isIRI>
  def call(:isURI, args, execution), do: call(:isIRI, args, execution)

  # Checks if the given argument is a blank node.
  # - <https://www.w3.org/TR/sparql11-query/#func-isBlank>
  def call(:isBLANK, [%BlankNode{}], _), do: XSD.true
  def call(:isBLANK, [:error], _),       do: :error
  def call(:isBLANK, _, _),              do: XSD.false

  # Checks if the given argument is a RDF literal.
  # - <https://www.w3.org/TR/sparql11-query/#func-isLiteral>
  def call(:isLITERAL, [%Literal{}], _), do: XSD.true
  def call(:isLITERAL, [:error], _),     do: :error
  def call(:isLITERAL, _, _),            do: XSD.false

  # Checks if the given argument is a RDF literal with a numeric datatype.
  # - <https://www.w3.org/TR/sparql11-query/#func-isNumeric>
  def call(:isNUMERIC, [%Literal{} = literal], _) do
    if XSD.Numeric.datatype?(literal) and Literal.valid?(literal) do
      XSD.true
    else
      XSD.false
    end
  end
  def call(:isNUMERIC, [:error], _), do: :error
  def call(:isNUMERIC, _, _),        do: XSD.false

  # Returns the lexical form of a literal or the codepoint representation of an IRI
  # - <https://www.w3.org/TR/sparql11-query/#func-str>
  def call(:STR, [%Literal{} = literal], _), do: literal |> to_string() |> XSD.string()
  def call(:STR, [%IRI{} = iri], _),         do: iri     |> to_string() |> XSD.string()
  def call(:STR, _, _),                      do: :error

  # Returns the language tag of language tagged literal.
  #
  # It returns `~L""` if the given literal has no language tag. Note that the RDF
  # data model does not include literals with an empty language tag.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-lang>
  def call(:LANG, [%Literal{} = literal], _),
    do: literal |> Literal.language() |> to_string() |> XSD.string()
  def call(:LANG, _, _), do: :error

  # Returns the datatype IRI of a literal.
  # - <https://www.w3.org/TR/sparql11-query/#func-datatype>
  def call(:DATATYPE, [%Literal{} = literal], _), do: Literal.datatype_id(literal)
  def call(:DATATYPE, _, _),                          do: :error

  # Constructs a literal with lexical form and type as specified by the arguments.
  # - <https://www.w3.org/TR/sparql11-query/#func-strdt>
  def call(:STRDT, [%Literal{literal: %XSD.String{}} = literal, %IRI{} = datatype], _) do
    literal |> Literal.lexical() |> Literal.new(datatype: datatype)
  end
  def call(:STRDT, _, _), do: :error

  # Constructs a literal with lexical form and language tag as specified by the arguments.
  # - <https://www.w3.org/TR/sparql11-query/#func-strlang>
  def call(:STRLANG, [%Literal{literal: %XSD.String{}} = lexical_form_literal,
                      %Literal{literal: %XSD.String{}} = language_literal], _) do
    language = language_literal |> to_string() |> String.trim()
    if language != "" do
      RDF.LangString.new(to_string(lexical_form_literal), language: language)
    else
      :error
    end
  end
  def call(:STRLANG, _, _), do: :error

  # Constructs an IRI from the given string argument.
  #
  # It constructs an IRI by resolving the string argument (see RFC 3986 and RFC 3987
  # or any later RFC that supersedes RFC 3986 or RFC 3987). The IRI is resolved
  # against the base IRI of the query and must result in an absolute IRI.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-iri>
  def call(:IRI, [%Literal{literal: %XSD.String{}} = literal], execution) do
    literal |> to_string() |> IRI.absolute(Map.get(execution, :base)) || :error
  end
  def call(:IRI, [%IRI{} = iri], _), do: iri
  def call(:IRI, _, _),              do: :error

  # Checks if the given argument is an IRI.
  #
  # Alias for `IRI`.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-isIRI>
  def call(:URI, args, execution), do: call(:IRI, args, execution)

  # Constructs a blank node.
  #
  # The constructed blank node is distinct from all blank nodes in the dataset
  # being queried and distinct from all blank nodes created by calls to this
  # constructor for other query solutions.
  #
  # If the no argument form is used, every call results in a distinct blank node.
  # If the form with a simple literal is used, every call results in distinct
  # blank nodes for different simple literals, and the same blank node for calls
  # with the same simple literal within expressions for one solution mapping.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-bnode>
  def call(:BNODE, [], %{bnode_generator: generator}) do
    BlankNode.Generator.generate(generator)
  end

  def call(:BNODE, [%Literal{literal: %XSD.String{}} = literal],
        %{bnode_generator: generator, solution_id: solution_id}) do
    BlankNode.Generator.generate_for(generator, {solution_id, to_string(literal)})
  end

  def call(:BNODE, _, _), do: :error

  # Return a fresh IRI from the UUID URN scheme.
  #
  # Each call of UUID() returns a different UUID.
  #
  # Currently, UUID v4 ids according to RFC 4122 are produced.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-uuid>
  def call(:UUID, [], _), do: uuid(:urn) |> IRI.new()
  def call(:UUID, _, _),  do: :error

  # Return a string literal that is the scheme specific part of UUID.
  #
  # Currently, UUID v4 ids according to RFC 4122 are produced.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-struuid>
  def call(:STRUUID, [], _), do: uuid(:default) |> XSD.string()
  def call(:STRUUID, _, _),  do: :error

  # Returns an `xsd:integer` equal to the length in characters of the lexical form of a literal.
  # - <https://www.w3.org/TR/sparql11-query/#func-strlen>
  # - <http://www.w3.org/TR/xpath-functions/#func-string-length>
  def call(:STRLEN, [%Literal{literal: %datatype{}} = literal], _)
      when datatype in [XSD.String, RDF.LangString],
      do: literal |> to_string() |> String.length() |> XSD.integer()
  def call(:STRLEN, _, _), do: :error

  # Returns a portion of a string .
  #
  # The arguments startingLoc and length may be derived types of `xsd:integer`. The
  # index of the first character in a strings is 1.
  #
  # Returns a literal of the same kind (simple literal, literal with language tag,
  # xsd:string typed literal) as the source input parameter but with a lexical form
  # formed from the substring of the lexical form of the source.
  #
  # The substr function corresponds to the XPath `fn:substring` function.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-substr>
  # - <http://www.w3.org/TR/xpath-functions/#func-substring>
  def call(:SUBSTR, [%Literal{literal: %source_datatype{}} = source, %Literal{} = starting_loc], _)
      when source_datatype in [XSD.String, RDF.LangString] do
    if XSD.Integer.valid?(starting_loc) do
      Literal.update(source, fn source_string ->
        String.slice(source_string, (XSD.Integer.value(starting_loc) - 1) .. -1)
      end)
    else
      :error
    end
  end

  def call(:SUBSTR, [%Literal{literal: %source_datatype{}} = source,
                     %Literal{} = starting_loc, %Literal{} = length], _)
      when source_datatype in [XSD.String, RDF.LangString] do
    if XSD.Integer.valid?(starting_loc) and XSD.Integer.valid?(length) do
      Literal.update(source, fn source_string ->
        String.slice(source_string, (XSD.Integer.value(starting_loc) - 1), XSD.Integer.value(length))
      end)
    else
      :error
    end
  end

  def call(:SUBSTR, _, _), do: :error

  # Returns a string literal whose lexical form is the upper case of the lexcial form of the argument.
  #
  # The UCASE function corresponds to the XPath `fn:upper-case` function.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-ucase>
  # - <http://www.w3.org/TR/xpath-functions/#func-upper-case>
  def call(:UCASE, [%Literal{literal: %datatype{}} = str], _)
      when datatype in [XSD.String, RDF.LangString] do
    Literal.update(str, &String.upcase/1)
  end

  def call(:UCASE, _, _), do: :error

  # Returns a string literal whose lexical form is the lower case of the lexcial form of the argument.
  #
  # The LCASE function corresponds to the XPath `fn:lower-case` function.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-lcase>
  # - <http://www.w3.org/TR/xpath-functions/#func-lower-case>
  def call(:LCASE, [%Literal{literal: %datatype{}} = str], _)
      when datatype in [XSD.String, RDF.LangString] do
    Literal.update(str, &String.downcase/1)
  end

  def call(:LCASE, _, _), do: :error

  # Returns true if the lexical form of arg1 starts with the lexical form of arg2, otherwise it returns false.
  #
  # The STRSTARTS function corresponds to the XPath `fn:starts-with` function.
  #
  # The arguments must be `compatible_arguments?/2` otherwise `:error` is returned.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-strstarts>
  # - <http://www.w3.org/TR/xpath-functions/#func-starts-with>
  def call(:STRSTARTS, [arg1, arg2], _) do
    if compatible_arguments?(arg1, arg2) do
      if arg1 |> to_string() |> String.starts_with?(to_string(arg2)) do
        XSD.true
      else
        XSD.false
      end
    else
      :error
    end
  end

  def call(:STRSTARTS, _, _), do: :error

  # Returns true if the lexical form of arg1 ends with the lexical form of arg2, otherwise it returns false.
  #
  # The STRENDS function corresponds to the XPath `fn:ends-with` function.
  #
  # The arguments must be `compatible_arguments?/2` otherwise `:error` is returned.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-strends>
  # - <http://www.w3.org/TR/xpath-functions/#func-ends-with>
  def call(:STRENDS, [arg1, arg2], _) do
    if compatible_arguments?(arg1, arg2) do
      if arg1 |> to_string() |> String.ends_with?(to_string(arg2)) do
        XSD.true
      else
        XSD.false
      end
    else
      :error
    end
  end

  def call(:STRENDS, _, _), do: :error

  # Returns true if the lexical form of arg1 contains the lexical form of arg2, otherwise it returns false.
  #
  # The CONTAINS function corresponds to the XPath `fn:contains` function.
  #
  # The arguments must be `compatible_arguments?/2` otherwise `:error` is returned.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-contains>
  # - <http://www.w3.org/TR/xpath-functions/#func-contains>
  def call(:CONTAINS, [arg1, arg2], _) do
    if compatible_arguments?(arg1, arg2) do
      if arg1 |> to_string() |> String.contains?(to_string(arg2)) do
        XSD.true
      else
        XSD.false
      end
    else
      :error
    end
  end

  def call(:CONTAINS, _, _), do: :error

  # Returns the substring of the lexical form of arg1 that precedes the first occurrence of the lexical form of arg2.
  #
  # The STRBEFORE function corresponds to the XPath `fn:substring-before` function.
  #
  # The arguments must be `compatible_arguments?/2` otherwise `:error` is returned.
  #
  # For compatible arguments, if the lexical part of the second argument occurs as
  # a substring of the lexical part of the first argument, the function returns a
  # literal of the same kind as the first argument arg1 (simple literal, plain
  # literal same language tag, xsd:string). The lexical form of the result is the
  # substring of the lexical form of arg1 that precedes the first occurrence of
  # the lexical form of arg2. If the lexical form of arg2 is the empty string,
  # this is considered to be a match and the lexical form of the result is the
  # empty string.
  #
  # If there is no such occurrence, an empty simple literal is returned.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-strbefore>
  # - <http://www.w3.org/TR/xpath-functions/#func-substring-before>
  def call(:STRBEFORE, [arg1, arg2], _) do
    cond do
      not compatible_arguments?(arg1, arg2) -> :error
      Literal.lexical(arg2) == ""           -> Literal.update(arg1, fn _ -> "" end)
      true ->
        case String.split(Literal.lexical(arg1), Literal.lexical(arg2), parts: 2) do
          [left, _] -> Literal.update(arg1, fn _ -> left end)
          [_]       -> Literal.new("")
        end
    end
  end

  def call(:STRBEFORE, _, _), do: :error

  # Returns the substring of the lexical form of arg1 that follows the first occurrence of the lexical form of arg2.
  #
  # The STRAFTER function corresponds to the XPath `fn:substring-before` function.
  #
  # The arguments must be `compatible_arguments?/2` otherwise `:error` is returned.
  #
  # For compatible arguments, if the lexical part of the second argument occurs as
  # a substring of the lexical part of the first argument, the function returns a
  # literal of the same kind as the first argument arg1 (simple literal, plain
  # literal same language tag, xsd:string). The lexical form of the result is the
  # substring of the lexical form of arg1 that precedes the first occurrence of
  # the lexical form of arg2. If the lexical form of arg2 is the empty string,
  # this is considered to be a match and the lexical form of the result is the
  # lexical form of arg1.
  #
  # If there is no such occurrence, an empty simple literal is returned.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-strafter>
  # - <http://www.w3.org/TR/xpath-functions/#func-substring-after>
  def call(:STRAFTER, [arg1, arg2], _) do
    cond do
      not compatible_arguments?(arg1, arg2) -> :error
      Literal.lexical(arg2) == ""           -> arg1
      true ->
        case String.split(Literal.lexical(arg1), Literal.lexical(arg2), parts: 2) do
          [_, right] -> Literal.update(arg1, fn _ -> right end)
          [_]        -> Literal.new("")
        end
    end
  end

  def call(:STRAFTER, _, _), do: :error

  # Returns a simple literal with the lexical form obtained from the lexical form of its input after translating reserved characters according to the fn:encode-for-uri function.
  #
  # The ENCODE_FOR_URI function corresponds to the XPath `fn:encode-for-uri` function.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-encode>
  # - <http://www.w3.org/TR/xpath-functions/#func-encode-for-uri>
  def call(:ENCODE_FOR_URI, [%Literal{literal: %datatype{}} = str], _)
      when datatype in [XSD.String, RDF.LangString] do
    str
    |> to_string()
    |> URI.encode(&URI.char_unreserved?/1)
    |> Literal.new()
  end

  def call(:ENCODE_FOR_URI, _, _), do: :error

  # Returns a string literal with the lexical form being obtained by concatenating the lexical forms of its inputs.
  #
  # If all input literals are typed literals of type `xsd:string`, then the returned
  # literal is also of type `xsd:string`, if all input literals are plain literals
  # with identical language tag, then the returned literal is a plain literal with
  # the same language tag, in all other cases, the returned literal is a simple literal.
  #
  # The CONCAT function corresponds to the XPath `fn:concat` function.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-concat>
  # - <http://www.w3.org/TR/xpath-functions/#func-concat>
  def call(:CONCAT, [], _), do: XSD.string("")
  def call(:CONCAT, [%Literal{literal: %datatype{}} = first |rest], _)
      when datatype in [XSD.String, RDF.LangString] do
    rest
    |> Enum.reduce_while({to_string(first), Literal.language(first)}, fn
         %Literal{literal: %datatype{}} = str, {acc, language}
               when datatype in [XSD.String, RDF.LangString] ->
           {:cont, {
               acc <> to_string(str),
               if language && language == Literal.language(str) do
                 language
               else
                 nil
               end
             }
           }
         _, _ ->
           {:halt, :error}
       end)
    |> case do
         {str, nil}      -> XSD.string(str)
         {str, language} -> RDF.lang_string(str, language: language)
         _               -> :error
       end
  end

  def call(:CONCAT, _, _), do: :error

  # Checks if a language tagged string literal or language tag matches a language range.
  #
  # The check is performed per the basic filtering scheme defined in
  # [RFC4647](http://www.ietf.org/rfc/rfc4647.txt) section 3.3.1.
  # A language range is a basic language range per _Matching of Language Tags_ in
  # RFC4647 section 2.1.
  # A language range of `"*"` matches any non-empty language-tag string.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-langMatches>
  def call(:LANGMATCHES, [%Literal{literal: %XSD.String{value: language_tag}},
                          %Literal{literal: %XSD.String{value: language_range}}], _) do
    if RDF.LangString.match_language?(language_tag, language_range) do
      XSD.true
    else
      XSD.false
    end
  end

  def call(:LANGMATCHES, _, _), do: :error

  # Matches text against a regular expression pattern.
  #
  # The regular expression language is defined in _XQuery 1.0 and XPath 2.0 Functions and Operators_.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-regex>
  # - <https://www.w3.org/TR/xpath-functions/#func-matches>
  def call(:REGEX, [text, pattern], _),        do: match_regex(text, pattern, XSD.string(""))
  def call(:REGEX, [text, pattern, flags], _), do: match_regex(text, pattern, flags)
  def call(:REGEX, _, _),                      do: :error

  # Replaces each non-overlapping occurrence of the regular expression pattern with the replacement string.
  #
  # Regular expression matching may involve modifier flags. See REGEX.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-replace>
  # - <http://www.w3.org/TR/xpath-functions/#func-replace>
  def call(:REPLACE, [text, pattern, replacement], _),
    do: replace_regex(text, pattern, replacement, XSD.string(""))
  def call(:REPLACE, [text, pattern, replacement, flags], _),
    do: replace_regex(text, pattern, replacement, flags)
  def call(:REPLACE, _, _), do: :error

  # Returns the absolute value of the argument.
  #
  # If the argument is not a numeric value `:error` is returned.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-abs>
  # - <http://www.w3.org/TR/xpath-functions/#func-abs>
  def call(:ABS, [%Literal{} = literal], _) do
    XSD.Numeric.abs(literal) || :error
  end

  def call(:ABS, _, _), do: :error

  # Rounds a value to a specified number of decimal places, rounding upwards if two such values are equally near.
  #
  # The function returns the nearest (that is, numerically closest) value to the
  # given literal value that is a multiple of ten to the power of minus `precision`.
  # If two such values are equally near (for example, if the fractional part in the
  # literal value is exactly .5), the function returns the one that is closest to
  # positive infinity.
  #
  # If the argument is not a numeric value `:error` is returned.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-round>
  # - <http://www.w3.org/TR/xpath-functions/#func-round>
  def call(:ROUND, [%Literal{} = literal], _) do
    XSD.Numeric.round(literal) || :error
  end

  def call(:ROUND, _, _), do: :error

  # Rounds a numeric value upwards to a whole number.
  #
  # If the argument is not a numeric value `:error` is returned.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-ceil>
  # - <http://www.w3.org/TR/xpath-functions/#func-ceil>
  def call(:CEIL, [%Literal{} = literal], _) do
    XSD.Numeric.ceil(literal) || :error
  end

  def call(:CEIL, _, _), do: :error

  # Rounds a numeric value downwards to a whole number.
  #
  # If the argument is not a numeric value `:error` is returned.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-floor>
  # - <http://www.w3.org/TR/xpath-functions/#func-floor>
  def call(:FLOOR, [%Literal{} = literal], _) do
    XSD.Numeric.floor(literal) || :error
  end

  def call(:FLOOR, _, _), do: :error

  # Returns a pseudo-random number between 0 (inclusive) and 1.0e0 (exclusive).
  # - <https://www.w3.org/TR/sparql11-query/#idp2130040>
  def call(:RAND, [], _) do
    :rand.uniform() |> XSD.double()
  end

  def call(:RAND, _, _), do: :error

  # Returns an XSD dateTime value for the current query execution.
  #
  # All calls to this function in any one query execution return the same value.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-now>
  def call(:NOW, [], %{time: time}) do
    XSD.date_time(time)
  end

  def call(:NOW, _, _), do: :error

  # Returns the year part of the given datetime as an integer.
  # - <https://www.w3.org/TR/sparql11-query/#func-year>
  # - <https://www.w3.org/TR/xpath-functions/#func-year-from-dateTime>
  def call(:YEAR, [%Literal{literal: %XSD.DateTime{} = literal}], _) do
    naive_datetime_part(literal, :year)
  end

  def call(:YEAR, _, _), do: :error

  # Returns the month part of the given datetime as an integer.
  # - <https://www.w3.org/TR/sparql11-query/#func-month>
  # - <https://www.w3.org/TR/xpath-functions/#func-month-from-dateTime>
  def call(:MONTH, [%Literal{literal: %XSD.DateTime{} = literal}], _) do
    naive_datetime_part(literal, :month)
  end

  def call(:MONTH, _, _), do: :error

  # Returns the day part of the given datetime as an integer.
  # - <https://www.w3.org/TR/sparql11-query/#func-day>
  # - <https://www.w3.org/TR/xpath-functions/#func-day-from-dateTime>
  def call(:DAY, [%Literal{literal: %XSD.DateTime{} = literal}], _) do
    naive_datetime_part(literal, :day)
  end

  def call(:DAY, _, _), do: :error

  # Returns the hours part of the given datetime as an integer.
  # - <https://www.w3.org/TR/sparql11-query/#func-hours>
  # - <https://www.w3.org/TR/xpath-functions/#func-hours-from-dateTime>
  def call(:HOURS, [%Literal{literal: %XSD.DateTime{} = literal}], _) do
    naive_datetime_part(literal, :hour)
  end

  def call(:HOURS, _, _), do: :error

  # Returns the minutes part of the given datetime as an integer.
  # - <https://www.w3.org/TR/sparql11-query/#func-minutes>
  # - <https://www.w3.org/TR/xpath-functions/#func-minutes-from-dateTime>
  def call(:MINUTES, [%Literal{literal: %XSD.DateTime{} = literal}], _) do
    naive_datetime_part(literal, :minute)
  end

  def call(:MINUTES, _, _), do: :error

  # Returns the seconds part of the given datetime as a decimal.
  # - <https://www.w3.org/TR/sparql11-query/#func-seconds>
  # - <https://www.w3.org/TR/xpath-functions/#func-seconds-from-dateTime>
  def call(:SECONDS, [%Literal{literal: %XSD.DateTime{} = literal}], _) do
    if XSD.DateTime.valid?(literal) do
      case literal.value.microsecond do
        {_, 0} ->
          literal.value.second
          |> to_string() # This is needed to get the lexical integer form; required for the SPARQL 1.1 test suite
          |> XSD.decimal()

        {microsecond, _} ->
          %Decimal{coef: microsecond, exp: -6}
          |> Decimal.add(literal.value.second)
          |> XSD.decimal()

        _ ->
          :error
      end
    else
      :error
    end
  end

  def call(:SECONDS, _, _), do: :error

  # Returns the timezone part of the given datetime as an `xsd:dayTimeDuration` literal.
  #
  # Returns `:error` if there is no timezone.
  #
  # - <https://www.w3.org/TR/sparql11-query/#func-timezone>
  # - <http://www.w3.org/TR/xpath-functions/#func-timezone-from-dateTime>
  def call(:TIMEZONE, [%Literal{literal: %XSD.DateTime{} = literal}], _) do
    literal
    |> XSD.DateTime.tz()
    |> tz_duration()
    || :error
  end

  def call(:TIMEZONE, _, _), do: :error

  # Returns the timezone part of a given datetime as a simple literal or the empty string if there is no timezone.
  # - <https://www.w3.org/TR/sparql11-query/#func-tz>
  def call(:TZ, [%Literal{literal: %XSD.DateTime{} = literal}], _) do
    if tz = XSD.DateTime.tz(literal) do
      XSD.string(tz)
    else
      :error
    end
  end

  def call(:TZ, _, _), do: :error

  # Returns the MD5 checksum, as a hex digit string.
  # - <https://www.w3.org/TR/sparql11-query/#func-md5>
  def call(:MD5, [%Literal{literal: %XSD.String{}} = literal], _) do
    hash(:md5, Literal.value(literal))
  end

  def call(:MD5, _, _), do: :error

  # Returns the SHA1 checksum, as a hex digit string.
  # - <https://www.w3.org/TR/sparql11-query/#func-sha1>
  def call(:SHA1, [%Literal{literal: %XSD.String{}} = literal], _) do
    hash(:sha, Literal.value(literal))
  end

  def call(:SHA1, _, _), do: :error

  @doc """
  Returns the SHA256 checksum, as a hex digit string.

  - <https://www.w3.org/TR/sparql11-query/#func-sha256>
  """
  def call(:SHA256, [%Literal{literal: %XSD.String{}} = literal], _) do
    hash(:sha256, Literal.value(literal))
  end

  def call(:SHA256, _, _), do: :error

  # Returns the SHA384 checksum, as a hex digit string.
  # - <https://www.w3.org/TR/sparql11-query/#fun  c-sha384>
  def call(:SHA384, [%Literal{literal: %XSD.String{}} = literal], _) do
    hash(:sha384, Literal.value(literal))
  end

  def call(:SHA384, _, _), do: :error

  # Returns the SHA512 checksum, as a hex digit string.
  # - <https://www.w3.org/TR/sparql11-query/#func-sha512>
  def call(:SHA512, [%Literal{literal: %XSD.String{}} = literal], _) do
    hash(:sha512, Literal.value(literal))
  end

  def call(:SHA512, _, _), do: :error

  defp hash(type, value) do
    :crypto.hash(type, value)
    |> Base.encode16()
    |> String.downcase()
    |> XSD.string()
  end

  defp match_regex(%Literal{literal: %datatype{}} = text,
                   %Literal{literal: %XSD.String{}} = pattern,
                   %Literal{literal: %XSD.String{}} = flags)
       when datatype in [XSD.String, RDF.LangString] do
    text
    |> Literal.matches?(pattern, flags)
    |> ebv()
  rescue
    _error -> :error
  end

  defp match_regex(_, _, _), do: :error

  defp replace_regex(%Literal{literal: %datatype{}} = text,
                     %Literal{literal: %XSD.String{} = pattern},
                     %Literal{literal: %XSD.String{} = replacement},
                     %Literal{literal: %XSD.String{} = flags})
       when datatype in [XSD.String, RDF.LangString] do
    case XSD.Utils.Regex.xpath_pattern(pattern.value, flags.value) do
      {:regex, regex} ->
        Literal.update(text, fn text_value ->
          String.replace(text_value, regex, xpath_to_erlang_regex_variables(replacement.value))
        end)

      {:q, pattern} ->
        Literal.update(text, fn text_value ->
          String.replace(text_value, pattern, replacement.value)
        end)

      {:qi, _pattern} ->
        Logger.error "The combination of the q and the i flag is currently not supported in REPLACE"
        :error

      _ ->
        :error
    end
  end

  defp replace_regex(_, _, _, _), do: :error

  defp xpath_to_erlang_regex_variables(text) do
    String.replace(text, ~r/(?<!\\)\$/, "\\")
  end


  defp naive_datetime_part(%XSD.DateTime{value: %DateTime{} = datetime,
                                         uncanonical_lexical: nil}, field) do
    datetime
    |> Map.get(field)
    |> XSD.integer()
  end

  defp naive_datetime_part(%XSD.DateTime{value: %NaiveDateTime{} = datetime}, field) do
    datetime
    |> Map.get(field)
    |> XSD.integer()
  end

  defp naive_datetime_part(literal, field) do
    with {:ok, datetime} <-
           literal
           |> XSD.DateTime.lexical()
           |> NaiveDateTime.from_iso8601()
    do
      datetime
      |> Map.get(field)
      |> XSD.integer()
    else
      _ -> :error
    end
  end

  defp tz_duration(""),  do: nil
  defp tz_duration("Z"), do: day_time_duration("PT0S")
  defp tz_duration(tz) do
    [_, sign, hours, minutes] = Regex.run(~r/\A(?:([\+\-])(\d{2}):(\d{2}))\Z/, tz)
    sign = if sign == "-", do: "-", else: ""
    hours = String.trim_leading(hours, "0") <> "H"
    minutes = if minutes != "00", do: (minutes <> "M"), else: ""

    day_time_duration(sign <> "PT" <> hours <> minutes)
  end

  # TODO: This is just a preliminary implementation until we have a proper XSD.Duration datatype
  defp day_time_duration(value) do
    Literal.new(value, datatype: NS.XSD.dayTimeDuration)
  end

  @doc """
  Argument Compatibility Rules

  see <https://www.w3.org/TR/sparql11-query/#func-arg-compatibility>
  """
  def compatible_arguments?(arg1, arg2)

  # The arguments are simple literals or literals typed as xsd:string
  def compatible_arguments?(%Literal{literal: %XSD.String{}},
                            %Literal{literal: %XSD.String{}}), do: true
  # The first argument is a plain literal with language tag and the second argument is a simple literal or literal typed as xsd:string
  def compatible_arguments?(%Literal{literal: %RDF.LangString{}},
                            %Literal{literal: %XSD.String{}}), do: true
  # The arguments are plain literals with identical language tags
  def compatible_arguments?(%Literal{literal: %RDF.LangString{language: language}},
                            %Literal{literal: %RDF.LangString{language: language}}), do: true

  def compatible_arguments?(_, _), do: false


  defp ebv(value),    do: XSD.Boolean.ebv(value) || :error
  defp fn_not(value), do: XSD.Boolean.fn_not(value) || :error

  defp uuid(format), do: UUID.uuid4(format)

end