defmodule Credo.Code.Parameters do
  @moduledoc """
  This module provides helper functions to analyse the parameters taken by a
  function.
  """

  @def_ops [:def, :defp, :defmacro]

  @doc "Returns the parameter count for the given function's AST"
  def count(nil), do: 0

  for op <- @def_ops do
    def count({unquote(op), _, arguments}) when is_list(arguments) do
      case List.first(arguments) do
        {_atom, _meta, nil} ->
          0

        {:when, _when_meta, [{_fun, _meta, args} | _guards]} ->
          length(args || [])

        {_atom, _meta, list} ->
          Enum.count(list)

        _ ->
          0
      end
    end
  end

  @doc "Returns the names of all parameters for the given function's AST"
  def names(nil), do: nil

  for op <- @def_ops do
    def names({unquote(op), _meta, arguments}) when is_list(arguments) do
      arguments
      |> List.first()
      |> get_param_names
    end
  end

  defp get_param_names({:when, _meta, arguments}) do
    arguments
    |> List.first()
    |> get_param_names
  end

  defp get_param_names(arguments) when is_tuple(arguments) do
    arguments
    |> Tuple.to_list()
    |> List.last()
    |> Enum.map(&get_param_name/1)
    |> Enum.reject(&is_nil/1)
  end

  defp get_param_name({:"::", _, [var, _type]}) do
    get_param_name(var)
  end

  defp get_param_name({:<<>>, _, arguments}) do
    arguments
    |> Enum.map(&get_param_name/1)
    |> Enum.reject(&is_nil/1)
  end

  defp get_param_name({:=, _, arguments}) do
    arguments
    |> Enum.map(&get_param_name/1)
    |> Enum.reject(&is_nil/1)
  end

  defp get_param_name({:%, _, [{:__aliases__, _meta, _mod_list}, {:%{}, _meta2, arguments}]}) do
    get_param_name(arguments)
  end

  defp get_param_name({:%{}, _, arguments}) do
    get_param_name(arguments)
  end

  defp get_param_name({:\\, _, arguments}) do
    Enum.find_value(arguments, &get_param_name/1)
  end

  defp get_param_name(list) when is_list(list) do
    list
    |> Enum.map(fn {atom, tuple} when is_atom(atom) and is_tuple(tuple) ->
      get_param_name(tuple)
    end)
    |> Enum.reject(&is_nil/1)
  end

  defp get_param_name({name, _, nil}) when is_atom(name), do: name

  defp get_param_name(_) do
    nil
  end
end
