# encoding: binary

require "amq/endianness"
require "amq/protocol/type_constants"
require "amq/protocol/float_32bit"

module AMQ
  module Protocol

    class TableValueDecoder

      #
      # Behaviors
      #

      include TypeConstants


      #
      # API
      #

      def self.decode_array(data, initial_offset)
        array_length = data.slice(initial_offset, 4).unpack(PACK_UINT32).first

        ary    = Array.new
        offset = initial_offset + 4

        while offset <= (initial_offset + array_length)
          type, offset = decode_value_type(data, offset)

          i = case type
              when TYPE_STRING
                v, offset = decode_string(data, offset)
                v
              when TYPE_BYTE_ARRAY
                # Ruby doesn't have a direct counterpart to
                # ByteBuffer or byte[], so using a string feels
                # more appropriate than an array of fixnums
                v, offset = decode_string(data, offset)
                v
              when TYPE_INTEGER
                v, offset = decode_integer(data, offset)
                v
              when TYPE_DECIMAL
                v, offset = decode_big_decimal(data, offset)
                v
              when TYPE_TIME
                v, offset = decode_time(data, offset)
                v
              when TYPE_HASH
                v, offset = decode_hash(data, offset)
                v
              when TYPE_BOOLEAN
                v, offset = decode_boolean(data, offset)
                v
              when TYPE_BYTE then
                v, offset = decode_byte(data, offset)
                v
              when TYPE_SIGNED_16BIT then
                v, offset = decode_short(data, offset)
                v
              when TYPE_SIGNED_64BIT then
                v, offset = decode_long(data, offset)
                v
              when TYPE_32BIT_FLOAT then
                v, offset = decode_32bit_float(data, offset)
                v
              when TYPE_64BIT_FLOAT then
                v, offset = decode_64bit_float(data, offset)
                v
              when TYPE_VOID
                nil
              when TYPE_ARRAY
                v, offset = TableValueDecoder.decode_array(data, offset)
                v
              else
                raise ArgumentError.new("unsupported type in a table value: #{type.inspect}, do not know how to decode!")
              end

          ary << i
        end


        [ary, initial_offset + array_length + 4]
      end # self.decode_array(data, initial_offset)


      def self.decode_string(data, offset)
        length = data.slice(offset, 4).unpack(PACK_UINT32).first
        offset += 4
        v = data.slice(offset, length)
        offset += length

        [v, offset]
      end # self.decode_string(data, offset)


      def self.decode_integer(data, offset)
        v = data.slice(offset, 4).unpack(PACK_UINT32).first
        offset += 4

        [v, offset]
      end # self.decode_integer(data, offset)


      if AMQ::Endianness.big_endian?
        def self.decode_long(data, offset)
          v    = data.slice(offset, 8).unpack(PACK_INT64)

          offset += 8
          [v, offset]
        end
      else
        def self.decode_long(data, offset)
          slice = data.slice(offset, 8).bytes.to_a.reverse.map(&:chr).join
          v     = slice.unpack(PACK_INT64).first

          offset += 8
          [v, offset]
        end
      end


      def self.decode_big_decimal(data, offset)
        decimals, raw = data.slice(offset, 5).unpack(PACK_UCHAR_UINT32)
        offset += 5
        v = BigDecimal(raw.to_s) * (BigDecimal(TEN) ** -decimals)

        [v, offset]
      end # self.decode_big_decimal(data, offset)


      def self.decode_time(data, offset)
        timestamp = data.slice(offset, 8).unpack(PACK_UINT64_BE).last
        v = Time.at(timestamp)
        offset += 8

        [v, offset]
      end # self.decode_time(data, offset)


      def self.decode_boolean(data, offset)
        integer = data.slice(offset, 2).unpack(PACK_CHAR).first # 0 or 1
        offset += 1
        [(integer == 1), offset]
      end # self.decode_boolean(data, offset)


      def self.decode_32bit_float(data, offset)
        v = data.slice(offset, 4).unpack(PACK_32BIT_FLOAT).first
        offset += 4

        [v, offset]
      end # self.decode_32bit_float(data, offset)


      def self.decode_64bit_float(data, offset)
        v = data.slice(offset, 8).unpack(PACK_64BIT_FLOAT).first
        offset += 8

        [v, offset]
      end # self.decode_64bit_float(data, offset)


      def self.decode_value_type(data, offset)
        [data.slice(offset, 1), offset + 1]
      end # self.decode_value_type(data, offset)



      def self.decode_hash(data, offset)
        length = data.slice(offset, 4).unpack(PACK_UINT32).first
        v = Table.decode(data.slice(offset, length + 4))
        offset += 4 + length

        [v, offset]
      end # self.decode_hash(data, offset)


      # Decodes/Converts a byte value from the data at the provided offset.
      #
      # @param [Array] data - A big-endian ordered array of bytes.
      # @param [Fixnum] offset - The offset which bytes the byte is consumed.
      # @return [Array] - The Fixnum value and new offset pair.
      def self.decode_byte(data, offset)
        [data.slice(offset, 1).unpack(PACK_CHAR).first, offset += 1]
      end


      def self.decode_short(data, offset)
        v = AMQ::Hacks.unpack_int16_big_endian(data.slice(offset, 2)).first
        offset += 2
        [v, offset]
      end
    end # TableValueDecoder
  end # Protocol
end # AMQ
