lib/sparql/algebra/expression/function_call/builtin.ex

defmodule SPARQL.Algebra.FunctionCall.Builtin do
  defstruct name: nil,
            arguments: []

  alias SPARQL.Algebra.FunctionCall
  alias SPARQL.Functions.Builtins

  alias RDF.XSD
  
  @xsd_true XSD.true
  @xsd_false XSD.false

  @doc """
  Invokes a SPARQL builtin function.

  For most functions this is done by delegating to `SPARQL.Functions.Builtins.call/2`.
  However, some functions have special "functional forms" which have different
  evaluation rules. All of these are implemented here directly.

  see <https://www.w3.org/TR/sparql11-query/#invocation>
  """
  def invoke(name, arguments, data, execution)

  def invoke(:&&, [left, right], data, execution) do
    case evaluate_to_ebv(left, data, execution) do
      @xsd_false ->
        @xsd_false

      @xsd_true ->
        case evaluate_to_ebv(right, data, execution) do
          @xsd_true  -> @xsd_true
          @xsd_false -> @xsd_false
          nil        -> :error
        end

      nil ->
        if match?(@xsd_false, evaluate_to_ebv(right, data, execution)) do
          @xsd_false
        else
          :error
        end
    end
  end

  def invoke(:||, [left, right], data, execution) do
    case evaluate_to_ebv(left, data, execution) do
      @xsd_true ->
        @xsd_true

      @xsd_false  ->
        case evaluate_to_ebv(right, data, execution) do
          @xsd_true  -> @xsd_true
          @xsd_false -> @xsd_false
          nil        -> :error
        end

      nil ->
        if match?(@xsd_true, evaluate_to_ebv(right, data, execution)) do
          @xsd_true
        else
          :error
        end
    end
  end

  def invoke(:BOUND, [variable], %{solution: solution}, _) when is_binary(variable) do
    if Map.has_key?(solution, variable) do
      @xsd_true
    else
      @xsd_false
    end
  end

  def invoke(:BOUND, _, _, _), do: :error

  def invoke(:IF, [cond_expression, then_expression, else_expression], data, execution) do
    case evaluate_to_ebv(cond_expression, data, execution) do
      @xsd_true  -> FunctionCall.evaluate_argument(then_expression, data, execution)
      @xsd_false -> FunctionCall.evaluate_argument(else_expression, data, execution)
      nil        -> :error
    end
  end

  def invoke(:COALESCE, expressions, data, execution) do
    expressions
    |> Stream.map(&(FunctionCall.evaluate_argument(&1, data, execution)))
    |> Enum.find(:error, &(&1 != :error))
  end

  def invoke(:IN, [lhs, expression_list], data, execution) do
    case FunctionCall.evaluate_argument(lhs, data, execution) do
      :error -> :error
      value ->
        expression_list
        |> Enum.reduce_while(@xsd_false, fn expression, acc ->
             case FunctionCall.evaluate_argument(expression, data, execution) do
               :error -> {:cont, :error}
               result ->
                 case RDF.Term.equal_value?(value, result) do
                   true  -> {:halt, @xsd_true}
                   false -> {:cont, acc}
                   _     -> {:cont, :error}
                 end
             end
           end)
    end
  end

  def invoke(:NOT_IN, [lhs, expression_list], data, execution) do
    case FunctionCall.evaluate_argument(lhs, data, execution) do
      :error -> :error
      value ->
        expression_list
        |> Enum.reduce_while(@xsd_true, fn expression, acc ->
             case FunctionCall.evaluate_argument(expression, data, execution) do
               :error -> {:cont, :error}
               result ->
                 case RDF.Term.equal_value?(value, result) do
                   true  -> {:halt, @xsd_false}
                   false -> {:cont, acc}
                   _     -> {:cont, :error}
                 end
             end
           end)
    end
  end

  def invoke(name, arguments, %{solution: solution} = data, execution) do
    with {:ok, evaluated_arguments} <-
            FunctionCall.evaluate_arguments(arguments, data, execution)
    do
      Builtins.call(name, evaluated_arguments,
        Map.put(execution, :solution_id, solution.__id__))
    end
  end


  defp evaluate_to_ebv(expr, data, execution) do
    expr
    |> FunctionCall.evaluate_argument(data, execution)
    |> XSD.Boolean.ebv()
  end


  defimpl SPARQL.Algebra.Expression do
    def evaluate(%FunctionCall.Builtin{name: name, arguments: arguments}, data, execution) do
      FunctionCall.Builtin.invoke(name, arguments, data, execution)
    end

    def variables(_function_call) do
      # TODO: return used and/or introduced variables???
    end
  end
end