# encoding: utf-8
# encoding: binary

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

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

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}

    # caching
    EMPTY_STRING = "".freeze

    PACK_CHAR               = 'C'.freeze
    PACK_UINT16             = 'n'.freeze
    PACK_UINT16_X2          = 'n2'.freeze
    PACK_UINT32             = 'N'.freeze
    PACK_UINT32_X2          = 'N2'.freeze
    PACK_INT64              = 'q'.freeze
    PACK_UCHAR_UINT32       = 'CN'.freeze
    PACK_CHAR_UINT16_UINT32 = 'cnN'.freeze

    PACK_32BIT_FLOAT        = 'f'.freeze
    PACK_64BIT_FLOAT        = 'd'.freeze



    # @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

    class Error < StandardError
      DEFAULT_MESSAGE = "AMQP error".freeze

      def self.inherited(subclass)
        @_subclasses ||= []
        @_subclasses << subclass
      end # self.inherited(subclazz)

      def self.subclasses_with_values
        @_subclasses.select{ |k| defined?(k::VALUE) }
      end # self.subclasses_with_values

      def self.[](code)
        if result = subclasses_with_values.detect { |klass| klass::VALUE == code }
          result
        else
          raise "No such exception class for code #{code}" unless result
        end # if
      end # self.[]

      def initialize(message = self.class::DEFAULT_MESSAGE)
        super(message)
      end
    end

    class FrameTypeError < Protocol::Error
      def initialize(types)
        super("Must be one of #{types.inspect}")
      end
    end

    class EmptyResponseError < Protocol::Error
      DEFAULT_MESSAGE = "Empty response received from the server."

      def initialize(message = self.class::DEFAULT_MESSAGE)
        super(message)
      end
    end

    class BadResponseError < Protocol::Error
      def initialize(argument, expected, actual)
        super("Argument #{argument} has to be #{expected.inspect}, was #{data.inspect}")
      end
    end

    class SoftError < Protocol::Error
      def self.inherited(subclass)
        Error.inherited(subclass)
      end # self.inherited(subclass)
    end

    class HardError < Protocol::Error
      def self.inherited(subclass)
        Error.inherited(subclass)
      end # self.inherited(subclass)
    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

    # We don't instantiate the following classes,
    # as we don't actually need any per-instance state.
    # Also, this is pretty low-level functionality,
    # hence it should have a reasonable performance.
    # As everyone knows, garbage collector in MRI performs
    # really badly, which is another good reason for
    # not creating any objects, but only use class as
    # a struct. Creating classes is quite expensive though,
    # but here the inheritance comes handy and mainly
    # as we can't simply make a reference to a function,
    # we can't use a hash or an object. I've been also
    # considering to have just a bunch of methods, but
    # here's the problem, that after we'd require this file,
    # all these methods would become global which would
    # be a bad, bad thing to do.
    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?

        # See https://dev.rabbitmq.com/wiki/Amqp091Errata#section_11
        limit        = frame_size - 8
        limit_plus_1 = limit + 1

        array = Array.new
        while body
          payload, body = body[0, limit_plus_1], body[limit_plus_1, body.length - limit]
          # array << [0x03, payload]
          array << BodyFrame.new(payload, channel)
        end

        array
      end

      # We can return different:
      # - instantiate given subclass of Method
      # - create an OpenStruct object
      # - create a hash
      # - yield params into the block rather than just return
      # @api plugin
      def self.instantiate(*args, &block)
        self.new(*args, &block)
        # or OpenStruct.new(args.first)
        # or args.first
        # or block.call(*args)
      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

      ## TODO: not only basic, resp. in fact it's only this class, but not necessarily in the future, rather check if properties are empty #}
      % if klass.name == "basic" :
      def self.encode_properties(body_size, properties)
        pieces, flags = [], 0

        properties.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::Hacks.pack_64_big_endian(body_size)
        result += [flags].pack(PACK_UINT16)
        result + pieces.join(EMPTY_STRING)
      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_UINT32_X2).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 = 0
          % 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 = ''
          buffer << @packed_indexes
          % 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)
          # TODO: what shall I do with the headers?
          if properties.nil? or properties.empty?
            raise RuntimeError.new("Properties can not be empty!") # TODO: or can they?
          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)
          % endif
          % else:
          % if os.getenv("DEVELOPMENT"):
          STDERR.puts("~ [\e[31m#{channel}\e[m] \e[32m${klass.name}.${method.name}\e[m(${", ".join([method + "=#{" + method + ".inspect}" for method in method.not_ignored_args()])})") if $DEBUG
          % endif
          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