lib/islands/engine/game_server.ex

defmodule Islands.Engine.GameServer do
  @moduledoc """
  A server process that holds a game struct as its state.
  Times out after 30 minutes of inactivity.
  """

  use GenServer, restart: :transient
  use PersistConfig

  alias __MODULE__

  alias __MODULE__.{
    AddPlayer,
    GuessCoord,
    PositionAllIslands,
    PositionIsland,
    ReplyTuple,
    SetIslands,
    Stop
  }

  alias Islands.Engine.Log
  alias Islands.{Game, Player, Request}

  @ets get_env(:ets_name)
  # @reg get_env(:registry)
  @timeout :timer.minutes(30)
  # @wait 50

  @doc """
  Spawns a game server process to be registered via a game name.
  """
  @spec start_link({Game.name(), Player.name(), Player.gender(), pid}) ::
          GenServer.on_start()
  def start_link({game_name, player1_name, gender, pid} = _init_arg) do
    GenServer.start_link(
      GameServer,
      {game_name, player1_name, gender, pid},
      name: via(game_name)
    )
  end

  # @spec via(Game.name()) :: {:via, module, {atom, tuple}}
  # def via(game_name), do: {:via, Registry, {@reg, key(game_name)}}

  @doc """
  Allows to register or look up a game server process via `game_name`.
  """
  @spec via(Game.name()) :: {:global, tuple}
  def via(game_name), do: {:global, key(game_name)}

  @doc """
  Takes a backup of `game` and returns it.
  """
  @spec save(Game.t()) :: Game.t()
  def save(game) do
    :ok = Log.info(:save, {game, __ENV__})
    true = :ets.insert(@ets, {key(game.name), game})
    game
  end

  ## Private functions

  @spec key(Game.name()) :: tuple
  defp key(game_name), do: {GameServer, game_name}

  @spec game(Game.name(), Player.name(), Player.gender(), pid) :: Game.t()
  defp game(game_name, player1_name, gender, pid) do
    case :ets.lookup(@ets, key(game_name)) do
      [] ->
        :ok = Log.info(:spawned, {game_name, player1_name, __ENV__})
        Game.new(game_name, player1_name, gender, pid) |> save()

      [{_key, game}] ->
        :ok = Log.info(:restarted, {game_name, player1_name, __ENV__})
        game
    end
  end

  ## Callbacks

  @spec init({Game.name(), Player.name(), Player.gender(), pid}) ::
          {:ok, Game.t(), timeout}
  def init({game_name, player1_name, gender, pid} = _init_arg),
    do: {:ok, game(game_name, player1_name, gender, pid), @timeout}

  @spec handle_call(Request.t(), GenServer.from(), Game.t()) :: ReplyTuple.t()
  def handle_call({:add_player, _, _, _} = request, from, game),
    do: AddPlayer.handle_call(request, from, game)

  def handle_call({:position_island, _, _, _, _} = request, from, game),
    do: PositionIsland.handle_call(request, from, game)

  def handle_call({:position_all_islands, _} = request, from, game),
    do: PositionAllIslands.handle_call(request, from, game)

  def handle_call({:stop, _} = request, from, game),
    do: Stop.handle_call(request, from, game)

  def handle_call({:set_islands, _} = request, from, game),
    do: SetIslands.handle_call(request, from, game)

  def handle_call({:guess_coord, _, _, _} = request, from, game),
    do: GuessCoord.handle_call(request, from, game)

  def handle_call({:tally, player_id}, _from, game),
    do: ReplyTuple.new(game, player_id)

  @spec handle_info(term, Game.t()) ::
          {:stop, reason :: tuple, Game.t()} | {:noreply, Game.t()}
  def handle_info(:timeout, game) do
    :ok = Log.info(:timeout, {@timeout, game, __ENV__})
    {:stop, {:shutdown, :timeout}, game}
  end

  def handle_info(_message, game), do: {:noreply, game}

  @spec terminate(term, Game.t()) :: true
  def terminate(reason = :shutdown, game) do
    :ok = Log.info(:terminate, {reason, game, __ENV__})
    true = :ets.delete(@ets, key(game.name))
    # Ensure message logged before exiting...
    # Process.sleep(@wait)
  end

  def terminate(reason, game) do
    :ok = Log.error(:terminate, {reason, game, __ENV__})
    true = :ets.delete(@ets, key(game.name))
    # Ensure message logged before exiting...
    # Process.sleep(@wait)
  end
end