# encoding: utf-8
# encoding: binary

# THIS IS AN AUTOGENERATED FILE, DO NOT MODIFY
# IT DIRECTLY ! FOR CHANGES, PLEASE UPDATE FILES
# IN THE ./codegen DIRECTORY OF THE AMQ-PROTOCOL REPOSITORY.<% import codegen_helpers as helpers %><% import re, os, codegen %>

require "amq/pack"

require "amq/protocol/table"
require "amq/protocol/frame"

require "amq/protocol/constants"
require "amq/protocol/exceptions"

module AMQ
  module Protocol
    PROTOCOL_VERSION = "${spec.major}.${spec.minor}.${spec.revision}".freeze
    PREAMBLE         = "${'AMQP\\x00\\x%02x\\x%02x\\x%02x' % (spec.major, spec.minor, spec.revision)}".freeze
    DEFAULT_PORT     = ${spec.port}

    # @return [Array] Collection of subclasses of AMQ::Protocol::Class.
    def self.classes
      Protocol::Class.classes
    end

    # @return [Array] Collection of subclasses of AMQ::Protocol::Method.
    def self.methods
      Protocol::Method.methods
    end

    % for tuple in spec.constants:
    % if tuple[2] == "soft-error" or tuple[2] == "hard-error":
    class ${codegen.to_ruby_class_name(tuple[0])} < ${codegen.to_ruby_class_name(tuple[2])}
      VALUE = ${tuple[1]}
    end

    % endif
    % endfor

    class Class
      @classes = Array.new

      def self.method_id
        @method_id
      end

      def self.name
        @name
      end

      def self.inherited(base)
        if self == Protocol::Class
          @classes << base
        end
      end

      def self.classes
        @classes
      end
    end

    class Method
      @methods = Array.new
      def self.method_id
        @method_id
      end

      def self.name
        @name
      end

      def self.index
        @index
      end

      def self.inherited(base)
        if self == Protocol::Method
          @methods << base
        end
      end

      def self.methods
        @methods
      end

      def self.split_headers(user_headers)
        properties, headers = {}, {}
        user_headers.each do |key, value|
          # key MUST be a symbol since symbols are not garbage-collected
          if Basic::PROPERTIES.include?(key)
            properties[key] = value
          else
            headers[key] = value
          end
        end

        return [properties, headers]
      end

      def self.encode_body(body, channel, frame_size)
        return [] if body.empty?

        # 8 = 1 + 2 + 4 + 1
        # 1 byte of frame type
        # 2 bytes of channel number
        # 4 bytes of frame payload length
        # 1 byte of payload trailer FRAME_END byte
        limit        = frame_size - 8
        return [BodyFrame.new(body, channel)] if body.bytesize < limit

        # Otherwise String#slice on 1.9 will operate with code points,
        # and we need bytes. MK.
        body.force_encoding("ASCII-8BIT") if RUBY_VERSION.to_f >= 1.9

        array = Array.new
        while body && !body.empty?
          payload, body = body[0, limit], body[limit, body.length - limit]
          array << BodyFrame.new(payload, channel)
        end

        array
      end

      def self.instantiate(*args, &block)
        self.new(*args, &block)
      end
    end

    % for klass in spec.classes :
    class ${klass.constant_name} < Protocol::Class
      @name = "${klass.name}"
      @method_id = ${klass.index}

      % if klass.fields: ## only the Basic class has fields (refered as properties in the JSON)
      PROPERTIES = [
      % for field in klass.fields:
        :${field.ruby_name}, # ${spec.resolveDomain(field.domain)}
        % endfor
      ]

      % for f in klass.fields:
      # <% i = klass.fields.index(f) %>1 << ${15 - i}
      def self.encode_${f.ruby_name}(value)
        buffer = ''
        % for line in helpers.genSingleEncode(spec, "value", f.domain):
        ${line}
        % endfor
        [${i}, ${"0x%04x" % ( 1 << (15-i),)}, buffer]
      end

      % endfor

      % endif

      % if klass.name == "basic" :
      def self.encode_properties(body_size, properties)
        pieces, flags = [], 0

        properties.reject {|key, value| value.nil?}.each do |key, value|
          i, f, result = self.__send__(:"encode_#{key}", value)
          flags |= f
          pieces[i] = result
        end

        # result = [${klass.index}, 0, body_size, flags].pack('n2Qn')
        result = [${klass.index}, 0].pack(PACK_UINT16_X2)
        result += AMQ::Pack.pack_uint64_big_endian(body_size)
        result += [flags].pack(PACK_UINT16)
        pieces_joined = pieces.join(EMPTY_STRING)
        result.force_encoding(pieces_joined.encoding) + pieces_joined
      end

      # THIS DECODES ONLY FLAGS
      DECODE_PROPERTIES = {
        % for f in klass.fields:
        ${"0x%04x" % ( 1 << (15 - klass.fields.index(f)),)} => :${f.ruby_name},
        % endfor
      }

      DECODE_PROPERTIES_TYPE = {
        % for f in klass.fields:
        ${"0x%04x" % ( 1 << (15 - klass.fields.index(f)),)} => :${spec.resolveDomain(f.domain)},
        % endfor
      }

      # Hash doesn't give any guarantees on keys order, we will do it in a
      # straightforward way
      DECODE_PROPERTIES_KEYS = [
        % for f in klass.fields:
        ${"0x%04x" % ( 1 << (15 - klass.fields.index(f)),)},
        % endfor
      ]

      def self.decode_properties(data)
        offset, data_length, properties = 0, data.bytesize, {}

        compressed_index = data[offset, 2].unpack(PACK_UINT16)[0]
        offset += 2
        while data_length > offset
          DECODE_PROPERTIES_KEYS.each do |key|
            next unless compressed_index >= key
            compressed_index -= key
            name = DECODE_PROPERTIES[key] || raise(RuntimeError.new("No property found for index #{index.inspect}!"))
            case DECODE_PROPERTIES_TYPE[key]
            when :shortstr
              size = data[offset, 1].unpack(PACK_CHAR)[0]
              offset += 1
              result = data[offset, size]
            when :octet
              size = 1
              result = data[offset, size].unpack(PACK_CHAR).first
            when :timestamp
              size = 8
              result = Time.at(data[offset, size].unpack(PACK_UINT64_BE).last)
            when :table
              size = 4 + data[offset, 4].unpack(PACK_UINT32)[0]
              result = Table.decode(data[offset, size])
            end
            properties[name] = result
            offset += size
          end
        end

        properties
      end
      % endif

      % for method in klass.methods:
      class ${method.constant_name} < Protocol::Method
        @name = "${klass.name}.${method.name}"
        @method_id = ${method.index}
        @index = ${method.binary()}
        @packed_indexes = [${klass.index}, ${method.index}].pack(PACK_UINT16_X2).freeze

        % if (spec.type == "client" and method.accepted_by("client")) or (spec.type == "server" and method.accepted_by("server") or spec.type == "all"):
        # @return
        def self.decode(data)
          offset = offset = 0 # self-assigning offset to eliminate "assigned but unused variable" warning even if offset is not used in this method
          % for line in helpers.genDecodeMethodDefinition(spec, method):
          ${line}
          % endfor
          % if (method.klass.name == "connection" or method.klass.name == "channel") and method.name == "close":
          self.new(${', '.join([f.ruby_name for f in method.arguments])})
          % else:
          self.new(${', '.join([f.ruby_name for f in method.arguments])})
          % endif
        end

        % if len(method.arguments) > 0:
        attr_reader ${', '.join([":" + f.ruby_name for f in method.arguments])}
        % endif
        def initialize(${', '.join([f.ruby_name for f in method.arguments])})
          % for f in method.arguments:
          @${f.ruby_name} = ${f.ruby_name}
          % endfor
        end
        % endif

        def self.has_content?
          % if method.hasContent:
          true
          % else:
          false
          % endif
        end

        % if (spec.type == "client" and method.accepted_by("server")) or (spec.type == "server" and method.accepted_by("client")) or spec.type == "all":
        # @return
        # ${method.params()}
        % if klass.name == "connection":
        def self.encode(${(", ").join(method.not_ignored_args())})
        % else:
        def self.encode(${(", ").join(["channel"] + method.not_ignored_args())})
        % endif
          % for argument in method.ignored_args():
          ${codegen.convert_to_ruby(argument)}
          % endfor
          % if klass.name == "connection":
          channel = 0
          % endif
          buffer = @packed_indexes.dup
          % for line in helpers.genEncodeMethodDefinition(spec, method):
          ${line}
          % endfor
          % if "payload" in method.args() or "user_headers" in method.args():
          frames = [MethodFrame.new(buffer, channel)]
          % if "user_headers" in method.args():
          properties, _headers = self.split_headers(user_headers)
          if properties.nil? or properties.empty?
            raise RuntimeError.new("Properties can not be empty!")
          end
          properties_payload = Basic.encode_properties(payload.bytesize, properties)
          frames << HeaderFrame.new(properties_payload, channel)
          % endif
          % if "payload" in method.args():
          frames += self.encode_body(payload, channel, frame_size)
          frames
          % endif
          % else:
          MethodFrame.new(buffer, channel)
          % endif
        end
        % endif

      end

      % endfor
    end

    % endfor

    METHODS = begin
      Method.methods.inject(Hash.new) do |hash, klass|
        hash.merge!(klass.index => klass)
      end
    end
  end
end
