defmodule Mix.Tasks.Ejabberd.Deps.Tree do
  use Mix.Task

  alias Ejabberd.Config.EjabberdModule

  @shortdoc "Lists all ejabberd modules and their dependencies"

  @moduledoc """
  Lists all ejabberd modules and their dependencies.

  The project must have ejabberd as a dependency.
  """

  def run(_argv) do
    # First we need to start manually the store to be available
    # during the compilation of the config file.
    Ejabberd.Config.Store.start_link
    Ejabberd.Config.init(:ejabberd_config.path())

    Mix.shell.info "ejabberd modules"

    Ejabberd.Config.Store.get(:modules)
    |> Enum.reverse # Because of how mods are stored inside the store
    |> format_mods
    |> Mix.shell.info
  end

  defp format_mods(mods) when is_list(mods) do
    deps_tree = build_dependency_tree(mods)
    mods_used_as_dependency = get_mods_used_as_dependency(deps_tree)

    keep_only_mods_not_used_as_dep(deps_tree, mods_used_as_dependency)
    |> format_mods_into_string
  end

  defp build_dependency_tree(mods) do
    Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} ->
      deps = attrs[:dependency]
      build_dependency_tree(mods, mod, deps)
    end
  end

  defp build_dependency_tree(_mods, mod, []), do: %{module: mod, dependency: []}
  defp build_dependency_tree(mods, mod, deps) when is_list(deps) do
    dependencies = Enum.map deps, fn dep ->
      dep_deps = get_dependencies_of_mod(mods, dep)
      build_dependency_tree(mods, dep, dep_deps)
    end

    %{module: mod, dependency: dependencies}
  end

  defp get_mods_used_as_dependency(mods) when is_list(mods) do
    Enum.reduce mods, [], fn(mod, acc) ->
      case mod do
        %{dependency: []} -> acc
        %{dependency: deps} -> get_mod_names(deps) ++ acc
      end
    end
  end

  defp get_mod_names([]), do: []
  defp get_mod_names(mods) when is_list(mods), do: Enum.map(mods, &get_mod_names/1) |> List.flatten
  defp get_mod_names(%{module: mod, dependency: deps}), do: [mod | get_mod_names(deps)]

  defp keep_only_mods_not_used_as_dep(mods, mods_used_as_dep) do
    Enum.filter mods, fn %{module: mod} ->
      not (mod in mods_used_as_dep)
    end
  end

  defp get_dependencies_of_mod(deps, mod_name) do
    Enum.find(deps, &(Map.get(&1, :module) == mod_name))
    |> Map.get(:attrs)
    |> Keyword.get(:dependency)
  end

  defp format_mods_into_string(mods), do: format_mods_into_string(mods, 0)
  defp format_mods_into_string([], _indentation), do: ""
  defp format_mods_into_string(mods, indentation) when is_list(mods) do
    Enum.reduce mods, "", fn(mod, acc) ->
      acc <> format_mods_into_string(mod, indentation)
    end
  end

  defp format_mods_into_string(%{module: mod, dependency: deps}, 0) do
    "\n├── #{mod}" <> format_mods_into_string(deps,  2)
  end

  defp format_mods_into_string(%{module: mod, dependency: deps}, indentation) do
    spaces = Enum.reduce 0..indentation, "", fn(_, acc) -> " " <> acc end
    "\n│#{spaces}└── #{mod}" <> format_mods_into_string(deps, indentation + 4)
  end
end
