defmodule Nostrum.Struct.Message do
  @moduledoc """
  A `Nostrum.Struct.Message` represents a message.

  More information can be found on the
  [Discord API Message Object Documentation](https://discord.com/developers/docs/resources/channel#message-object).
  """

  alias Nostrum.Struct.{Channel, Embed, Guild, Interaction, Sticker, User}
  alias Nostrum.Struct.Guild.{Member, Role}

  alias Nostrum.Struct.Message.{
    Activity,
    Application,
    Attachment,
    Component,
    Poll,
    Reaction,
    Reference
  }

  alias Nostrum.{Snowflake, Util}

  defstruct [
    :activity,
    :application,
    :application_id,
    :attachments,
    :author,
    :channel_id,
    :content,
    :components,
    :edited_timestamp,
    :embeds,
    :id,
    :interaction,
    :guild_id,
    :member,
    :mention_everyone,
    :mention_roles,
    :mention_channels,
    :mentions,
    :message_reference,
    :nonce,
    :pinned,
    :poll,
    :reactions,
    :referenced_message,
    :sticker_items,
    :timestamp,
    :thread,
    :tts,
    :type,
    :webhook_id
  ]

  @typedoc "The id of the message"
  @type id :: Snowflake.t()

  @typedoc "The id of the guild"
  @type guild_id :: Guild.id() | nil

  @typedoc "The id of the channel"
  @type channel_id :: Channel.id()

  @typedoc "The user struct of the author"
  @type author :: User.t()

  @typedoc "The content of the message"
  @type content :: String.t()

  @typedoc "When the message was sent"
  @type timestamp :: DateTime.t()

  @typedoc "When the message was edited"
  @type edited_timestamp :: DateTime.t() | nil

  @typedoc "Whether this was a TTS message"
  @type tts :: boolean

  @typedoc """
  Whether this message mentions everyone
  """
  @type mention_everyone :: boolean

  @typedoc """
  List of channels mentioned in the message

  [Discord API Channel Mention Object Documentation](https://discord.com/developers/docs/resources/channel#channel-mention-object-channel-mention-structure)
  """
  @type mention_channels :: [Channel.channel_mention()]

  @typedoc "List of users mentioned in the message"
  @type mentions :: [User.t()]

  @typedoc "List of roles ids mentioned in the message"
  @type mention_roles :: [Role.id()]

  @typedoc "List of attached files in the message"
  @type attachments :: [Attachment.t()]

  @typedoc """
  List of Message Components
  """
  @type components :: [Component.t()]

  @typedoc """
  Array of Message Sticker Item Objects
  """
  @type sticker_items :: [Sticker.t()]

  @typedoc """
  Message interaction object
  """
  @type interaction :: Interaction.t() | nil

  @typedoc "The poll object attached to the message"
  @type poll :: Poll.t() | nil

  @typedoc "List of embedded content in the message"
  @type embeds :: [Embed.t()]

  @typedoc "Reactions to the message."
  @type reactions :: [Reaction.t()] | nil

  @typedoc "Validates if a message was sent"
  @type nonce :: String.t() | nil

  @typedoc "Whether this message is pinned"
  @type pinned :: boolean

  @typedoc """
  If the message is generated by a webhook, this is the webhook's id
  """
  @type webhook_id :: Snowflake.t() | nil

  @typedoc """
  [Discord API Message Object Type Documentation](https://discord.com/developers/docs/resources/channel#message-object-message-types)

  - `0`  - `DEFAULT`
  - `1`  - `RECIPIENT_ADD`
  - `2`  - `RECIPIENT_REMOVE`
  - `3`  - `CALL`
  - `4`  - `CHANNEL_NAME_CHANGE`
  - `5`  - `CHANNEL_ICON_CHANGE`
  - `6`  - `CHANNEL_PINNED_MESSAGE`
  - `7`  - `GUILD_MEMBER_JOIN`
  - `8`  - `GUILD_BOOST`
  - `9`  - `GUILD_BOOST_TIER_1`
  - `10` - `GUILD_BOOST_TIER_2`
  - `11` - `GUILD_BOOST_TIER_3`
  - `12` - `CHANNEL_FOLLOW_ADD`
  - `14` - `GUILD_DISCOVERY_DISQUALIFIED`
  - `15` - `GUILD_DISCOVERY_REQUALIFIED`
  - `16` - `GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING`
  - `17` - `GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING`
  - `18` - `THREAD_CREATED`
  - `19` - `REPLY`
  - `20` - `CHAT_INPUT_COMMAND`
  - `21` - `THREAD_STARTER_MESSAGE`
  - `22` - `GUILD_INVITE_REMINDER`
  - `23` - `CONTEXT_MENU_COMMAND`
  - `24` - `AUTO_MODERATION_ACTION`
  - `25` - `ROLE_SUBSCRIPTION_PURCHASE`
  - `26` - `INTERACTION_PREMIUM_UPSELL`
  - `27` - `STAGE_START`
  - `28` - `STAGE_END`
  - `29` - `STAGE_SPEAKER`
  - `31` - `STAGE_TOPIC`
  - `32` - `GUILD_APPLICATION_PREMIUM_SUBSCRIPTION`
  - `36` - `GUILD_INCIDENT_ALERT_MODE_ENABLED`
  - `37` - `GUILD_INCIDENT_ALERT_MODE_DISABLED`
  - `38` - `GUILD_INCIDENT_REPORT_RAID`
  - `39` - `GUILD_INCIDENT_REPORT_FALSE_ALARM`
  - `44` - `PURCHASE_NOTIFICATION`
  - `46` - `POLL_RESULT`
  """
  @type type :: integer()

  @typedoc """
  The activity of the message. Sent with Rich Presence-related chat embeds
  """
  @type activity :: Activity.t() | nil

  @typedoc """
  The application of the message. Sent with Rich Presence-related chat embeds
  """
  @type application :: Application.t() | nil

  @typedoc """
  if the message is a response to an interaction, this is the ID of the interaction's application
  """
  @type application_id :: Application.id() | nil
  @typedoc """
  Partial Guild Member object received with the Message Create event if message came from a guild channel
  """
  @type member :: Member.t() | nil

  @typedoc """
  Reference data sent with crossposted messages and replies.

  For `THREAD_STARTER_MESSAGE` messages, this field points to the message that the thread was started from.
  """
  @type message_reference :: Reference.t() | nil

  @typedoc """
  The message associated with the `:message_reference`

  This field is only returned for messages with a `type: 19` (Reply). If the message is a reply but the`:referenced_message` field is not present, the backend did not attempt to fetch the message that was being replied to,so its state is unknown. If the field exists but is `nil`, the referenced message was deleted.
  """
  @type referenced_message :: __MODULE__.t() | nil

  @typedoc """
  The thread that was started from this message, includes a thread member object
  """
  @type thread :: Channel.t() | nil

  @type t :: %__MODULE__{
          activity: activity,
          application_id: application_id,
          application: application,
          attachments: attachments,
          author: author,
          channel_id: channel_id,
          components: components,
          content: content,
          edited_timestamp: edited_timestamp,
          embeds: embeds,
          guild_id: guild_id,
          id: id,
          interaction: interaction,
          member: member,
          mention_everyone: mention_everyone,
          mention_roles: mention_roles,
          mentions: mentions,
          message_reference: message_reference,
          nonce: nonce,
          pinned: pinned,
          poll: poll,
          reactions: reactions,
          referenced_message: referenced_message,
          sticker_items: sticker_items,
          thread: thread,
          timestamp: timestamp,
          tts: tts,
          type: type,
          webhook_id: webhook_id
        }

  @doc false
  def to_struct(map) do
    new =
      map
      |> Map.new(fn {k, v} -> {Util.maybe_to_atom(k), v} end)
      |> Map.update(:activity, nil, &Util.cast(&1, {:struct, Activity}))
      |> Map.update(:application_id, nil, &Util.cast(&1, Snowflake))
      |> Map.update(:application, nil, &Util.cast(&1, {:struct, Application}))
      |> Map.update(:attachments, nil, &Util.cast(&1, {:list, {:struct, Attachment}}))
      |> Map.update(:author, nil, &Util.cast(&1, {:struct, User}))
      |> Map.update(:channel_id, nil, &Util.cast(&1, Snowflake))
      |> Map.update(:components, nil, &Util.cast(&1, {:list, {:struct, Component}}))
      |> Map.update(:edited_timestamp, nil, &Util.maybe_to_datetime/1)
      |> Map.update(:embeds, nil, &Util.cast(&1, {:list, {:struct, Embed}}))
      |> Map.update(:guild_id, nil, &Util.cast(&1, Snowflake))
      |> Map.update(:id, nil, &Util.cast(&1, Snowflake))
      |> Map.update(:interaction, nil, &Util.cast(&1, {:struct, Interaction}))
      |> Map.update(:member, nil, &Util.cast(&1, {:struct, Member}))
      |> Map.update(:mention_channels, nil, &Util.cast(&1, {:list, {:struct, Channel}}))
      |> Map.update(:mention_roles, nil, &Util.cast(&1, {:list, Snowflake}))
      |> Map.update(:mentions, nil, &Util.cast(&1, {:list, {:struct, User}}))
      |> Map.update(:message_reference, nil, &Util.cast(&1, {:struct, Reference}))
      |> Map.update(:nonce, nil, &Util.cast(&1, Snowflake))
      |> Map.update(:poll, nil, &Util.cast(&1, {:struct, Poll}))
      |> Map.update(:reactions, nil, &Util.cast(&1, {:list, {:struct, Reaction}}))
      |> Map.update(:referenced_message, nil, &Util.cast(&1, {:struct, __MODULE__}))
      |> Map.update(:sticker_items, nil, &Util.cast(&1, {:list, {:struct, Sticker}}))
      |> Map.update(:thread, nil, &Util.cast(&1, {:struct, Channel}))
      |> Map.update(:timestamp, nil, &Util.maybe_to_datetime/1)
      |> Map.update(:webhook_id, nil, &Util.cast(&1, Snowflake))

    struct(__MODULE__, new)
  end

  # differs from the above in that
  # for message updates we have the old message
  # and want to merge in the unchanged fields.
  # Discord's docs say that for updates, we'll
  # only get a subset of the fields.
  @doc false
  @spec to_struct(map :: map(), old_message :: __MODULE__.t() | nil) :: __MODULE__.t()
  def to_struct(map, nil), do: to_struct(map)

  def to_struct(map, old_message) do
    new =
      map
      |> Map.new(fn {k, v} -> {Util.maybe_to_atom(k), v} end)
      |> Util.map_update_if_present(:activity, &Util.cast(&1, {:struct, Activity}))
      |> Util.map_update_if_present(:application_id, &Util.cast(&1, Snowflake))
      |> Util.map_update_if_present(:application, &Util.cast(&1, {:struct, Application}))
      |> Util.map_update_if_present(:attachments, &Util.cast(&1, {:list, {:struct, Attachment}}))
      |> Util.map_update_if_present(:author, &Util.cast(&1, {:struct, User}))
      |> Util.map_update_if_present(:channel_id, &Util.cast(&1, Snowflake))
      |> Util.map_update_if_present(:components, &Util.cast(&1, {:list, {:struct, Component}}))
      |> Util.map_update_if_present(:edited_timestamp, &Util.maybe_to_datetime/1)
      |> Util.map_update_if_present(:embeds, &Util.cast(&1, {:list, {:struct, Embed}}))
      |> Util.map_update_if_present(:guild_id, &Util.cast(&1, Snowflake))
      |> Util.map_update_if_present(:id, &Util.cast(&1, Snowflake))
      |> Util.map_update_if_present(:interaction, &Util.cast(&1, {:struct, Interaction}))
      |> Util.map_update_if_present(:member, &Util.cast(&1, {:struct, Member}))
      |> Util.map_update_if_present(
        :mention_channels,
        &Util.cast(&1, {:list, {:struct, Channel}})
      )
      |> Util.map_update_if_present(:mention_roles, &Util.cast(&1, {:list, Snowflake}))
      |> Util.map_update_if_present(:mentions, &Util.cast(&1, {:list, {:struct, User}}))
      |> Util.map_update_if_present(:message_reference, &Util.cast(&1, {:struct, Reference}))
      |> Util.map_update_if_present(:nonce, &Util.cast(&1, Snowflake))
      |> Util.map_update_if_present(:poll, &Util.cast(&1, {:struct, Poll}))
      |> Util.map_update_if_present(:reactions, &Util.cast(&1, {:list, {:struct, Reaction}}))
      |> Util.map_update_if_present(:referenced_message, &Util.cast(&1, {:struct, __MODULE__}))
      |> Util.map_update_if_present(:sticker_items, &Util.cast(&1, {:list, {:struct, Sticker}}))
      |> Util.map_update_if_present(:thread, &Util.cast(&1, {:struct, Channel}))
      |> Util.map_update_if_present(:timestamp, &Util.maybe_to_datetime/1)
      |> Util.map_update_if_present(:webhook_id, &Util.cast(&1, Snowflake))

    struct(old_message, new)
  end

  @doc """
  Takes the message and produces a URL that, when clicked from the user client, will
  jump them to that message, assuming they have access to the message and the message
  is valid.
  """
  @doc since: "0.5.0"
  @spec to_url(%__MODULE__{}) :: String.t()
  def to_url(%__MODULE__{id: id, guild_id: guild_id, channel_id: channel_id}) do
    guild_id =
      if is_nil(guild_id) do
        "@me"
      else
        to_string(guild_id)
      end

    [
      "https://discordapp.com/channels/",
      guild_id,
      "/",
      "#{channel_id}",
      "/",
      "#{id}"
    ]
    |> IO.iodata_to_binary()
  end
end
