lib/solver/search/strategy/partition.ex

defmodule CPSolver.Search.Partition do
  alias CPSolver.Variable.Interface

  alias CPSolver.Search.ValueSelector.{Min, Max, Random, Split}

  require Logger

  def initialize(partition, _space_data) do
    ## TODO:
    # strategy(partition).initialize(space_data)
    partition
  end

  def partition(variable, value_choice) do
    {:ok, partition_impl(variable, value_choice)}
  end

  defp partition_impl(variable, value) when is_integer(value) do
    partition_by_fix(variable, value)
  end

  defp partition_impl(variable, value_choice) when is_function(value_choice) do
    partition_impl(variable, value_choice.(variable))
  end

  defp partition_impl(variable, value_choice) when is_atom(value_choice) do
    impl = strategy(value_choice)

    selected_value = impl.select_value(variable)

    case impl.partition(selected_value) do
      reduction when is_function(reduction, 1) ->
        reduction.(variable)

      functions when is_list(functions) ->
        functions
    end
    |> Enum.map(fn
      reduction when is_map(reduction) ->
        reduction

      reduction when is_function(reduction, 1) ->
        %{variable.id => reduction}
    end)
  end

  defp strategy(:indomain_min) do
    Min
  end

  defp strategy(:indomain_max) do
    Max
  end

  defp strategy(:indomain_random) do
    Random
  end

  defp strategy(:indomain_split) do
    Split
  end

  defp strategy(impl) when is_atom(impl) do
    if Code.ensure_loaded(impl) == {:module, impl} && function_exported?(impl, :select, 2) do
      impl
    else
      throw({:unknown_strategy, impl})
    end
  end

  ## Default partitioning
  def partition_by_fix(variable, value) when is_integer(value) do
    [
      # Equal.new(variable, value)
      fixed_value_partition(variable, value),
      # NotEqual.new(variable, value)
      removed_value_partition(variable, value)
    ]
  end

  def fixed_value_partition(variable, value) do
    new(
      variable,
      fn variable -> Interface.fix(variable, value) end
    )
  end

  def removed_value_partition(variable, value) do
    new(
      variable,
      fn variable -> Interface.remove(variable, value) end
    )
  end

  ## Here we build a reduction that removes multiple values
  ## from the domain of variable.
  def remove_multiple_values_partition(variable, values) do
    new(
      variable,
      fn variable ->
        Enum.reduce(values, Map.new(), fn val, acc ->
          if Interface.contains?(variable, val) do
            Map.put(acc, variable.id, Interface.remove(variable, val))
          else
            acc
          end
        end)
      end
    )
  end

  def new(variable, reduction) when is_function(reduction, 1) do
    %{variable.id => reduction}
  end
end