require 'gir_ffi/allocation_helper'
require 'gir_ffi/callback_helper'
require 'gir_ffi/builder'

module GirFFI
  module ArgHelper
    OBJECT_STORE = {}

    POINTER_SIZE = FFI.type_size(:pointer)

    SIMPLE_G_TYPES = [
      :gint8, :gint16, :gint, :gint32, :gint64,
      :guint8, :guint16, :guint32, :guint64,
      :gfloat, :gdouble
    ]

    # FIXME: Hideous
    # TODO: Move this implementation to InPointer
    def self.object_to_inptr obj
      return nil if obj.nil?
      return obj.to_ptr if obj.respond_to? :to_ptr
      return obj if obj.is_a? FFI::Pointer

      FFI::Pointer.new(obj.object_id).tap {|ptr|
        OBJECT_STORE[ptr.address] = obj }
    end

    def self.ptr_to_typed_array type, ptr, size
      if type.is_a? Class
        ptr_to_interface_array type, ptr, size
      elsif type.is_a? Array
        ptr_to_interface_pointer_array type[1], ptr, size
      elsif type == :utf8
        ptr_to_utf8_array ptr, size
      else
        self.send "ptr_to_#{type}_array", ptr, size
      end
    end

    def self.setup_ptr_to_type_array_handler_for *types
      types.flatten.each do |type|
        ffi_type = TypeMap.map_basic_type type
        defn =
          "def self.ptr_to_#{type}_array ptr, size
            return [] if ptr.nil? or ptr.null?
            ptr.get_array_of_#{ffi_type}(0, size)
          end"
        eval defn
      end
    end

    setup_ptr_to_type_array_handler_for SIMPLE_G_TYPES

    def self.ptr_to_utf8_array ptr, size
      return [] if ptr.nil? or ptr.null?
      ptrs = ptr.read_array_of_pointer(size)
      ptrs.map { |pt| ptr_to_utf8 pt }
    end

    def self.ptr_to_interface_array klass, ptr, size
      return [] if ptr.nil? or ptr.null?
      struct_size = klass::Struct.size
      size.times.map do |idx|
        klass.wrap(ptr + struct_size * idx)
      end
    end

    def self.ptr_to_interface_pointer_array klass, ptr, size
      return [] if ptr.nil? or ptr.null?
      ptrs = ptr.read_array_of_pointer(size)
      ptrs.map do |optr|
        klass.wrap(optr)
      end
    end

    if RUBY_VERSION < "1.9"
      def self.ptr_to_utf8 ptr
        ptr.null? ? nil : ptr.read_string
      end
    else
      def self.ptr_to_utf8 ptr
        ptr.null? ? nil : ptr.read_string.force_encoding("utf-8")
      end
    end

    def self.ptr_to_utf8_length ptr, len
      ptr.null? ? nil : ptr.read_string(len)
    end

    # Set up gtype handlers depending on type size.
    class << self
      sz = FFI.type_size(:size_t) * 8
      type = "guint#{sz}"
      alias_method :ptr_to_GType_array, "ptr_to_#{type}_array"
    end

    def self.check_error errpp
      err = GLib::Error.wrap(errpp.read_pointer)
      raise err.message if err
    end

    def self.check_fixed_array_size size, arr, name
      unless arr.size == size
        raise ArgumentError, "#{name} should have size #{size}"
      end
    end

    def self.allocate_array_of_type type, length
      AllocationHelper.safe_malloc FFI.type_size(type) * length
    end

    def self.object_pointer_to_object optr
      gtype = GObject.type_from_instance_pointer optr
      wrap_object_pointer_by_gtype optr, gtype
    end

    def self.wrap_object_pointer_by_gtype optr, gtype
      return nil if optr.null?
      klass = Builder.build_by_gtype gtype
      klass.direct_wrap optr
    end

    def self.cast_from_pointer type, it
      case type
      when :utf8, :filename
        ptr_to_utf8 it
      when :gint32
        cast_pointer_to_int32 it
      else
        it.address
      end
    end

    def self.cast_uint32_to_int32 val
      if val >= 0x80000000
        -(0x100000000-val)
      else
        val
      end
    end

    def self.cast_pointer_to_int32 ptr
      cast_uint32_to_int32(ptr.address & 0xffffffff)
    end
  end
end
