defmodule NSQ.Connection.Command do
  @moduledoc """
  Both consumers and producers use Connection, which, at a lower level, kicks
  off message processing and issues commands to NSQD. This module handles some
  of the trickier command queueing, flushing, etc.
  """

  alias NSQ.Connection, as: C
  alias NSQ.Connection.Buffer
  alias NSQ.ConnInfo
  import NSQ.Protocol

  def exec(state, cmd, kind, {_, ref} = from) do
    if state.connected do
      state = send_data_and_queue_resp(state, cmd, from, kind)
      state = update_state_from_cmd(cmd, state)
      {{:ok, ref}, state}
    else
      # Not connected currently; add this call onto a queue to be run as soon
      # as we reconnect.
      state = %{state | cmd_queue: :queue.in({cmd, from, kind}, state.cmd_queue)}
      {{:queued, :no_socket}, state}
    end
  end

  @spec send_data_and_queue_resp(C.state(), tuple, {reference, pid}, atom) :: C.state()
  def send_data_and_queue_resp(state, cmd, from, kind) do
    state |> Buffer.send!(encode(cmd))

    if kind == :noresponse do
      state
    else
      %{state | cmd_resp_queue: :queue.in({cmd, from, kind}, state.cmd_resp_queue)}
    end
  end

  @spec send_response_to_caller(C.state(), binary) :: {:ok, C.state()}
  def send_response_to_caller(state, data) do
    :gen_event.notify(state.event_manager_pid, {:response, data})
    {item, cmd_resp_queue} = :queue.out(state.cmd_resp_queue)

    case item do
      {:value, {_cmd, {pid, ref}, :reply}} ->
        send(pid, {ref, data})

      :empty ->
        :ok
    end

    {:ok, %{state | cmd_resp_queue: cmd_resp_queue}}
  end

  @spec flush_cmd_queue(C.state()) :: C.state()
  def flush_cmd_queue(state) do
    {item, new_queue} = :queue.out(state.cmd_queue)

    case item do
      {:value, {cmd, from, kind}} ->
        state = send_data_and_queue_resp(state, cmd, from, kind)
        flush_cmd_queue(%{state | cmd_queue: new_queue})

      :empty ->
        {:ok, %{state | cmd_queue: new_queue}}
    end
  end

  def flush_cmd_queue!(state) do
    {:ok, state} = flush_cmd_queue(state)
    state
  end

  @spec update_state_from_cmd(tuple, C.state()) :: C.state()
  def update_state_from_cmd(cmd, state) do
    case cmd do
      {:rdy, count} ->
        ConnInfo.update(state, %{rdy_count: count, last_rdy: count})
        state

      _any ->
        state
    end
  end
end
