lib/ex_gram/macros/helpers.ex

defmodule ExGram.Macros.Helpers do
  @moduledoc """
  Helpers for the ExGram.Macros module
  """

  def analyze_param({:{}, line, [{name, _line, nil}]}), do: {{name, line, nil}, [name, [:any]]}

  def analyze_param({:{}, line, [{name, _line, nil}, types, :optional]}),
    do: {{:\\, line, [{name, line, nil}, nil]}, [name, types, :optional]}

  def analyze_param({name, _line, nil} = full), do: {full, [name, [:any]]}

  def analyze_param({{name, line, nil}, :optional}),
    do: {{:\\, line, [{name, line, nil}, nil]}, [name, [:any], :optional]}

  def analyze_param({{name, line, nil}, types}), do: {{name, line, nil}, [name, types]}

  def analyze_param({{name, line, nil}, types, :optional}),
    do: {{:\\, line, [{name, line, nil}, nil]}, [name, types, :optional]}

  def mandatory_type_specs(analyzed) do
    analyzed
    |> Enum.map(&elem(&1, 1))
    |> Enum.filter(&(not is_par_optional(&1)))
    |> Enum.map(fn [n, ts] -> parameter_type_spec(n, types_list_to_spec(ts)) end)
  end

  def mandatory_value_type(analyzed) do
    analyzed
    |> Enum.map(&elem(&1, 1))
    |> Enum.filter(&(not is_par_optional(&1)))
    |> Enum.map(fn [name, types] -> [nid(name), types] end)
  end

  def optional_type_specs(analyzed) do
    analyzed
    |> Enum.map(&elem(&1, 1))
    |> Enum.filter(&is_par_optional/1)
    |> Enum.map(fn [n, ts, :optional] -> {n, types_list_to_spec(ts)} end)
    |> Kernel.++(common_opts())
  end

  def mandatory_parameters(analyzed) do
    mandatory =
      Enum.filter(analyzed, fn {_name, desc} -> not is_par_optional(desc) end)

    names = Enum.map(mandatory, fn {name, _desc} -> name end)

    mand_atoms = Enum.map(names, &extract_param_name/1)
    mand_values = Enum.map(mand_atoms, &nid/1)

    body = Enum.zip(mand_atoms, mand_values)

    {names, body}
  end

  def optional_parameters(analyzed) do
    optionals =
      analyzed
      |> Enum.map(&elem(&1, 1))
      |> Enum.filter(&is_par_optional/1)

    optional_types = Enum.map(optionals, fn [name, types, :optional] -> {name, types} end)
    optional_names = Enum.map(optionals, fn [name, _, _] -> name end)

    {optional_names, optional_types}
  end

  def file_parameters(analyzed) do
    analyzed
    |> Enum.map(fn {_n, types} -> types end)
    |> Enum.filter(fn [_n, t | _] -> Enum.any?(t, &(&1 == :file or &1 == :file_content)) end)
    |> Enum.map(fn
      [n, _t] -> {nid(n), Atom.to_string(n)}
      [n, _t, :optional] -> n
    end)
  end

  def type_to_spec(:string), do: {{:., [], [{:__aliases__, [alias: false], [:String]}, :t]}, [], []}

  def type_to_spec(:enum), do: {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :t]}, [], []}

  def type_to_spec(:file) do
    orT(
      {:file, type_to_spec(:string)},
      type_to_spec(:file_content)
    )
  end

  def type_to_spec(:file_content) do
    {:{}, [], [:file_content, type_to_spec(:file_data), type_to_spec(:string)]}
  end

  def type_to_spec(:file_data) do
    orT(type_to_spec(:iodata), type_to_spec(:enum))
  end

  def type_to_spec({:array, t}), do: {:list, [], [type_to_spec(t)]}
  def type_to_spec(:integer), do: {:integer, [], []}
  def type_to_spec(:boolean), do: {:boolean, [], []}
  def type_to_spec(true), do: true

  def type_to_spec({:__aliases__, _a, [:ExGram, :Model | _]} = f) do
    quote do
      unquote(f).t
    end
  end

  def type_to_spec({:__aliases__, _a, _} = f) do
    quote do
      ExGram.Model.unquote(f).t
    end
  end

  def type_to_spec(t) when is_atom(t), do: {t, [], Elixir}
  def type_to_spec(l) when is_list(l), do: types_list_to_spec(l)

  defp types_list_to_spec([e1]) do
    type_to_spec(e1)
  end

  defp types_list_to_spec([e1 | rest]) do
    orT(type_to_spec(e1), types_list_to_spec(rest))
  end

  defp types_list_to_spec([]) do
    type_to_spec(:any)
  end

  @common_opts [
    adapter: :atom,
    bot: :atom,
    token: :string,
    debug: :boolean,
    check_params: :boolean
  ]
  defp common_opts do
    Enum.map(@common_opts, fn {k, v} -> {k, type_to_spec(v)} end)
  end

  defp parameter_type_spec(n, t) when is_atom(n), do: {:"::", [], [type_to_spec(n), t]}
  defp parameter_type_spec(n, t), do: {:"::", [], [n, t]}

  defp nid(x), do: {x, [], nil}

  defp is_par_optional([_n, _t, :optional]), do: true
  defp is_par_optional(_), do: false

  defp extract_param_name({name, _line, nil}), do: name
  defp extract_param_name({:\\, _line, [{name, _line2, nil}, nil]}), do: name

  # MODEL helpers

  def params_to_decode_as(params) do
    params
    |> Stream.map(fn
      {k, v} -> {k, v}
      {:{}, _, [k, v, :optional]} -> {k, v}
    end)
    |> Stream.map(fn {k, [v | _]} ->
      {k, param_to_decode_as(v)}
    end)
    |> Enum.filter(fn {_k, v} -> not is_nil(v) end)
  end

  defp param_to_decode_as({:array, type}) do
    case param_to_decode_as(type) do
      nil ->
        nil

      t ->
        quote do
          [unquote(t)]
        end
    end
  end

  defp param_to_decode_as({:__aliases__, _, _} = st) do
    quote do
      ExGram.Model.unquote(st)
    end
  end

  defp param_to_decode_as(_other), do: nil

  def struct_type_specs([], acc), do: acc

  def struct_type_specs([{id, t} | xs], acc) do
    act = acc ++ [{id, type_to_spec(t)}]

    struct_type_specs(xs, act)
  end

  def struct_type_specs([{:{}, _line, [id, t, :optional]} | xs], acc) do
    act = acc ++ [{id, {:|, [], [type_to_spec(t), nil]}}]

    struct_type_specs(xs, act)
  end

  def struct_type_specs(_x, acc) do
    # Logger.error "WTF struct?"
    # Logger.error inspect(x)
    struct_type_specs(acc)
  end

  def struct_type_specs(initial), do: struct_type_specs(initial, [])

  # credo:disable-for-next-line
  defp orT({:|, _, [x, y]}, z), do: orT(x, orT(y, z))
  defp orT(x, y), do: {:|, [], [x, y]}
end