require 'dl'
require 'dl/value'
require 'dl/pack.rb'

module DL
  # C struct shell
  class CStruct
    # accessor to DL::CStructEntity
    def CStruct.entity_class()
      CStructEntity
    end
  end

  # C union shell
  class CUnion
    # accessor to DL::CUnionEntity
    def CUnion.entity_class()
      CUnionEntity
    end
  end

  # Used to construct C classes (CUnion, CStruct, etc)
  #
  # DL::Importer#struct and DL::Importer#union wrap this functionality in an
  # easy-to-use manner.
  module CStructBuilder
    # Construct a new class given a C:
    # * class +klass+ (CUnion, CStruct, or other that provide an
    #   #entity_class)
    # * +types+ (DL:TYPE_INT, DL::TYPE_SIZE_T, etc., see the C types
    #   constants)
    # * corresponding +members+
    #
    # DL::Importer#struct and DL::Importer#union wrap this functionality in an
    # easy-to-use manner.
    #
    # Example:
    #
    #   require 'dl/struct'
    #   require 'dl/cparser'
    #
    #   include DL::CParser
    #
    #   types, members = parse_struct_signature(['int i','char c'])
    #
    #   MyStruct = DL::CStructBuilder.create(CUnion, types, members)
    #
    #   obj = MyStruct.allocate
    #
    def create(klass, types, members)
      new_class = Class.new(klass){
        define_method(:initialize){|addr|
          @entity = klass.entity_class.new(addr, types)
          @entity.assign_names(members)
        }
        define_method(:to_ptr){ @entity }
        define_method(:to_i){ @entity.to_i }
        members.each{|name|
          define_method(name){ @entity[name] }
          define_method(name + "="){|val| @entity[name] = val }
        }
      }
      size = klass.entity_class.size(types)
      new_class.module_eval(<<-EOS, __FILE__, __LINE__+1)
        def new_class.size()
          #{size}
        end
        def new_class.malloc()
          addr = DL.malloc(#{size})
          new(addr)
        end
      EOS
      return new_class
    end
    module_function :create
  end

  # A C struct wrapper
  class CStructEntity < (DL.fiddle? ? Fiddle::Pointer : CPtr)
    include PackInfo
    include ValueUtil

    # Allocates a C struct the +types+ provided.  The C function +func+ is
    # called when the instance is garbage collected.
    def CStructEntity.malloc(types, func = nil)
      addr = DL.malloc(CStructEntity.size(types))
      CStructEntity.new(addr, types, func)
    end

    # Given +types+, returns the offset for the packed sizes of those types
    #
    #   DL::CStructEntity.size([DL::TYPE_DOUBLE, DL::TYPE_INT, DL::TYPE_CHAR,
    #                           DL::TYPE_VOIDP])
    #   => 24
    def CStructEntity.size(types)
      offset = 0

      max_align = types.map { |type, count = 1|
        last_offset = offset

        align = PackInfo::ALIGN_MAP[type]
        offset = PackInfo.align(last_offset, align) +
                 (PackInfo::SIZE_MAP[type] * count)

        align
      }.max

      PackInfo.align(offset, max_align)
    end

    # Wraps the C pointer +addr+ as a C struct with the given +types+.  The C
    # function +func+ is called when the instance is garbage collected.
    #
    # See also DL::CPtr.new
    def initialize(addr, types, func = nil)
      set_ctypes(types)
      super(addr, @size, func)
    end

    # Set the names of the +members+ in this C struct
    def assign_names(members)
      @members = members
    end

    # Given +types+, calculate the offsets and sizes for the types in the
    # struct.
    def set_ctypes(types)
      @ctypes = types
      @offset = []
      offset = 0

      max_align = types.map { |type, count = 1|
        orig_offset = offset
        align = ALIGN_MAP[type]
        offset = PackInfo.align(orig_offset, align)

        @offset << offset

        offset += (SIZE_MAP[type] * count)

        align
      }.max

      @size = PackInfo.align(offset, max_align)
    end

    # Fetch struct member +name+
    def [](name)
      idx = @members.index(name)
      if( idx.nil? )
        raise(ArgumentError, "no such member: #{name}")
      end
      ty = @ctypes[idx]
      if( ty.is_a?(Array) )
        r = super(@offset[idx], SIZE_MAP[ty[0]] * ty[1])
      else
        r = super(@offset[idx], SIZE_MAP[ty.abs])
      end
      packer = Packer.new([ty])
      val = packer.unpack([r])
      case ty
      when Array
        case ty[0]
        when TYPE_VOIDP
          val = val.collect{|v| CPtr.new(v)}
        end
      when TYPE_VOIDP
        val = CPtr.new(val[0])
      else
        val = val[0]
      end
      if( ty.is_a?(Integer) && (ty < 0) )
        return unsigned_value(val, ty)
      elsif( ty.is_a?(Array) && (ty[0] < 0) )
        return val.collect{|v| unsigned_value(v,ty[0])}
      else
        return val
      end
    end

    # Set struct member +name+, to value +val+
    def []=(name, val)
      idx = @members.index(name)
      if( idx.nil? )
        raise(ArgumentError, "no such member: #{name}")
      end
      ty  = @ctypes[idx]
      packer = Packer.new([ty])
      val = wrap_arg(val, ty, [])
      buff = packer.pack([val].flatten())
      super(@offset[idx], buff.size, buff)
      if( ty.is_a?(Integer) && (ty < 0) )
        return unsigned_value(val, ty)
      elsif( ty.is_a?(Array) && (ty[0] < 0) )
        return val.collect{|v| unsigned_value(v,ty[0])}
      else
        return val
      end
    end

    def to_s() # :nodoc:
      super(@size)
    end
  end

  # A C union wrapper
  class CUnionEntity < CStructEntity
    include PackInfo

    # Allocates a C union the +types+ provided.  The C function +func+ is
    # called when the instance is garbage collected.
    def CUnionEntity.malloc(types, func=nil)
      addr = DL.malloc(CUnionEntity.size(types))
      CUnionEntity.new(addr, types, func)
    end

    # Given +types+, returns the size needed for the union.
    #
    #   DL::CUnionEntity.size([DL::TYPE_DOUBLE, DL::TYPE_INT, DL::TYPE_CHAR,
    #                          DL::TYPE_VOIDP])
    #   => 8
    def CUnionEntity.size(types)
      types.map { |type, count = 1|
        PackInfo::SIZE_MAP[type] * count
      }.max
    end

    # Given +types+, calculate the necessary offset and for each union member
    def set_ctypes(types)
      @ctypes = types
      @offset = Array.new(types.length, 0)
      @size   = self.class.size types
    end
  end
end

