defmodule Nostrum.Struct.Embed do
  @moduledoc ~S"""
  Functions that work on Discord embeds.

  ## Building Embeds

  `Nostrum.Struct.Embed`s can be built using this module's builder functions
  or standard `Map` syntax:

  ```elixir
  iex> import Nostrum.Struct.Embed
  ...> embed =
  ...>   %Nostrum.Struct.Embed{}
  ...>   |> put_title("craig")
  ...>   |> put_description("nostrum")
  ...>   |> put_url("https://google.com/")
  ...>   |> put_timestamp("2016-05-05T21:04:13.203Z")
  ...>   |> put_color(431_948)
  ...>   |> put_field("Field 1", "Test")
  ...>   |> put_field("Field 2", "More test", true)
  ...> embed
  %Nostrum.Struct.Embed{
    title: "craig",
    description: "nostrum",
    url: "https://google.com/",
    timestamp: "2016-05-05T21:04:13.203Z",
    color: 431_948,
    fields: [
      %Nostrum.Struct.Embed.Field{name: "Field 1", value: "Test"},
      %Nostrum.Struct.Embed.Field{name: "Field 2", value: "More test", inline: true}
    ]
  }
  ```

  ## Using structs

  You can also create `Nostrum.Struct.Embed`s from structs, by using the
  `Nostrum.Struct.Embed` module. Here's how the example above could be build using structs

  ```elixir
    defmodule MyApp.MyStruct do
      use Nostrum.Struct.Embed

      defstruct []

      def title(_), do: "craig"
      def description(_), do: "nostrum"
      def url(_), do: "https://google.com/"
      def timestamp(_), do: "2016-05-05T21:04:13.203Z"
      def color(_), do: 431_948

      def fields(_) do
        [
          %Nostrum.Struct.Embed.Field{name: "Field 1", value: "Test"},
          %Nostrum.Struct.Embed.Field{name: "Field 2", value: "More test", inline: true}
        ]
      end
    end

  iex> Nostrum.Struct.Embed.from(%MyApp.MyStruct{})
  %Nostrum.Struct.Embed{
    title: "craig",
    description: "nostrum",
    url: "https://google.com/",
    timestamp: "2016-05-05T21:04:13.203Z",
    color: 431_948,
    fields: [
      %Nostrum.Struct.Embed.Field{name: "Field 1", value: "Test"},
      %Nostrum.Struct.Embed.Field{name: "Field 2", value: "More test", inline: true}
    ]
  }
  ```
  See this modules callbacks for a list of all the functions that can be implemented.

  The implementation of these callbacks is optional. Not implemented functions will simply
  be ignored.
  """

  alias Nostrum.Struct.Embed.{Author, Field, Footer, Image, Provider, Thumbnail, Video}
  alias Nostrum.Util
  alias Jason.{Encode, Encoder}

  defstruct [
    :title,
    :type,
    :description,
    :url,
    :timestamp,
    :color,
    :footer,
    :image,
    :thumbnail,
    :video,
    :provider,
    :author,
    :fields
  ]

  defimpl Encoder do
    def encode(embed, options) do
      embed
      |> Map.from_struct()
      |> Enum.filter(fn {_, v} -> v != nil end)
      |> Map.new()
      |> Encode.map(options)
    end
  end

  @typedoc "Title of the embed"
  @type title :: String.t() | nil

  @typedoc "Type of the embed"
  @type type :: String.t() | nil

  @typedoc "Description of the embed"
  @type description :: String.t() | nil

  @typedoc "Url of the embed"
  @type url :: String.t() | nil

  @typedoc "Timestamp of embed content"
  @type timestamp :: String.t() | nil

  @typedoc "Color code of the embed"
  @type color :: integer() | nil

  @typedoc "Footer information"
  @type footer :: Footer.t() | nil

  @typedoc "Image information"
  @type image :: Image.t() | nil

  @typedoc "Thumbnail information"
  @type thumbnail :: Thumbnail.t() | nil

  @typedoc "Video information"
  @type video :: Video.t() | nil

  @typedoc "Provider information"
  @type provider :: Provider.t() | nil

  @typedoc "Author information"
  @type author :: Author.t() | nil

  @typedoc "Fields information"
  @type fields :: [Field.t()] | nil

  @type t :: %__MODULE__{
          title: title,
          type: type,
          description: description,
          url: url,
          timestamp: timestamp,
          color: color,
          footer: footer,
          image: image,
          thumbnail: thumbnail,
          video: video,
          provider: provider,
          author: author,
          fields: fields
        }

  @callback author(struct) :: author()
  @callback color(struct) :: integer() | nil
  @callback fields(struct) :: fields()
  @callback description(struct) :: description()
  @callback footer(struct) :: footer()
  @callback image(struct) :: url()
  @callback thumbnail(struct) :: url()
  @callback timestamp(struct) :: timestamp()
  @callback title(struct) :: title()
  @callback url(struct) :: url()

  defmacro __using__(_) do
    quote do
      @behaviour Nostrum.Struct.Embed

      def author(_), do: nil
      def color(_), do: nil
      def fields(_), do: nil
      def description(_), do: nil
      def footer(_), do: nil
      def image(_), do: nil
      def thumbnail(_), do: nil
      def timestamp(_), do: nil
      def title(_), do: nil
      def url(_), do: nil

      defoverridable(
        author: 1,
        color: 1,
        fields: 1,
        description: 1,
        footer: 1,
        image: 1,
        thumbnail: 1,
        timestamp: 1,
        title: 1,
        url: 1
      )
    end
  end

  @doc ~S"""
  Puts the given `value` under `:title` in `embed`.

  ## Examples

  ```elixir
  iex> embed = %Nostrum.Struct.Embed{}
  ...> Nostrum.Struct.Embed.put_title(embed, "nostrum")
  %Nostrum.Struct.Embed{title: "nostrum"}
  ```
  """
  @spec put_title(t, title) :: t
  def put_title(%__MODULE__{} = embed, value) do
    %__MODULE__{embed | title: value}
  end

  @doc false
  @spec put_type(t, type) :: t
  def put_type(%__MODULE__{} = embed, value) do
    %__MODULE__{embed | type: value}
  end

  @doc ~S"""
  Puts the given `value` under `:description` in `embed`.

  ## Examples

  ```elixir
  iex> embed = %Nostrum.Struct.Embed{}
  ...> Nostrum.Struct.Embed.put_description(embed, "An elixir library for the discord API.")
  %Nostrum.Struct.Embed{description: "An elixir library for the discord API."}
  ```
  """
  @spec put_description(t, description) :: t
  def put_description(%__MODULE__{} = embed, value) do
    %__MODULE__{embed | description: value}
  end

  @doc ~S"""
  Puts the given `value` under `:url` in `embed`.

  ## Examples

  ```elixir
  iex> embed = %Nostrum.Struct.Embed{}
  ...> Nostrum.Struct.Embed.put_url(embed, "https://github.com/Kraigie/nostrum")
  %Nostrum.Struct.Embed{url: "https://github.com/Kraigie/nostrum"}
  ```
  """
  @spec put_url(t, url) :: t
  def put_url(%__MODULE__{} = embed, value) do
    %__MODULE__{embed | url: value}
  end

  @doc ~S"""
  Puts the given `value` under `:timestamp` in `embed`.

  ## Examples

  ```elixir
  iex> embed = %Nostrum.Struct.Embed{}
  ...> Nostrum.Struct.Embed.put_timestamp(embed, "2018-04-21T17:33:51.893000Z")
  %Nostrum.Struct.Embed{timestamp: "2018-04-21T17:33:51.893000Z"}
  ```
  """
  @spec put_timestamp(t, timestamp) :: t
  def put_timestamp(%__MODULE__{} = embed, value) do
    %__MODULE__{embed | timestamp: value}
  end

  @doc ~S"""
  Puts the given `value` under `:color` in `embed`.

  ## Examples

  ```elixir
  iex> embed = %Nostrum.Struct.Embed{}
  ...> Nostrum.Struct.Embed.put_color(embed, 431948)
  %Nostrum.Struct.Embed{color: 431948}
  ```
  """
  @spec put_color(t, color) :: t
  def put_color(%__MODULE__{} = embed, value) do
    %__MODULE__{embed | color: value}
  end

  @doc ~S"""
  Puts a `Nostrum.Struct.Embed.Footer` under `:footer` in `embed`.

  ## Examples

  ```elixir
  iex> embed = %Nostrum.Struct.Embed{}
  ...> Nostrum.Struct.Embed.put_footer(embed, "Discord API", nil)
  %Nostrum.Struct.Embed{
    footer: %Nostrum.Struct.Embed.Footer{
      text: "Discord API",
      icon_url: nil
    }
  }

  iex> embed = %Nostrum.Struct.Embed{}
  ...> Nostrum.Struct.Embed.put_footer(embed, "nostrum footer", "https://discord.com/assets/53ef346458017da2062aca5c7955946b.svg")
  %Nostrum.Struct.Embed{
    footer: %Nostrum.Struct.Embed.Footer{
      text: "nostrum footer",
      icon_url: "https://discord.com/assets/53ef346458017da2062aca5c7955946b.svg"
    }
  }
  ```
  """
  @spec put_footer(t, Footer.text(), Footer.icon_url()) :: t
  def put_footer(%__MODULE__{} = embed, text, icon_url \\ nil) do
    footer = %Footer{
      text: text,
      icon_url: icon_url
    }

    %__MODULE__{embed | footer: footer}
  end

  @doc ~S"""
  Puts a `Nostrum.Struct.Embed.Image` under `:image` in `embed`.

  ## Examples

  ```elixir
  iex> embed = %Nostrum.Struct.Embed{}
  ...> Nostrum.Struct.Embed.put_image(embed, "https://discord.com/assets/af92e60c16b7019f34a467383b31490a.svg")
  %Nostrum.Struct.Embed{
    image: %Nostrum.Struct.Embed.Image{
      url: "https://discord.com/assets/af92e60c16b7019f34a467383b31490a.svg"
    }
  }
  ```
  """
  @spec put_image(t, Image.url()) :: t
  def put_image(%__MODULE__{} = embed, nil) do
    %__MODULE__{embed | image: nil}
  end

  def put_image(%__MODULE__{} = embed, url) do
    image = %Image{
      url: url
    }

    %__MODULE__{embed | image: image}
  end

  @doc ~S"""
  Puts a `Nostrum.Struct.Embed.Thumbnail` under `:thumbnail` in `embed`.

  ## Examples

  ```elixir
  iex> embed = %Nostrum.Struct.Embed{}
  ...> Nostrum.Struct.Embed.put_thumbnail(embed, "https://discord.com/assets/af92e60c16b7019f34a467383b31490a.svg")
  %Nostrum.Struct.Embed{
    thumbnail: %Nostrum.Struct.Embed.Thumbnail{
      url: "https://discord.com/assets/af92e60c16b7019f34a467383b31490a.svg"
    }
  }
  ```
  """
  @spec put_thumbnail(t, Thumbnail.url()) :: t
  def put_thumbnail(%__MODULE__{} = embed, nil) do
    %__MODULE__{embed | thumbnail: nil}
  end

  def put_thumbnail(%__MODULE__{} = embed, url) do
    thumbnail = %Thumbnail{
      url: url
    }

    %__MODULE__{embed | thumbnail: thumbnail}
  end

  @doc false
  @spec put_video(t, Video.url()) :: t
  def put_video(%__MODULE__{} = embed, url) do
    video = %Video{
      url: url
    }

    %__MODULE__{embed | video: video}
  end

  @doc false
  @spec put_provider(t, Provider.name(), Provider.url()) :: t
  def put_provider(%__MODULE__{} = embed, name, url) do
    provider = %Provider{
      name: name,
      url: url
    }

    %__MODULE__{embed | provider: provider}
  end

  @doc ~S"""
  Puts a `Nostrum.Struct.Embed.Author` under `:author` in `embed`.

  ## Examples

  ```elixir
  iex> embed = %Nostrum.Struct.Embed{}
  ...> Nostrum.Struct.Embed.put_author(embed, "skippi", "https://github.com/skippi", nil)
  %Nostrum.Struct.Embed{
    author: %Nostrum.Struct.Embed.Author{
      name: "skippi",
      url: "https://github.com/skippi",
      icon_url: nil
    }
  }
  ```
  """
  @spec put_author(t, Author.name(), Author.url(), Author.icon_url()) :: t
  def put_author(%__MODULE__{} = embed, name, url, icon_url) do
    author = %Author{
      name: name,
      url: url,
      icon_url: icon_url
    }

    %__MODULE__{embed | author: author}
  end

  @doc ~S"""
  Adds a `Nostrum.Struct.Embed.Field` under `:fields` in `embed`.

  ## Examples

  ```elixir
  iex> embed = %Nostrum.Struct.Embed{}
  ...> Nostrum.Struct.Embed.put_field(embed, "First User", "b1nzy")
  %Nostrum.Struct.Embed{
    fields: [
      %Nostrum.Struct.Embed.Field{name: "First User", value: "b1nzy"}
    ]
  }

  iex> embed = %Nostrum.Struct.Embed{
  ...>   fields: [
  ...>     %Nostrum.Struct.Embed.Field{name: "First User", value: "b1nzy"}
  ...>   ]
  ...> }
  ...> Nostrum.Struct.Embed.put_field(embed, "Second User", "Danny")
  %Nostrum.Struct.Embed{
    fields: [
      %Nostrum.Struct.Embed.Field{name: "First User", value: "b1nzy"},
      %Nostrum.Struct.Embed.Field{name: "Second User", value: "Danny"}
    ]
  }
  ```
  """
  @spec put_field(t, Field.name(), Field.value(), Field.inline()) :: t
  def put_field(embed, name, value, inline \\ nil)

  def put_field(%__MODULE__{fields: fields} = embed, name, value, inline) when is_list(fields) do
    field = %Field{
      name: name,
      value: value,
      inline: inline
    }

    %__MODULE__{embed | fields: fields ++ [field]}
  end

  def put_field(embed, name, value, inline) do
    put_field(%__MODULE__{embed | fields: []}, name, value, inline)
  end

  @doc """
  Create an embed from a struct that implements the `Nostrum.Struct.Embed` behaviour
  """
  def from(%module{} = struct) do
    # checks if the struct implements the behaviour
    if not Enum.member?(module.module_info(:attributes), {:behaviour, [__MODULE__]}) do
      raise "#{module} does not implement the behaviour #{__MODULE__}"
    end

    embed =
      %__MODULE__{}
      |> put_color(module.color(struct))
      |> put_description(module.description(struct))
      |> put_image(module.image(struct))
      |> put_thumbnail(module.thumbnail(struct))
      |> put_timestamp(module.timestamp(struct))
      |> put_title(module.title(struct))
      |> put_url(module.url(struct))

    embed =
      case module.author(struct) do
        %Author{} = author -> put_author(embed, author.name, author.url, author.icon_url)
        nil -> embed
        other -> raise "\"#{inspect(other)}\" is invalid for type author()"
      end

    embed =
      case module.footer(struct) do
        %Footer{} = footer -> put_footer(embed, footer.text, footer.icon_url)
        nil -> embed
        other -> raise "\"#{inspect(other)}\" is invalid for type footer()"
      end

    struct
    |> module.fields()
    |> List.wrap()
    |> Enum.reduce(embed, fn
      %Field{} = field, embed -> put_field(embed, field.name, field.value, field.inline)
      other, _ -> raise "\"#{inspect(other)}\" is invalid for type fields()"
    end)
  end

  # TODO: Jump down the rabbit hole
  @doc false
  def p_encode do
    %__MODULE__{}
  end

  @doc false
  def to_struct(map) do
    new =
      map
      |> Map.new(fn {k, v} -> {Util.maybe_to_atom(k), v} end)
      |> Map.update(:footer, nil, &Util.cast(&1, {:struct, Footer}))
      |> Map.update(:image, nil, &Util.cast(&1, {:struct, Image}))
      |> Map.update(:thumbnail, nil, &Util.cast(&1, {:struct, Thumbnail}))
      |> Map.update(:video, nil, &Util.cast(&1, {:struct, Video}))
      |> Map.update(:provider, nil, &Util.cast(&1, {:struct, Provider}))
      |> Map.update(:author, nil, &Util.cast(&1, {:struct, Author}))
      |> Map.update(:fields, nil, &Util.cast(&1, {:list, {:struct, Field}}))

    struct(__MODULE__, new)
  end
end
