defmodule Credo.ExsLoader do
  @moduledoc false

  def parse(exs_string, filename, exec, safe \\ false)

  def parse(exs_string, filename, _exec, true) do
    case Code.string_to_quoted(exs_string, file: filename) do
      {:ok, ast} ->
        {:ok, process_exs(ast)}

      {:error, {line_meta, message, trigger}} when is_list(line_meta) ->
        {:error, {line_meta[:line], message, trigger}}

      {:error, value} ->
        {:error, value}
    end
  end

  def parse(exs_string, filename, exec, false) when is_atom(filename) do
    parse(exs_string, "mod:#{filename}", exec, false)
  end

  def parse(exs_string, filename, exec, false) when is_binary(filename) do
    {result, _binding} =
      Code.eval_string(exs_string, [exec: exec], file: to_string(filename) || "nofile")

    {:ok, result}
  rescue
    error ->
      case error do
        %SyntaxError{description: "syntax error before: " <> trigger, line: line_meta}
        when is_list(line_meta) ->
          {:error, {line_meta[:line], "syntax error before: ", trigger}}

        %SyntaxError{description: "syntax error before: " <> trigger, line: line_no} ->
          {:error, {line_no, "syntax error before: ", trigger}}

        error ->
          {:error, error}
      end
  end

  @doc false
  def parse_safe(exs_string) do
    case Code.string_to_quoted(exs_string) do
      {:ok, ast} ->
        process_exs(ast)

      _ ->
        %{}
    end
  end

  defp process_exs(v)
       when is_atom(v) or is_binary(v) or is_float(v) or is_integer(v),
       do: v

  defp process_exs(list) when is_list(list) do
    Enum.map(list, &process_exs/1)
  end

  defp process_exs({:sigil_w, _, [{:<<>>, _, [list_as_string]}, []]}) do
    String.split(list_as_string, ~r/\s+/)
  end

  # TODO: support regex modifiers
  defp process_exs({:sigil_r, _, [{:<<>>, _, [regex_as_string]}, []]}) do
    Regex.compile!(regex_as_string)
  end

  defp process_exs({:%{}, _meta, body}) do
    process_map(body, %{})
  end

  defp process_exs({:{}, _meta, body}) do
    process_tuple(body, {})
  end

  defp process_exs({:__aliases__, _meta, name_list}) do
    Module.safe_concat(name_list)
  end

  defp process_exs({{:__aliases__, _meta, name_list}, options}) do
    {Module.safe_concat(name_list), process_exs(options)}
  end

  defp process_exs({key, value}) when is_atom(key) or is_binary(key) do
    {process_exs(key), process_exs(value)}
  end

  defp process_tuple([], acc), do: acc

  defp process_tuple([head | tail], acc) do
    acc = process_tuple_item(head, acc)
    process_tuple(tail, acc)
  end

  defp process_tuple_item(value, acc) do
    Tuple.insert_at(acc, tuple_size(acc), process_exs(value))
  end

  defp process_map([], acc), do: acc

  defp process_map([head | tail], acc) do
    acc = process_map_item(head, acc)
    process_map(tail, acc)
  end

  defp process_map_item({key, value}, acc)
       when is_atom(key) or is_binary(key) do
    Map.put(acc, key, process_exs(value))
  end
end
