defmodule Hologram.Compiler.Normalizer do
  @moduledoc false

  alias Hologram.Compiler.AST
  alias Hologram.Compiler.Helpers
  alias Hologram.Reflection

  @doc """
  Normalizes Elixir AST by ensuring that:
  * single expression blocks are wrapped in __block__ tuple
  * alias atoms are wrapped in __aliases__ tuple
  * unquote fragments are expanded

  ## Examples

      iex> ast = Code.string_to_quoted!("cond do true -> 123 end")
      {:cond, [line: 1], [[do: [{:->, [line: 1], [[true], 987]}]]]}
      iex> normalize(ast)
      {:cond, [line: 1], [[do: [{:->, [line: 1], [[true], {:__block__, [], [987]}]}]]]}
  """
  @spec normalize(AST.t()) :: AST.t()
  def normalize(ast)

  def normalize({:->, meta, [pattern, block]}) do
    {:->, meta, [normalize(pattern), normalize_block(block)]}
  end

  def normalize({:__aliases__, meta, [module]} = ast) do
    maybe_normalize_alias(module, meta, ast)
  end

  def normalize({marker, meta, [name, [do: block]]}) when marker in [:def, :defp] do
    {marker, meta, [normalize(name), [do: normalize_block(block)]]}
  end

  def normalize({:defmodule, meta, [name, [do: block]]}) do
    {:defmodule, meta, [normalize(name), [do: normalize_block(block)]]}
  end

  def normalize({:for, meta, parts}) when is_list(parts) do
    {:for, meta, Enum.map(parts, &normalize_comprehension_part/1)}
  end

  def normalize({:try, meta, [opts]}) when is_list(opts) do
    {:try, meta, [Enum.map(opts, &normalize_try_opt/1)]}
  end

  def normalize({{:unquote, _meta_1, [marker]}, meta_2, children}) do
    {marker, meta_2, normalize(children)}
  end

  def normalize(ast) when is_atom(ast) do
    maybe_normalize_alias(ast, [alias: false], ast)
  end

  def normalize(ast) when is_list(ast) do
    Enum.map(ast, &normalize/1)
  end

  def normalize(ast) when is_tuple(ast) do
    ast
    |> Tuple.to_list()
    |> normalize()
    |> List.to_tuple()
  end

  def normalize(ast), do: ast

  defp maybe_normalize_alias(module, meta, ast) do
    if Reflection.alias?(module) do
      segments = Helpers.alias_segments(module)
      {:__aliases__, meta, segments}
    else
      ast
    end
  end

  defp normalize_block({:__block__, meta, exprs}) do
    {:__block__, meta, normalize(exprs)}
  end

  defp normalize_block(expr) do
    {:__block__, [], [normalize(expr)]}
  end

  defp normalize_comprehension_opt({:do, block}) do
    {:do, normalize_block(block)}
  end

  defp normalize_comprehension_opt(opt), do: normalize(opt)

  defp normalize_comprehension_part(opts) when is_list(opts) do
    Enum.map(opts, &normalize_comprehension_opt/1)
  end

  defp normalize_comprehension_part(part), do: normalize(part)

  defp normalize_try_opt({:after, block}) do
    {:after, normalize_block(block)}
  end

  defp normalize_try_opt({:do, block}) do
    {:do, normalize_block(block)}
  end

  defp normalize_try_opt(opt), do: normalize(opt)
end
