# Copyright (c) 2021 Anand Panchapakesan
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
defmodule Helper.Macro do
@moduledoc """
Common functions for manipulating ast / quoted code
"""
@typedoc """
AST type
"""
@type t() :: Macro.t()
@doc """
Returns the module from the AST alias.
## Example
iex> ast2mod!({:__aliases__, [], [Elixir, String]})
Elixir.String
iex> ast2mod!([{:__aliases__, [], [Elixir, String]}])
Elixir.String
iex> ast2mod!(String)
String
iex> ast2mod!("String")
** (ArgumentError) "String": Invalid value for module
"""
@spec ast2mod!(module() | t()) :: module() | no_return()
def ast2mod!([m]), do: ast2mod!(m)
def ast2mod!(m) when is_atom(m), do: m
def ast2mod!({:__aliases__, _, m}) when is_list(m), do: Module.concat(m)
def ast2mod!(e), do: raise(ArgumentError, "#{inspect(e)}: Invalid value for module")
@doc """
Parses the AST and returns the node and value in a keyword list
## Example
iex> ast2kw([{:primary_key, [], [:id]}, {:timestamps, [], [[type: :utc_datetime_usec]]}])
[primary_key: :id, timestamps: [type: :utc_datetime_usec]]
"""
@spec ast2kw(t()) :: keyword()
def ast2kw(ast), do: for({key, _, [value]} <- ast, do: {key, value})
@doc """
Parses the AST and returns the node and value in a map
## Example
iex> attr2map([{:source, [], ["embedded"]}])
%{source: "embedded"}
iex> attr2map([{:repo, [], [{Helper.Example.Repo, []}]}])
%{repo: {Helper.Example.Repo, []}}
iex> attr2map([{:name, [], [{:__aliases__, [], [Helper, Example, User]}]}])
%{name: Helper.Example.User}
"""
@spec attr2map(t()) :: map()
def attr2map(ast), do: Enum.reduce(ast, %{}, &do_attr2map/2)
defp do_attr2map({:name, _, [value]}, map), do: Map.put(map, :name, ast2mod!(value))
defp do_attr2map({:repo, _, [{repo, options}]}, map),
do: Map.put(map, :repo, {ast2mod!(repo), options})
defp do_attr2map({:source, _, [value]}, map), do: Map.put(map, :source, value)
@doc """
Split the AST with nodes matching the values in the first value and the remain in the second value
## Example
iex> split({:primary_key, [], []}, :primary_key)
{[{:primary_key, [], []}], []}
iex> split({:timestamps, [], []}, :primary_key)
{[], [{:timestamps, [], []}]}
iex> split([{:primary_key, [], []}], [:timestamps, :primary_key])
{[{:primary_key, [], []}], []}
"""
@spec split(t(), list(atom()) | atom()) :: {t(), t()}
def split(ast, value) when is_list(ast), do: Enum.split_with(ast, &can_split?(&1, value))
def split(ast, value), do: if(can_split?(ast, value), do: {[ast], []}, else: {[], [ast]})
@spec can_split?(t(), list(atom()) | atom()) :: boolean()
defp can_split?({node, _, _}, value) when is_atom(value), do: node == value
defp can_split?({node, _, _}, values), do: Enum.member?(values, node)
end