lib/solver/core/propagator/constraint_graph.ex

defmodule CPSolver.Propagator.ConstraintGraph do
  @moduledoc """
  The constraint graph connects propagators and their variables.
  The edge between a propagator and a variable represents a notification
  the propagator receives upon varable's domain change.
  """
  alias CPSolver.Propagator
  alias CPSolver.Propagator.Variable
  alias CPSolver.Variable.Interface

  @spec create([Propagator.t()] | %{reference() => Propagator.t()}) :: Graph.t()
  def create(propagators) when is_list(propagators) do
    propagators
    |> Enum.map(fn p -> {p.id, p} end)
    |> Map.new()
    |> create()
  end

  def create(propagators) when is_map(propagators) do
    Enum.reduce(propagators, Graph.new(), fn {propagator_id,
                                              %{mod: propagator_mod, args: args} = p},
                                             acc ->
      args
      |> propagator_mod.variables()
      |> Enum.reduce(acc, fn var, acc2 ->
        Graph.add_vertex(acc2, propagator_vertex(propagator_id))
        |> Graph.add_edge({:variable, Interface.id(var)}, propagator_vertex(propagator_id),
          label: get_propagate_on(var)
        )
      end)
      |> Graph.label_vertex(propagator_vertex(propagator_id), p)
    end)
  end

  ## Get a list of propagator ids that "listen" to the domain change of given variable.
  def get_propagators(
        constraint_graph,
        variable_id,
        domain_change
      ) do
    constraint_graph
    |> Graph.edges({:variable, variable_id})
    |> Enum.flat_map(fn edge ->
      if domain_change in edge.label do
        {:propagator, p_id} = edge.v2
        [p_id]
      else
        []
      end
    end)
  end

  def has_variable?(graph, variable_id) do
    Graph.has_vertex?(graph, {:variable, variable_id})
  end

  def get_propagator(%Graph{} = graph, propagator_id) do
    case Graph.vertex_labels(graph, propagator_vertex(propagator_id)) do
      [] -> nil
      [p] -> p
    end
  end

  def propagator_vertex(propagator_id) do
    {:propagator, propagator_id}
  end

  def remove_propagator(graph, propagator_id) do
    remove_vertex(graph, propagator_vertex(propagator_id))
  end

  ## Remove variable and all propagators that are isolated points as a result of variable removal
  def remove_variable(graph, variable_id) do
    var_vertex = {:variable, variable_id}
    var_propagators = Graph.neighbors(graph, var_vertex)

    graph
    |> remove_vertex(var_vertex)
    |> then(fn g ->
      Enum.reduce(var_propagators, g, fn p_vertex, acc ->
        fix_propagator_variable(acc, p_vertex, variable_id)
      end)
    end)
  end

  def remove_fixed(graph, vars) do
    Enum.reduce(vars, graph, fn v, acc ->
      if Variable.fixed?(v) do
        remove_variable(acc, v.id)
      else
        acc
      end
    end)
  end

  def remove_vertex(graph, vertex) do
    Graph.delete_vertex(graph, vertex)
  end

  defp get_propagate_on(variable) do
    Map.get(variable, :propagate_on, Propagator.to_domain_events(:fixed))
  end

  defp fix_propagator_variable(graph, {:propagator, p_id} = p_vertex, variable_id) do
    graph
    |> get_propagator(p_id)
    |> Map.update(:args, %{}, fn args ->
      Enum.map(
        args,
        fn
          %{id: id} = arg when id == variable_id ->
            Map.put(arg, :fixed?, true)

          other ->
            other
        end
      )
    end)
    |> then(fn updated_propagator ->
      graph
      |> Graph.remove_vertex_labels(p_vertex)
      |> Graph.label_vertex(p_vertex, updated_propagator)
    end)
  end

  defp fix_propagator_variable(graph, p_id, variable_id) when is_reference(p_id) do
    fix_propagator_variable(graph, {:propagator, p_id}, variable_id)
  end
end