defmodule NifCall.Runner do
  use GenServer

  defstruct [:nif_module, :on_evaluated, :refs, :monitors]

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts[:runner_opts], opts)
  end

  def register(name, function) when is_function(function, 1) do
    GenServer.call(name, {:register, self(), function}, :infinity)
  end

  def unregister(name, {_pid, ref}) do
    GenServer.call(name, {:unregister, ref}, :infinity)
  end

  def init(opts) do
    opts = Keyword.validate!(opts, [:nif_module, on_evaluated: :nif_call_evaluated])

    {:ok,
     %__MODULE__{nif_module: opts[:nif_module], on_evaluated: opts[:on_evaluated], refs: %{}, monitors: %{}}}
  end

  def handle_call({:register, owner, function}, _from, state) do
    ref = Process.monitor(owner)
    {:reply, {self(), ref}, %{state | refs: Map.put(state.refs, ref, function)}}
  end

  def handle_call({:unregister, ref}, _from, state) do
    Process.demonitor(ref, [:flush])
    {:reply, :ok, %{state | refs: Map.delete(state.refs, ref)}}
  end

  def handle_info({:DOWN, ref, _, _, _}, state) when is_map_key(state.monitors, ref) do
    {:noreply, %{state | refs: Map.delete(state.refs, state.monitors[ref]), monitors: Map.delete(state.monitors, ref)}}
  end

  def handle_info({:DOWN, ref, _, _, _}, state) do
    {:noreply, %{state | refs: Map.delete(state.refs, ref)}}
  end

  def handle_info({:execute, resource, ref, args}, state) do
    function = Map.fetch!(state.refs, ref)

    task =
      Task.async(fn ->
        try do
          apply(state.nif_module, state.on_evaluated, [
            resource,
            {:ok, apply(function, List.wrap(args))}
          ])
        catch
          kind, reason ->
            apply(state.nif_module, state.on_evaluated, [resource, {kind, reason}])
        end
      end)

    {:noreply, %{state | monitors: Map.put(state.monitors, task.ref, ref)}}
  end

  def handle_info({{:eval, resource}, _, _, _, reason}, state) do
    if reason != :normal do
      apply(state.nif_module, :nif_call_evaluated, [resource, {:exit, reason}])
    end

    {:noreply, state}
  end

  def handle_info({ref, _}, state) when is_map_key(state.monitors, ref) do
    {:noreply, state}
  end
end
