defmodule Koans do
  @moduledoc false
  defp valid_name(name) do
    Regex.match?(~r/([A-Z]|\.\.\.).+/, name)
  end

  defmacro koan(name, do: body) do
    if not valid_name(name) do
      raise "Name does not start with a capital letter: #{name}"
    end

    compiled_name = String.to_atom(name)
    number_of_args = Blanks.count(body)
    compiled_body = Blanks.replace_line(body, &blank_line_replacement/1)

    quote do
      @koans unquote(compiled_name)

      generate_test_method(unquote(compiled_name), unquote(number_of_args), unquote(body))

      def unquote(compiled_name)() do
        unquote(compiled_body)
        :ok
      rescue
        e -> {:error, __STACKTRACE__, e}
      end
    end
  end

  defmacro generate_test_method(_name, 0, _body), do: false

  defmacro generate_test_method(name, 1, body) do
    single_var = Blanks.replace(body, Macro.var(:answer, __MODULE__))

    quote do
      def unquote(name)(answer) do
        unquote(single_var)
        :ok
      rescue
        e -> {:error, __STACKTRACE__, e}
      end
    end
  end

  defmacro generate_test_method(name, number_of_args, body) do
    answer_vars =
      for id <- 1..number_of_args, do: Macro.var(String.to_atom("answer#{id}"), __MODULE__)

    multi_var = Blanks.replace(body, answer_vars)

    quote do
      def unquote(name)({:multiple, unquote(answer_vars)}) do
        unquote(multi_var)
        :ok
      rescue
        e -> {:error, __STACKTRACE__, e}
      end
    end
  end

  defp blank_line_replacement({:assert, _meta, [expr]}) do
    code = Macro.escape(expr)
    quote do: raise(ExUnit.AssertionError, expr: unquote(code))
  end

  defp blank_line_replacement(line) do
    code = Macro.escape(line)
    quote do: raise(ExUnit.AssertionError, expr: unquote(code))
  end

  defmacro __using__(_opts) do
    quote do
      @compile :nowarn_unused_vars
      Module.register_attribute(__MODULE__, :koans, accumulate: true)

      require ExUnit.Assertions
      import ExUnit.Assertions
      import Koans

      @before_compile Koans
    end
  end

  defmacro __before_compile__(env) do
    koans = koans(env)

    quote do
      def all_koans do
        unquote(koans)
      end

      def intro, do: @intro
    end
  end

  defp koans(env) do
    env.module
    |> Module.get_attribute(:koans)
    |> Enum.reverse()
  end
end
