require 'net/ssh/buffer'
require 'net/ssh/loggable'
require 'net/ssh/ruby_compat'

module Net; module SSH

  # This module is used to extend sockets and other IO objects, to allow
  # them to be buffered for both read and write. This abstraction makes it
  # quite easy to write a select-based event loop
  # (see Net::SSH::Connection::Session#listen_to).
  #
  # The general idea is that instead of calling #read directly on an IO that
  # has been extended with this module, you call #fill (to add pending input
  # to the internal read buffer), and then #read_available (to read from that
  # buffer). Likewise, you don't call #write directly, you call #enqueue to
  # add data to the write buffer, and then #send_pending or #wait_for_pending_sends
  # to actually send the data across the wire.
  #
  # In this way you can easily use the object as an argument to IO.select,
  # calling #fill when it is available for read, or #send_pending when it is
  # available for write, and then call #enqueue and #read_available during
  # the idle times.
  #
  #   socket = TCPSocket.new(address, port)
  #   socket.extend(Net::SSH::BufferedIo)
  #
  #   ssh.listen_to(socket)
  #
  #   ssh.loop do
  #     if socket.available > 0
  #       puts socket.read_available
  #       socket.enqueue("response\n")
  #     end
  #   end
  #
  # Note that this module must be used to extend an instance, and should not
  # be included in a class. If you do want to use it via an include, then you
  # must make sure to invoke the private #initialize_buffered_io method in
  # your class' #initialize method:
  #
  #   class Foo < IO
  #     include Net::SSH::BufferedIo
  #
  #     def initialize
  #       initialize_buffered_io
  #       # ...
  #     end
  #   end
  module BufferedIo
    include Loggable

    # Called when the #extend is called on an object, with this module as the
    # argument. It ensures that the modules instance variables are all properly
    # initialized.
    def self.extended(object) #:nodoc:
      # need to use __send__ because #send is overridden in Socket
      object.__send__(:initialize_buffered_io)
    end

    # Tries to read up to +n+ bytes of data from the remote end, and appends
    # the data to the input buffer. It returns the number of bytes read, or 0
    # if no data was available to be read.
    def fill(n=8192)
      input.consume!
      data = recv(n)
      debug { "read #{data.length} bytes" }
      input.append(data)
      return data.length
    end

    # Read up to +length+ bytes from the input buffer. If +length+ is nil,
    # all available data is read from the buffer. (See #available.)
    def read_available(length=nil)
      input.read(length || available)
    end

    # Returns the number of bytes available to be read from the input buffer.
    # (See #read_available.)
    def available
      input.available
    end

    # Enqueues data in the output buffer, to be written when #send_pending
    # is called. Note that the data is _not_ sent immediately by this method!
    def enqueue(data)
      output.append(data)
    end

    # Returns +true+ if there is data waiting in the output buffer, and
    # +false+ otherwise.
    def pending_write?
      output.length > 0
    end

    # Sends as much of the pending output as possible. Returns +true+ if any
    # data was sent, and +false+ otherwise.
    def send_pending
      if output.length > 0
        sent = send(output.to_s, 0)
        debug { "sent #{sent} bytes" }
        output.consume!(sent)
        return sent > 0
      else
        return false
      end
    end

    # Calls #send_pending repeatedly, if necessary, blocking until the output
    # buffer is empty.
    def wait_for_pending_sends
      send_pending
      while output.length > 0
        result = Net::SSH::Compat.io_select(nil, [self]) or next
        next unless result[1].any?
        send_pending
      end
    end

    public # these methods are primarily for use in tests

      def write_buffer #:nodoc:
        output.to_s
      end

      def read_buffer #:nodoc:
        input.to_s
      end

    private

      #--
      # Can't use attr_reader here (after +private+) without incurring the
      # wrath of "ruby -w". We hates it.
      #++

      def input; @input; end
      def output; @output; end

      # Initializes the intput and output buffers for this object. This method
      # is called automatically when the module is mixed into an object via
      # Object#extend (see Net::SSH::BufferedIo.extended), but must be called
      # explicitly in the +initialize+ method of any class that uses
      # Module#include to add this module.
      def initialize_buffered_io
        @input = Net::SSH::Buffer.new
        @output = Net::SSH::Buffer.new
      end
  end

end; end
