defmodule Dx.Scope.Compiler do
  @moduledoc false

  alias Dx.Defd.Ast
  alias Dx.Defd.Ast.State
  alias Dx.Defd.Util

  import Ast.Guards

  @rewriters %{
    DateTime => Dx.DateTime,
    Enum => Dx.Enum,
    :erlang => Dx.Defd.Kernel,
    Kernel => Dx.Defd.Kernel,
    String.Chars => Dx.Defd.String.Chars
  }

  def generate_fallback(ast, meta, state) do
    state = %{state | warn_non_dx?: false}
    {_external_args, internal_args} = Enum.unzip(state.scope_args)

    Ast.with_args_no_loaders!(internal_args, state, fn state ->
      args = internal_args ++ [state.eval_var]

      {ast, new_state} =
        Ast.with_root_args(args, state, fn state ->
          Dx.Defd.Compiler.normalize(ast, state)
        end)

      result_var = {:result, [], __MODULE__}

      arg_assigns =
        Enum.map(state.scope_args, fn {external_arg, internal_arg} ->
          quote do
            unquote(internal_arg) =
              case unquote(external_arg) do
                {:ref, :a0} -> unquote(result_var)
                _else -> unquote(external_arg)
              end
          end
        end)

      {:fn, meta,
       [{:->, meta, [[result_var, state.eval_var], {:__block__, [], arg_assigns ++ [ast]}]}]}
      |> with_state(new_state)
    end)
  end

  def prepend_fallback_assigns({:fn, meta, [{:->, meta2, [args, body]}]}, state) do
    result_var = {:result, [], __MODULE__}

    arg_assigns =
      Enum.map(args, fn arg ->
        quote do
          unquote(arg) =
            case unquote(arg) do
              {:ref, :a0} -> unquote(result_var)
              _else -> unquote(arg)
            end
        end
      end)

    ast =
      {:fn, meta, [{:->, meta2, [args, {:__block__, [], arg_assigns ++ [body]}]}]}

    {:error, {:fn, meta, [{:->, meta2, [[result_var, state.eval_var], ast]}]}}
  end

  def normalize_function({:v1, _kind, _meta, [{_clause_meta, args, [], ast}]}, state) do
    external_scope_args = Macro.generate_arguments(length(args), Dx.Scope.Compiler)
    internal_scope_args = Ast.mark_vars_as_generated(args)
    scope_args_map = Enum.zip(external_scope_args, internal_scope_args)

    scope_args =
      Enum.map(scope_args_map, fn {external_arg, internal_arg} ->
        quote do: unquote(external_arg) = unquote(internal_arg)
      end)

    state = Map.put(state, :scope_args, scope_args_map)

    {scope_ast, _state} =
      State.pass_in(state, [warn_non_dx?: false], fn state ->
        Ast.with_args_no_loaders!(args, state, fn state ->
          normalize(ast, state)
        end)
      end)

    {{scope_args, scope_ast}, state}
  end

  def normalize_function({:v1, _kind, _meta, _clauses}, state) do
    {{state.all_args, {:error, nil}}, state}
  end

  def normalize({:fn, _meta, [{:->, _meta2, [[{:when, _meta3, _args}], _body]}]}, state) do
    {:error, state}
  end

  def normalize({:fn, meta, [{:->, meta2, [args, body]}]}, state) do
    external_scope_args = Macro.generate_arguments(length(args), Dx.Scope.Compiler)
    internal_scope_args = Ast.mark_vars_as_generated(args)
    scope_args_map = Enum.zip(external_scope_args, internal_scope_args)

    args =
      Enum.map(scope_args_map, fn {external_arg, internal_arg} ->
        quote do: unquote(external_arg) = unquote(internal_arg)
      end)

    {body, state} =
      State.pass_in(state, [scope_args: scope_args_map], fn state ->
        Ast.with_args_no_loaders!(args, state, fn state ->
          normalize(body, state)
        end)
      end)

    {:fn, meta, [{:->, meta2, [args, body]}]}
    |> with_state(state)
  end

  def normalize({:fn, _meta, _clauses}, state) do
    {:error, state}
  end

  def normalize(var, state) when is_var(var) do
    {var, state}
  end

  def normalize(value, state) when is_simple(value) do
    value
    |> with_state(state)
  end

  def normalize({:case, _meta, _args} = ast, state) do
    Dx.Scope.Case.normalize(ast, state)
  end

  # &local_fun/2
  def normalize({:&, meta, [{:/, [], [{fun_name, meta2, nil}, arity]}]} = ast, state) do
    if {fun_name, arity} in state.defds do
      scope_name = Util.scope_name(fun_name)
      args = Macro.generate_arguments(arity, __MODULE__)

      {:fn, meta, [{:->, meta, [args, {scope_name, meta2, args}]}]}
      |> with_state(state)
    else
      {fallback, state} = generate_fallback(ast, meta, state)

      {:error, fallback}
      |> with_state(state)
    end
  end

  # &Mod.fun/3
  def normalize(
        {:&, meta, [{:/, [], [{{:., meta2, [module, fun_name]}, meta3, []}, arity]}]} = ast,
        state
      ) do
    cond do
      Util.is_defd?(module, fun_name, arity) ->
        scope_name = Util.scope_name(fun_name)
        args = Macro.generate_arguments(arity, __MODULE__)

        {:fn, meta, [{:->, meta, [args, {{:., meta2, [module, scope_name]}, meta3, args}]}]}
        |> with_state(state)

      true ->
        {fun, _state} =
          Ast.with_args_no_loaders!(state.scope_args, state, fn state ->
            args = state.scope_args

            {ast, new_state} =
              Ast.with_root_args(args, state, fn state ->
                Dx.Defd.Compiler.normalize(ast, state)
              end)

            {:fn, meta, [{:->, meta, [args, ast]}]}
            |> with_state(new_state)
          end)

        {:error, fun}
        |> with_state(state)
    end
  end

  # local_fun()
  def normalize({fun_name, meta, args} = call, state)
      when is_atom(fun_name) and is_list(args) do
    arity = length(args)

    cond do
      {fun_name, arity} in state.defds ->
        scope_name = Util.scope_name(fun_name)

        {args, state} = Enum.map_reduce(args, state, &normalize/2)

        {scope_name, meta, args}
        |> with_state(state)

      true ->
        {fallback, state} = generate_fallback(call, meta, state)

        {:error, fallback}
        |> with_state(state)
    end
  end

  # Mod.fun()
  def normalize({{:., meta, [module, fun_name]}, meta2, args} = fun, state)
      when is_atom(fun_name) and is_list(args) do
    arity = length(args)

    cond do
      # Access.get/2
      meta2[:no_parens] ->
        {module, state} = normalize(module, state)

        case module do
          {:ok, module} ->
            quote line: meta[:line] do
              {:ok, {:field_or_assoc, unquote(module), unquote(fun_name)}}
            end

          _other ->
            quote line: meta[:line] do
              {:field_or_assoc, unquote(module), unquote(fun_name)}
            end
        end
        |> with_state(state)

      # function call on dynamically computed module
      not is_atom(module) ->
        {fallback, state} = generate_fallback(fun, meta, state)

        {:error, fallback}
        |> with_state(state)

      rewriter = @rewriters[module] ->
        fun_info = Util.fun_info(rewriter, fun_name, arity)

        cond do
          Util.scope_defined?(rewriter, fun_name, arity) ->
            {args, state} = Enum.map_reduce(args, state, &normalize/2)

            args =
              Enum.zip_with(args, fun_info.args, fn arg, arg_info ->
                if arg_info.atom_to_scope do
                  if is_atom(arg) do
                    quote do: Dx.Scope.all(unquote(arg))
                  else
                    quote do: Dx.Scope.maybe_atom(unquote(arg))
                  end
                else
                  arg
                end
              end)

            {fallback, state} = generate_fallback(fun, meta, state)
            generate_fallback = fn -> fallback end

            apply(rewriter, Util.scope_name(fun_name), args ++ [generate_fallback])
            |> with_state(state)

          fun_info.can_return_scope ->
            {args, state} = Enum.map_reduce(args, state, &normalize/2)

            {{:., meta, [rewriter, fun_name]}, meta2, args}
            |> with_state(state)

          true ->
            {fallback, state} = generate_fallback(fun, meta, state)

            {:error, fallback}
            |> with_state(state)
        end

      Util.is_defd?(module, fun_name, arity) ->
        scope_name = Util.scope_name(fun_name)

        {{:., meta, [module, scope_name]}, meta2, args}
        |> with_state(state)

      true ->
        {fallback, state} = generate_fallback(fun, meta, state)

        {:error, fallback}
        |> with_state(state)
    end
  end

  def normalize(other, state) do
    meta = Ast.closest_meta(other)
    {fallback, state} = generate_fallback(other, meta, state)

    {:error, fallback}
    |> with_state(state)
  end

  ## Helpers

  @compile {:inline, with_state: 2}
  defp with_state(ast, state), do: {ast, state}
end
