# Copyright 2010-present Basho Technologies, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require 'riak/client/beefcake/messages'
require 'riak/client/beefcake/message_codes'
require 'riak/errors/failed_request'
require 'riak/errors/protobuffs_error'

module Riak
  class Client
    class BeefcakeProtobuffsBackend < ProtobuffsBackend
      class Protocol
        include Riak::Util::Translation
        attr_reader :socket, :read_timeout, :write_timeout

        # @param [Socket]
        # @param [Hash] options
        # @option options [Numeric] :read_timeout (nil) The read timeout, in seconds
        # @option options [Numeric] :write_timeout (nil) The write timeout, in seconds
        def initialize(socket, options = {})
          @socket = socket
          @read_timeout = options[:read_timeout]
          @write_timeout = options[:write_timeout]
        end

        # Encodes and writes a Riak-formatted message, including protocol buffer
        # payload if given.
        #
        # @param [Symbol, Integer] code the symbolic or numeric code for the
        #   message
        # @param [Beefcake::Message, nil] message the protocol buffer message
        #   payload, or nil if the message carries no payload
        def write(code, message = nil)
          if code.is_a? Symbol
            code = BeefcakeMessageCodes.index code
          end

          serialized = serialize message

          header = [serialized.length + 1, code].pack 'NC'

          payload = header + serialized

          if write_timeout
            begin
              loop do
                bytes_written = socket.write_nonblock(payload)
                # write_nonblock doesn't guarantee to write all data at once,
                # so check if there are bytes left to be written
                break if bytes_written >= payload.bytesize
                payload.slice!(0, bytes_written)
              end
            rescue IO::WaitWritable, Errno::EINTR
              # wait with the retry until socket is writable again
              unless IO.select(nil, [socket], nil, write_timeout)
                raise Errno::ETIMEDOUT, 'write timeout'
              end
              retry
            end
          else
            socket.write(payload)
          end
          socket.flush
        end

        # Receives a Riak-formatted message, and returns the symbolic name of
        # the message along with the string payload from the network.
        #
        # @return [Array<Symbol, String>]
        def receive
          if read_timeout && !IO.select([socket], nil, nil, read_timeout)
            raise Errno::ETIMEDOUT, 'read timeout'
          end
          header = socket.read 5

          raise ProtobuffsFailedHeader.new if header.nil?
          message_length, code = header.unpack 'NC'
          body_length = message_length - 1
          body = nil
          body = socket.read body_length if body_length > 0

          name = BeefcakeMessageCodes[code]

          return name, body
        end

        # Receives a Riak-formatted message, checks the symbolic name against
        # the given code, decodes it if it matches, and can optionally return
        # success if the payload is empty.
        #
        # @param [Symbol] code the code for the message
        # @param [Class, nil] decoder_class the class to attempt to decode
        #   the payload with
        # @param [Hash] options
        # @option options [Boolean] :empty_body_acceptable Whether to accept
        #   an empty body and not attempt decoding. In this case, this method
        #   will return the symbol `:empty` instead of a `Beefcake::Message`
        #   instance
        # @return [Beefcake::Message, :empty]
        # @raise {ProtobuffsErrorResponse} if the message from Riak was a
        #   255-ErrorResp
        # @raise {ProtobuffsUnexpectedResponse} if the message from riak did
        #   not match `code`
        def expect(code, decoder_class = nil, options = { })
          code = BeefcakeMessageCodes[code] unless code.is_a? Symbol
          name, body = receive

          if name == :ErrorResp
            raise ProtobuffsErrorResponse.new RpbErrorResp.decode(body)
          end

          if name != code
            raise ProtobuffsUnexpectedResponse.new name, code
          end

          return true if decoder_class.nil?

          return :empty if body.nil? && options[:empty_body_acceptable]

          return decoder_class.decode body
        end

        private

        def serialize(message)
          return '' if message.nil?
          return message if message.is_a? String
          return message.encode.to_s if message.is_a? Beefcake::Message

          raise ArgumentError.new t('pbc.unknown_serialize', message: message)
        end
      end
    end
  end
end
