=begin

= net/protocol.rb

Copyright (c) 1999-2002 Yukihiro Matsumoto

written & maintained by Minero Aoki <aamine@loveruby.net>

This program is free software. You can re-distribute and/or
modify this program under the same terms as Ruby itself,
Ruby Distribute License or GNU General Public License.

NOTE: You can find Japanese version of this document in
the doc/net directory of the standard ruby interpreter package.

$Id: protocol.rb,v 1.65 2002/11/21 11:50:09 aamine Exp $

=end

require 'socket'
require 'timeout'


module Net

  class Protocol

    Version  = '1.2.3'
    Revision = '$Revision: 1.65 $'.slice(/[\d\.]+/)


    class << self

      def port
        default_port
      end

      private

      def protocol_param( name, val )
        module_eval <<-EOS, __FILE__, __LINE__ + 1
          def self.#{name.id2name}
            #{val}
          end
        EOS
      end
        
    end


    #
    # --- Configuration Staffs for Sub Classes ---
    #
    #   class method default_port
    #   class method command_type
    #   class method socket_type
    #
    #   private method do_start
    #   private method do_finish
    #
    #   private method conn_address
    #   private method conn_port
    #


    def Protocol.start( address, port = nil, *args )
      instance = new(address, port)
      if block_given?
        instance.start(*args) {
            return yield(instance)
        }
      else
        instance.start(*args)
        instance
      end
    end

    def initialize( addr, port = nil )
      @address = addr
      @port    = port || self.class.default_port

      @command = nil
      @socket  = nil

      @started = false

      @open_timeout = 30
      @read_timeout = 60

      @debug_output = nil
    end

    attr_reader :address
    attr_reader :port

    attr_reader :command
    attr_reader :socket

    attr_accessor :open_timeout

    attr_reader :read_timeout

    def read_timeout=( sec )
      @socket.read_timeout = sec if @socket
      @read_timeout = sec
    end

    def started?
      @started
    end

    alias active? started?

    def set_debug_output( arg )   # un-documented
      @debug_output = arg
    end

    def inspect
      "#<#{self.class} #{@address}:#{@port} open=#{active?}>"
    end

    #
    # open
    #

    def start( *args )
      @started and raise IOError, 'protocol has been opened already'

      if block_given?
        begin
          do_start(*args)
          @started = true
          return yield(self)
        ensure
          finish if @started
        end
      end

      do_start(*args)
      @started = true
      self
    end

    private

    # abstract do_start()

    def conn_socket
      @socket = self.class.socket_type.open(
              conn_address(), conn_port(),
              @open_timeout, @read_timeout, @debug_output )
      on_connect
    end

    alias conn_address address
    alias conn_port    port

    def reconn_socket
      @socket.reopen @open_timeout
      on_connect
    end

    def conn_command
      @command = self.class.command_type.new(@socket)
    end

    def on_connect
    end

    #
    # close
    #

    public

    def finish
      raise IOError, 'closing already closed protocol' unless @started
      do_finish
      @started = false
      nil
    end

    private

    # abstract do_finish()

    def disconn_command
      @command.quit if @command and not @command.critical?
      @command = nil
    end

    def disconn_socket
      @socket.close if @socket and not @socket.closed?
      @socket = nil
    end
    
  end

  Session = Protocol


  class Response

    def initialize( ctype, code, msg )
      @code_type = ctype
      @code      = code
      @message   = msg
      super()
    end

    attr_reader :code_type
    attr_reader :code
    attr_reader :message
    alias msg message

    def inspect
      "#<#{self.class} #{@code}>"
    end

    def error!
      raise error_type().new(code + ' ' + @message.dump, self)
    end

    def error_type
      @code_type.error_type
    end

  end


  class ProtocolError          < StandardError; end
  class ProtoSyntaxError       < ProtocolError; end
  class ProtoFatalError        < ProtocolError; end
  class ProtoUnknownError      < ProtocolError; end
  class ProtoServerError       < ProtocolError; end
  class ProtoAuthError         < ProtocolError; end
  class ProtoCommandError      < ProtocolError; end
  class ProtoRetriableError    < ProtocolError; end
  ProtocRetryError = ProtoRetriableError

  class ProtocolError
  
    def initialize( msg, resp )
      super msg
      @response = resp
    end

    attr_reader :response
    alias data response

    def inspect
      "#<#{self.class} #{self.message}>"
    end
  
  end


  class Code

    def initialize( paren, err )
      @parents = [self] + paren
      @error_type = err
    end

    def parents
      @parents.dup
    end

    attr_reader :error_type

    def inspect
      "#<#{self.class} #{sprintf '0x%x', __id__}>"
    end

    def ===( response )
      response.code_type.parents.each do |c|
        return true if c == self
      end
      false
    end

    def mkchild( err = nil )
      self.class.new(@parents, err || @error_type)
    end
  
  end
  
  ReplyCode       = Code.new([], ProtoUnknownError)
  InformationCode = ReplyCode.mkchild(ProtoUnknownError)
  SuccessCode     = ReplyCode.mkchild(ProtoUnknownError)
  ContinueCode    = ReplyCode.mkchild(ProtoUnknownError)
  ErrorCode       = ReplyCode.mkchild(ProtocolError)
  SyntaxErrorCode = ErrorCode.mkchild(ProtoSyntaxError)
  FatalErrorCode  = ErrorCode.mkchild(ProtoFatalError)
  ServerErrorCode = ErrorCode.mkchild(ProtoServerError)
  AuthErrorCode   = ErrorCode.mkchild(ProtoAuthError)
  RetriableCode   = ReplyCode.mkchild(ProtoRetriableError)
  UnknownCode     = ReplyCode.mkchild(ProtoUnknownError)


  class Command

    def initialize( sock )
      @socket = sock
      @last_reply = nil
      @atomic = false
    end

    attr_accessor :socket
    attr_reader :last_reply

    def inspect
      "#<#{self.class} socket=#{@socket.inspect} critical=#{@atomic}>"
    end

    # abstract quit()

    private

    def check_reply( *oks )
      @last_reply = get_reply()
      reply_must @last_reply, *oks
    end

    # abstract get_reply()

    def reply_must( rep, *oks )
      oks.each do |i|
        return rep if i === rep
      end
      rep.error!
    end

    def getok( line, expect = SuccessCode )
      @socket.writeline line
      check_reply expect
    end

    #
    # critical session
    #

    public

    def critical?
      @atomic
    end

    def error_ok
      @atomic = false
    end

    private

    def atomic
      @atomic = true
      ret = yield
      @atomic = false
      ret
    end

  end


  class InternetMessageIO

    class << self
      alias open new
    end

    def initialize( addr, port, otime = nil, rtime = nil, dout = nil )
      @address      = addr
      @port         = port
      @read_timeout = rtime
      @debug_output = dout

      @socket  = nil
      @rbuf    = nil

      connect otime
      D 'opened'
    end

    attr_reader :address
    attr_reader :port

    def ip_address
      @socket or return ''
      @socket.addr[3]
    end

    attr_accessor :read_timeout

    attr_reader :socket

    def connect( otime )
      D "opening connection to #{@address}..."
      timeout(otime) {
          @socket = TCPsocket.new(@address, @port)
      }
      @rbuf = ''
    end
    private :connect

    def close
      if @socket
        @socket.close
        D 'closed'
      else
        D 'close call for already closed socket'
      end
      @socket = nil
      @rbuf = ''
    end

    def reopen( otime = nil )
      D 'reopening...'
      close
      connect otime
      D 'reopened'
    end

    def closed?
      not @socket
    end

    def inspect
      "#<#{self.class} #{closed? ? 'closed' : 'opened'}>"
    end

    ###
    ###  READ
    ###

    public

    def read( len, dest = '', ignore = false )
      D_off "reading #{len} bytes..."

      rsize = 0
      begin
        while rsize + @rbuf.size < len
          rsize += rbuf_moveto(dest, @rbuf.size)
          rbuf_fill
        end
        rbuf_moveto dest, len - rsize
      rescue EOFError
        raise unless ignore
      end

      D_on "read #{len} bytes"
      dest
    end

    def read_all( dest = '' )
      D_off 'reading all...'

      rsize = 0
      begin
        while true
          rsize += rbuf_moveto(dest, @rbuf.size)
          rbuf_fill
        end
      rescue EOFError
        ;
      end

      D_on "read #{rsize} bytes"
      dest
    end

    def readuntil( target, ignore = false )
      dest = ''
      begin
        until idx = @rbuf.index(target)
          rbuf_fill
        end
        rbuf_moveto dest, idx + target.size
      rescue EOFError
        raise unless ignore
        rbuf_moveto dest, @rbuf.size
      end
      dest
    end
        
    def readline
      ret = readuntil("\n")
      ret.chop!
      ret
    end

    private

    BLOCK_SIZE = 1024

    def rbuf_fill
      until IO.select [@socket], nil, nil, @read_timeout
        on_read_timeout
      end
      @rbuf << @socket.sysread(BLOCK_SIZE)
    end

    def on_read_timeout
      raise TimeoutError, "socket read timeout (#{@read_timeout} sec)"
    end

    def rbuf_moveto( dest, len )
      dest << (s = @rbuf.slice!(0, len))
      @debug_output << %Q[-> #{s.dump}\n] if @debug_output
      len
    end

    #
    # message read
    #

    public

    def read_message_to( dest )
      D_off 'reading text...'

      rsize = 0
      while (str = readuntil("\r\n")) != ".\r\n"
        rsize += str.size
        dest << str.sub(/\A\./, '')
      end

      D_on "read #{rsize} bytes"
      dest
    end
  
    # private use only (cannot handle 'break')
    def each_list_item
      while (str = readuntil("\r\n")) != ".\r\n"
        yield str.chop
      end
    end


    ###
    ###  WRITE
    ###

    #
    # basic write
    #

    public

    def write( str )
      writing {
          do_write str
      }
    end

    def writeline( str )
      writing {
          do_write str + "\r\n"
      }
    end

    private

    def writing
      @writtensize = 0
      @debug_output << '<- ' if @debug_output
      yield
      @socket.flush
      @debug_output << "\n" if @debug_output
      @writtensize
    end

    def do_write( str )
      @debug_output << str.dump if @debug_output
      @writtensize += (n = @socket.write(str))
      n
    end

    #
    # message write
    #

    public

    def write_message( src )
      D_off "writing text from #{src.class}"

      wsize = using_each_crlf_line {
          wpend_in src
      }

      D_on "wrote #{wsize} bytes text"
      wsize
    end

    def through_message
      D_off 'writing text from block'

      wsize = using_each_crlf_line {
          yield WriteAdapter.new(self, :wpend_in)
      }

      D_on "wrote #{wsize} bytes text"
      wsize
    end

    private

    def wpend_in( src )
      line = nil
      pre = @writtensize
      each_crlf_line(src) do |line|
        do_write '.' if line[0] == ?.
        do_write line
      end

      @writtensize - pre
    end

    def using_each_crlf_line
      writing {
          @wbuf = ''

          yield

          if not @wbuf.empty?       # unterminated last line
            if @wbuf[-1] == ?\r
              @wbuf.chop!
            end
            @wbuf.concat "\r\n"
            do_write @wbuf
          elsif @writtensize == 0   # empty src
            do_write "\r\n"
          end
          do_write ".\r\n"

          @wbuf = nil
      }
    end

    def each_crlf_line( src )
      str = m = beg = nil

      adding(src) do
        beg = 0
        buf = @wbuf
        while buf.index(/\n|\r\n|\r/, beg)
          m = Regexp.last_match
          if m.begin(0) == buf.size - 1 and buf[-1] == ?\r
            # "...\r" : can follow "\n..."
            break
          end
          str = buf[ beg ... m.begin(0) ]
          str.concat "\r\n"
          yield str
          beg = m.end(0)
        end
        @wbuf = buf[ beg ... buf.size ]
      end
    end

    def adding( src )
      i = s = nil

      case src
      when String
        0.step(src.size - 1, 2048) do |i|
          @wbuf << src[i,2048]
          yield
        end

      when File
        while s = src.read(2048)
          s[0,0] = @wbuf
          @wbuf = s
          yield
        end

      else
        src.each do |s|
          @wbuf << s
          yield if @wbuf.size > 2048
        end
        yield unless @wbuf.empty?
      end
    end

    ###
    ### DEBUG
    ###

    private

    def D_off( msg )
      D msg
      @savedo, @debug_output = @debug_output, nil
    end

    def D_on( msg )
      @debug_output = @savedo
      D msg
    end

    def D( msg )
      @debug_output or return
      @debug_output << msg
      @debug_output << "\n"
    end
  
  end


  class WriteAdapter

    def initialize( sock, mid )
      @socket = sock
      @mid = mid
    end

    def inspect
      "#<#{self.class} socket=#{@socket.inspect}>"
    end

    def write( str )
      @socket.__send__ @mid, str
    end

    alias print write

    def <<( str )
      write str
      self
    end

    def puts( str = '' )
      write str.sub(/\n?/, "\n")
    end

    def printf( *args )
      write sprintf(*args)
    end
  
  end


  class ReadAdapter

    def initialize( block )
      @block = block
    end

    def inspect
      "#<#{self.class}>"
    end

    def <<( str )
      call_block(str, &@block) if @block
    end

    private

    def call_block( str )
      yield str
    end
  
  end


  # for backward compatibility
  module NetPrivate
    Response = ::Net::Response
    Command = ::Net::Command
    Socket = ::Net::InternetMessageIO
    BufferedSocket = ::Net::InternetMessageIO
    WriteAdapter = ::Net::WriteAdapter
    ReadAdapter = ::Net::ReadAdapter
  end
  BufferedSocket = ::Net::InternetMessageIO

end   # module Net
