defmodule Plug do
  @moduledoc """
  The plug specification.

  ## Types of plugs

  There are two kind of plugs: function plugs and module plugs.

  ### Function plugs

  A function plug is by definition any function that receives a connection
  and a set of options and returns a connection. Function plugs must have
  the following type signature:

      (Plug.Conn.t, Plug.opts) :: Plug.Conn.t

  ### Module plugs

  A module plug is an extension of the function plug. It is a module that must
  export:

    * a `c:call/2` function with the signature defined above
    * an `c:init/1` function which takes a set of options and initializes it.

  The result returned by `c:init/1` is passed as second argument to `c:call/2`. Note
  that `c:init/1` may be called during compilation and as such it must not return
  pids, ports or values that are specific to the runtime.

  The API expected by a module plug is defined as a behaviour by the
  `Plug` module (this module).

  ## Examples

  Here's an example of a function plug:

      def json_header_plug(conn, _opts) do
        Plug.Conn.put_resp_content_type(conn, "application/json")
      end

  Here's an example of a module plug:

      defmodule JSONHeaderPlug do
        @behaviour Plug

        import Plug.Conn

        def init(opts) do
          opts
        end

        def call(conn, _opts) do
          put_resp_content_type(conn, "application/json")
        end
      end

  ## The Plug pipeline

  The `Plug.Builder` module provides conveniences for building plug pipelines.
  """

  @type opts ::
          binary
          | tuple
          | atom
          | integer
          | float
          | [opts]
          | %{optional(opts) => opts}
          | MapSet.t()

  @callback init(opts) :: opts
  @callback call(conn :: Plug.Conn.t(), opts) :: Plug.Conn.t()

  require Logger

  @doc """
  Run a series of plugs at runtime.

  The plugs given here can be either a tuple, representing a module plug
  and their options, or a simple function that receives a connection and
  returns a connection.

  If any plug halts, the connection won't invoke the remaining plugs. If the
  given connection was already halted, none of the plugs are invoked either.

  While `Plug.Builder` is designed to operate at compile-time, the `run` function
  serves as a straightforward alternative for runtime executions.

  ## Examples

      Plug.run(conn, [{Plug.Head, []}, &IO.inspect/1])

  ## Options

    * `:log_on_halt` - a log level to be used if a plug halts

  """
  @spec run(Plug.Conn.t(), [{module, opts} | (Plug.Conn.t() -> Plug.Conn.t())], Keyword.t()) ::
          Plug.Conn.t()
  def run(conn, plugs, opts \\ [])

  def run(%Plug.Conn{halted: true} = conn, _plugs, _opts),
    do: conn

  def run(%Plug.Conn{} = conn, plugs, opts),
    do: do_run(conn, plugs, Keyword.get(opts, :log_on_halt))

  defp do_run(conn, [{mod, opts} | plugs], level) when is_atom(mod) do
    case mod.call(conn, mod.init(opts)) do
      %Plug.Conn{halted: true} = conn ->
        level && Logger.log(level, "Plug halted in #{inspect(mod)}.call/2")
        conn

      %Plug.Conn{} = conn ->
        do_run(conn, plugs, level)

      other ->
        raise "expected #{inspect(mod)} to return Plug.Conn, got: #{inspect(other)}"
    end
  end

  defp do_run(conn, [fun | plugs], level) when is_function(fun, 1) do
    case fun.(conn) do
      %Plug.Conn{halted: true} = conn ->
        level && Logger.log(level, "Plug halted in #{inspect(fun)}")
        conn

      %Plug.Conn{} = conn ->
        do_run(conn, plugs, level)

      other ->
        raise "expected #{inspect(fun)} to return Plug.Conn, got: #{inspect(other)}"
    end
  end

  defp do_run(conn, [], _level), do: conn

  @doc """
  Forwards requests to another plug while setting the connection to a trailing subpath of the request.

  The `path_info` on the forwarded connection will only include the request path trailing segments
  supplied to the `forward` function. The `conn.script_name` attribute retains the correct base path,
  e.g., url generation.

  ## Example

      defmodule Router do
        @behaviour Plug

        def init(opts), do: opts

        def call(conn, opts) do
          case conn do
            # Match subdomain
            %{host: "admin." <> _} ->
              AdminRouter.call(conn, opts)

            # Match path on localhost
            %{host: "localhost", path_info: ["admin" | rest]} ->
              Plug.forward(conn, rest, AdminRouter, opts)

            _ ->
              MainRouter.call(conn, opts)
          end
        end
      end

  """
  @spec forward(Plug.Conn.t(), [String.t()], atom, Plug.opts()) :: Plug.Conn.t()
  def forward(%Plug.Conn{path_info: path, script_name: script} = conn, new_path, target, opts) do
    {base, split_path} = Enum.split(path, length(path) - length(new_path))

    conn = do_forward(target, %{conn | path_info: split_path, script_name: script ++ base}, opts)
    %{conn | path_info: path, script_name: script}
  end

  defp do_forward({mod, fun}, conn, opts), do: apply(mod, fun, [conn, opts])
  defp do_forward(mod, conn, opts), do: mod.call(conn, opts)
end
