# This module provides an interface GValue via ruby-ffi.
#
# Author::    John Cupitt  (mailto:jcupitt@gmail.com)
# License::   MIT

require "ffi"

module GObject
  # Represent a GValue. Example use:
  #
  # ```ruby
  # gvalue = GValue::alloc
  # gvalue.init GObject::GDOUBLE_TYPE
  # gvalue.set 3.1415
  # value = gvalue.get
  # # optional -- drop any ref the gvalue had
  # gvalue.unset
  # ```
  #
  # Lifetime is managed automatically. It doesn't know about all GType values,
  # but it does know the ones that libvips uses.

  class GValue < FFI::ManagedStruct
    layout :gtype, :GType,
      :data, [:ulong_long, 2]

    # convert an enum value (str/symb/int) into an int ready for libvips
    def self.enum_from_nick(gtype, value)
      value = value.to_s if value.is_a? Symbol

      if value.is_a? String
        # libvips expects "-" as a separator in enum names, but "_" is more
        # convenient for ruby, eg. :b_w
        value = Vips.vips_enum_from_nick "ruby-vips", gtype, value.tr("_", "-")
        if value == -1
          raise Vips::Error
        end
      end

      value
    end

    # compatibility ... we used to call it this, perhaps someone has used this
    # internal method
    def self.from_nick(gtype, value)
      GValue.enum_from_nick(gtype, value)
    end

    # convert an flags value (array[str/symb/int] | str/symb/int) into an
    # int ready for libvips
    def self.flags_from_nick(gtype, value)
      if value.is_a? String
        # libvips will parse strings like "sub|up" etc.
        name = value.tr("_", "-")
        value = Vips.vips_flags_from_nick "ruby-vips", gtype, name
      else
        value = [value] if !value.is_a? Array

        # convert each item to a set of bits, OR them together
        result = 0
        value.map do |item|
          item = item.to_s if item.is_a? Symbol
          if item.is_a? String
            name = item.tr("_", "-")
            item = Vips.vips_flags_from_nick "ruby-vips", gtype, name
            if item == -1
              raise Vips::Error
            end
          end

          result |= item
        end

        value = result
      end

      value
    end

    # convert an int enum back into a symbol
    def self.to_nick(gtype, enum_value)
      enum_name = Vips.vips_enum_nick gtype, enum_value
      if enum_name.nil?
        raise Vips::Error
      end

      enum_name.to_sym
    end

    def self.release ptr
      # GLib::logger.debug("GObject::GValue::release") {"ptr = #{ptr}"}
      ::GObject.g_value_unset ptr
    end

    # Allocate memory for a GValue and return a class wrapper. Memory will
    # be freed automatically when it goes out of scope. The GValue is inited
    # to 0, use {GValue.init} to set a type.
    #
    # @return [GValue] a new gvalue set to 0
    def self.alloc
      # allocate memory
      memory = FFI::MemoryPointer.new GValue

      # make this alloc autorelease ... we mustn't release in
      # GValue::release, since we are used to wrap GValue pointers
      # made by other people
      pointer = FFI::Pointer.new GValue, memory

      # ... and wrap in a GValue
      GValue.new pointer
    end

    # Set the type of thing a gvalue can hold.
    #
    # @param gtype [GType] the type of thing this GValue can hold.
    def init gtype
      ::GObject.g_value_init self, gtype
    end

    # Set the value of a GValue. The value is converted to the type of the
    # GValue, if possible.
    #
    # @param value [Any] The value to set
    def set value
      # GLib::logger.debug("GObject::GValue.set") {
      #     "value = #{value.inspect[0..50]}"
      # }

      gtype = self[:gtype]
      fundamental = ::GObject.g_type_fundamental gtype

      case gtype
      when GBOOL_TYPE
        ::GObject.g_value_set_boolean self, (value ? 1 : 0)

      when GINT_TYPE
        ::GObject.g_value_set_int self, value

      when GUINT64_TYPE
        ::GObject.g_value_set_uint64 self, value

      when GDOUBLE_TYPE
        ::GObject.g_value_set_double self, value

      when GSTR_TYPE
        ::GObject.g_value_set_string self, value

      when Vips::REFSTR_TYPE
        ::Vips.vips_value_set_ref_string self, value

      when Vips::ARRAY_INT_TYPE
        value = [value] unless value.is_a? Array

        Vips.vips_value_set_array_int self, nil, value.length
        ptr = Vips.vips_value_get_array_int self, nil
        ptr.write_array_of_int32 value

      when Vips::ARRAY_DOUBLE_TYPE
        value = [value] unless value.is_a? Array

        # this will allocate an array in the gvalue
        Vips.vips_value_set_array_double self, nil, value.length

        # pull the array out and fill it
        ptr = Vips.vips_value_get_array_double self, nil

        ptr.write_array_of_double value

      when Vips::ARRAY_IMAGE_TYPE
        value = [value] unless value.is_a? Array

        Vips.vips_value_set_array_image self, value.length
        ptr = Vips.vips_value_get_array_image self, nil
        ptr.write_array_of_pointer value

        # the gvalue needs a ref on each of the images
        value.each { |image| ::GObject.g_object_ref image }

      when Vips::BLOB_TYPE
        len = value.bytesize
        ptr = GLib.g_malloc len
        Vips.vips_value_set_blob self, GLib::G_FREE, ptr, len
        ptr.write_bytes value

      else
        case fundamental
        when GFLAGS_TYPE
          flags_value = GValue.flags_from_nick(self[:gtype], value)
          ::GObject.g_value_set_flags self, flags_value

        when GENUM_TYPE
          enum_value = GValue.enum_from_nick(self[:gtype], value)
          ::GObject.g_value_set_enum self, enum_value

        when GOBJECT_TYPE
          ::GObject.g_value_set_object self, value

        else
          raise Vips::Error, "unimplemented gtype for set: " \
            "#{::GObject.g_type_name gtype} (#{gtype})"
        end
      end
    end

    # Get the value of a GValue. The value is converted to a Ruby type in
    # the obvious way.
    #
    # @return [Any] the value held by the GValue
    def get
      gtype = self[:gtype]
      fundamental = ::GObject.g_type_fundamental gtype
      result = nil

      case gtype
      when GBOOL_TYPE
        result = ::GObject.g_value_get_boolean(self) != 0

      when GINT_TYPE
        result = ::GObject.g_value_get_int self

      when GUINT64_TYPE
        result = ::GObject.g_value_get_uint64 self

      when GDOUBLE_TYPE
        result = ::GObject.g_value_get_double self

      when GSTR_TYPE
        result = ::GObject.g_value_get_string self

      when Vips::REFSTR_TYPE
        len = Vips::SizeStruct.new
        result = ::Vips.vips_value_get_ref_string self, len

      when Vips::ARRAY_INT_TYPE
        len = Vips::IntStruct.new
        array = Vips.vips_value_get_array_int self, len
        result = array.get_array_of_int32 0, len[:value]

      when Vips::ARRAY_DOUBLE_TYPE
        len = Vips::IntStruct.new
        array = Vips.vips_value_get_array_double self, len
        result = array.get_array_of_double 0, len[:value]

      when Vips::ARRAY_IMAGE_TYPE
        len = Vips::IntStruct.new
        array = Vips.vips_value_get_array_image self, len
        result = array.get_array_of_pointer 0, len[:value]
        result.map! do |pointer|
          ::GObject.g_object_ref pointer
          Vips::Image.new pointer
        end

      when Vips::BLOB_TYPE
        len = Vips::SizeStruct.new
        array = Vips.vips_value_get_blob self, len
        result = array.get_bytes 0, len[:value]

      else
        case fundamental
        when GFLAGS_TYPE
          result = ::GObject.g_value_get_flags self

        when GENUM_TYPE
          enum_value = ::GObject.g_value_get_enum(self)
          result = GValue.to_nick self[:gtype], enum_value

        when GOBJECT_TYPE
          obj = ::GObject.g_value_get_object self
          # g_value_get_object() does not add a ref ... we need to add
          # one to match the unref in gobject release
          ::GObject.g_object_ref obj
          result = Vips::Image.new obj

        else
          raise Vips::Error, "unimplemented gtype for get: " \
            "#{::GObject.g_type_name gtype} (#{gtype})"
        end
      end

      # GLib::logger.debug("GObject::GValue.get") {
      #     "result = #{result.inspect[0..50]}"
      # }

      result
    end

    # Clear the thing held by a GValue.
    #
    # This happens automatically when a GValue is GCed, but this method can be
    # handy if you need to drop a reference explicitly for some reason.
    def unset
      ::GObject.g_value_unset self
    end
  end

  attach_function :g_value_init, [GValue.ptr, :GType], :void

  # we must use a plain :pointer here, since we call this from #release, which
  # just gives us the unwrapped pointer, not the ruby class
  attach_function :g_value_unset, [:pointer], :void

  attach_function :g_value_set_boolean, [GValue.ptr, :int], :void
  attach_function :g_value_set_int, [GValue.ptr, :int], :void
  attach_function :g_value_set_uint64, [GValue.ptr, :uint64], :void
  attach_function :g_value_set_double, [GValue.ptr, :double], :void
  attach_function :g_value_set_enum, [GValue.ptr, :int], :void
  attach_function :g_value_set_flags, [GValue.ptr, :uint], :void
  attach_function :g_value_set_string, [GValue.ptr, :string], :void
  attach_function :g_value_set_object, [GValue.ptr, :pointer], :void

  attach_function :g_value_get_boolean, [GValue.ptr], :int
  attach_function :g_value_get_int, [GValue.ptr], :int
  attach_function :g_value_get_uint64, [GValue.ptr], :uint64
  attach_function :g_value_get_double, [GValue.ptr], :double
  attach_function :g_value_get_enum, [GValue.ptr], :int
  attach_function :g_value_get_flags, [GValue.ptr], :int
  attach_function :g_value_get_string, [GValue.ptr], :string
  attach_function :g_value_get_object, [GValue.ptr], :pointer

  # use :pointer rather than GObject.ptr to avoid casting later
  attach_function :g_object_set_property,
    [:pointer, :string, GValue.ptr], :void
  attach_function :g_object_get_property,
    [:pointer, :string, GValue.ptr], :void
end
