# frozen_string_literal: true

module Dalli
  module Protocol
    class Meta
      ##
      # Class that encapsulates logic for processing meta protocol responses
      # from memcached.  Includes logic for pulling data from an IO source
      # and parsing into local values.  Handles errors on unexpected values.
      ##
      class ResponseProcessor
        EN = 'EN'
        END_TOKEN = 'END'
        EX = 'EX'
        HD = 'HD'
        MN = 'MN'
        NF = 'NF'
        NS = 'NS'
        OK = 'OK'
        RESET = 'RESET'
        STAT = 'STAT'
        VA = 'VA'
        VERSION = 'VERSION'
        SERVER_ERROR = 'SERVER_ERROR'

        def initialize(io_source, value_marshaller)
          @io_source = io_source
          @value_marshaller = value_marshaller
        end

        def meta_get_with_value(cache_nils: false)
          tokens = error_on_unexpected!([VA, EN, HD])
          return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
          return true unless tokens.first == VA

          @value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
        end

        def meta_get_with_value_and_cas
          tokens = error_on_unexpected!([VA, EN, HD])
          return [nil, 0] if tokens.first == EN

          cas = cas_from_tokens(tokens)
          return [nil, cas] unless tokens.first == VA

          [@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens)), cas]
        end

        def meta_get_without_value
          tokens = error_on_unexpected!([EN, HD])
          tokens.first == EN ? nil : true
        end

        def meta_set_with_cas
          tokens = error_on_unexpected!([HD, NS, NF, EX])
          return false unless tokens.first == HD

          cas_from_tokens(tokens)
        end

        def meta_set_append_prepend
          tokens = error_on_unexpected!([HD, NS, NF, EX])
          return false unless tokens.first == HD

          true
        end

        def meta_delete
          tokens = error_on_unexpected!([HD, NF, EX])
          tokens.first == HD
        end

        def decr_incr
          tokens = error_on_unexpected!([VA, NF, NS, EX])
          return false if [NS, EX].include?(tokens.first)
          return nil if tokens.first == NF

          read_line.to_i
        end

        def stats
          tokens = error_on_unexpected!([END_TOKEN, STAT])
          values = {}
          while tokens.first != END_TOKEN
            values[tokens[1]] = tokens[2]
            tokens = next_line_to_tokens
          end
          values
        end

        def flush
          error_on_unexpected!([OK])

          true
        end

        def reset
          error_on_unexpected!([RESET])

          true
        end

        def version
          tokens = error_on_unexpected!([VERSION])
          tokens.last
        end

        def consume_all_responses_until_mn
          tokens = next_line_to_tokens

          tokens = next_line_to_tokens while tokens.first != MN
          true
        end

        def tokens_from_header_buffer(buf)
          header = header_from_buffer(buf)
          tokens = header.split
          header_len = header.bytesize + TERMINATOR.length
          body_len = body_len_from_tokens(tokens)
          [tokens, header_len, body_len]
        end

        def full_response_from_buffer(tokens, body, resp_size)
          value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
          [resp_size, tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value]
        end

        ##
        # This method returns an array of values used in a pipelined
        # getk process.  The first value is the number of bytes by
        # which to advance the pointer in the buffer.  If the
        # complete response is found in the buffer, this will
        # be the response size.  Otherwise it is zero.
        #
        # The remaining three values in the array are the ResponseHeader,
        # key, and value.
        ##
        def getk_response_from_buffer(buf)
          # There's no header in the buffer, so don't advance
          return [0, nil, nil, nil, nil] unless contains_header?(buf)

          tokens, header_len, body_len = tokens_from_header_buffer(buf)

          # We have a complete response that has no body.
          # This is either the response to the terminating
          # noop or, if the status is not MN, an intermediate
          # error response that needs to be discarded.
          return [header_len, true, nil, nil, nil] if body_len.zero?

          resp_size = header_len + body_len + TERMINATOR.length
          # The header is in the buffer, but the body is not.  As we don't have
          # a complete response, don't advance the buffer
          return [0, nil, nil, nil, nil] unless buf.bytesize >= resp_size

          # The full response is in our buffer, so parse it and return
          # the values
          body = buf.slice(header_len, body_len)
          full_response_from_buffer(tokens, body, resp_size)
        end

        def contains_header?(buf)
          buf.include?(TERMINATOR)
        end

        def header_from_buffer(buf)
          buf.split(TERMINATOR, 2).first
        end

        def error_on_unexpected!(expected_codes)
          tokens = next_line_to_tokens

          return tokens if expected_codes.include?(tokens.first)

          raise Dalli::ServerError, tokens.join(' ').to_s if tokens.first == SERVER_ERROR

          raise Dalli::DalliError, "Response error: #{tokens.first}"
        end

        def bitflags_from_tokens(tokens)
          value_from_tokens(tokens, 'f')&.to_i
        end

        def cas_from_tokens(tokens)
          value_from_tokens(tokens, 'c')&.to_i
        end

        def key_from_tokens(tokens)
          encoded_key = value_from_tokens(tokens, 'k')
          base64_encoded = tokens.any?('b')
          KeyRegularizer.decode(encoded_key, base64_encoded)
        end

        def body_len_from_tokens(tokens)
          value_from_tokens(tokens, 's')&.to_i
        end

        def value_from_tokens(tokens, flag)
          bitflags_token = tokens.find { |t| t.start_with?(flag) }
          return 0 unless bitflags_token

          bitflags_token[1..]
        end

        def read_line
          @io_source.read_line&.chomp!(TERMINATOR)
        end

        def next_line_to_tokens
          line = read_line
          line&.split || []
        end

        def read_data(data_size)
          @io_source.read(data_size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
        end
      end
    end
  end
end
