class Vips::Operation

def self.call name, supplied, optional = {}, option_string = ""

def self.call name, supplied, optional = {}, option_string = ""
  GLib.logger.debug("Vips::VipsOperation.call") {
    "name = #{name}, supplied = #{supplied}, " \
      "optional = #{optional}, option_string = #{option_string}"
  }
  introspect = Introspect.get name
  required_input = introspect.required_input
  required_output = introspect.required_output
  optional_input = introspect.optional_input
  optional_output = introspect.optional_output
  destructive = introspect.destructive
  unless supplied.is_a? Array
    raise Vips::Error, "unable to call #{name}: " \
      "argument array is not an array"
  end
  unless optional.is_a? Hash
    raise Vips::Error, "unable to call #{name}: " \
      "optional arguments are not a hash"
  end
  if supplied.length != required_input.length
    raise Vips::Error, "unable to call #{name}: " \
      "you supplied #{supplied.length} arguments, " \
      "but operation needs #{required_input.length}."
  end
  # all supplied_optional keys should be in optional_input or
  # optional_output
  optional.each do |key, _value|
    arg_name = key.to_s
    unless optional_input.has_key?(arg_name) ||
        optional_output.has_key?(arg_name)
      raise Vips::Error, "unable to call #{name}: " \
        "unknown option #{arg_name}"
    end
  end
  # the first image arg is the thing we expand constants to match ...
  # we need to find it
  #
  # look inside array and hash arguments, since we may be passing an
  # array of images
  #
  # also enforce the rules around mutable and non-mutable images
  match_image = nil
  flat_find(supplied) do |value|
    if match_image
      # no non-first image arg can ever be mutable
      if value.is_a?(MutableImage)
        raise Vips::Error, "unable to call #{name}: " \
          "only the first image argument can be mutable"
      end
    elsif destructive
      if value.is_a?(Image)
        raise Vips::Error, "unable to call #{name}: " \
          "first image argument to a destructive " \
          "operation must be mutable"
      elsif value.is_a?(MutableImage)
        match_image = value
      end
    elsif value.is_a?(MutableImage)
      # non destructive operation, so no mutable images
      raise Vips::Error, "unable to call #{name}: " \
        "must not pass mutable images to " \
        "non-destructive operations"
    elsif value.is_a?(Image)
      match_image = value
    end
    # keep looping
    false
  end
  op = Operation.new introspect.vips_name
  # set any string args first so they can't be overridden
  unless option_string.nil?
    if Vips.vips_object_set_from_string(op, option_string) != 0
      raise Vips::Error
    end
  end
  # dedupe all input references here
  deduped_references = Set.new
  add_reference = lambda do |x|
    if x.is_a?(Vips::Image)
      deduped_references.merge x.references
    end
    false
  end
  # set all required inputs
  required_input.each_index do |i|
    details = required_input[i]
    arg_name = details[:arg_name]
    flags = details[:flags]
    gtype = details[:gtype]
    value = supplied[i]
    flat_find value, &add_reference
    op.set arg_name, value, match_image, flags, gtype, destructive
  end
  # set all optional inputs
  optional.each do |key, value|
    next if value.nil?
    arg_name = key.to_s
    if optional_input.has_key? arg_name
      details = optional_input[arg_name]
      flags = details[:flags]
      gtype = details[:gtype]
      flat_find value, &add_reference
      op.set arg_name, value, match_image, flags, gtype, destructive
    end
  end
  op = op.build
  # we need an array of references for output objects
  references = deduped_references.to_a
  # attach all input refs to output x
  set_reference = lambda do |x|
    # stop early if there are no refs to attach
    return true if references == []
    if x.is_a? Vips::Image
      references.each { |i| x.references << i }
    end
    false
  end
  # get all required results
  result = []
  required_output.each do |details|
    value = op.get(details[:arg_name])
    flat_find value, &set_reference
    result << value
  end
  # fetch all optional ones
  optional_results = {}
  optional.each do |key, _value|
    arg_name = key.to_s
    if optional_output.has_key? arg_name
      value = op.get arg_name
      flat_find value, &set_reference
      optional_results[arg_name] = value
    end
  end
  result << optional_results if optional_results != {}
  if result.length == 1
    result = result.first
  elsif result.length == 0
    result = nil
  end
  GLib.logger.debug("Vips::Operation.call") { "result = #{result}" }
  Vips.vips_object_unref_outputs op
  result
end

def self.flat_find object, &block

inside subarrays and sub-hashes. Equlvalent to x.flatten.find{}.
Search an object for the first element to match a predicate. Search
def self.flat_find object, &block
  if object.respond_to? :each
    object.each do |x|
      result = flat_find x, &block
      return result unless result.nil?
    end
  elsif yield object
    return object
  end
  nil
end

def self.imageize match_image, value

expand a constant into an image
def self.imageize match_image, value
  return value if value.is_a?(Image) || value.is_a?(MutableImage)
  # 2D array values become tiny 2D images
  # if there's nothing to match to, we also make a 2D image
  if (value.is_a?(Array) && value[0].is_a?(Array)) || match_image.nil?
    Image.new_from_array value
  else
    # we have a 1D array ... use that as a pixel constant and
    # expand to match match_image
    match_image.new_from_image value
  end
end

def argument_map &block

def argument_map &block
  fn = proc do |_op, pspec, argument_class, argument_instance, _a, _b|
    block.call pspec, argument_class, argument_instance
  end
  Vips.vips_argument_map self, fn, nil, nil
end

def build

def build
  op = Vips.vips_cache_operation_build self
  if op.null?
    Vips.vips_object_unref_outputs self
    raise Vips::Error
  end
  Operation.new op
end

def initialize value

def initialize value
  # allow init with a pointer so we can wrap the return values from
  # things like _build
  if value.is_a? String
    value = Vips.vips_operation_new value
    raise Vips::Error if value.null?
  end
  super
end

def set name, value, match_image, flags, gtype, destructive

required
set an operation argument, expanding constants and copying images as
def set name, value, match_image, flags, gtype, destructive
  if gtype == IMAGE_TYPE
    value = Operation.imageize match_image, value
    # in non-destructive mode, make sure we have a unique copy
    if (flags & ARGUMENT_MODIFY) != 0 &&
        !destructive
      value = value.copy.copy_memory
    end
  elsif gtype == ARRAY_IMAGE_TYPE
    value = value.map { |x| Operation.imageize match_image, x }
  end
  super(name, value)
end