lib/solver/store/ets_store.ex

defmodule CPSolver.Store.ETS do
  alias CPSolver.DefaultDomain, as: Domain

  use CPSolver.ConstraintStore
  alias CPSolver.ConstraintStore

  @impl true
  def create(variables, _opts \\ []) do
    table_id =
      :ets.new(__MODULE__, [:set, :public, read_concurrency: true, write_concurrency: true])

    Enum.each(
      variables,
      fn var ->
        :ets.insert(
          table_id,
          {var.id, %{id: var.id, domain: Domain.new(var.domain), subscriptions: []}}
        )
      end
    )

    {:ok, table_id}
  end

  @impl true
  def dispose(_store, _vars) do
    :ok
  end

  @impl true
  def get_variables(store) do
    :ets.tab2list(store) |> Enum.map(fn {_id, var} -> var.id end)
  end

  @impl true
  def get(store, variable, operation, args \\ []) do
    handle_request(:get, store, variable, operation, args)
  end

  @impl true
  def update_domain(store, variable, operation, args) do
    handle_request(:update, store, variable, operation, args)
  end

  @impl true
  def on_change(_store, _variable, _change) do
    :ok
  end

  @impl true
  def on_no_change(_store, _variable) do
    :ok
  end

  @impl true
  def on_fail(_store, _variable) do
    :ok
  end

  @impl true
  def subscribe(store, subscriptions) do
    subscriptions_by_var =
      subscriptions
      |> Enum.map(&ConstraintStore.normalize_subscription/1)
      |> Enum.group_by(fn s -> s.variable end)

    Enum.each(subscriptions_by_var, fn {var_id, subscrs} ->
      var_rec = lookup(store, var_id)

      updated_rec = %{
        var_rec
        | subscriptions: (var_rec.subscriptions ++ subscrs) |> Enum.uniq_by(fn rec -> rec.pid end)
      }

      true = :ets.insert(store, {var_id, updated_rec})
    end)
  end

  defp update_variable_domain(
         store,
         %{id: var_id, subscriptions: subscriptions} = variable,
         domain,
         event
       ) do
    :ets.insert(store, {variable.id, Map.put(variable, :domain, domain)})
    |> tap(fn _ -> ConstraintStore.notify_subscribers(var_id, event, subscriptions) end)
  end

  @impl true
  def domain(store, variable) do
    store
    |> lookup(variable)
    |> Map.get(:domain)
  end

  def lookup(store, %{id: var_id} = _variable) do
    lookup(store, var_id)
  end

  def lookup(store, var_id) do
    store
    |> :ets.lookup(var_id)
    |> hd
    |> elem(1)
  end

  defp handle_request(kind, store, var_id, operation, args) do
    variable = lookup(store, var_id)
    handle_request_impl(kind, store, variable, operation, args)
  end

  def handle_request_impl(_kind, _store, %{domain: :fail} = _variable, _operation, _args) do
    :fail
  end

  def handle_request_impl(:get, _store, %{domain: domain} = _variable, operation, args) do
    apply(Domain, operation, [domain | args])
  end

  def handle_request_impl(:update, store, %{domain: domain} = variable, operation, args) do
    case apply(Domain, operation, [domain | args]) do
      :fail ->
        :fail
        |> tap(fn _ -> update_variable_domain(store, variable, :fail, :fail) end)

      :no_change ->
        :no_change

      {domain_change, new_domain} ->
        domain_change
        |> tap(fn _ -> update_variable_domain(store, variable, new_domain, domain_change) end)
    end
  end
end