defmodule Yggdrasil.Publisher.Adapter do
  @moduledoc """
  Publisher adapter behaviour.
  """
  alias Yggdrasil.Channel
  alias Yggdrasil.Registry

  @doc """
  Callback to start a publisher with a `namespace` and some `GenServer`
  `options`.
  """
  @callback start_link(
              namespace :: atom(),
              options :: GenServer.options()
            ) :: GenServer.on_start()

  @doc """
  Callback for publishing a `message` in a `channel` using a `publisher`.
  """
  @callback publish(
              publisher :: GenServer.server(),
              channel :: Channel.t(),
              message :: term()
            ) :: :ok | {:error, term()}

  @doc """
  Publishes a `message` in a `channel` using a `publisher` and some `options`.
  """
  @callback publish(
              publisher :: GenServer.server(),
              channel :: Channel.t(),
              message :: term(),
              options :: keyword()
            ) :: :ok | {:error, term()}

  @doc """
  Use to implement `Yggdrasil.Publisher.Adapter` behaviour.
  """
  defmacro __using__(_) do
    quote do
      @behaviour Yggdrasil.Publisher.Adapter

      @doc """
      Starts a publisher adapter for an adapter given a `namespace`.
      Optionally, receives `GenServer` `options`.
      """
      @impl Yggdrasil.Publisher.Adapter
      def start_link(namespace, options \\ [])

      def start_link(namespace, options) do
        GenServer.start_link(__MODULE__, namespace, options)
      end

      @doc """
      Publishes a `message` in a `channel` using a `publisher`.
      """
      @impl Yggdrasil.Publisher.Adapter
      def publish(publisher, channel, message, options \\ [])

      def publish(publisher, %Channel{} = channel, message, _options) do
        GenServer.call(publisher, {:publish, channel, message})
      end

      defoverridable start_link: 1,
                     start_link: 2,
                     publish: 3,
                     publish: 4
    end
  end

  @doc """
  Generic publisher adapter starter that receives a `channel` and an optional
  `GenServer` options.
  """
  @spec start_link(Channel.t()) :: GenServer.on_start()
  @spec start_link(Channel.t(), GenServer.options()) :: GenServer.on_start()
  def start_link(channel, options \\ [])

  def start_link(
        %Channel{
          adapter: adapter,
          namespace: namespace
        },
        options
      ) do
    with {:ok, module} <- Registry.get_publisher_module(adapter) do
      module.start_link(namespace, options)
    end
  end

  @doc """
  Generic publisher adapter publish function. Publisher a `message` in a
  `channel` using a `publisher` and some `options`.
  """
  @spec publish(GenServer.server(), Channel.t(), term(), keyword()) ::
          :ok | {:error, term()}
  def publish(publish, channel, message, options)

  def publish(
        publisher,
        %Channel{adapter: adapter} = channel,
        message,
        options
      ) do
    with {:ok, module} <- Registry.get_publisher_module(adapter) do
      module.publish(publisher, channel, message, options)
    end
  end

  @doc """
  Generic publisher adapter stopper that receives the `pid`.
  """
  @spec stop(GenServer.server()) :: :ok
  def stop(pid) do
    GenServer.stop(pid)
  end
end
