defmodule Bento do
  @moduledoc """
  An incredibly fast, correct, pure-Elixir Bencoding library.

  This module contains high-level methods to encode and decode Bencoded data.
  """

  alias Bento.{Encoder, Decoder, Metainfo}

  @doc """
  Bencode a value.

      iex> Bento.encode([1, "two", [3]])
      {:ok, "li1e3:twoli3eee"}

  """
  @spec encode(Encoder.bencodable(), Keyword.t()) :: success | failure
        when success: {:ok, Encoder.t() | String.t()},
             failure: {:error, Encoder.encode_err()}
  def encode(value, options \\ []) do
    {:ok, encode!(value, options)}
  rescue
    exception in [Bento.EncodeError] ->
      {:error, {:invalid, exception.value}}
  end

  @doc """
  Bencode a value, raising an exception on error.

      iex> Bento.encode!([1, "two", [3]])
      "li1e3:twoli3eee"
  """
  @spec encode!(Encoder.bencodable(), Keyword.t()) :: success | no_return()
        when success: Encoder.t() | String.t()
  def encode!(value, options \\ [])

  def encode!(value, iodata: true), do: Encoder.encode(value)

  def encode!(value, _) do
    encode!(value, iodata: true) |> IO.iodata_to_binary()
  end

  @doc """
  Bencode a value as iodata.

      iex> Bento.encode_to_iodata([1, "two", [3]])
      {:ok, [108, [[105, "1", 101], ["3", 58, "two"], [108, [[105, "3", 101]], 101]], 101]}
  """
  @spec encode_to_iodata(Encoder.bencodable(), Keyword.t()) :: success | failure
        when success: {:ok, iodata()},
             failure: {:error, Encoder.encode_err()}
  def encode_to_iodata(value, options \\ []) do
    encode(value, Keyword.merge(options, iodata: true))
  end

  @doc """
  Bencode a value as iodata, raises an exception on error.

      iex> Bento.encode_to_iodata!([1, "two", [3]])
      [108, [[105, "1", 101], ["3", 58, "two"], [108, [[105, "3", 101]], 101]], 101]
  """
  @spec encode_to_iodata!(Encoder.bencodable(), Keyword.t()) :: iodata() | no_return()
  def encode_to_iodata!(value, options \\ []) do
    encode!(value, Keyword.merge(options, iodata: true))
  end

  @doc """
  Decode bencoded data to a value.

      iex> Bento.decode("li1e3:twoli3eee")
      {:ok, [1, "two", [3]]}

  Use `:as` as option to transform the parsed value into a struct.

      defmodule User do
        defstruct name: "John", age: 27
      end

      Bento.decode("d4:name3:Bobe", as: %User{})
      # {:ok, %User{name: "Bob", age: 27}}
  """
  @spec decode(iodata(), Decoder.opts()) :: {:ok, Decoder.t()} | failure
        when failure: {:error, Decoder.decode_err()}
  def decode(iodata, options \\ []), do: Decoder.decode(iodata, options)

  @doc """
  Decode bencoded data to a value, raising an exception on error.

      iex> Bento.decode!("li1e3:twoli3eee")
      [1, "two", [3]]

  Use `:as` as option to transform the parsed value into a struct.

      defmodule User do
        defstruct name: "John", age: 27
      end

      Bento.decode!("d4:name3:Bobe", as: %User{})
      # %User{name: "Bob", age: 27}
  """
  @spec decode!(iodata(), Decoder.opts()) :: Decoder.t() | no_return()
  def decode!(iodata, options \\ []), do: Decoder.decode!(iodata, options)

  @doc """
  Like `decode`, but ensures the data is a valid torrent metainfo file.
  """
  @spec torrent(iodata()) :: {:ok, Metainfo.Torrent.t()} | failure
        when failure: {:error, Decoder.decode_err() | String.t()}
  def torrent(iodata) do
    with {:ok, decoded} <- decode(iodata, as: %Metainfo.Torrent{}),
         {:ok, info} <- Metainfo.info(decoded) do
      {:ok, struct(decoded, info: info)}
    end
  end

  @doc """
  Like `decode!`, but ensures the data is a valid torrent metainfo file.
  """
  @spec torrent!(iodata()) :: Metainfo.Torrent.t() | no_return()
  def torrent!(iodata) do
    decoded = decode!(iodata, as: %Metainfo.Torrent{})
    struct(decoded, info: Metainfo.info!(decoded))
  end
end
