lib/solver/store/store.ex

defmodule CPSolver.ConstraintStore do
  @moduledoc """
  Constraint store is a key-value store, where `key` is a variable id,
  and `value` is a implementation-dependent structure that allows to
  update and keep track of variables' domains.
  """
  #################
  alias CPSolver.Common
  alias CPSolver.Variable

  @type get_operation :: Common.domain_get_operation() | nil
  @type update_operation :: Common.domain_update_operation()

  def default_store() do
    CPSolver.Store.Registry
  end

  ### Callbacks

  ## Tell basic constraints (a.k.a, domains) to a constraint store
  @callback create(variables :: Enum.t(), opts :: Keyword.t()) ::
              {:ok, any()} | {:error, any()}

  ## Get variable details
  @callback get(store :: any(), variable :: Variable.t(), get_operation(), [any()]) ::
              {:ok, any()} | {:error, any()}

  @callback update(store :: any(), variable :: Variable.t(), update_operation(), [any()]) ::
              any()

  @callback update_domain(store :: any(), variable :: Variable.t(), update_operation(), [any()]) ::
              any()

  @callback dispose(store :: any(), variables :: [Variable.t()]) :: :ok | :not_found

  @callback domain(store :: any(), variable :: Variable.t()) :: {:ok, any()} | {:error, any()}

  @callback on_fail(variable :: Variable.t()) :: any()

  @callback on_no_change(variable :: Variable.t()) :: any()

  @callback on_change(
              variable :: Variable.t(),
              change :: :fixed | :min_change | :max_change | :domain_change
            ) :: any()

  @callback get_variables(store :: any()) :: [any()]

  ### API
  defmacro __using__(_) do
    quote do
      @behaviour CPSolver.ConstraintStore
      @domain_changes CPSolver.Common.domain_changes()
      require Logger

      def update(store, variable, operation, args) do
        update_domain(store, variable, operation, args)
        |> tap(fn
          :fail -> on_fail(variable)
          :no_change -> on_no_change(variable)
          change when change in @domain_changes -> on_change(variable, change)
        end)
      end

      def on_change(var, domain_change) do
        publish(var, domain_change)
        |> tap(fn _ ->
          Logger.debug("Domain change (#{domain_change}) for #{inspect(var.id)}")
          maybe_unsubscribe_all(domain_change, var)
        end)
      end

      def on_fail(var) do
        Logger.debug("Failure for variable #{inspect(var.id)}")
        ## TODO: notify space (and maybe don't notify propagators)
        publish(var, :fail)
      end

      def on_no_change(_var) do
        :ok
      end

      defp publish(variable, event) do
        Variable.publish(variable, {event, variable.id})
      end

      defp maybe_unsubscribe_all(:fixed, var) do
        Enum.each(Variable.subscribers(var), fn pid -> Variable.unsubscribe(pid, var) end)
      end

      defp maybe_unsubscribe_all(_, _var) do
        :ok
      end

      defoverridable update: 4
      defoverridable on_change: 2
      defoverridable on_fail: 1
      defoverridable on_no_change: 1
    end
  end

  def create_store(store_impl, variables) do
    {:ok, store_instance} = store_impl.create(variables)

    {:ok,
     Enum.map(variables, fn var ->
       var
       |> Map.put(:id, var.id)
       |> Map.put(:name, var.name)
       |> Map.put(:store, store_instance)
     end), store_instance}
  end
end