defmodule Pigeon do
  @moduledoc """
  iOS (APNS), Android (FCM), and Amazon Android (ADM) push notifications for Elixir.

  ## Getting Started

  Check the module documentation for your push notification service.

    - `Pigeon.ADM` - Amazon Android.
    - `Pigeon.APNS` - Apple iOS.
    - `Pigeon.FCM` - Firebase Cloud Messaging v1 API.

  ## Creating Dynamic Runtime Dispatchers

  Pigeon can spin up dynamic dispatchers for a variety of advanced use-cases, such as
  supporting dozens of dispatcher configurations or custom connection pools.

  See `Pigeon.Dispatcher` for instructions.

  ## Writing a Custom Dispatcher Adapter

  Want to write a Pigeon adapter for an unsupported push notification service?

  See `Pigeon.Adapter` for instructions.
  """

  alias Pigeon.Tasks

  @default_timeout 5_000

  @typedoc ~S"""
  Async callback for push notifications response.

  ## Examples

      handler = fn(%Pigeon.ADM.Notification{response: response}) ->
        case response do
          :success ->
            Logger.debug "Push successful!"
          :unregistered ->
            Logger.error "Bad device token!"
          _error ->
            Logger.error "Some other error happened."
        end
      end

      n = Pigeon.ADM.Notification.new("token", %{"message" => "test"})
      Pigeon.ADM.push(n, on_response: handler)
  """
  @type on_response ::
          (notification -> no_return)
          | {module, atom}
          | {module, atom, [any]}

  @type notification :: %{
          :__struct__ => atom(),
          :__meta__ => Pigeon.Metadata.t(),
          optional(atom()) => any()
        }

  @typedoc ~S"""
  Options for sending push notifications.

  - `:on_response` - Optional async callback triggered on receipt of push.
    See `t:on_response/0`
  - `:timeout` - Push timeout. Defaults to 5000ms.
  """
  @type push_opts :: [
          on_response: on_response() | nil,
          timeout: non_neg_integer()
        ]

  @doc """
  Returns the configured default pool size for Pigeon dispatchers.

  To customize this value, include the following in your config/config.exs:

      config :pigeon, :default_pool_size, 5
  """
  @spec default_pool_size :: pos_integer()
  def default_pool_size() do
    Application.get_env(:pigeon, :default_pool_size, 5)
  end

  @doc """
  Returns the configured JSON encoding library for Pigeon.

  To customize the JSON library, include the following in your config/config.exs:

      config :pigeon, :json_library, Jason
  """
  @spec json_library :: module()
  def json_library do
    Application.get_env(:pigeon, :json_library, Jason)
  end

  @spec push(pid() | atom(), notification() | [notification()], push_opts()) ::
          notification() | [notification()] | :ok | [:ok]
  @doc """
  Sends a push notification with given options.
  """
  def push(pid, notifications, opts \\ [])

  def push(pid, notifications, opts) when is_list(notifications) do
    for n <- notifications, do: push(pid, n, opts)
  end

  def push(pid, notification, opts) do
    if Keyword.has_key?(opts, :on_response) do
      on_response = Keyword.get(opts, :on_response)
      notification = put_on_response(notification, on_response)
      push_async(pid, notification)
    else
      timeout = Keyword.get(opts, :timeout, @default_timeout)
      push_sync(pid, notification, timeout)
    end
  end

  defp push_sync(pid, notification, timeout) do
    myself = self()
    ref = :erlang.make_ref()
    on_response = fn x -> send(myself, {:"$push", ref, x}) end
    notification = put_on_response(notification, on_response)

    push_async(pid, notification)

    receive do
      {:"$push", ^ref, x} -> x
    after
      timeout -> %{notification | response: :timeout}
    end
  end

  defp push_async(pid, notification) do
    case Pigeon.Registry.next(pid) do
      nil ->
        Tasks.process_on_response(%{notification | response: :not_started})

      pid ->
        send(pid, {:"$push", notification})
        :ok
    end
  end

  defp put_on_response(notification, on_response) do
    meta = %{notification.__meta__ | on_response: on_response}
    %{notification | __meta__: meta}
  end
end
