defmodule Tracer.Utils do
  @moduledoc """
  Some utils functions and reimplemmenting some usefull functions, because if they traced, the
  calls, made from tracer shouldn't traced too.
  """

  defmacrop waiting(answer, process, mref, timeout) do
    quote do
      receive do
        unquote(answer) = reply ->
          Process.demonitor(unquote(mref), [:flush])
          {:ok, reply}

        {:DOWN, ^unquote(mref), _, _, :noconnection} ->
          {:error, {:nodedown, node(unquote(process))}}

        {:DOWN, ^unquote(mref), _, _, reason} ->
          {:error, reason}
      after
        unquote(timeout) ->
          Process.demonitor(unquote(mref), [:flush])
          {:error, :timeout}
      end
    end
  end

  def call!(identifier, request, timeout \\ 10000) do
    case call(identifier, request, timeout) do
      {:ok, reply} -> reply
      {:error, error} -> exit(error)
    end
  end

  def call(identifier, request, timeout \\ 10000) do
    pid = get_process(identifier)
    mref = Process.monitor(pid)

    try do
      Process.send(pid, {{self(), mref}, request}, [:noconnect])
    catch
      _, _ -> :ok
    end

    with {:ok, {_, reply}} <- waiting({^mref, _}, pid, mref, timeout), do: {:ok, reply}
  end

  defp get_process({name, node}), do: rpc(node, :erlang, :whereis, [name])
  defp get_process(pid) when is_pid(pid), do: pid
  defp get_process(name), do: :erlang.whereis(name)

  def load_modules(node, module_list \\ [Tracer.Utils]) do
    data = for m <- module_list, do: :code.get_object_code(m)

    for {m, object_code, filename} <- data do
      ## We need to use :rpc, because on remote node our code may be not loaded and not exists
      {:module, _} = :rpc.call(node, :code, :load_binary, [m, filename, object_code])
    end

    true
  end

  def rpc(node, module, function, args, timeout \\ 5000) do
    tag = make_ref()

    pid =
      if node() == node do
        spawn(Tracer.Utils, :rpc_local, [self(), tag, module, function, args])
      else
        :erlang.spawn(node, Tracer.Utils, :rpc_local, [self(), tag, module, function, args])
      end

    mref = Process.monitor(pid)

    case waiting({^tag, _}, pid, mref, timeout) do
      {:ok, {_, reply}} -> reply
      {:error, error} -> exit(error)
    end
  end

  def rpc_local(parent, tag, module, function, args) do
    result = apply(module, function, args)
    send(parent, {tag, result})
  end

  def apply_func(func, arguments \\ [])

  def apply_func(func, arguments) when not is_list(arguments), do: apply_func(func, List.wrap(arguments))
  def apply_func({m, f, opts}, arguments), do: apply(m, f, arguments ++ opts)
  def apply_func(fun, arguments), do: apply(fun, arguments)
end
