defmodule Hologram.Commons.ETS do
  @moduledoc false

  # This helps to avoid Dialyzer warnings related to :ets.tid() type opaqueness.
  @type tid :: :ets.tid() | atom

  @doc """
  Creates a named, public ETS table.
  """
  @spec create_named_table(atom) :: tid
  def create_named_table(table_name) do
    :ets.new(table_name, [
      :named_table,
      :public,
      {:read_concurrency, true},
      {:write_concurrency, true}
    ])

    :ets.whereis(table_name)
  end

  @doc """
  Creates an unnamed, public ETS table.
  """
  @spec create_unnamed_table() :: tid
  def create_unnamed_table do
    :ets.new(__MODULE__, [:public, {:read_concurrency, true}, {:write_concurrency, true}])
  end

  @doc """
  Deletes the ETS table if it exists.
  """
  @spec delete(tid) :: boolean
  def delete(table_name_or_ref) do
    delete!(table_name_or_ref)
  rescue
    ArgumentError -> false
  end

  @doc """
  Deletes the ETS table if it exists or raises ArgumentError otherwise.
  """
  @spec delete!(tid) :: true
  def delete!(table_name_or_ref) do
    :ets.delete(table_name_or_ref)
  end

  @doc """
  Deletes a key-value pair from the ETS table.
  """
  @spec delete(tid, any) :: true
  def delete(table_name_or_ref, key) do
    :ets.delete(table_name_or_ref, key)
  end

  @doc """
  Returns the value stored in the ETS table under the given key.
  If the key doesn't exist the :error :atom is returned.
  """
  @spec get(tid, any) :: {:ok, term} | :error
  def get(table_name_or_ref, key) do
    case :ets.lookup(table_name_or_ref, key) do
      [{^key, value}] ->
        {:ok, value}

      _fallback ->
        :error
    end
  end

  @doc """
  Returns the value stored in the ETS table under the given key.
  If the key doesn't exist a KeyError is raised.
  """
  @spec get!(tid, any) :: term
  def get!(table_name_or_ref, key) do
    case get(table_name_or_ref, key) do
      {:ok, value} -> value
      _fallback -> raise KeyError, message: "key #{inspect(key)} not found in the ETS table"
    end
  end

  @doc """
  Returns all items stored in the ETS table.
  """
  @spec get_all(tid) :: map
  def get_all(table_name_or_ref) do
    table_name_or_ref
    |> :ets.tab2list()
    |> Enum.into(%{})
  end

  @doc """
  Puts multiple items into the ETS table.
  """
  @spec put(tid, list({any, any})) :: true
  def put(table_name_or_ref, items) do
    :ets.insert(table_name_or_ref, items)
  end

  @doc """
  Puts an item into the ETS table.
  """
  @spec put(tid, any, any) :: true
  def put(table_name_or_ref, key, value) do
    :ets.insert(table_name_or_ref, {key, value})
  end

  @doc """
  Removes all items from the ETS table.
  """
  @spec reset(tid) :: true
  def reset(table_name_or_ref) do
    :ets.delete_all_objects(table_name_or_ref)
  end

  @doc """
  Determines whether the given ETS table exists.
  """
  @spec table_exists?(tid()) :: boolean
  def table_exists?(table_ref_or_name)
      when is_atom(table_ref_or_name) or is_reference(table_ref_or_name) do
    table_ref_or_name
    |> :ets.info()
    |> is_list()
  rescue
    ArgumentError -> false
  end
end
