lib/solver/core/shared.ex

defmodule CPSolver.Shared do
  def init_shared_data(solver_pid \\ self()) do
    %{
      caller: self(),
      sync_mode: false,
      solver_pid: solver_pid,
      statistics:
        :ets.new(__MODULE__, [:set, :public, read_concurrency: true, write_concurrency: false])
        |> tap(fn stats_ref -> :ets.insert(stats_ref, {:stats, 0, 0, 0, 0}) end),
      solutions:
        :ets.new(__MODULE__, [:set, :public, read_concurrency: true, write_concurrency: true]),
      active_nodes:
        :ets.new(__MODULE__, [:set, :public, read_concurrency: true, write_concurrency: false]),
      complete_flag: init_complete_flag()
    }
  end

  def complete?(%{complete_flag: complete_flag} = _solver) do
    :persistent_term.get(complete_flag, true)
  end

  def set_complete(%{complete_flag: complete_flag, caller: caller, sync_mode: sync?} = solver) do
    :persistent_term.put(complete_flag, true)
    |> tap(fn _ -> sync? && send(caller, {:solver_completed, complete_flag}) end)
    |> tap(fn _ -> CPSolver.stop_spaces(solver) end)
  end

  defp init_complete_flag() do
    make_ref()
    |> tap(fn ref -> :persistent_term.put(ref, false) end)
  end

  @active_node_count_pos 2
  @failure_count_pos 3
  @solution_count_pos 4
  @node_count_pos 5

  def add_active_spaces(
        %{statistics: stats_table, active_nodes: active_nodes_table} = _solver,
        spaces
      ) do
    try do
      incr = length(spaces)

      update_stats_counters(stats_table, [{@active_node_count_pos, incr}, {@node_count_pos, incr}])

      Enum.each(spaces, fn n -> :ets.insert(active_nodes_table, {n, n}) end)
    rescue
      _e -> :ok
    end
  end

  def remove_space(
        %{statistics: stats_table, active_nodes: active_nodes_table} = solver,
        space,
        reason
      ) do
    try do
      update_stats_counters(stats_table, [
        {@active_node_count_pos, -1} | update_stats_ops(reason)
      ])

      :ets.delete(active_nodes_table, space)
      ## The solving is done when there is no more active nodes
      :ets.info(active_nodes_table, :size) == 0 && set_complete(solver)
    rescue
      _e -> :ok
    end
  end

  def cleanup(%{solver_pid: solver_pid, complete_flag: complete_flag} = solver) do
    Enum.each([:solutions, :statistics, :active_nodes], fn item ->
      Map.get(solver, item) |> :ets.delete()
    end)

    Process.alive?(solver_pid) && GenServer.stop(solver_pid)
    :persistent_term.erase(complete_flag)
  end

  def stop_spaces(solver) do
    Enum.each(active_nodes(solver), fn space ->
      Process.alive?(space) && Process.exit(space, :normal)
    end)
  end

  defp update_stats_ops(:failure) do
    [{@failure_count_pos, 1}]
  end

  defp update_stats_ops(:solved) do
    []
  end

  defp update_stats_ops(_) do
    []
  end

  defp update_stats_counters(stats_table, update_ops) do
    :ets.update_counter(stats_table, :stats, update_ops)
  end

  def statistics(solver) do
    [{:stats, active_node_count, failure_count, solution_count, node_count}] =
      :ets.lookup(solver.statistics, :stats)

    %{
      active_node_count: active_node_count,
      failure_count: failure_count,
      solution_count: solution_count,
      node_count: node_count
    }
  end

  def solutions(%{solutions: solution_table} = _solver) do
    try do
      solution_table
      |> :ets.tab2list()
      |> Enum.map(fn {_ref, solution} ->
        Enum.map(solution, fn {_var, value} ->
          value
        end)
      end)
    rescue
      _e -> []
    end
  end

  def active_nodes(%{active_nodes: active_nodes_table} = _solver) do
    try do
      :ets.tab2list(active_nodes_table) |> Enum.map(fn {_k, n} -> n end)
    rescue
      _e -> []
    end
  end

  def add_failure(%{statistics: stats_table} = _solver) do
    update_stats_counters(stats_table, [{@failure_count_pos, 1}])
  end

  def add_solution(%{solutions: solution_table, statistics: stats_table} = _solver, solution) do
    try do
      update_stats_counters(stats_table, [{@solution_count_pos, 1}])
      :ets.insert(solution_table, {make_ref(), solution})
    rescue
      _e -> :ok
    end
  end
end