defmodule Supervisor.Spec do
  @moduledoc """
  Convenience functions for defining supervisor specifications.

  ## Example

  By using the functions in this module one can specify the children
  to be used under a supervisor, started with `Supervisor.start_link/2`:

      import Supervisor.Spec

      children = [
        worker(MyWorker, [arg1, arg2, arg3]),
        supervisor(MySupervisor, [arg1])
      ]

      Supervisor.start_link(children, strategy: :one_for_one)

  Sometimes, it may be handy to define supervisors backed
  by a module:

      defmodule MySupervisor do
        use Supervisor

        def start_link(arg) do
          Supervisor.start_link(__MODULE__, arg)
        end

        def init(arg) do
          children = [
            worker(MyWorker, [arg], restart: :temporary)
          ]

          supervise(children, strategy: :simple_one_for_one)
        end
      end

  Notice in this case we don't have to explicitly import
  `Supervisor.Spec` as `use Supervisor` automatically does so.
  Defining a module-based supervisor can be useful, for example,
  to perform initialization tasks in the `init/1` callback.

  ## Supervisor and worker options

  In the example above, we defined specs for workers and supervisors.
  These specs (both for workers as well as supervisors) accept the
  following options:

    * `:id` - a name used to identify the child specification
      internally by the supervisor; defaults to the given module
      name for the child worker/supervisor

    * `:function` - the function to invoke on the child to start it

    * `:restart` - an atom that defines when a terminated child process should
      be restarted (see the "Restart values" section below)

    * `:shutdown` - an atom that defines how a child process should be
      terminated (see the "Shutdown values" section below)

    * `:modules` - it should be a list with one element `[module]`,
      where module is the name of the callback module only if the
      child process is a `Supervisor` or `GenServer`; if the child
      process is a `GenEvent`, `:modules` should be `:dynamic`

  ### Restart values (:restart)

  The following restart values are supported in the `:restart` option:

    * `:permanent` - the child process is always restarted

    * `:temporary` - the child process is never restarted (not even
      when the supervisor's strategy is `:rest_for_one` or `:one_for_all`)

    * `:transient` - the child process is restarted only if it
      terminates abnormally, i.e., with an exit reason other than
      `:normal`, `:shutdown` or `{:shutdown, term}`

  ### Shutdown values (:shutdown)

  The following shutdown values are supported in the `:shutdown` option:

    * `:brutal_kill` - the child process is unconditionally terminated
      using `exit(child, :kill)`

    * `:infinity` - if the child process is a supervisor, this is a mechanism
      to give the subtree enough time to shutdown; it can also be used with
      workers with care

    * any integer - the value of `:shutdown` can also be any integer meaning
      that the supervisor tells the child process to terminate by calling
      `Process.exit(child, :shutdown)` and then waits for an exit signal back.
      If no exit signal is received within the specified time (the value of this
      option, in milliseconds), the child process is unconditionally terminated
      using `Process.exit(child, :kill)`

  """

  # TODO: Update and provide a digest of strategies once we include DynamicSupervisor.

  @typedoc "Supported strategies"
  @type strategy :: :simple_one_for_one | :one_for_one | :one_for_all | :rest_for_one

  @typedoc "Supported restart values"
  @type restart :: :permanent | :transient | :temporary

  @typedoc "Supported shutdown values"
  @type shutdown :: :brutal_kill | :infinity | non_neg_integer

  @typedoc "Supported worker values"
  @type worker :: :worker | :supervisor

  @typedoc "Supported module values"
  @type modules :: :dynamic | [module]

  @typedoc "Supported id values"
  @type child_id :: term

  @typedoc "The supervisor specification"
  @type spec :: {child_id,
                  start_fun :: {module, atom, [term]},
                  restart,
                  shutdown,
                  worker,
                  modules}

  @doc """
  Receives a list of children (workers or supervisors) to
  supervise and a set of options.

  Returns a tuple containing the supervisor specification. This tuple can be
  used as the return value of the `init/1` callback when implementing a
  module-based supervisor.

  ## Examples

      supervise(children, strategy: :one_for_one)

  ## Options

    * `:strategy` - the restart strategy option. It can be either
      `:one_for_one`, `:rest_for_one`, `:one_for_all`, or
      `:simple_one_for_one`. You can learn more about strategies
      in the `Supervisor` module docs.

    * `:max_restarts` - the maximum amount of restarts allowed in
      a time frame. Defaults to `3`.

    * `:max_seconds` - the time frame in which `:max_restarts` applies.
      Defaults to `5`.

  The `:strategy` option is required and by default a maximum of 3 restarts is
  allowed within 5 seconds. Check the `Supervisor` module for a detailed
  description of the available strategies.
  """
  @spec supervise([spec], strategy: strategy,
                          max_restarts: non_neg_integer,
                          max_seconds: non_neg_integer) :: {:ok, tuple}
  # TODO: Make it return a tuple of format {:ok, children, opts}
  # TODO: Deprecate once the new tuple format has been established
  def supervise(children, options) do
    unless strategy = options[:strategy] do
      raise ArgumentError, "expected :strategy option to be given"
    end

    maxR = Keyword.get(options, :max_restarts, 3)
    maxS = Keyword.get(options, :max_seconds, 5)

    assert_unique_ids(Enum.map(children, &elem(&1, 0)))
    {:ok, {{strategy, maxR, maxS}, children}}
  end

  defp assert_unique_ids([id | rest]) do
    if id in rest do
      raise ArgumentError,
        "duplicated id #{inspect id} found in the supervisor specification, " <>
        "please explicitly pass the :id option when defining this worker/supervisor"
    else
      assert_unique_ids(rest)
    end
  end

  defp assert_unique_ids([]) do
    :ok
  end

  @doc """
  Defines the given `module` as a worker which will be started
  with the given arguments.

      worker(ExUnit.Runner, [], restart: :permanent)

  By default, the function `start_link` is invoked on the given
  module. Overall, the default values for the options are:

      [id: module,
       function: :start_link,
       restart: :permanent,
       shutdown: 5000,
       modules: [module]]

  Check the documentation for the `Supervisor.Spec` module for more
  information on the options.
  """
  @spec worker(module, [term], [restart: restart, shutdown: shutdown,
                                id: term, function: atom, modules: modules]) :: spec
  def worker(module, args, options \\ []) do
    child(:worker, module, args, options)
  end

  @doc """
  Defines the given `module` as a supervisor which will be started
  with the given arguments.

      supervisor(ExUnit.Runner, [], restart: :permanent)

  By default, the function `start_link` is invoked on the given
  module. Overall, the default values for the options are:

      [id: module,
       function: :start_link,
       restart: :permanent,
       shutdown: :infinity,
       modules: [module]]

  Check the documentation for the `Supervisor.Spec` module for more
  information on the options.
  """
  @spec supervisor(module, [term], [restart: restart, shutdown: shutdown,
                                    id: term, function: atom, modules: modules]) :: spec
  def supervisor(module, args, options \\ []) do
    options = Keyword.put_new(options, :shutdown, :infinity)
    child(:supervisor, module, args, options)
  end

  # TODO: Do and expose proper child validation
  defp child(type, module, args, options) do
    id       = Keyword.get(options, :id, module)
    modules  = Keyword.get(options, :modules, modules(module))
    function = Keyword.get(options, :function, :start_link)
    restart  = Keyword.get(options, :restart, :permanent)
    shutdown = Keyword.get(options, :shutdown, 5000)

    {id, {module, function, args},
      restart, shutdown, type, modules}
  end

  # TODO: Remove GenEvent when there is no more GenEvent v2.0
  defp modules(GenEvent), do: :dynamic
  defp modules(module),   do: [module]
end
