module Excon
  class Socket

    extend Forwardable

    def_delegators(:@socket, :close,    :close)
    def_delegators(:@socket, :readline, :readline)

    def initialize(connection_params = {}, proxy = {})
      @connection_params, @proxy = connection_params, proxy
      @read_buffer, @write_buffer = '', ''

      @socket = ::Socket.new(::Socket::Constants::AF_INET, ::Socket::Constants::SOCK_STREAM, 0)

      # nonblocking connect
      if @proxy
        sockaddr = ::Socket.sockaddr_in(@proxy[:port], @proxy[:host])
      else
        sockaddr = ::Socket.sockaddr_in(@connection_params[:port], @connection_params[:host])
      end
      begin
        @socket.connect_nonblock(sockaddr)
      rescue Errno::EINPROGRESS
        IO.select(nil, [@socket], nil, @connection_params[:connect_timeout])
        begin
          @socket.connect_nonblock(sockaddr)
        rescue Errno::EISCONN
        end
      end

      # ssl setup
      if @connection_params[:scheme] == 'https'
        # create ssl context
        ssl_context = OpenSSL::SSL::SSLContext.new

        if Excon.ssl_verify_peer
          # turn verification on
          ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER

          if Excon.ssl_ca_path
            ssl_context.ca_path = Excon.ssl_ca_path
          else
            # use default cert store
            store = OpenSSL::X509::Store.new
            store.set_default_paths
            ssl_context.cert_store = store
          end
        else
          # turn verification off
          ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
        end

        if @connection_params.has_key?(:client_cert) && @connection_params.has_key?(:client_key)
          ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(@connection_params[:client_cert]))
          ssl_context.key = OpenSSL::PKey::RSA.new(File.read(@connection_params[:client_key]))
        end

        @socket = OpenSSL::SSL::SSLSocket.new(@socket, ssl_context)
        @socket.sync_close = true

        if @proxy
          @socket << "CONNECT " << @connection_params[:host] << ":" << @connection_params[:port] << HTTP_1_1
          @socket << "Host: " << @connection_params[:host] << ":" << @connection_params[:port] << CR_NL << CR_NL

          # eat the proxy's connection response
          while line = @socket.readline.strip
            break if line.empty?
          end
        end
      end

      if @connection_params[:scheme] == 'https'
        # verify connection
        if Excon.ssl_verify_peer
          @socket.post_connection_check(@connection_params[:host])
        end
      end

      @socket
    end

    def read(max_length)
      begin
        until @read_buffer.length >= max_length
          @read_buffer << @socket.read_nonblock(max_length)
        end
      rescue Errno::EAGAIN, Errno::EWOULDBLOCK
        if IO.select([@socket], nil, nil, @connection_params[:read_timeout])
          retry
        else
          raise(Timeout::Error)
        end
      end
      @read_buffer.slice!(0, max_length)
    end

    def write(data)
      @write_buffer << data
      until @write_buffer.empty?
        begin
          max_length = [@write_buffer.length, Excon::CHUNK_SIZE].min
          @socket.write_nonblock(@write_buffer.slice!(0, max_length))
        rescue Errno::EAGAIN, Errno::EWOULDBLOCK
          if IO.select(nil [@socket], nil, @connection_params[:write_timeout])
            retry
          else
            raise(Timeout::Error)
          end
        end
      end
    end

  end
end