## This Source Code Form is subject to the terms of the Mozilla Public
## License, v. 2.0. If a copy of the MPL was not distributed with this
## file, You can obtain one at https://mozilla.org/MPL/2.0/.
##
## Copyright (c) 2007-2020 VMware, Inc. or its affiliates.  All rights reserved.

defmodule RabbitMQ.CLI.CommandBehaviour do
  alias RabbitMQ.CLI.Core.Helpers

  @type pair_of_strings :: nonempty_list(String.t())

  @callback usage() :: String.t() | [String.t()]
  # validates CLI arguments
  @callback validate(list(), map()) :: :ok | {:validation_failure, atom() | {atom(), String.t()}}
  @callback merge_defaults(list(), map()) :: {list(), map()}
  @callback banner(list(), map()) :: [String.t()] | String.t() | nil
  @callback run(list(), map()) :: any
  # Coerces run/2 return value into the standard command output form
  # that is then formatted, printed and returned as an exit code.
  # There is a default implementation for this callback in DefaultOutput module
  @callback output(any, map()) ::
              :ok
              | {:ok, any}
              | {:stream, Enum.t()}
              | {:error, RabbitMQ.CLI.Core.ExitCodes.exit_code(), [String.t()]}

  @optional_callbacks formatter: 0,
                      printer: 0,
                      scopes: 0,
                      usage_additional: 0,
                      usage_doc_guides: 0,
                      description: 0,
                      help_section: 0,
                      switches: 0,
                      aliases: 0,
                      # validates execution environment, e.g. file presence,
                      # whether RabbitMQ is in an expected state on a node, etc
                      validate_execution_environment: 2,
                      distribution: 1

  @callback validate_execution_environment(list(), map()) ::
              :ok | {:validation_failure, atom() | {atom(), any}}
  @callback switches() :: Keyword.t()
  @callback aliases() :: Keyword.t()

  @callback formatter() :: atom()
  @callback printer() :: atom()
  @callback scopes() :: [atom()] | nil
  @callback description() :: String.t()
  @callback help_section() :: String.t()
  @callback usage_additional() :: String.t() | [String.t()] | nonempty_list(pair_of_strings()) | [{String.t(), String.t()}]
  @callback usage_doc_guides() :: String.t() | [String.t()]
  ## Erlang distribution control
  ## :cli - default rabbitmqctl generated node name
  ## :none - disable erlang distribution
  ## {:fun, fun} - use a custom function to start distribution
  @callback distribution(map()) :: :cli | :none | {:fun, (map() -> :ok | {:error, any()})}

  defmacro defcmd(map) do
    usage_q = case map[:usage] do
      nil -> :ok
      usage ->
        quote do def usage(), do: unquote(usage) end
    end
    scopes_q = case map[:scopes] do
      nil -> :ok
      scopes ->
        quote do def scopes(), do: unquote(scopes) end
    end
    description_q = case map[:description] do
      nil -> :ok
      description ->
        quote do def description(), do: unquote(description) end
    end
    help_section_q = case map[:help_section] do
      nil -> :ok
      help_section ->
        quote do def help_section(), do: unquote(help_section) end
    end
    usage_additional_q = case map[:usage_additional] do
      nil -> :ok
      usage_additional ->
        quote do def usage_additional(), do: unquote(usage_additional) end
    end
    formatter_q = case map[:formatter] do
      nil -> :ok
      formatter ->
        quote do def formatter(), do: unquote(formatter) end
    end
    switches_q = case map[:switches] do
      nil -> :ok
      switches ->
        quote do def switches(), do: unquote(switches) end
    end
    aliases_q = case map[:aliases] do
      nil -> :ok
      aliases ->
        quote do def aliases(), do: unquote(aliases) end
    end

    quote do
      unquote(usage_q)
      unquote(scopes_q)
      unquote(description_q)
      unquote(help_section_q)
      unquote(usage_additional_q)
      unquote(formatter_q)
      unquote(switches_q)
      unquote(aliases_q)
    end
  end

  def usage(cmd) do
    cmd.usage()
  end

  def scopes(cmd) do
    Helpers.apply_if_exported(cmd, :scopes, [], nil)
  end

  def description(cmd) do
    Helpers.apply_if_exported(cmd, :description, [], "")
  end

  def help_section(cmd) do
    case Helpers.apply_if_exported(cmd, :help_section, [], :other) do
      :other ->
        case Application.get_application(cmd) do
          :rabbitmqctl -> :other
          plugin -> {:plugin, plugin}
        end
      section ->
        section
    end
  end

  def usage_additional(cmd) do
    Helpers.apply_if_exported(cmd, :usage_additional, [], [])
  end

  def usage_doc_guides(cmd) do
    Helpers.apply_if_exported(cmd, :usage_doc_guides, [], [])
  end

  def formatter(cmd, default) do
    Helpers.apply_if_exported(cmd, :formatter, [], default)
  end

  def printer(cmd, default) do
    Helpers.apply_if_exported(cmd, :printer, [], default)
  end

  def switches(cmd) do
    Helpers.apply_if_exported(cmd, :switches, [], [])
  end

  def aliases(cmd) do
    Helpers.apply_if_exported(cmd, :aliases, [], [])
  end

  def validate_execution_environment(cmd, args, options) do
    Helpers.apply_if_exported(cmd,
                              :validate_execution_environment, [args, options],
                              :ok)
  end

  def distribution(cmd, options) do
    Helpers.apply_if_exported(cmd, :distribution, [options], :cli)
  end
end
