defmodule Ejabberd.Config do
  @moduledoc """
  Base module for configuration file.

  Imports macros for the config DSL and contains functions
  for working/starting the configuration parsed.
  """

  alias Ejabberd.Config.EjabberdModule
  alias Ejabberd.Config.Attr
  alias Ejabberd.Config.EjabberdLogger

  defmacro __using__(_opts) do
    quote do
      import Ejabberd.Config, only: :macros
      import Ejabberd.Logger

      @before_compile Ejabberd.Config
    end
  end

  # Validate the modules parsed and log validation errors at compile time.
  # Could be also possible to interrupt the compilation&execution by throwing
  # an exception if necessary.
  def __before_compile__(_env) do
    get_modules_parsed_in_order()
    |> EjabberdModule.validate
    |> EjabberdLogger.log_errors
  end

  @doc """
  Given the path of the config file, it evaluates it.
  """
  def init(file_path, force \\ false) do
    init_already_executed = Ejabberd.Config.Store.get(:module_name) != []

    case force do
      true ->
        Ejabberd.Config.Store.stop()
        Ejabberd.Config.Store.start_link()
        do_init(file_path)
      false ->
        if not init_already_executed, do: do_init(file_path)
    end
  end

  @doc """
  Returns a list with all the opts, formatted for ejabberd.
  """
  def get_ejabberd_opts do
    get_general_opts()
    |> Map.put(:modules, get_modules_parsed_in_order())
    |> Map.put(:listeners, get_listeners_parsed_in_order())
    |> Ejabberd.Config.OptsFormatter.format_opts_for_ejabberd
  end

  @doc """
  Register the hooks defined inside the elixir config file.
  """
  def start_hooks do
    get_hooks_parsed_in_order()
    |> Enum.each(&Ejabberd.Config.EjabberdHook.start/1)
  end

  ###
  ### MACROS
  ###

  defmacro listen(module, do: block) do
    attrs = Attr.extract_attrs_from_block_with_defaults(block)

    quote do
      Ejabberd.Config.Store.put(:listeners, %EjabberdModule{
        module: unquote(module),
        attrs: unquote(attrs)
      })
    end
  end

  defmacro module(module, do: block) do
    attrs = Attr.extract_attrs_from_block_with_defaults(block)

    quote do
      Ejabberd.Config.Store.put(:modules, %EjabberdModule{
        module: unquote(module),
        attrs: unquote(attrs)
      })
    end
  end

  defmacro hook(hook_name, opts, fun) do
    quote do
      Ejabberd.Config.Store.put(:hooks, %Ejabberd.Config.EjabberdHook{
        hook: unquote(hook_name),
        opts: unquote(opts),
        fun: unquote(fun)
      })
    end
  end

  # Private API

  defp do_init(file_path) do
    # File evaluation
    Code.eval_file(file_path) |> extract_and_store_module_name()

    # Getting start/0 config
    [module] = Ejabberd.Config.Store.get(:module_name)
    call_start_func_and_store_data(module)

    # Fetching git modules and install them
    get_modules_parsed_in_order()
    |> EjabberdModule.fetch_git_repos
  end

  # Returns the modules from the store
  defp get_modules_parsed_in_order,
    do: Ejabberd.Config.Store.get(:modules) |> Enum.reverse

  # Returns the listeners from the store
  defp get_listeners_parsed_in_order,
    do: Ejabberd.Config.Store.get(:listeners) |> Enum.reverse

  defp get_hooks_parsed_in_order,
    do: Ejabberd.Config.Store.get(:hooks) |> Enum.reverse

  # Returns the general config options
  defp get_general_opts,
    do: Ejabberd.Config.Store.get(:general) |> List.first

  # Gets the general ejabberd options calling
  # the start/0 function and stores them.
  defp call_start_func_and_store_data(module) do
    opts = apply(module, :start, [])
    Ejabberd.Config.Store.put(:general, opts)
  end

  # Stores the configuration module name
  defp extract_and_store_module_name({{:module, mod, _bytes, _}, _}) do
    Ejabberd.Config.Store.put(:module_name, mod)
  end
end
