if Code.ensure_loaded?(:mnesia) do
  defmodule Nostrum.Cache.GuildCache.Mnesia do
    @moduledoc """
    An Mnesia-based cache for guilds.

    #{Nostrum.Cache.Base.mnesia_note()}
    """
    @moduledoc since: "0.8.0"

    @base_table_name :nostrum_guilds

    @behaviour Nostrum.Cache.GuildCache

    alias Nostrum.Bot
    alias Nostrum.Cache.GuildCache
    alias Nostrum.Struct.Channel
    alias Nostrum.Struct.Emoji
    alias Nostrum.Struct.Guild
    alias Nostrum.Struct.Guild.Role
    alias Nostrum.Struct.Sticker
    alias Nostrum.Util
    use Supervisor

    @doc "Start the supervisor."
    def start_link(opts) do
      Supervisor.start_link(__MODULE__, opts)
    end

    @impl Supervisor
    @doc "Set up the cache's Mnesia table."
    def init(opts) do
      table_name = :"#{@base_table_name}_#{Keyword.fetch!(opts, :name)}"

      table_options = [
        attributes: [:id, :data],
        record_name: record_name()
      ]

      case :mnesia.create_table(table_name, table_options) do
        {:atomic, :ok} -> :ok
        {:aborted, {:already_exists, _tab}} -> :ok
      end

      Supervisor.init([], strategy: :one_for_one)
    end

    @doc "Retrieve the Mnesia table name used for the cache."
    @spec table :: atom()
    def table, do: :"#{@base_table_name}_#{Bot.fetch_bot_name()}"

    defp record_name, do: :nostrum_guild

    @doc "Drop the table used for caching."
    @spec teardown() :: {:atomic, :ok} | {:aborted, term()}
    def teardown, do: :mnesia.delete_table(table())

    @doc "Clear any objects in the cache."
    @spec clear() :: :ok
    def clear do
      {:atomic, :ok} = :mnesia.clear_table(table())
      :ok
    end

    @impl GuildCache
    @doc "Get a guild from the cache."
    @spec get(Guild.id()) :: {:ok, Guild.t()} | {:error, :not_found}
    def get(guild_id) do
      :mnesia.activity(:sync_transaction, fn ->
        case :mnesia.read(table(), guild_id) do
          [{_tag, _id, guild}] -> {:ok, guild}
          [] -> {:error, :not_found}
        end
      end)
    end

    @impl GuildCache
    @doc since: "0.10.0"
    @spec all() :: Enumerable.t(Guild.t())
    def all do
      ms = [{{:_, :_, :"$1"}, [], [:"$1"]}]

      Stream.resource(
        fn -> :mnesia.select(table(), ms, 100, :read) end,
        fn items ->
          case items do
            {matches, cont} ->
              {matches, :mnesia.select(cont)}

            :"$end_of_table" ->
              {:halt, nil}
          end
        end,
        fn _cont -> :ok end
      )
    end

    # Used by dispatch

    @impl GuildCache
    @doc "Create a guild from upstream data."
    @spec create(map()) :: Guild.t()
    def create(payload) do
      guild = Guild.to_struct(payload)
      record = {record_name(), guild.id, guild}
      writer = fn -> :mnesia.write(table(), record, :write) end
      :ok = :mnesia.activity(:sync_transaction, writer)
      guild
    end

    @impl GuildCache
    @doc "Update the given guild in the cache."
    @spec update(map()) :: {Guild.t() | nil, Guild.t()}
    def update(payload) do
      new_guild = Guild.to_struct(payload)

      old_guild =
        :mnesia.activity(:sync_transaction, fn ->
          case :mnesia.read(table(), new_guild.id, :write) do
            [{_tag, _id, old_guild} = entry] ->
              updated_guild = Guild.merge(old_guild, new_guild)

              :mnesia.write(table(), put_elem(entry, 2, updated_guild), :write)
              old_guild

            [] ->
              nil
          end
        end)

      {old_guild, new_guild}
    end

    @impl GuildCache
    @doc "Remove the given guild from the cache."
    @spec delete(Guild.id()) :: Guild.t() | nil
    def delete(guild_id) do
      :mnesia.activity(:sync_transaction, fn ->
        case :mnesia.read(table(), guild_id, :write) do
          [{_tag, _guild_id, guild}] ->
            :mnesia.delete(table(), guild_id, :write)
            guild

          _ ->
            nil
        end
      end)
    end

    @impl GuildCache
    @doc "Create the given channel for the given guild in the cache."
    @spec channel_create(Guild.id(), map()) :: Channel.t()
    def channel_create(guild_id, channel) do
      new_channel = Channel.to_struct(channel)

      update_guild!(guild_id, fn guild ->
        new_channels = Map.put(guild.channels, channel.id, new_channel)
        {%{guild | channels: new_channels}, :ok}
      end)

      new_channel
    end

    @impl GuildCache
    @doc "Delete the channel from the given guild in the cache."
    @spec channel_delete(Guild.id(), Channel.id()) :: Channel.t() | :noop
    def channel_delete(guild_id, channel_id) do
      old_channel =
        update_guild!(guild_id, fn guild ->
          {popped, new_channels} = Map.pop(guild.channels, channel_id)
          {%{guild | channels: new_channels}, popped}
        end)

      if old_channel, do: old_channel, else: :noop
    end

    @doc "Update the channel on the given guild in the cache."
    @impl GuildCache
    @spec channel_update(Guild.id(), map()) :: {Channel.t() | nil, Channel.t()}
    def channel_update(guild_id, channel) do
      update_guild!(guild_id, fn guild ->
        {old, new, new_channels} =
          GuildCache.Base.upsert(guild.channels, channel.id, channel, Channel)

        {%{guild | channels: new_channels}, {old, new}}
      end)
    end

    @impl GuildCache
    @doc "Update the emoji list for the given guild in the cache."
    @spec emoji_update(Guild.id(), [map()]) :: {[Emoji.t()], [Emoji.t()]}
    def emoji_update(guild_id, payload) do
      new_emojis = Util.cast(payload, {:list, {:struct, Emoji}})

      old_emojis =
        update_guild!(guild_id, fn guild ->
          {%{guild | emojis: new_emojis}, guild.emojis}
        end)

      {old_emojis, new_emojis}
    end

    @impl GuildCache
    @doc "Update the sticker list for the given guild in the cache."
    @spec stickers_update(Guild.id(), [map()]) :: {[Sticker.t()], [Sticker.t()]}
    def stickers_update(guild_id, stickers) do
      new_stickers = Util.cast(stickers, {:list, {:struct, Sticker}})

      old_stickers =
        update_guild!(guild_id, fn guild ->
          {%{guild | stickers: new_stickers}, guild.stickers}
        end)

      {old_stickers, new_stickers}
    end

    @impl GuildCache
    @doc "Create the given role in the given guild in the cache."
    @spec role_create(Guild.id(), map()) :: {Guild.id(), Role.t()}
    def role_create(guild_id, payload) do
      update_guild!(guild_id, fn guild ->
        {_old, new, new_roles} = GuildCache.Base.upsert(guild.roles, payload.id, payload, Role)
        {%{guild | roles: new_roles}, {guild_id, new}}
      end)
    end

    @doc "Delete the given role from the given guild in the cache."
    @impl GuildCache
    @spec role_delete(Guild.id(), Role.id()) :: {Guild.id(), Role.t()} | :noop
    def role_delete(guild_id, role_id) do
      update_guild!(guild_id, fn guild ->
        {popped, new_roles} = Map.pop(guild.roles, role_id)
        result = if popped, do: {guild_id, popped}, else: :noop
        {%{guild | roles: new_roles}, result}
      end)
    end

    @impl GuildCache
    @doc "Update the given role in the given guild in the cache."
    @spec role_update(Guild.id(), map()) :: {Guild.id(), Role.t() | nil, Role.t()}
    def role_update(guild_id, role) do
      update_guild!(guild_id, fn guild ->
        {old, new_role, new_roles} = GuildCache.Base.upsert(guild.roles, role.id, role, Role)
        new_guild = %{guild | roles: new_roles}
        {new_guild, {guild_id, old, new_role}}
      end)
    end

    @impl GuildCache
    @doc "Update guild voice states with the given voice state in the cache."
    @spec voice_state_update(Guild.id(), map()) :: {Guild.id(), [map()]}
    def voice_state_update(guild_id, payload) do
      trimmed_update = Map.drop(payload, [:member, "member"])

      update_guild!(guild_id, fn guild ->
        # Trim the `member` from the update payload.
        # Remove both `"member"` and `:member` in case of future key changes.
        state_without_user =
          Enum.reject(guild.voice_states, &(&1.user_id == trimmed_update.user_id))

        # If the `channel_id` is nil, then the user is leaving.
        # Otherwise, the voice state was updated.
        new_state =
          if(is_nil(trimmed_update.channel_id),
            do: state_without_user,
            else: [trimmed_update | state_without_user]
          )

        {%{guild | voice_states: new_state}, {guild_id, new_state}}
      end)
    end

    @doc "Increment the guild member count by one."
    @doc since: "0.7.0"
    @impl GuildCache
    @spec member_count_up(Guild.id()) :: true
    def member_count_up(guild_id) do
      # May not be `update_guild!` for the case where guild intent is off.
      update_guild(guild_id, fn guild ->
        {%{guild | member_count: guild.member_count + 1}, true}
      end)

      true
    end

    @doc "Decrement the guild member count by one."
    @doc since: "0.7.0"
    @impl GuildCache
    @spec member_count_down(Guild.id()) :: true
    def member_count_down(guild_id) do
      # May not be `update_guild!` for the case where guild intent is off.
      update_guild(guild_id, fn guild ->
        {%{guild | member_count: guild.member_count - 1}, true}
      end)

      true
    end

    defp update_guild(guild_id, updater) do
      :mnesia.activity(
        :sync_transaction,
        fn ->
          case :mnesia.read(table(), guild_id, :write) do
            [{_tag, _id, old_guild} = entry] ->
              {new_guild, result} = updater.(old_guild)
              :mnesia.write(table(), put_elem(entry, 2, new_guild), :write)
              result

            [] ->
              nil
          end
        end
      )
    end

    defp update_guild!(guild_id, updater) do
      :mnesia.activity(
        :sync_transaction,
        fn ->
          [{_tag, _id, old_guild} = entry] = :mnesia.read(table(), guild_id, :write)
          {new_guild, result} = updater.(old_guild)
          :mnesia.write(table(), put_elem(entry, 2, new_guild), :write)
          result
        end
      )
    end

    @impl GuildCache
    @doc "Wrap queries in a transaction."
    def wrap_query(fun) do
      :mnesia.activity(:sync_transaction, fun)
    end
  end
end
