defmodule Application do
  @moduledoc """
  A module for working with applications and defining application callbacks.

  In Elixir (actually, in Erlang/OTP), an application is a component
  implementing some specific functionality, that can be started and stopped
  as a unit, and which can be re-used in other systems as well.

  Applications are defined with an application file named `APP.app` where
  `APP` is the APP name, usually in `underscore_case` convention. The
  application file must reside in the same `ebin` directory as the
  application's modules bytecode.

  In Elixir, Mix is responsible for compiling your source code and
  generating your application `.app` file. Furthermore, Mix is also
  responsible for configuring, starting and stopping your application
  and its dependencies. For this reason, this documentation will focus
  on the remaining aspects of your application: the application environment,
  and the application callback module.

  You can learn more about Mix compilation of `.app` files by typing
  `mix help compile.app`.

  ## Application environment

  Once an application is started, OTP provides an application environment
  that can be used to configure applications.

  Assuming you are inside a Mix project, you can edit your application
  function in the `mix.exs` file to the following:

      def application do
        [env: [hello: :world]]
      end

  In the application function, we can define the default environment values
  for our application. By starting your application with `iex -S mix`, you
  can access the default value:

      Application.get_env(:APP_NAME, :hello)
      #=> {:ok, :hello}

  It is also possible to put and delete values from the application value,
  including new values that are not defined in the environment file (although
  those should be avoided).

  In the future, we plan to support configuration files which allows
  developers to configure the environment of their dependencies.

  Keep in mind that each application is responsible for its environment.
  Do not use the functions in this module for directly access or modify
  the environment of other application (as it may lead to inconsistent
  data in the application environment).

  ## Application module callback

  Often times, an application defines a supervision tree that must be started
  and stopped when the application starts and stops. For such, we need to
  define an application module callback. The first step is to define the
  module callback in the application definition in the `mix.exs` file:

      def application do
        [mod: {MyApp, []}]
      end

  Our application now requires the `MyApp` module to provide an application
  callback. This can be done by invoking `use Application` in that module
  and defining a `start/2` callback, for example:

      defmodule MyApp do
        use Application

        def start(_type, _args) do
          MyApp.Supervisor.start_link()
        end
      end

  `start/2` most commonly returns `{:ok, pid}` or `{:ok, pid, state}` where
  `pid` identifies the supervision tree and the state is the application state.
  `args` is second element of the tuple given to the `:mod` option.

  The `type` passed into `start/2` is usually `:normal` unless in a distributed
  setup where applications takeover and failovers are configured. This particular
  aspect of applications can be read with more detail in the OTP documentation:

    * http://www.erlang.org/doc/man/application.html
    * http://www.erlang.org/doc/design_principles/applications.html

  A developer may also implement the `stop/1` callback (automatically defined
  by `use Application`) which does any application cleanup. It receives the
  application state and can return any value. Notice that shutting down the
  supervisor is automatically handled by the VM;
  """

  @doc false
  defmacro __using__(_) do
    quote location: :keep do
      @behaviour :application

      @doc false
      def stop(_state) do
        :ok
      end

      defoverridable [stop: 1]
    end
  end

  @type app :: atom
  @type key :: atom
  @type value :: term
  @type start_type :: :permanent | :transient | :temporary

  @doc """
  Returns all key-value pairs for `app`.
  """
  @spec get_all_env(app) :: [{key,value}]
  def get_all_env(app) do
    :application.get_all_env(app)
  end

  @doc """
  Returns the value for `key` in `app`'s environment.

  If the specified application is not loaded, or the configuration parameter
  does not exist, the function returns the `default` value.
  """
  @spec get_env(app, key, value) :: value
  def get_env(app, key, default \\ nil) do
    case :application.get_env(app, key) do
      {:ok, value} -> value
      :undefined -> default
    end
  end

  @doc """
  Returns the value for `key` in `app`'s environment in a tuple.

  If the specified application is not loaded, or the configuration parameter
  does not exist, the function returns `:error`.
  """
  @spec fetch_env(app, key) :: {:ok, value} | :error
  def fetch_env(app, key) do
    case :application.get_env(app, key) do
      {:ok, value} -> {:ok, value}
      :undefined -> :error
    end
  end

  @doc """
  Puts the `value` in `key` for the given `app`.

  ## Options

    * `:timeout`    - the timeout for the change (defaults to 5000ms)
    * `:persistent` - persists the given value on application load and reloads

  If `put_env/4` is called before the application is loaded, the application
  environment values specified in the `.app` file will override the ones
  previously set.

  The persistent option can be set to true when there is a need to guarantee
  parameters set with this function will not be overridden by the ones defined
  in the application resource file on load. This means persistent values will
  stick after the application is loaded and also on application reload.
  """
  @spec put_env(app, key, value, [timeout: timeout, persistent: boolean]) :: :ok
  def put_env(app, key, value, opts \\ []) do
    :application.set_env(app, key, value, opts)
  end

  @doc """
  Deletes the `key` from the given `app` environment.

  See `put_env/4` for a description of the options.
  """
  @spec delete_env(app, key, [timeout: timeout, persistent: boolean]) :: :ok
  def delete_env(app, key, opts \\ []) do
    :application.unset_env(app, key, opts)
  end

  @doc """
  Ensures the given `app` is started.

  Same as `start/2` but returns `:ok` if the application was already
  started. This is useful in scripts and in test setup, where test
  applications need to be explicitly started:

      :ok = Application.ensure_started(:my_test_dep)

  """
  @spec ensure_started(app, start_type) :: :ok | {:error, term}
  def ensure_started(app, type \\ :temporary) when is_atom(app) do
    :application.ensure_started(app, type)
  end

  @doc """
  Ensures the given `app` and its applications are started.

  Same as `start/2` but also starts the applications listed under
  `:applications` in the `.app` file in case they were not previously
  started.
  """
  @spec ensure_all_started(app, start_type) :: {:ok, [app]} | {:error, term}
  def ensure_all_started(app, type \\ :temporary) when is_atom(app) do
    :application.ensure_all_started(app, type)
  end

  @doc """
  Starts the given `app`.

  If the `app` is not loaded, the application will first be loaded using `load/1`.
  Any included application, defined in the `:included_applications` key of the
  `.app` file will also be loaded, but they won't be started.

  Furthermore, all applications listed in the `:applications` key must be explicitly
  started before this application is. If not, `{:error, {:not_started, app}}` is
  returned, where `app` is the name of the missing application.

  In case you want to automatically  load **and start** all of `app`'s dependencies,
  see `ensure_all_started/2`.

  The `type` argument specifies the type of the application:

    * `:permanent` - if `app` terminates, all other applications and the entire
      node are also terminated.

    * `:transient` - if `app` terminates with `:normal` reason, it is reported
      but no other applications are terminated. If a transient application
      terminates abnormally, all other applications and the entire node are
      also terminated.

    * `:temporary` - if `app` terminates, it is reported but no other
      applications are terminated (the default).

  Note that it is always possible to stop an application explicitly by calling
  `stop/1`. Regardless of the type of the application, no other applications will
  be affected.

  Note also that the `:transient` type is of little practical use, since when a
  supervision tree terminates, the reason is set to `:shutdown`, not `:normal`.
  """
  @spec start(app, start_type) :: :ok | {:error, term}
  def start(app, type \\ :temporary) when is_atom(app) do
    :application.start(app, type)
  end

  @doc """
  Stops the given `app`.

  When stopped, the application is still loaded.
  """
  @spec stop(app) :: :ok | {:error, term}
  def stop(app) do
    :application.stop(app)
  end

  @doc """
  Loads the given `app`.

  In order to be loaded, an `.app` file must be in the load paths.
  All `:included_applications` will also be loaded.

  Loading the application does not start it nor load its modules, but
  it does load its environment.
  """
  @spec load(app) :: :ok | {:error, term}
  def load(app) when is_atom(app) do
    :application.load(app)
  end

  @doc """
  Unloads the given `app`.

  It will also unload all `:included_applications`.
  Note that the function does not purge the application modules.
  """
  @spec unload(app) :: :ok | {:error, term}
  def unload(app) when is_atom(app) do
    :application.unload(app)
  end

  @doc """
  Gets the directory for app.

  This information is returned based on the code path. Here is an
  example:

      File.mkdir_p!("foo/ebin")
      Code.prepend_path("foo/ebin")
      Application.app_dir(:foo)
      #=> "foo"

  Even though the directory is empty and there is no `.app` file
  it is considered the application directory based on the name
  "foo/ebin". The name may contain a dash `-` which is considered
  to be the app version and it is removed for the lookup purposes:

      File.mkdir_p!("bar-123/ebin")
      Code.prepend_path("bar-123/ebin")
      Application.app_dir(:bar)
      #=> "bar-123"

  For more information on code paths, check the `Code` module in
  Elixir and also Erlang's `:code` module.
  """
  @spec app_dir(app) :: String.t
  def app_dir(app) when is_atom(app) do
    case :code.lib_dir(app) do
      lib when is_list(lib) -> IO.chardata_to_string(lib)
      {:error, :bad_name} -> raise ArgumentError, "unknown application: #{inspect app}"
    end
  end

  @doc """
  Returns the given path inside `app_dir/1`.
  """
  @spec app_dir(app, String.t) :: String.t
  def app_dir(app, path) when is_binary(path) do
    Path.join(app_dir(app), path)
  end

  @doc """
  Formats the error reason returned by `start/2`,
  `ensure_started/2`, `stop/1`, `load/1` and `unload/1`,
  returns a string.
  """
  @spec format_error(any) :: String.t
  def format_error(reason) do
    try do
      impl_format_error(reason)
    catch
      # A user could create an error that looks like a builtin one
      # causing an error.
      :error, _ ->
        inspect(reason)
    end
  end

  # exit(:normal) call is special cased, undo the special case.
  defp impl_format_error({{:EXIT, :normal}, {mod, :start, args}}) do
    Exception.format_exit({:normal, {mod, :start, args}})
  end

  # {:error, reason} return value
  defp impl_format_error({reason, {mod, :start, args}}) do
    Exception.format_mfa(mod, :start, args) <> " returned an error: " <>
      Exception.format_exit(reason)
  end

  # error or exit(reason) call, use exit reason as reason.
  defp impl_format_error({:bad_return, {{mod, :start, args}, {:EXIT, reason}}}) do
    Exception.format_exit({reason, {mod, :start, args}})
  end

  # bad return value
  defp impl_format_error({:bad_return, {{mod, :start, args}, return}}) do
    Exception.format_mfa(mod, :start, args) <>
      " returned a bad value: " <> inspect(return)
  end

  defp impl_format_error({:already_started, app}) when is_atom(app) do
    "already started application #{app}"
  end

  defp impl_format_error({:not_started, app}) when is_atom(app) do
    "not started application #{app}"
  end

  defp impl_format_error({:bad_application, app}) do
    "bad application: #{inspect(app)}"
  end

  defp impl_format_error({:already_loaded, app}) when is_atom(app) do
    "already loaded application #{app}"
  end

  defp impl_format_error({:not_loaded, app}) when is_atom(app) do
    "not loaded application #{app}"
  end

  defp impl_format_error({:invalid_restart_type, restart}) do
    "invalid application restart type: #{inspect(restart)}"
  end

  defp impl_format_error({:invalid_name, name}) do
    "invalid application name: #{inspect(name)}"
  end

  defp impl_format_error({:invalid_options, opts}) do
    "invalid application options: #{inspect(opts)}"
  end

  defp impl_format_error({:badstartspec, spec}) do
    "bad application start specs: #{inspect(spec)}"
  end

  defp impl_format_error({'no such file or directory', file}) do
    "could not find application file: #{file}"
  end

  defp impl_format_error(reason) do
    Exception.format_exit(reason)
  end
end
