lib/riot/lor/deck.ex

defmodule Riot.LoR.Deck do
  @moduledoc """
  Type and functions to represent a LoR deck, i.e., cards and their respective
  counts.
  """

  alias Riot.LoR.Card
  alias Riot.LoR.Faction

  @type t :: %{Card.t() => pos_integer}
  @type card_counts :: [{binary, pos_integer}]

  defguardp is_pos_integer(v) when is_integer(v) and v >= 1

  @doc """
  Create an empty `Riot.LoR.Deck`

  ## Examples

      iex> Riot.LoR.Deck.new()
      %{}

  """
  @doc since: "1.0.0"
  @spec new :: t()
  def new, do: %{}

  @doc """
  Populate a new `Riot.LoR.Deck` from a list of cards and their counts.

  ## Examples

      iex> cards = [{"01DE001", 3}, {"01DE002", 2}]
      iex> Riot.LoR.Deck.from_card_counts!(cards)
      %{
        %Riot.LoR.Card{fac: 0, num: 1, set: 1} => 3,
        %Riot.LoR.Card{fac: 0, num: 2, set: 1} => 2
      }

  """
  @doc since: "1.0.0"
  @spec from_card_counts!(card_counts) :: t()
  def from_card_counts!(card_counts) do
    # NOTE: intentionally doesn't use Enum.into/3 to utilize add_card/3
    Enum.reduce(card_counts, new(), fn {card_code, count}, deck ->
      add_card_code!(deck, card_code, count)
    end)
  end

  @doc """
  Add a `Riot.LoR.Card` count to a `Riot.LoR.Deck`. By default it will
  increment the count by 1. You can increment by any positive integer.

  ## Examples

      iex> deck = Riot.LoR.Deck.new()
      iex> deck = Riot.LoR.Deck.add_card(deck, %Riot.LoR.Card{set: 1, fac: 0, num: 1})
      %{%Riot.LoR.Card{fac: 0, num: 1, set: 1} => 1}
      iex> deck = Riot.LoR.Deck.add_card(deck, %Riot.LoR.Card{set: 1, fac: 0, num: 2}, 2)
      %{
        %Riot.LoR.Card{fac: 0, num: 1, set: 1} => 1,
        %Riot.LoR.Card{fac: 0, num: 2, set: 1} => 2
      }
      iex> Riot.LoR.Deck.add_card(deck, %Riot.LoR.Card{set: 1, fac: 0, num: 1}, 2)
      %{
        %Riot.LoR.Card{fac: 0, num: 1, set: 1} => 3,
        %Riot.LoR.Card{fac: 0, num: 2, set: 1} => 2
      }

  """
  @doc since: "1.0.0"
  @spec add_card(t(), Card.t(), pos_integer) :: t()
  def add_card(deck, %Card{} = card, count \\ 1) when is_pos_integer(count) do
    Map.update(deck, card, count, &(&1 + count))
  end

  @doc """
  Add a raw card code to a `Riot.LoR.Deck`. By default it will increment the
  count by 1. You can intrement by any positive integer.

  This is a convenience wrapper around `Riot.LoR.Deck.add_card/3`.

  ## Examples

      iex> deck = Riot.LoR.Deck.new()
      iex> deck = Riot.LoR.Deck.add_card_code!(deck, "01DE001")
      %{%Riot.LoR.Card{fac: 0, num: 1, set: 1} => 1}
      iex> Riot.LoR.Deck.add_card_code!(deck, "01DE001", 2)
      %{%Riot.LoR.Card{fac: 0, num: 1, set: 1} => 3}

  """
  @doc since: "1.0.0"
  @spec add_card_code!(t(), binary, pos_integer) :: t()
  def add_card_code!(deck, card_code, count \\ 1) when is_pos_integer(count) do
    card = Card.parse!(card_code)
    add_card(deck, card, count)
  end

  @doc """
  Remove a `Riot.LoR.Card` from a `Riot.LoR.Deck`. By default it will decrement
  the count by 1. You can can specify any positive integer to decrement the
  count by.

  If you pass `:all` or an integer greater than or equal to the current count
  the `Riot.LoR.Card` will be removed from the `Riot.LoR.Deck`.

  ## Examples

      iex> cards = [{"01DE001", 3}, {"01DE002", 2}, {"01DE003", 1}]
      iex> deck = Riot.LoR.Deck.from_card_counts!(cards)
      %{
        %Riot.LoR.Card{fac: 0, num: 1, set: 1} => 3,
        %Riot.LoR.Card{fac: 0, num: 2, set: 1} => 2,
        %Riot.LoR.Card{fac: 0, num: 3, set: 1} => 1
      }
      iex> deck = Riot.LoR.Deck.remove_card(deck, %Riot.LoR.Card{set: 1, fac: 0, num: 3})
      %{
        %Riot.LoR.Card{fac: 0, num: 1, set: 1} => 3,
        %Riot.LoR.Card{fac: 0, num: 2, set: 1} => 2
      }
      iex> deck = Riot.LoR.Deck.remove_card(deck, %Riot.LoR.Card{set: 1, fac: 0, num: 2}, 2)
      %{%Riot.LoR.Card{fac: 0, num: 1, set: 1} => 3}
      iex> deck = Riot.LoR.Deck.remove_card(deck, %Riot.LoR.Card{set: 1, fac: 0, num: 1})
      %{%Riot.LoR.Card{fac: 0, num: 1, set: 1} => 2}
      iex> Riot.LoR.Deck.remove_card(deck, %Riot.LoR.Card{set: 1, fac: 0, num: 1}, :all)
      %{}

  """
  @doc since: "1.0.0"
  @spec remove_card(t(), Card.t(), pos_integer | :all) :: t()
  def remove_card(deck, card, count \\ 1)

  def remove_card(deck, %Card{} = card, :all) do
    {_, deck} = Map.pop(deck, card)
    deck
  end

  def remove_card(deck, %Card{} = card, count) when is_pos_integer(count) do
    {_, deck} =
      Map.get_and_update(deck, card, fn
        nil -> :pop
        quantity when quantity <= count -> :pop
        quantity -> {quantity, quantity - count}
      end)

    deck
  end

  @doc """
  Returns the minimum version required to support the cards in deck.
  If no minimum is found (e.g., the deck is empty), return the smallest version
  supported by the library.

  ## Examples

      iex> cards = [{"03MT054", 1}, {"05BC198", 1}]
      iex> deck = Riot.LoR.Deck.from_card_counts!(cards)
      iex> Riot.LoR.Deck.min_required_version(deck)
      4

  """
  @doc since: "1.0.0"
  @spec min_required_version(t()) :: pos_integer
  def min_required_version(deck) when is_map(deck) do
    deck
    |> Map.keys()
    |> Enum.map(fn %Card{fac: f} ->
      {ver, _id, _code} = Faction.fetch_by_id!(f)
      ver
    end)
    |> Enum.max(&Faction.min_version/0)
  end

  @doc """
  Return a list of card codes and their counts.

  ## Examples

      iex> deck = Riot.LoR.Deck.new()
      %{}
      iex> deck = Riot.LoR.Deck.add_card(deck, %Riot.LoR.Card{set: 1, fac: 0, num: 1}, 3)
      %{%Riot.LoR.Card{fac: 0, num: 1, set: 1} => 3}
      iex> Riot.LoR.Deck.code_count(deck)
      [{"01DE001", 3}]

  """
  @doc since: "1.0.0"
  @spec code_count(t()) :: card_counts()
  def code_count(deck) when is_map(deck) do
    deck
    |> Enum.map(fn {card, count} ->
      {Card.to_code!(card), count}
    end)
    |> Enum.sort()
  end
end