lib/solver/objective/objective.ex

defmodule CPSolver.Objective do
  alias CPSolver.Objective.Propagator, as: ObjectivePropagator
  alias CPSolver.Variable.Interface
  import CPSolver.Variable.View.Factory
  import CPSolver.Utils

  @spec minimize(Variable.t() | View.t()) :: %{
          :propagator => Propagator.t(),
          :variable => Variable.t() | View.t(),
          :bound_handle => reference()
        }

  def minimize(variable) do
    bound_handle = init_bound_handle()
    propagator = ObjectivePropagator.new(variable, bound_handle)

    %{
      propagator: propagator,
      variable: variable,
      bound_handle: bound_handle,
      target: :minimize
    }
  end

  def maximize(variable) do
    minimize(minus(variable))
    |> Map.put(:target, :maximize)
  end

  def get_bound(%{bound_handle: handle}) do
    get_bound(handle)
  end

  def get_bound(handle) when is_reference(handle) do
    (on_primary_node?(handle) && get_bound_impl(handle)) || remote_call(handle, :get_bound_impl)
  end

  def get_bound_impl(handle) when is_reference(handle) do
    :atomics.get(handle, 1)
  end

  def update_bound(handle, value) when is_reference(handle) do
    (on_primary_node?(handle) && update_bound_impl(handle, value)) ||
      remote_call(handle, :update_bound_impl, [value])
  end

  def update_bound_impl(bound_handle, value) do
    case :atomics.get(bound_handle, 1) do
      prev_value when prev_value > value ->
        case :atomics.compare_exchange(bound_handle, 1, prev_value, value) do
          :ok ->
            value

          _changed_in_between ->
            update_bound_impl(bound_handle, value)
        end

      prev_value ->
        # previous value lesser than the proposed one - keep
        prev_value
    end
  end

  def tighten(%{variable: variable, bound_handle: handle} = _objective) do
    tighten(variable, handle)
  end

  def tighten(variable, bound_handle) do
    update_bound(bound_handle, Interface.max(variable) - 1)
  end

  def init_bound_handle() do
    ref = :atomics.new(1, signed: true)
    reset_bound(ref)
    ref
  end

  def reset_bound(%{bound_handle: ref} = _objective) do
    reset_bound(ref)
  end

  def reset_bound(handle) when is_reference(handle) do
    (on_primary_node?(handle) && reset_bound_impl(handle)) ||
      remote_call(handle, :reset_bound_impl)
  end

  def reset_bound_impl(ref) when is_reference(ref) do
    :atomics.put(ref, 1, :atomics.info(ref).max)
  end

  def get_objective_value(%{target: target, bound_handle: handle} = _objective) do
    (get_bound(handle) + 1) * ((target == :minimize && 1) || -1)
  end

  defp remote_call(ref, fun_name, args \\ []) when is_reference(ref) and is_atom(fun_name) do
    :erpc.call(node(ref), __MODULE__, fun_name, [ref | args])
  end
end