defmodule Patch.Mock.Code.Generators.Facade do
  @moduledoc """
  Generator for `facade` modules.

  `facade` modules are generated by taking the `target` module and creating a stub function for
  each function in the module that calls the `delegate` modules corresponding function.

  The `facade` module can optionally expose private functions as public.
  """

  alias Patch.Mock.Naming
  alias Patch.Mock.Code
  alias Patch.Mock.Code.Transform

  @generated [generated: true]

  @doc """
  Generates a new facade module based on the forms of the provided module.
  """
  @spec generate(
          abstract_forms :: [Code.form()],
          module :: module(),
          exports :: Code.exports()
        ) :: [Code.form()]
  def generate(abstract_forms, module, exports) do
    abstract_forms
    |> Transform.clean()
    |> Transform.export(exports)
    |> Transform.filter(exports)
    |> module(module)
  end

  ## Private

  defp arguments(0) do
    []
  end

  defp arguments(arity) do
    Enum.map(1..arity, &{:var, @generated, :"_arg#{&1}"})
  end

  defp body(module, name, arity) do
    delegate = Naming.delegate(module)

    [
      {
        :call,
        @generated,
        {:remote, @generated, {:atom, @generated, delegate}, {:atom, @generated, name}},
        arguments(arity)
      }
    ]
  end

  defp function(module, name, arity) do
    clause = {
      :clause,
      @generated,
      patterns(arity),
      [],
      body(module, name, arity)
    }

    {:function, @generated, name, arity, [clause]}
  end

  @spec module(abstract_forms :: [Code.form()], module :: module()) ::
          [Code.form()]
  defp module(abstract_forms, module) do
    Enum.map(abstract_forms, fn
      {:function, _, name, arity, _} ->
        function(module, name, arity)

      other ->
        other
    end)
  end

  defp patterns(0) do
    []
  end

  defp patterns(arity) do
    Enum.map(1..arity, fn position ->
      {:var, @generated, :"_arg#{position}"}
    end)
  end
end
