defmodule Yggdrasil.Transformer do
  @moduledoc """
  Transformer behaviour that defines how to decode and encode messages from a
  `Yggdrasil.Channel`.

  ## Small example

  Let's say we want to implement a transformer to send any Elixir term as a
  string to our subscribers. The transformer module would be implemented as
  follows:

  ```
  defmodule Yggdrasil.Transformer.Code do
    use Yggdrasil.Transformer

    def decode(_channel, message) do
      with {decoded, []} <- Code.eval_string(message) do
        {:ok, decoded}
      else
        _ ->
          {:error, "Bad message"}
      end
    end

    def encode(_channel, message) do
      encoded = inspect(message)
      {:ok, encoded}
    end
  end
  ```

  And we could use the following `Channel` to publish or subscribe to this
  messages:

  ```
  %Channel{
    name: "my_channel",
    adapter: :redis,
    transformer: Yggdrasil.Transformer.Code
  }
  ```

  ## Transformer alias

  When defining transformers it is possible to define aliases for the module
  as follows:

  ```
  defmodule Yggdrasil.Transformer.Code do
    use Yggdrasil.Transformer, name: :code

    (... same implementation as above ...)
  end
  ```

  And adding the following to our application supervision tree:

  ```
  Supervisor.start_link([
    {Yggdrasil.Transformer.Code, []}
    ...
  ])
  ```

  This will allow you to use the following as a `Channel` to subscribe and
  publish:

  ```
  %Channel{name: "my_channel", adapter: :redis, transformer: :code}
  ```
  """
  alias Yggdrasil.Channel
  alias Yggdrasil.Registry

  @doc """
  Callback to define how to decode the `message`s coming from a `channel`.
  """
  @callback decode(
              channel :: Channel.t(),
              message :: term()
            ) :: {:ok, term()} | {:error, term()}

  @doc """
  Callback to define how to encode the `message`s going to a `channel`.
  """
  @callback encode(
              channel :: Channel.t(),
              message :: term()
            ) :: {:ok, term()} | {:error, term()}

  @doc """
  Macro for using `Yggdrasil.Transformer`.

  The following are the available options:
  - `:name` - Name of the transformer. Must be an atom.
  """
  defmacro __using__(options) do
    transformer_alias =
      options[:name] || raise ArgumentError, message: "transformer not found"

    quote do
      @behaviour Yggdrasil.Transformer

      use Task, restart: :transient

      @doc """
      Start task to register the transformer in the `Registry`.
      """
      @spec start_link(term()) :: {:ok, pid()}
      def start_link(_) do
        Task.start_link(__MODULE__, :register, [])
      end

      @doc """
      Registers transformer in `Registry`.
      """
      @spec register() :: :ok
      def register do
        name = unquote(transformer_alias)

        Registry.register_transformer(name, __MODULE__)
      end

      @doc """
      Decodes a `message` for a `channel`.
      """
      @impl Yggdrasil.Transformer
      def decode(channel, message)

      def decode(%Channel{} = _channel, message) do
        {:ok, message}
      end

      def decode(_channel, _message) do
        {:error, "invalid channel"}
      end

      @doc """
      Encodes a `message` for a `channel`.
      """
      @impl Yggdrasil.Transformer
      def encode(channel, message)

      def encode(%Channel{} = _channel, message) do
        {:ok, message}
      end

      def encode(_channel, _message) do
        {:error, "invalid channel"}
      end

      defoverridable decode: 2, encode: 2
    end
  end

  @doc """
  Generic `message` decoder for a `channel`.
  """
  @spec decode(Channel.t(), term()) :: {:ok, term()} | {:error, term()}
  def decode(channel, message)

  def decode(%Channel{transformer: name} = channel, message) do
    with {:ok, module} <- Registry.get_transformer_module(name) do
      module.decode(channel, message)
    end
  end

  @doc """
  Generic `message` encoder for a `channel`.
  """
  @spec encode(Channel.t(), term()) :: {:ok, term()} | {:error, term()}
  def encode(channel, message)

  def encode(%Channel{transformer: name} = channel, message) do
    with {:ok, module} <- Registry.get_transformer_module(name) do
      module.encode(channel, message)
    end
  end
end
