defmodule Phoenix.Endpoint.SyncCodeReloadPlug do
  @moduledoc ~S"""
  Wraps an Endpoint, attempting to sync with Phoenix's code reloader if 
  an exception is raised which indicates that we may be in the middle of a reload.

  We detect this by looking at the raised exception and seeing if it indicates
  that the endpoint is not defined. This indicates that the code reloader may be 
  midway through a compile, and that we should attempt to retry the request
  after the compile has completed. This is also why this must be implemented in
  a separate module (one that is not recompiled in a typical code reload cycle),
  since otherwise it may be the case that the endpoint itself is not defined.
  """

  @behaviour Plug

  def init({endpoint, opts}), do: {endpoint, endpoint.init(opts)}

  def call(conn, {endpoint, opts}), do: do_call(conn, endpoint, opts, true)

  defp do_call(conn, endpoint, opts, retry?) do
    try do
      endpoint.call(conn, opts)
    rescue
      exception in [UndefinedFunctionError] ->
        case exception do
          %UndefinedFunctionError{module: ^endpoint} when retry? ->
            # Sync with the code reloader and retry once
            Phoenix.CodeReloader.sync()
            do_call(conn, endpoint, opts, false)

          exception ->
            reraise(exception, __STACKTRACE__)
        end
    end
  end
end
