# This module provides an interface to the top level bits of libvips
# via ruby-ffi.
#
# Author::    John Cupitt  (mailto:jcupitt@gmail.com)
# License::   MIT

require 'ffi'

module Vips
    private

    attach_function :vips_operation_new, [:string], :pointer

    attach_function :vips_cache_operation_build, [:pointer], :pointer
    attach_function :vips_object_unref_outputs, [:pointer], :void

    callback :argument_map_fn, [:pointer,
                                GObject::GParamSpec.ptr, 
                                ArgumentClass.ptr, 
                                ArgumentInstance.ptr, 
                                :pointer, :pointer], :pointer
    attach_function :vips_argument_map, [:pointer,
                                         :argument_map_fn, 
                                         :pointer, :pointer], :pointer

    OPERATION_SEQUENTIAL = 1
    OPERATION_NOCACHE = 4
    OPERATION_DEPRECATED = 8

    OPERATION_FLAGS = {
        :sequential => OPERATION_SEQUENTIAL,
        :nocache => OPERATION_NOCACHE,
        :deprecated => OPERATION_DEPRECATED
    }

    attach_function :vips_operation_get_flags, [:pointer], :int

    class Operation < Object

        # the layout of the VipsOperation struct
        module OperationLayout
            def self.included base
                base.class_eval do
                    layout :parent, Object::Struct
                    # rest opaque
                end
            end
        end

        class Struct < Object::Struct
            include OperationLayout

        end

        class ManagedStruct < Object::ManagedStruct
            include OperationLayout

        end

        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 == nil
            end

            super value
        end

        def build 
            op = Vips::vips_cache_operation_build self 
            if op == nil
                raise Vips::Error
            end

            return Operation.new op
        end

        def argument_map &block
            fn = Proc.new 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 get_flags
            Vips::vips_operation_get_flags self
        end

        # not quick! try to call this infrequently
        def get_construct_args
            args = []

            argument_map do |pspec, argument_class, argument_instance|
                flags = argument_class[:flags]
                if (flags & ARGUMENT_CONSTRUCT) != 0 
                    # names can include - as punctuation, but we always use _ in
                    # Ruby
                    name = pspec[:name].gsub("-", "_")

                    args << [name, flags] 
                end
            end

            return args
        end

        # search array for the first element to match a predicate ...
        # search inside subarrays and sub-hashes
        def self.find_inside object, &block
            return object if block.call object

            if object.is_a? Enumerable
                object.find {|value| block.call value, block}
            end

            return nil
        end

        # expand a constant into an image
        def self.imageize match_image, value
            return value if value.is_a? Image

            # 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 and value[0].is_a? Array) or match_image == nil
                return Image.new_from_array value
            else
                # we have a 1D array ... use that as a pixel constant and
                # expand to match match_image
                return match_image.new_from_image value
            end
        end

        # set an operation argument, expanding constants and copying images as
        # required
        def set name, value, match_image = nil, flags = 0
            gtype = get_typeof name

            if gtype == IMAGE_TYPE 
                value = Operation::imageize match_image, value

                if (flags & ARGUMENT_MODIFY) != 0
                    # make sure we have a unique copy
                    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

        public

        # This is the public entry point for the vips binding. {call} will run
        # any vips operation, for example:
        #
        # ```ruby
        # out = Vips::Operation.call "black", [100, 100], {:bands => 12}
        # ```
        #
        # will call the C function 
        #
        # ```C
        # vips_black( &out, 100, 100, "bands", 12, NULL );
        # ```
        # 
        # There are {Image#method_missing} hooks which will run {call} for you 
        # on {Image} for undefined instance or class methods. So you can also 
        # write:
        #
        # ```ruby
        # out = Vips::Image.black 100, 100, bands: 12
        # ```
        #
        # Or perhaps:
        #
        # ```ruby
        # x = Vips::Image.black 100, 100
        # y = x.invert
        # ```
        #
        # to run the `vips_invert()` operator.
        #
        # There are also a set of operator overloads and some convenience 
        # functions, see {Image}. 
        #
        # If the operator needs a vector constant, {call} will turn a scalar 
        # into a
        # vector for you. So for `x.linear a, b`, which calculates 
        # `x * a + b` where `a` and `b` are vector constants, you can write:
        #
        # ```ruby
        # x = Vips::Image.black 100, 100, bands: 3
        # y = x.linear 1, 2
        # y = x.linear [1], 4
        # y = x.linear [1, 2, 3], 4
        # ```
        #
        # or any other combination. The operator overloads use this facility to
        # support all the variations on:
        #
        # ```ruby
        # x = Vips::Image.black 100, 100, bands: 3
        # y = x * 2
        # y = x + [1,2,3]
        # y = x % [1]
        # ```
        #
        # Similarly, wherever an image is required, you can use a constant. The
        # constant will be expanded to an image matching the first input image
        # argument. For example, you can write:
        #
        # ```
        # x = Vips::Image.black 100, 100, bands: 3
        # y = x.bandjoin 255
        # ```
        #
        # to add an extra band to the image where each pixel in the new band has 
        # the constant value 255. 

        def self.call name, supplied, optional = {}, option_string = ""
            GLib::logger.debug("Vips::VipsOperation.call") {
                "name = #{name}, supplied = #{supplied}, " +
                "optional = #{optional}, option_string = #{option_string}"
            }

            op = Operation.new name

            # find and classify all the arguments the operator can take
            args = op.get_construct_args
            required_input = [] 
            optional_input = {}
            required_output = [] 
            optional_output = {}
            args.each do |name, flags|
                next if (flags & ARGUMENT_DEPRECATED) != 0

                if (flags & ARGUMENT_INPUT) != 0 
                    if (flags & ARGUMENT_REQUIRED) != 0 and
                        required_input << [name, flags]
                    else
                        optional_input[name] = flags
                    end
                end

                # MODIFY INPUT args count as OUTPUT as well
                if (flags & ARGUMENT_OUTPUT) != 0 or
                    ((flags & ARGUMENT_INPUT) != 0 and
                     (flags & ARGUMENT_MODIFY) != 0)
                    if (flags & ARGUMENT_REQUIRED) != 0 and
                        required_output << [name, flags]
                    else
                        optional_output[name] = flags
                    end
                end

            end

            # so we should have been supplied with n_required_input values, or
            # n_required_input + 1 if there's a hash of options at the end
            if not supplied.is_a? Array
                raise Vips::Error, "unable to call #{name}: " + 
                    "argument array is not an array"
            end
            if not 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

            # very that all supplied_optional keys are in optional_input or
            # optional_output
            optional.each do |key, value|
                arg_name = key.to_s

                if not optional_input.has_key? arg_name and
                    not 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
            match_image = find_inside(supplied) do |value|
                value.is_a? Image
            end

            # set any string args first so they can't be overridden
            if option_string != nil
                if Vips::vips_object_set_from_string(op, option_string) != 0
                    raise Vips::Error
                end
            end

            # set all required inputs
            required_input.each_index do |i|
                arg_name = required_input[i][0]
                flags = required_input[i][1]
                value = supplied[i]

                op.set arg_name, value, match_image, flags
            end

            # set all optional inputs
            optional.each do |key, value|
                arg_name = key.to_s

                if optional_input.has_key? arg_name
                    flags = optional_input[arg_name]

                    op.set arg_name, value, match_image, flags
                end
            end

            op = op.build

            # get all required results
            result = []
            required_output.each do |arg_name, flags|
                result << op.get(arg_name)
            end

            # fetch all optional ones
            optional_results = {}
            optional.each do |key, value|
                arg_name = key.to_s

                if optional_output.has_key? arg_name
                    flags = optional_output[arg_name]

                    optional_results[arg_name] = op.get arg_name
                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

            return result
        end

    end

end