#!/usr/bin/env ruby
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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.
#
# Usage: output_directory xml_spec_file [xml_spec_file...]
# 
$: << '..'
require 'cppgen'

class StructGen < CppGen

  def initialize(outdir, amqp)
    super(outdir, amqp)
  end

  SizeMap={
    "Octet"=>1,
    "Short"=>2,
    "Long"=>4,
    "LongLong"=>8,
    "int8"=>1,
    "int16"=>2,
    "int32"=>4,
    "int64"=>8,
    "uint8"=>1,
    "uint16"=>2,
    "uint32"=>4,
    "uint64"=>8,
    "timestamp"=>8
  }

  StringSizeMap={
    "LongString"=>4,
    "MediumString"=>2,
    "ShortString"=>1
  }
  
  SizeType={
    1=>"Octet",
    2=>"Short",
    4=>"Long",
    8=>"LongLong"
  }

  ValueTypes=["uint8_t", "uint16_t", "uint32_t", "uint64_t"]

  def is_packed(s) s.pack and s.pack != "0" end

  def execution_header?(s)
    s.is_a? AmqpMethod and not s.parent.control?
    # s.kind_of? AmqpMethod and s.parent.name.include?("010") and not s.parent.control?
  end

  def has_bitfields_only(s)
    s.fields.select {|f| f.type_ != "bit"}.empty?
  end

  def default_initialisation(s)
    params = s.fields.select {|f| ValueTypes.include?(f.cpptype.name) || (!is_packed(s) && f.type_ == "bit")}
    strings = params.collect {|f| "#{f.cppname}(#{f.default_value})"}   
    strings << "flags(0)" if (is_packed(s))
    if strings.empty?
      return ""
    else
      return " : " + strings.join(", ")
    end
  end

  def printable_form(f)
    if (f.cpptype.name == "uint8_t")
      return "(int) " + f.cppname
    elsif (f.type_ == "bit")
      return "get#{f.name.caps}()"
    else
      return f.cppname
    end
  end

  def flag_mask(s, i)
    pos = s.pack.to_i*8 - 8 - (i/8)*8 + (i % 8)
    return "(1 << #{pos})"
  end

  def encode_packed_struct(s)
    genl s.cpp_pack_type.encode('flags', 'buffer')
    process_packed_fields(s) { |f, i| encode_packed_field(s, f, i) unless f.type_ == "bit" }
  end

  def decode_packed_struct(s)
    genl "#{s.cpp_pack_type.decode('flags', 'buffer')}"
    process_packed_fields(s) { |f, i| decode_packed_field(s, f, i) unless f.type_ == "bit" }
  end

  def size_packed_struct(s)
    genl "total += #{s.pack};"
    process_packed_fields(s) { |f, i| size_packed_field(s, f, i) unless f.type_ == "bit" }
  end

  def print_packed_struct(s)
    process_packed_fields(s) { |f, i| print_packed_field(s, f, i) }
  end

  def encode_packed_field(s, f, i)
    genl "if (flags & #{flag_mask(s, i)})"
    indent { genl f.cpptype.encode(f.cppname,"buffer") }
  end

  def decode_packed_field(s, f, i)
    genl "if (flags & #{flag_mask(s, i)})"
    indent { genl f.cpptype.decode(f.cppname,"buffer") }
  end

  def size_packed_field(s, f, i)
      genl "if (flags & #{flag_mask(s, i)})"
      indent { generate_size(f, []) }
  end

  def print_packed_field(s, f, i)
    classname = s.cppname
    if (s.kind_of? AmqpMethod)
      classname = s.body_name
    end
    genl "if (flags & #{flag_mask(s, i)})"
    indent { 
      unless (classname == "ConnectionStartOkBody" && f.name == "response")
        genl "out << \"#{f.name}=\" << #{printable_form(f)} << \"; \";"
      else
        genl "out << \"response=\" << \"xxxxxx\" << \"; \";"
      end
    }
  end

  def generate_encode(f, combined)
    if (f.type_ == "bit")
      genl "uint8_t #{f.cppname}_bits = #{f.cppname};"
      count = 0
      combined.each { |c| genl "#{f.cppname}_bits |= #{c.cppname} << #{count += 1};" }
      genl "buffer.putOctet(#{f.cppname}_bits);"
    else
      genl f.cpptype.encode(f.cppname,"buffer")
    end
  end

  def generate_decode(f, combined)
    if (f.type_ == "bit")
      genl "uint8_t #{f.cppname}_bits = buffer.getOctet();"
      genl "#{f.cppname} = 1 & #{f.cppname}_bits;"
      count = 0
      combined.each { |c| genl "#{c.cppname} = (1 << #{count += 1}) & #{f.cppname}_bits;" }
    else
      genl f.cpptype.decode(f.cppname,"buffer")
    end
  end

  def generate_size(f, combined)
    if (f.type_ == "bit")
      names = ([f] + combined).collect {|g| g.cppname}
      genl "total += 1;//#{names.join(", ")}"
    else
      size = SizeMap[f.cpptype.encoded]
      if (size)
        genl "total += #{size};//#{f.cppname}"
      elsif (f.cpptype.name == "SequenceNumberSet")
        genl "total += #{f.cppname}.encodedSize();"
      elsif (size = StringSizeMap[f.cpptype.encoded])
        genl "total += #{size} + #{f.cppname}.size();"
      else
        genl "total += #{f.cppname}.encodedSize();"
      end
    end
  end

  def check_field(f)
    if (size = StringSizeMap[f.cpptype.encoded] and size < 4)
      limit = 2 ** (size * 8)
      genl "if (#{f.cppname}.size() >= #{limit}) throw IllegalArgumentException(\"Value for #{f.cppname} is too large\");"
    end
  end

  def process_packed_fields(s)
    s.fields.each { |f| yield f, s.fields.index(f) }
  end

  def process_fields(s)
    last = nil
    count = 0  
    bits = []
    s.fields.each { 
      |f| if (last and last.bit? and f.bit? and count < 7) 
            count += 1
            bits << f
          else
            if (last and last.bit?)
              yield last, bits
              count = 0
              bits = []
            end
            if (not f.bit?)
              yield f
            end
            last = f
          end
    }
    if (last and last.bit?)
      yield last, bits
    end
  end

  def all_fields_via_accessors(s)
    s.fields.collect { |f| "get#{f.name.caps}()" }.join(", ")
  end

  def methodbody_extra_defs(s)
    if (s.parent.control?) 
      genl "virtual uint8_t type() const { return 0;/*control segment*/ }"
    end
    

    gen <<EOS
    typedef #{s.result ? s.result.cpptype.name : 'void'} ResultType;

    template <class T> ResultType invoke(T& invocable) const {
        return invocable.#{s.cppname}(#{all_fields_via_accessors(s)});
    }

    using  AMQMethodBody::accept;
    void accept(MethodBodyConstVisitor& v) const { v.visit(*this); }
    boost::intrusive_ptr<AMQBody> clone() const { return BodyFactory::copy(*this); }

    ClassId amqpClassId() const { return CLASS_ID; }
    MethodId amqpMethodId() const { return METHOD_ID; }
    bool isContentBearing() const { return  #{s.content ? "true" : "false" }; }
    bool resultExpected() const { return  #{s.result ? "true" : "false"}; }
    bool responseExpected() const { return  #{s.responses().empty? ? "false" : "true"}; }
EOS
  end

  def define_constructor(name, s)
    if (s.fields.size > 0)
      genl "#{name}("
      if (s.kind_of? AmqpMethod)
        indent {gen "ProtocolVersion, "}
      end
      indent { gen s.fields.collect { |f| "#{f.cpptype.param} _#{f.cppname}" }.join(",\n") }
      genl ") : "
      if (is_packed(s))
        initialisers = s.fields.select { |f| f.type_ != "bit"}.collect { |f| "#{f.cppname}(_#{f.cppname})"}

        initialisers << "flags(0)"
        indent { gen initialisers.join(",\n") }
        genl "{"
        indent {
          process_packed_fields(s) { |f, i| genl "set#{f.name.caps}(_#{f.cppname});" if f.type_ == "bit"}
          process_packed_fields(s) { |f, i| genl "flags |= #{flag_mask(s, i)};" unless f.type_ == "bit"}
          s.fields.each { |f| check_field(f) }
        }
        genl "}"          
      else
        indent { gen s.fields.collect { |f| " #{f.cppname}(_#{f.cppname})" }.join(",\n") }
        genl "{"
        indent {
          s.fields.each { |f| check_field(f) }
        }
        genl "}"
      end
    end
    #default constructors:
    if (s.kind_of? AmqpMethod)
      genl "#{name}(ProtocolVersion=ProtocolVersion()) #{default_initialisation(s)} {}"
    end
    if (s.kind_of? AmqpStruct)
      genl "#{name}() #{default_initialisation(s)} {}"
    end
  end

  def define_packed_field_accessors(s, f, i)
    if (s.kind_of? AmqpMethod) 
      define_packed_field_accessors_for_method(s, f, i)
    else
      define_packed_field_accessors_for_struct(s, f, i)
    end
  end

  def define_packed_field_accessors_for_struct(s, f, i)
    if (f.type_ == "bit")
      genl "void #{s.cppname}::set#{f.name.caps}(#{f.cpptype.param} _#{f.cppname}) {"
      indent {
        genl "if (_#{f.cppname}) flags |= #{flag_mask(s, i)};"
        genl "else flags &= ~#{flag_mask(s, i)};"
      }
      genl "}"
      genl "#{f.cpptype.ret} #{s.cppname}::get#{f.name.caps}() const { return flags & #{flag_mask(s, i)}; }"
    else 
      genl "void #{s.cppname}::set#{f.name.caps}(#{f.cpptype.param} _#{f.cppname}) {"
      indent {
        genl "#{f.cppname} = _#{f.cppname};"
        genl "flags |= #{flag_mask(s, i)};"
        check_field(f)
      }
      genl "}"
      genl "#{f.cpptype.ret} #{s.cppname}::get#{f.name.caps}() const { return #{f.cppname}; }"
      if (f.cpptype.name == "FieldTable")
        genl "#{f.cpptype.name}& #{s.cppname}::get#{f.name.caps}() {"
        indent { 
          genl "flags |= #{flag_mask(s, i)};"#treat the field table as having been 'set'
          genl "return #{f.cppname};" 
        }
        genl "}"
      end
      genl "bool #{s.cppname}::has#{f.name.caps}() const { return flags & #{flag_mask(s, i)}; }"
      genl "void #{s.cppname}::clear#{f.name.caps}Flag() { flags &= ~#{flag_mask(s, i)}; }"
    end
    genl ""
  end

  def define_packed_field_accessors_for_method(s, f, i)
    if (f.type_ == "bit")
      genl "void #{s.body_name}::set#{f.name.caps}(#{f.cpptype.param} _#{f.cppname}) {"
      indent {
        genl "if (_#{f.cppname}) flags |= #{flag_mask(s, i)};"
        genl "else flags &= ~#{flag_mask(s, i)};"
      }
      genl "}"
      genl "#{f.cpptype.ret} #{s.body_name}::get#{f.name.caps}() const { return flags & #{flag_mask(s, i)}; }"
    else 
      genl "void #{s.body_name}::set#{f.name.caps}(#{f.cpptype.param} _#{f.cppname}) {"
      indent {
        genl "#{f.cppname} = _#{f.cppname};"
        genl "flags |= #{flag_mask(s, i)};"
        check_field(f)
      }
      genl "}"
      genl "#{f.cpptype.ret} #{s.body_name}::get#{f.name.caps}() const { return #{f.cppname}; }"
      if (f.cpptype.name == "FieldTable")
        genl "#{f.cpptype.name}& #{s.body_name}::get#{f.name.caps}() {"
        indent { 
          genl "flags |= #{flag_mask(s, i)};"#treat the field table as having been 'set'
          genl "return #{f.cppname};" 
        }
        genl "}"
      end
      genl "bool #{s.body_name}::has#{f.name.caps}() const { return flags & #{flag_mask(s, i)}; }"
      genl "void #{s.body_name}::clear#{f.name.caps}Flag() { flags &= ~#{flag_mask(s, i)}; }"
    end
    genl ""
  end

  def define_packed_accessors(s)
    process_packed_fields(s) { |f, i| define_packed_field_accessors(s, f, i) }
  end

  def declare_packed_accessors(f)
    genl "QPID_COMMON_EXTERN void set#{f.name.caps}(#{f.cpptype.param} _#{f.cppname});";
    genl "QPID_COMMON_EXTERN #{f.cpptype.ret} get#{f.name.caps}() const;"
    if (f.cpptype.name == "FieldTable")
      genl "QPID_COMMON_EXTERN #{f.cpptype.name}& get#{f.name.caps}();"
    end
    if (f.type_ != "bit")
      #extra 'accessors' for packed fields:
      genl "QPID_COMMON_EXTERN bool has#{f.name.caps}() const;"
      genl "QPID_COMMON_EXTERN void clear#{f.name.caps}Flag();"
    end
  end

  def define_accessors(f)
    genl "void set#{f.name.caps}(#{f.cpptype.param} _#{f.cppname}) {"
    indent {
      genl "#{f.cppname} = _#{f.cppname};"
      check_field(f)
    }
    genl "}"
    genl "#{f.cpptype.ret} get#{f.name.caps}() const { return #{f.cppname}; }"
    if (f.cpptype.name == "FieldTable")
      genl "#{f.cpptype.name}& get#{f.name.caps}() { return #{f.cppname}; }"
    end
  end

  def define_struct(s)
    classname = s.cppname
    inheritance = ""
    if (s.kind_of? AmqpMethod)
      classname = s.body_name
      if (execution_header?(s))
        inheritance = ": public ModelMethod"
      else
        inheritance = ": public AMQMethodBody"
      end
    else
      public_api("qpid/framing/#{classname}.h") # Non-method structs are public
    end

    h_file("qpid/framing/#{classname}.h") {       
      if (s.kind_of? AmqpMethod)
        gen <<EOS
#include "qpid/framing/AMQMethodBody.h"
#include "qpid/framing/AMQP_ServerOperations.h"
#include "qpid/framing/MethodBodyConstVisitor.h"
EOS
      end
      include "qpid/framing/ModelMethod.h" if (execution_header?(s))

      s.fields.each { |f| include "qpid/framing/#{f.cpptype.name}" if f.struct?}

      gen <<EOS

#include <ostream>
#include "qpid/framing/amqp_types_full.h"
#include "qpid/framing/reply_exceptions.h"
#include "qpid/CommonImportExport.h"

namespace qpid {
namespace framing {

class QPID_COMMON_CLASS_EXTERN #{classname} #{inheritance} {
EOS
  if (is_packed(s))
    indent { s.fields.each { |f| genl "#{f.cpptype.name} #{f.cppname};" unless f.type_ == "bit"} }
    indent {
      genl "#{s.cpp_pack_type.name} flags;"
    }
  else
    indent { s.fields.each { |f| genl "#{f.cpptype.name} #{f.cppname};" } }
  end
  genl "public:"
  if (s.kind_of? AmqpMethod)
    indent { genl "static const ClassId CLASS_ID = #{s.parent.code};" }
    indent { genl "static const MethodId METHOD_ID = #{s.code};" }
  end

  if (s.kind_of? AmqpStruct)
    if (s.code)
      indent { genl "static const uint16_t TYPE = #{s.full_code};" }
    end
  end

  indent { 
    define_constructor(classname, s)
    genl ""
    if (is_packed(s))
      s.fields.each { |f| declare_packed_accessors(f) } 
    else
      s.fields.each { |f| define_accessors(f) } 
    end
  }
  if (s.kind_of? AmqpMethod)
    methodbody_extra_defs(s)
  end
  if (s.kind_of? AmqpStruct)
    indent {genl "QPID_COMMON_EXTERN friend std::ostream& operator<<(std::ostream&, const #{classname}&);" }
  end

  gen <<EOS
    QPID_COMMON_EXTERN void encode(Buffer&) const;
    QPID_COMMON_EXTERN void decode(Buffer&, uint32_t=0);
    QPID_COMMON_EXTERN void encodeStructBody(Buffer&) const;
    QPID_COMMON_EXTERN void decodeStructBody(Buffer&, uint32_t=0);
    QPID_COMMON_EXTERN uint32_t encodedSize() const;
    QPID_COMMON_EXTERN uint32_t bodySize() const;
    QPID_COMMON_EXTERN void print(std::ostream& out) const;
}; /* class #{classname} */

}}
EOS
    }
    cpp_file("qpid/framing/#{classname}.cpp") { 
      if (is_packed(s) || s.fields.size > 0 || execution_header?(s))
        buffer = "buffer"
      else
        buffer = "/*buffer*/"
      end
      gen <<EOS
#include "qpid/framing/#{classname}.h"
#include "qpid/framing/Buffer.h"

using namespace qpid::framing;

EOS
    
      if (is_packed(s))
        define_packed_accessors(s)
      end
      gen <<EOS
void #{classname}::encodeStructBody(Buffer& #{buffer}) const
{
EOS
      if (execution_header?(s))
        genl "encodeHeader(buffer);"
      end

      if (is_packed(s))
        indent {encode_packed_struct(s)}
      else 
        indent { process_fields(s) { |f, combined| generate_encode(f, combined) } } 
      end
      gen <<EOS
}

void #{classname}::encode(Buffer& buffer) const
{
EOS
      indent {
        if (s.kind_of? AmqpStruct)
          if (s.code)
            genl "buffer.put#{SizeType[s.size.to_i]}(bodySize() + 2/*typecode*/);" if s.size and s.size.to_i != 0
            genl "buffer.putShort(TYPE);" 
          else
            genl "buffer.put#{SizeType[s.size.to_i]}(bodySize());" if s.size and s.size.to_i != 0
          end
        end
        genl "encodeStructBody(buffer);"
      }
      gen <<EOS
}

void #{classname}::decodeStructBody(Buffer& #{buffer}, uint32_t /*size*/)
{
EOS
      if (execution_header?(s))
        genl "decodeHeader(buffer);"
      end

      if (is_packed(s))
        indent {decode_packed_struct(s)}
      else 
        indent { process_fields(s) { |f, combined| generate_decode(f, combined) } } 
      end
      gen <<EOS
}

void #{classname}::decode(Buffer& buffer, uint32_t /*size*/)
{
EOS
      indent {
        if (s.kind_of? AmqpStruct)
          genl "buffer.get#{SizeType[s.size.to_i]}();" if s.size and s.size.to_i != 0
          genl "if (TYPE != buffer.getShort()) throw FramingErrorException(\"Bad type code for struct\");" if s.code
        end
        genl "decodeStructBody(buffer);"
      }
      gen <<EOS
}

uint32_t #{classname}::bodySize() const
{
    uint32_t total = 0;
EOS
      if (execution_header?(s))
        genl "total += headerSize();"
      end

      if (is_packed(s))
        indent {size_packed_struct(s)}
      else 
        indent { process_fields(s) { |f, combined| generate_size(f, combined) } } 
      end
      gen <<EOS
    return total;
}

uint32_t #{classname}::encodedSize() const {
    uint32_t total = bodySize();
EOS
        if (s.kind_of? AmqpStruct)
          genl "total += #{s.size}/*size field*/;" if s.size
          genl "total += 2/*typecode*/;" if s.code
        end
      gen <<EOS
    return total;
}

void #{classname}::print(std::ostream& out) const
{
    out << "{#{classname}: ";
EOS
      if (is_packed(s))
        indent {print_packed_struct(s)}
      else 
        copy = Array.new(s.fields)
        f = copy.shift
        
        indent { 
          genl "out << \"#{f.name}=\" << #{printable_form(f)};" if f
          copy.each { |f| genl "out << \"; #{f.name}=\" << #{printable_form(f)};" } 
        } 
      end
      gen <<EOS
    out << "}";
}
EOS

      if (s.kind_of? AmqpStruct)
        gen <<EOS
namespace qpid{
namespace framing{

    std::ostream& operator<<(std::ostream& out, const #{classname}& s) 
    {
      s.print(out);
      return out;
    }

}
}
EOS
      end 
}
  end

  def generate()
    structs = @amqp.collect_all(AmqpStruct).select { |s| not ["command-fragment"].include?(s.name) }
    structs.each { |s| define_struct(s) }
    @amqp.methods_.each { |m| define_struct(m) }
    #generate a single include file containing the list of structs for convenience
    public_api("qpid/framing/amqp_structs.h")
    h_file("qpid/framing/amqp_structs.h") { structs.each { |s| genl "#include \"qpid/framing/#{s.cppname}.h\"" } }
  end
end

StructGen.new($outdir, $amqp).generate()

