defmodule Croma.Defun do
  @moduledoc """
  Module that provides `Croma.Defun.defun/2` macro.
  """

  @doc """
  Defines a function together with its typespec.

  This provides a lighter-weight syntax for functions with type specifications and functions with multiple clauses.

  ## Example

  The following examples assume that `Croma.Defun` is imported
  (you can import it by `use Croma`).

      defun f(a :: integer, b :: String.t) :: String.t do
        "\#{a} \#{b}"
      end

  The code above is expanded to the following function definition.

      @spec f(integer, String.t) :: String.t
      def f(a, b) do
        "\#{a} \#{b}"
      end

  Function with multiple clauses and/or pattern matching on parameters can be defined
  in the same way as `case do ... end`:

      defun dumbmap(as :: [a], f :: (a -> b)) :: [b] when a: term, b: term do
        ([]     , _) -> []
        ([h | t], f) -> [f.(h) | dumbmap(t, f)]
      end

  is converted to

      @spec dumbmap([a], (a -> b)) :: [b] when a: term, b: term
      def dumbmap(as, f)
      def dumbmap([], _) do
        []
      end
      def dumbmap([h | t], f) do
        [f.(h) | dumbmap(t, f)]
      end

  ## Pattern matching on function parameter and omitting parameter's type

  If you omit parameter's type, its type is infered from the parameter's expression.
  Suppose we have the following function:

      defun f(%MyStruct{field1: field1, field2: field2}) :: String.t do
        "\#{field1} \#{field2}"
      end

  then the parameter type becomes `MyStruct.t`.

      @spec f(MyStruct.t) :: String.t
      def f(a1)
      def f(%MyStruct{field1: field1, field2: field2}) do
        "\#{field1} \#{field2}"
      end

  ## Generating guards from argument types

  Simple guard expressions can be generated by `defun/2` using `g[type]` syntax.
  For example,

      defun f(s :: g[String.t], i :: g[integer]) :: String.t do
        "\#{s} \#{i}"
      end

  is converted to the following function with `when is_integer(i)` guard.

      @spec f(String.t, integer) :: String.t
      def f(s, i)
      def f(s, i) when is_binary(s) and is_integer(i) do
        "\#{s} \#{i}"
      end

  For supported types of guard-generation please refer to the source code of `Croma.Guard.make/3`.

  Guard generation can be disabled by setting application config at compile time.
  For example, by putting the following into `config/config.exs`,

      config :croma, [
        defun_generate_guard: false
      ]

  then `g[String.t]` becomes semantically the same as `String.t`.

  ## Validating arguments and return value based on their types

  You can instrument check of pre/post conditions by specifying type as `v[type]`.
  For instance,

      defmodule MyString do
        use Croma.SubtypeOfString, pattern: ~r/^foo|bar$/
      end

      defun f(s :: v[MyString.t]) :: atom do
        String.to_atom(s)
      end

  becomes the following function definition that calls `valid?/1` at the top of its body:

      @spec f(MyString.t) :: atom
      def f(s)
      def f(s) do
        if !MyString.valid?(s) do
          raise "..."
        end
        String.to_atom(s)
      end

  The generated code assumes that `valid?/1` function is defined in the type module of the specified type.
  For primitive types croma defines their type modules and thus you can freely use e.g. `v[integer]`.

  Generating validation of arguments and return values can be disabled by setting application config during compilation.

      config :croma, [
        defun_generate_validation: false
      ]

  ## Known limitations
  - Overloaded typespecs are not supported.
  - Guard generation and validation are not allowed to be used with multi-clause syntax.
  - Using unquote fragment in parameter list is not fully supported.
  - `try` block is not implicitly started in body of `defun`, in contrast to `def`.
  """
  defmacro defun({:"::", _, [fun, ret]}, [do: block]) do
    defun_impl(:def, fun, ret, [], block, __CALLER__)
  end
  defmacro defun({:when, _, [{:"::", _, [fun, ret]}, type_params]}, [do: block]) do
    defun_impl(:def, fun, ret, type_params, block, __CALLER__)
  end
  defmacro defun(_, _) do
    raise %SyntaxError{description: "syntax error in usage of defun", file: __CALLER__.file, line: __CALLER__.line}
  end

  @doc """
  Defines a private function together with its typespec.

  See `defun/2` for usage of this macro.
  """
  defmacro defunp({:"::", _, [fun, ret]}, [do: block]) do
    defun_impl(:defp, fun, ret, [], block, __CALLER__)
  end
  defmacro defunp({:when, _, [{:"::", _, [fun, ret]}, type_params]}, [do: block]) do
    defun_impl(:defp, fun, ret, type_params, block, __CALLER__)
  end
  defmacro defunp(_, _) do
    raise %SyntaxError{description: "syntax error in usage of defunp", file: __CALLER__.file, line: __CALLER__.line}
  end

  @doc """
  Defines a unit-testable private function together with its typespec.

  See `defun/2` for usage of this macro.
  See also `Croma.Defpt.defpt/2`.
  """
  defmacro defunpt({:"::", _, [fun, ret]}, [do: block]) do
    defun_impl(:defpt, fun, ret, [], block, __CALLER__)
  end
  defmacro defunpt({:when, _, [{:"::", _, [fun, ret]}, type_params]}, [do: block]) do
    defun_impl(:defpt, fun, ret, type_params, block, __CALLER__)
  end
  defmacro defunpt(_, _) do
    raise %SyntaxError{description: "syntax error in usage of defunpt", file: __CALLER__.file, line: __CALLER__.line}
  end

  defmodule Arg do
    @moduledoc false
    defstruct [:arg_expr, :type, :default, :guard?, :validate?, :index]

    def new({:\\, _, [inner_expr, default]}, index) do
      %__MODULE__{new(inner_expr, index) | default: {:some, default}}
    end
    def new({:"::", _, [arg_expr, type_expr]}, index) do
      {type_expr2, g_used?, v_used?} = extract_guard_and_validate(type_expr)
      guard?    = g_used? and Application.get_env(:croma, :defun_generate_guard     , true)
      validate? = v_used? and Application.get_env(:croma, :defun_generate_validation, true)
      %__MODULE__{arg_expr: arg_expr, type: type_expr2, default: :none, guard?: guard?, validate?: validate?, index: index}
    end
    def new(arg_expr, index) do
      %__MODULE__{arg_expr: arg_expr, type: infer_type(arg_expr), default: :none, guard?: false, validate?: false, index: index}
    end

    defp extract_guard_and_validate({{:., _, [Access, :get]}, _, [{:g, _, _}, inner_expr]}), do: {inner_expr, true , false}
    defp extract_guard_and_validate({{:., _, [Access, :get]}, _, [{:v, _, _}, inner_expr]}), do: {inner_expr, false, true }
    defp extract_guard_and_validate(type_expr                                             ), do: {type_expr , false, false}

    defp infer_type(arg_expr) do
      case arg_expr do
        {:=, _, [{_, _, c}, inner]} when is_atom(c) -> infer_type(inner)
        {:=, _, [inner, {_, _, c}]} when is_atom(c) -> infer_type(inner)
        {_name, _, c}               when is_atom(c) -> quote do: any
        {:{}, _, elements}                          -> quote do: {unquote_splicing(Enum.map(elements, &infer_type/1))}
        {elem1, elem2}                              -> quote do: {unquote(infer_type(elem1)), unquote(infer_type(elem2))}
        l when is_list(l)                           -> quote do: []
        {:%{}, _, _}                                -> quote do: map
        {:%, _, [module_alias, _]}                  -> quote do: unquote(module_alias).t
        expr when is_atom(expr)                     -> expr
        expr when is_integer(expr)                  -> quote do: integer
        expr when is_float(expr)                    -> quote do: float
        expr when is_binary(expr)                   -> if String.valid?(expr), do: (quote do: String.t), else: (quote do: binary)
        _                                           -> quote do: any
      end
    end

    defp var_name(arg_expr) do
      case arg_expr do
        {name, _, context}               when is_atom(context) -> name
        {:=, _, [{name, _, context}, _]} when is_atom(context) -> name
        {:=, _, [_, {name, _, context}]} when is_atom(context) -> name
        _                                                      -> nil
      end
    end

    def as_var(%__MODULE__{arg_expr: arg_expr}) do
      case var_name(arg_expr) do
        nil  -> nil
        name -> Macro.var(name, nil)
      end
    end
    defp as_var!(%__MODULE__{arg_expr: arg_expr} = arg) do
      as_var(arg) || raise "parameter `#{Macro.to_string(arg_expr)}` is not a var"
    end

    def make_arg_expr(%__MODULE__{arg_expr: arg_expr, guard?: g?, validate?: v?, index: index}) do
      # For an underscored function parameter with guard/validation,
      # we have to bind it to another variable and use that variable in guard/validation
      # expression in order not to touch the underscored parameter.
      case var_name(arg_expr) |> Atom.to_string() do
        "_" <> _ when g? or v? ->
          var2 = Macro.var(:"croma_arg#{index}", nil)
          quote do
            unquote(arg_expr) = unquote(var2)
          end
        _ ->
          arg_expr
      end
    end

    def reassignment_expr(%__MODULE__{guard?: g?, validate?: v?} = arg) do
      # To enable compiler warning about unused function parameter,
      # we re-assign the variable to the same name after guard/validation check.
      if g? or v? do
        case as_var(arg) do
          nil                    -> nil
          {var_name, _, _} = var ->
            if Atom.to_string(var_name) |> String.starts_with?("_") do
              nil
            else
              quote do
                unquote(var) = unquote(var)
              end
            end
        end
      else
        nil
      end
    end

    def guard_expr(%__MODULE__{guard?: false}, _), do: nil
    def guard_expr(%__MODULE__{guard?: true, type: type, index: index} = arg, caller) do
      {var_name, _, _} = var = as_var!(arg)
      case Atom.to_string(var_name) do
        "_" <> _ -> Croma.Guard.make(type, Macro.var(:"croma_arg#{index}", nil), caller)
        _        -> Croma.Guard.make(type, var, caller)
      end
    end

    def validation_expr(%__MODULE__{validate?: false}, _), do: nil
    def validation_expr(%__MODULE__{validate?: true, type: type, index: index} = arg, caller) do
      {var_name, _, _} = var = as_var!(arg)
      case Atom.to_string(var_name) do
        "_" <> _ -> Croma.Validation.make(type, Macro.var(:"croma_arg#{index}", nil), caller)
        _        -> Croma.Validation.make(type, var, caller)
      end
    end
  end

  defmodule Ret do
    @moduledoc false
    defstruct [:type, :guard?, :validate?]

    def new(expr) do
      {type_expr, g_used?, v_used?} = extract(expr)
      guard?    = g_used? and Application.get_env(:croma, :defun_generate_guard     , true)
      validate? = v_used? and Application.get_env(:croma, :defun_generate_validation, true)
      %__MODULE__{type: type_expr, guard?: guard?, validate?: validate?}
    end

    defp extract({{:., _, [Access, :get]}, _, [{:g, _, _}, inner_expr]}), do: {inner_expr, true , false}
    defp extract({{:., _, [Access, :get]}, _, [{:v, _, _}, inner_expr]}), do: {inner_expr, false, true }
    defp extract(expr                                                  ), do: {expr      , false, false}

    def wrap_body_with_return_value_check(ret, body, caller) do
      cond do
        ret.guard?    -> wrap_body_with_guard(ret, body, caller)
        ret.validate? -> wrap_body_with_validation(ret, body, caller)
        :otherwise    -> body
      end
    end

    defp wrap_body_with_guard(%__MODULE__{type: type, guard?: true}, body, caller) do
      guard_expr = Croma.Guard.make(type, Macro.var(:return_value, __MODULE__), caller)
      quote do
        case unquote(body) do
          return_value when unquote(guard_expr) -> return_value
        end
      end
    end

    defp wrap_body_with_validation(%__MODULE__{type: type, validate?: true}, body, caller) do
      validation_expr = Croma.Validation.make(type, Macro.var(:return_value, __MODULE__), caller)
      quote do
        return_value = unquote(body)
        unquote(validation_expr)
        return_value
      end
    end
  end

  defp defun_impl(def_or_defp, {fname, env, args0}, ret0, type_params, block, caller) do
    args = case args0 do
      context when is_atom(context) -> [] # function definition without parameter list
      _ -> Enum.with_index(args0) |> Enum.map(fn {arg, i} -> Arg.new(arg, i) end)
    end
    ret = Ret.new(ret0)
    {:__block__, [], [
        typespec(fname, env, args, ret, type_params),
        bodyless_function(def_or_defp, fname, env, args),
        function_definition(def_or_defp, fname, env, args, ret, block, caller),
      ]}
  end

  defp typespec(fname, env, args, ret, type_params) do
    arg_types = Enum.map(args, &(&1.type))
    func_with_return_type = {:"::", [], [{fname, [], arg_types}, ret.type]}
    spec_expr = case type_params do
      [] -> func_with_return_type
      _  -> {:when, [], [func_with_return_type, type_params]}
    end
    {:@, env, [
        {:spec, [], [spec_expr]}
      ]}
  end

  defp bodyless_function(def_or_defp, fname, env, args) do
    arg_exprs = Enum.with_index(args) |> Enum.map(fn {%Arg{default: default} = arg, index} ->
      var = Arg.as_var(arg) || Macro.var(:"arg#{Integer.to_string(index)}", nil)
      case default do
        :none            -> var
        {:some, default} -> {:\\, [], [var, default]}
      end
    end)
    {def_or_defp, env, [{fname, env, arg_exprs}]}
  end

  defp function_definition(def_or_defp, fname, env, args, ret, block, caller) do
    defs = case block do
      {:__block__, _, multiple_defs} -> multiple_defs
      single_def                     -> List.wrap(single_def)
    end
    if !Enum.empty?(defs) and Enum.all?(defs, &pattern_match_expr?/1) do
      if Enum.any?(args, &(&1.guard?   )), do: raise "guard generation cannot be used with multi-clause syntax"
      if Enum.any?(args, &(&1.validate?)), do: raise "argument validation cannot be used with multi-clause syntax"
      if ret.guard? or ret.validate?     , do: raise "return value validation cannot be used with multi-clause syntax"
      clause_defs = Enum.map(defs, &to_clause_definition(def_or_defp, fname, &1))
      {:__block__, env, clause_defs}
    else
      call_expr = call_expr_with_guard(fname, env, args, caller)
      body = body_with_validation(args, block, caller)
      wrapped_body = Ret.wrap_body_with_return_value_check(ret, body, caller)
      {def_or_defp, env, [call_expr, [do: wrapped_body]]}
    end
  end

  defp pattern_match_expr?({:->, _, _}), do: true
  defp pattern_match_expr?(_          ), do: false

  defp to_clause_definition(def_or_defp, fname, {:->, env, [args, block]}) do
    case args do
      [{:when, _, when_args}] ->
        fargs = Enum.take(when_args, length(when_args) - 1)
        guards = List.last(when_args)
        {def_or_defp, env, [{:when, [], [{fname, [], fargs}, guards]}, [do: block]]}
      _ ->
        {def_or_defp, env, [{fname, env, args}, [do: block]]}
    end
  end

  defp call_expr_with_guard(fname, env, args, caller) do
    arg_exprs = Enum.map(args, &Arg.make_arg_expr/1) |> reset_hygienic_counter()
    guard_exprs = Enum.map(args, &Arg.guard_expr(&1, caller)) |> Enum.reject(&is_nil/1)
    if Enum.empty?(guard_exprs) do
      {fname, env, arg_exprs}
    else
      combined_guard_expr = Enum.reduce(guard_exprs, fn(expr, acc) -> {:and, env, [acc, expr]} end)
      {:when, env, [{fname, env, arg_exprs}, combined_guard_expr]}
    end
  end

  defp body_with_validation(args, block, caller) do
    validation_exprs = Enum.map(args, &Arg.validation_expr(&1, caller)) |> Enum.reject(&is_nil/1)
    reassignment_exprs = Enum.map(args, &Arg.reassignment_expr/1) |> Enum.reject(&is_nil/1)
    exprs =
      case reset_hygienic_counter(block) do
        {:__block__, _, exprs} -> exprs
        nil                    -> []
        expr                   -> [expr]
      end
    case validation_exprs ++ reassignment_exprs ++ exprs do
      []     -> nil
      [expr] -> expr
      exprs  -> {:__block__, [], exprs}
    end
  end

  defp reset_hygienic_counter(ast) do
    Macro.prewalk(ast, fn
      {name, meta, context} when is_atom(context) -> {name, Keyword.delete(meta, :counter), nil}
      t -> t
    end)
  end
end
