# 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"
require "set"

module Vips
  private

  attach_function :vips_operation_new, [:string], :pointer

  # We may well block during this (eg. if it's avg, or perhaps jpegsave), and
  # libvips might trigger some signals which ruby has handles for.
  #
  # We need FFI to drop the GIL lock during this call and reacquire it when
  # the call ends, or we'll deadlock.
  attach_function :vips_cache_operation_build, [:pointer], :pointer,
    blocking: true
  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

  # Introspect a vips operation and return a large structure containing
  # everything we know about it. This is used for doc generation as well as
  # call.
  class Introspect
    attr_reader :name, :description, :flags, :args, :required_input,
      :optional_input, :required_output, :optional_output, :member_x,
      :method_args, :vips_name, :destructive, :doc_optional_input,
      :doc_optional_output

    @@introspect_cache = {}

    def initialize name
      # if there's a trailing "!", this is a destructive version of an
      # operation
      if name[-1] == "!"
        @destructive = true
        # strip the trailing "!"
        @vips_name = name[0...-1]
      else
        @destructive = false
        @vips_name = name
      end

      @op = Operation.new @vips_name
      @args = []
      @required_input = []
      @optional_input = {}
      @required_output = []
      @optional_output = {}

      # find all the arguments the operator can take
      @op.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
          arg_name = pspec[:name].tr("-", "_")
          @args << {
            arg_name: arg_name,
            flags: flags,
            gtype: pspec[:value_type]
          }
        end

        nil
      end

      @args.each do |details|
        arg_name = details[:arg_name]
        flags = details[:flags]

        if (flags & ARGUMENT_INPUT) != 0
          if (flags & ARGUMENT_REQUIRED) != 0 &&
              (flags & ARGUMENT_DEPRECATED) == 0
            @required_input << details
          else
            # we allow deprecated optional args
            @optional_input[arg_name] = details
          end

          # MODIFY INPUT args count as OUTPUT as well in non-destructive mode
          if (flags & ARGUMENT_MODIFY) != 0 &&
              !@destructive
            if (flags & ARGUMENT_REQUIRED) != 0 &&
                (flags & ARGUMENT_DEPRECATED) == 0
              @required_output << details
            else
              @optional_output[arg_name] = details
            end
          end
        elsif (flags & ARGUMENT_OUTPUT) != 0
          if (flags & ARGUMENT_REQUIRED) != 0 &&
              (flags & ARGUMENT_DEPRECATED) == 0
            @required_output << details
          else
            # again, allow deprecated optional args
            @optional_output[arg_name] = details
          end
        end
      end

      # in destructive mode, the first required input arg must be MODIFY and
      # must be an image
      if @destructive
        if @required_input.length < 1 ||
            @required_input[0][:flags] & ARGUMENT_MODIFY == 0 ||
            @required_input[0][:gtype] != IMAGE_TYPE
          raise Vips::Error, "operation #{@vips_name} is not destructive"
        end
      end
    end

    # Yard comment generation needs a little more introspection. We add this
    # extra metadata in a separate method to keep the main path as fast as
    # we can.
    def add_yard_introspection name
      @name = name
      @description = Vips.vips_object_get_description @op
      @flags = Vips.vips_operation_get_flags @op
      @member_x = nil
      @method_args = []
      @doc_optional_input = {}
      @doc_optional_output = {}

      @args.each do |details|
        arg_name = details[:arg_name]
        flags = details[:flags]
        gtype = details[:gtype]

        details[:yard_name] = (arg_name == "in") ? "im" : arg_name
        pspec = @op.get_pspec arg_name
        details[:blurb] = GObject.g_param_spec_get_blurb pspec

        if (flags & ARGUMENT_INPUT) != 0 &&
            (flags & ARGUMENT_REQUIRED) != 0 &&
            (flags & ARGUMENT_DEPRECATED) == 0
          # the first required input image is the thing we will be a method
          # of
          if @member_x.nil? && gtype == IMAGE_TYPE
            @member_x = details
          else
            @method_args << details
          end
        end
      end

      # and make the arg sets to document by filtering out deprecated args
      @optional_input.each do |arg_name, details|
        next if (details[:flags] & ARGUMENT_DEPRECATED) != 0
        @doc_optional_input[details[:arg_name]] = details
      end

      @optional_output.each do |arg_name, details|
        next if (details[:flags] & ARGUMENT_DEPRECATED) != 0
        @doc_optional_output[details[:arg_name]] = details
      end
    end

    def self.get name
      @@introspect_cache[name] ||= Introspect.new name
    end

    def self.get_yard name
      introspect = Introspect.get name
      introspect.add_yard_introspection name
      introspect
    end
  end

  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.null?
      end

      super
    end

    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 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

    # Search an object for the first element to match a predicate. Search
    # inside subarrays and sub-hashes. Equlvalent to x.flatten.find{}.
    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

    # 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

    # set an operation argument, expanding constants and copying images as
    # required
    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

    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}"
      }

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