lib/sparql/algebra/expression/construct.ex

defmodule SPARQL.Algebra.Construct do
  defstruct [:template, :query]

  alias SPARQL.Algebra.Expression
  alias RDF.BlankNode

  def result(%SPARQL.Query.Result{results: results}, template, generator, prefixes) do
    template_bnodes = template_bnodes(template)
    prefixes = if Enum.empty?(prefixes), do: nil, else: prefixes
    Enum.reduce results, RDF.Graph.new(prefixes: prefixes), fn result, graph ->
      template_for_solution =
        template_bnodes
        |> create_solution_bnodes(generator)
        |> set_template_solution_bnodes(template)
      RDF.Graph.add(graph, solve_patterns(template_for_solution, result, generator))
    end
  end

  defp solve_patterns(template, solutions, generator) do
    template
    |> Stream.map(&(solve_pattern(&1, solutions, generator)))
    |> Enum.filter(&RDF.Triple.valid?/1)
  end

  defp solve_pattern({variable, predicate, object}, solutions, generator) when is_binary(variable) do
    if subject = solutions[variable] do
      {replace_solved_bnode(subject, solutions, generator), predicate, object}
      |> solve_pattern(solutions, generator)
    end
  end

  defp solve_pattern({subject, variable, object}, solutions, generator) when is_binary(variable) do
    if predicate = solutions[variable] do
      {subject, replace_solved_bnode(predicate, solutions, generator), object}
      |> solve_pattern(solutions, generator)
    end
  end

  defp solve_pattern({subject, predicate, variable}, solutions, generator) when is_binary(variable) do
    if object = solutions[variable] do
      {subject, predicate, replace_solved_bnode(object, solutions, generator)}
      |> solve_pattern(solutions, generator)
    end
  end

  defp solve_pattern(pattern, _, _), do: pattern


  defp template_bnodes(template) do
    Enum.reduce template, MapSet.new, fn pattern, bnodes ->
      MapSet.union(bnodes, pattern_bnodes(pattern))
    end
  end

  defp pattern_bnodes(pattern, acc \\ MapSet.new)
  defp pattern_bnodes({%BlankNode{} = bnode, p, o}, acc), do: pattern_bnodes({nil, p, o}, MapSet.put(acc, bnode))
  defp pattern_bnodes({s, %BlankNode{} = bnode, o}, acc), do: pattern_bnodes({s, nil, o}, MapSet.put(acc, bnode))
  defp pattern_bnodes({s, p, %BlankNode{} = bnode}, acc), do: pattern_bnodes({s, p, nil}, MapSet.put(acc, bnode))
  defp pattern_bnodes(_, acc),                            do: acc


  defp create_solution_bnodes(bnodes, generator) do
    Enum.reduce bnodes, %{}, fn bnode, solution_bnodes ->
      Map.put(solution_bnodes, bnode, BlankNode.Generator.generate(generator))
    end
  end


  defp set_template_solution_bnodes(bnodes, template) when map_size(bnodes) == 0, do: template

  defp set_template_solution_bnodes(bnodes, template) do
    Enum.map template, &(set_solution_bnodes(&1, bnodes))
  end

  defp set_solution_bnodes({s, p, o}, bnodes) do
    {set_solution_bnode(s, bnodes), set_solution_bnode(p, bnodes), set_solution_bnode(o, bnodes)}
  end

  defp set_solution_bnode(%BlankNode{} = bnode, solution_bnodes), do: solution_bnodes[bnode]
  defp set_solution_bnode(node, _), do: node

  defp replace_solved_bnode(%BlankNode{} = bnode, %{__id__: _solution_id}, generator) do
    BlankNode.Generator.generate_for(generator, {:construct, bnode})
  end

  defp replace_solved_bnode(node, _, _), do: node

  defimpl Expression do
    def evaluate(construct, data, execution) do
      Expression.evaluate(construct.query, data, execution)
      |> SPARQL.Algebra.Construct.result(construct.template, execution.bnode_generator, execution.prefixes)
    end

    def variables(construct) do
      Expression.variables(construct.query)
    end
  end
end