defmodule Quark.Curry do
  @moduledoc ~S"""
  [Currying](https://en.wikipedia.org/wiki/Currying) breaks up a function into a
  series of unary functions that apply their arguments to some inner
  n-ary function. This is a convenient way to achieve a general and flexible
  partial application on any curried function.
  """

  @doc ~S"""
  Curry a function at runtime, rather than upon definition

  ## Examples

      iex> curried_reduce_3 = curry &Enum.reduce/3
      ...> {_, arity} = :erlang.fun_info(curried_reduce_3, :arity)
      ...> arity
      1

      iex> curried_reduce_3 = curry &Enum.reduce/3
      ...> curried_reduce_3.([1,2,3]).(42).(&(&1 + &2))
      48

  """
  @spec curry(fun) :: fun
  def curry(fun) do
    {_, arity} = :erlang.fun_info(fun, :arity)
    curry(fun, arity, [])
  end

  @spec curry(fun, integer, [any]) :: fun
  defp curry(fun, 0, arguments), do: apply(fun, Enum.reverse(arguments))
  defp curry(fun, arity, arguments) do
    fn arg -> curry(fun, arity - 1, [arg | arguments]) end
  end

  @doc ~S"""
  Convert a curried function to a function on pairs

  ## Examples

      iex> curried_add = fn x -> (fn y -> x + y end) end
      iex> add = uncurry curried_add
      iex> add.(1,2)
      3

  """
  @spec uncurry((any -> fun)) :: ((any, any) -> any)
  def uncurry(fun), do: &(fun.(&1).(&2))

  @doc ~S"""
  Apply one or more arguments to a curried function

  ## Examples

      iex> curried_add = fn x -> (fn y -> x + y end) end
      ...> uncurry(curried_add, [1,2])
      3

      iex> add_one = &(&1 + 1)
      ...> uncurry(add_one, 1)
      2

      iex> curried_add = fn x -> (fn y -> x + y end) end
      ...> add_one = uncurry(curried_add, 1)
      ...> add_one.(3)
      4

  """
  @spec uncurry(fun, any | [any]) :: any
  def uncurry(fun, arg_list) when is_list(arg_list) do
    Enum.reduce(arg_list, fun, fn (arg, f) -> Kernel.apply(f, [arg]) end)
  end

  def uncurry(fun, arg), do: fun.(arg)

  @doc "Define a curried function"
  defmacro defcurry(head, do: body) do
    {fun_name, ctx, args} = head

    quote do
      def unquote({fun_name, ctx, []}), do: unquote(wrap(args, body))
    end
  end

  @doc "Define a curried private function"
  defmacro defcurryp(head, do: body) do
    {fun_name, ctx, args} = head

    quote do
      defp unquote({fun_name, ctx, []}), do: unquote(wrap(args, body))
    end
  end

  defp wrap([arg|args], body) do
    quote do
      fn unquote(arg) ->
        unquote(wrap(args, body))
      end
    end
  end

  defp wrap([], body), do: body
end
