require 'thread'
require 'mutex_m'

require 'qmp_client/messages'

module QMPClient

  # This class implements the interface used to interact with a QMP server.
  # It expects to be created with a pair of queues for message I/O - which is
  # precisely the interface provided by Connectors::Socket#run
  #
  # @author Nick Thomas <nick@lupine.me.uk>
  class API
    include Mutex_m
    attr_reader :receive_queue
    attr_reader :send_queue

    # API to a QMP server.
    # @param[Queue] receive_q Queue in which received messages will appear
    # @param[Queue] send_q Queue to push messages we want to send
    # @param[Fixum] thread_pool_size number of threads to run
    def initialize(receive_q, send_q)
      super()
      # Request ID generation
      @rid_m = Mutex.new
      @rid = 0

      @receive_queue = receive_q
      @send_queue = send_q

      @event_handlers = {}
      @callbacks = {}

      @greeting_q = Queue.new
    end

    def self.run(rq, wq, thread_pool_size=10, wait_for_greeting = true, &blk)
      new(rq, wq).run(thread_pool_size, wait_for_greeting, &blk)
    end

    def run(thread_pool_size, wait_for_greeting = true, &blk)
      thread_pool = build_thread_pool(@receive_queue, thread_pool_size)
      negotiate_capabilities! if wait_for_greeting
      yield self
    ensure
      if thread_pool
        thread_pool.size.times { @receive_queue.push(:close) }
        thread_pool.each {|t| t.join rescue nil }
      end
    end

    # Takes a query to run, and yields the block when it's complete, or an error
    # is raised. Returns as soon as the query is dispatched.
    def query(name, &blk)
      msg = Messages::Query.new(generate_request_id, name)
      send_message(msg, &blk)
    end

    # Convenience method - synchronous version of query
    def sync_query(name)
      sync_around(:query, name)
    end

    # Takes a command to run, and yields the block when it's complete, or an
    # error is raised. Returns as soon as the command is dispatched.
    def command(name, args = nil, &blk)
      msg = Messages::Command.new(generate_request_id, name, args)
      send_message(msg, &blk)
    end

    # Convenience method - synchronous version of command
    def sync_command(name, args = nil)
      sync_around(:command, name, args)
    end

    # Registers a block to be run whenever an event is received. We will yield
    # the block with the received message.
    #
    # You can specify only one handler per event.
    def on_event(name, &blk)
      synchronize do
        Kernel.warn("Already registered handler for #{name}, overwriting") if
          @event_handlers[name]
        @event_handlers[name] = blk
      end
    end

    # Blocks until all outstanding callbacks have been processed.
    def wait
      raise NotImplementedError.new
    end

    protected

    attr_reader :thread_pool

    # Wait for the server greeting and issue the qmp_capabilities command once
    # it has been received.
    def negotiate_capabilities!
      @greeting_q.pop
      sync_command('qmp_capabilities')
    end

    def send_message(msg, &blk)
      blk = Proc.new { } unless block_given? && blk

      synchronize { @callbacks[msg.request_id] = blk }
      send_queue.push(msg)
    end

    # Convenience method. Synchronous version of send_message.
    def sync_send_message(msg)
      sync_around(:send_message, msg)
    end

    def sync_around(method, *args)
      rq = Queue.new
      send(method, *args) {|result| rq.push(result) }
      rq.pop
    end

    def generate_request_id
      "%.8i" % @rid_m.synchronize { @rid += 1 }
    end

    def build_thread_pool(rq, tp_size)
      (1..(tp_size)).collect {|x| read_thread(rq, x)}
    end

    # @param[Queue] rqueue receive queue to operate on
    # @param[Object] x Indentifier for this read thread
    def read_thread(rqueue, x)
      Thread.new do
        Thread.current.abort_on_exception = true # Ensure exceptions bubble out
        loop do
          msg = rqueue.pop
          break if msg == :close
          dispatch_message_to_callback(msg)
        end
      end
    end

    # Checks the message type, looks to see if there's a callback that needs
    # executing, and calls it if so.
    def dispatch_message_to_callback(msg)
      case msg
      when Messages::Event
        cb = synchronize { @event_handlers[msg.name] }
        cb.call(msg) if cb && cb.respond_to?(:call)
      when Messages::Greeting
        @greeting_q.push(msg)
      else
        cb = synchronize { @callbacks.delete(msg.request_id) }
        cb.call(msg) if cb && cb.respond_to?(:call)
      end
    end

  end

end