Skip to main content

lib/chat_code/item.ex

defmodule GW2.ChatCode.Item do
  @moduledoc ~S"""
  Represents a Guild Wars 2 item chat code.

  Item chat codes are used by the game to link items in chat. An item link
  always contains an item id and quantity, and may also include a skin id and
  up to two upgrade ids.

  Use this struct with `GW2.ChatCode.encode/1` and `GW2.ChatCode.decode/1`.

  ## Examples

      iex> GW2.ChatCode.decode("[&AgH2twBA8F8AAA==]")
      {:ok, %GW2.ChatCode.Item{id: 47094, quantity: 1, skin_id: nil, upgrade_ids: [24560]}}

      iex> GW2.ChatCode.encode(%GW2.ChatCode.Item{id: 30691, skin_id: 13826, upgrade_ids: [91439, 91607]})
      {:ok, "[&AgHjdwDgAjYAAC9lAQDXZQEA]"}

  ## Fields

    * `:id` - the Guild Wars 2 item id.
    * `:quantity` - the linked item quantity. It must fit in one byte.
    * `:skin_id` - an optional skin id applied to the item.
    * `:upgrade_ids` - optional upgrade component ids. Only the first two
      upgrade ids are encoded, matching the chat code format.

  """
  import GW2.ChatCode.Encoder

  alias GW2.ChatCode.Flag
  alias GW2.ChatCode.Header

  @behaviour GW2.ChatCode.Encoder

  @flags %{
    skin_id: 0x80,
    first_upgrade_id: 0x40,
    second_upgrade_id: 0x20
  }

  @typedoc "An item link decoded from, or ready to encode as, a chat code."
  @type t :: %__MODULE__{
          id: non_neg_integer(),
          quantity: non_neg_integer(),
          skin_id: non_neg_integer() | nil,
          upgrade_ids: [non_neg_integer()]
        }

  defstruct [
    :id,
    quantity: 1,
    skin_id: nil,
    upgrade_ids: []
  ]

  ## Encoding

  @doc """
  Encodes an item struct into the binary payload used inside a chat code.

  Most applications should call `GW2.ChatCode.encode/1` instead, which also
  adds the chat code wrapper and Base64-encodes the payload.
  """
  @spec encode(t()) :: {:ok, binary()} | {:error, atom()}
  def encode(%__MODULE__{} = item) do
    with {:ok, header} <- Header.encode_type(:item),
         {:ok, quantity} <- encode_quantity(item),
         {:ok, id} <- encode_id(item.id),
         {:ok, flags} <- encode_flags(item),
         {:ok, payload} <- encode_payload(item) do
      {:ok, header <> quantity <> id <> flags <> payload}
    end
  end

  @spec encode_quantity(t()) :: {:ok, binary()} | {:error, :quantity_out_of_range}
  defp encode_quantity(%__MODULE__{quantity: quantity}) when quantity in 0..255 do
    {:ok, <<quantity::size(8)>>}
  end

  defp encode_quantity(%__MODULE__{}), do: {:error, :quantity_out_of_range}

  @spec encode_flags(t()) :: {:ok, binary()}
  defp encode_flags(%__MODULE__{skin_id: skin_id, upgrade_ids: upgrade_ids}) do
    flag =
      Flag.new()
      |> Flag.maybe_set(@flags.skin_id, skin_id != nil)
      |> Flag.maybe_set(@flags.first_upgrade_id, Enum.at(upgrade_ids, 0) != nil)
      |> Flag.maybe_set(@flags.second_upgrade_id, Enum.at(upgrade_ids, 1) != nil)

    {:ok, <<flag::size(8)>>}
  end

  @spec encode_payload(t()) :: {:ok, binary()} | {:error, atom()}
  defp encode_payload(%__MODULE__{skin_id: skin_id, upgrade_ids: upgrade_ids}) do
    [skin_id | Enum.take(upgrade_ids, 2)]
    |> Enum.filter(&(&1 != nil))
    |> Enum.map(&encode_id/1)
    |> Enum.reduce({:ok, <<>>}, fn
      {:ok, id}, {:ok, acc} ->
        # The spec requires a 0 byte after each payload
        {:ok, acc <> id <> <<0::size(8)>>}

      _, {:error, _} = error ->
        error

      {:error, _} = error, _ ->
        error
    end)
  end

  ## Decoding

  @doc """
  Decodes an item chat code payload into an item struct.

  Most applications should call `GW2.ChatCode.decode/1` instead, which accepts
  the full chat code string.
  """
  @spec decode(binary()) :: {:ok, t()} | {:error, atom()}
  def decode(<<quantity, id::little-24, flags, data::binary>>) do
    %__MODULE__{id: id, quantity: quantity}
    |> decode_payload(flags, data)
  end

  def decode(_), do: {:error, :invalid_code}

  @spec decode_payload(t(), non_neg_integer(), binary()) :: {:ok, t()} | {:error, atom()}
  defp decode_payload(%__MODULE__{} = item, flags, data) do
    [
      {@flags.skin_id, &decode_skin_id/2},
      {@flags.first_upgrade_id, &parse_upgrade_id/2},
      {@flags.second_upgrade_id, &parse_upgrade_id/2}
    ]
    |> Enum.filter(&Flag.check?(flags, elem(&1, 0)))
    |> Enum.reduce({:ok, item, data}, fn
      {_flag, decoder}, {:ok, acc, data} ->
        decoder.(acc, data)

      _, {:error, _} = error ->
        error
    end)
    |> case do
      # For item decoding we ignore any rest data as it's not relevant
      {:ok, acc, _} -> {:ok, acc}
      error -> error
    end
  end

  defp decode_skin_id(%__MODULE__{} = item, data) do
    case decode_id(data) do
      {:ok, {id, rest}} -> {:ok, %__MODULE__{item | skin_id: id}, rest}
      {:error, _} = error -> error
    end
  end

  defp parse_upgrade_id(%__MODULE__{upgrade_ids: upgrade_ids} = item, data) do
    case decode_id(data) do
      {:ok, {id, rest}} ->
        {:ok, %__MODULE__{item | upgrade_ids: [id | upgrade_ids] |> Enum.reverse()}, rest}

      {:error, _} = error ->
        error
    end
  end
end