module CarrierWave

  ##
  # This module simplifies manipulation with vips by providing a set
  # of convenient helper methods. If you want to use them, you'll need to
  # require this file:
  #
  #     require 'carrierwave/processing/vips'
  #
  # And then include it in your uploader:
  #
  #     class MyUploader < CarrierWave::Uploader::Base
  #       include CarrierWave::Vips
  #     end
  #
  # You can now use the provided helpers:
  #
  #     class MyUploader < CarrierWave::Uploader::Base
  #       include CarrierWave::Vips
  #
  #       process :resize_to_fit => [200, 200]
  #     end
  #
  # Or create your own helpers with the powerful vips! method, which
  # yields an ImageProcessing::Builder object. Check out the ImageProcessing
  # docs at http://github.com/janko-m/image_processing and the list of all
  # available Vips options at
  # https://libvips.github.io/libvips/API/current/using-cli.html for more info.
  #
  #     class MyUploader < CarrierWave::Uploader::Base
  #       include CarrierWave::Vips
  #
  #       process :radial_blur => 10
  #
  #       def radial_blur(amount)
  #         vips! do |builder|
  #           builder.radial_blur(amount)
  #           builder = yield(builder) if block_given?
  #           builder
  #         end
  #       end
  #     end
  #
  # === Note
  #
  # The ImageProcessing gem uses ruby-vips, a binding for the vips image
  # library. You can find more information here:
  #
  # https://github.com/libvips/ruby-vips
  #
  #
  module Vips
    extend ActiveSupport::Concern

    included do
      require "image_processing/vips"
      # We need to disable caching since we're editing images in place.
      ::Vips.cache_set_max(0)
    end

    module ClassMethods
      def convert(format)
        process :convert => format
      end

      def resize_to_limit(width, height)
        process :resize_to_limit => [width, height]
      end

      def resize_to_fit(width, height)
        process :resize_to_fit => [width, height]
      end

      def resize_to_fill(width, height, gravity='centre')
        process :resize_to_fill => [width, height, gravity]
      end

      def resize_and_pad(width, height, background=nil, gravity='centre', alpha=nil)
        process :resize_and_pad => [width, height, background, gravity, alpha]
      end

      def crop(left, top, width, height)
        process :crop => [left, top, width, height]
      end
    end

    ##
    # Changes the image encoding format to the given format
    #
    # See https://libvips.github.io/libvips/API/current/using-cli.html#using-command-line-conversion
    #
    # === Parameters
    #
    # [format (#to_s)] an abbreviation of the format
    #
    # === Yields
    #
    # [Vips::Image] additional manipulations to perform
    #
    # === Examples
    #
    #     image.convert(:png)
    #
    def convert(format, page=nil)
      vips! do |builder|
        builder = builder.convert(format)
        builder = builder.loader(page: page) if page
        builder
      end
    end

    ##
    # Resize the image to fit within the specified dimensions while retaining
    # the original aspect ratio. Will only resize the image if it is larger than the
    # specified dimensions. The resulting image may be shorter or narrower than specified
    # in the smaller dimension but will not be larger than the specified values.
    #
    # === Parameters
    #
    # [width (Integer)] the width to scale the image to
    # [height (Integer)] the height to scale the image to
    # [combine_options (Hash)] additional Vips options to apply before resizing
    #
    # === Yields
    #
    # [Vips::Image] additional manipulations to perform
    #
    def resize_to_limit(width, height, combine_options: {})
      width, height = resolve_dimensions(width, height)

      vips! do |builder|
        builder.resize_to_limit(width, height)
          .apply(combine_options)
      end
    end

    ##
    # Resize the image to fit within the specified dimensions while retaining
    # the original aspect ratio. The image may be shorter or narrower than
    # specified in the smaller dimension but will not be larger than the specified values.
    #
    # === Parameters
    #
    # [width (Integer)] the width to scale the image to
    # [height (Integer)] the height to scale the image to
    # [combine_options (Hash)] additional Vips options to apply before resizing
    #
    # === Yields
    #
    # [Vips::Image] additional manipulations to perform
    #
    def resize_to_fit(width, height, combine_options: {})
      width, height = resolve_dimensions(width, height)

      vips! do |builder|
        builder.resize_to_fit(width, height)
          .apply(combine_options)
      end
    end

    ##
    # Resize the image to fit within the specified dimensions while retaining
    # the aspect ratio of the original image. If necessary, crop the image in the
    # larger dimension.
    #
    # === Parameters
    #
    # [width (Integer)] the width to scale the image to
    # [height (Integer)] the height to scale the image to
    # [combine_options (Hash)] additional vips options to apply before resizing
    #
    # === Yields
    #
    # [Vips::Image] additional manipulations to perform
    #
    def resize_to_fill(width, height, _gravity = nil, combine_options: {})
      width, height = resolve_dimensions(width, height)

      vips! do |builder|
        builder.resize_to_fill(width, height).apply(combine_options)
      end
    end

    ##
    # Resize the image to fit within the specified dimensions while retaining
    # the original aspect ratio. If necessary, will pad the remaining area
    # with the given color, which defaults to transparent (for gif and png,
    # white for jpeg).
    #
    # See https://libvips.github.io/libvips/API/current/libvips-conversion.html#VipsCompassDirection
    # for gravity options.
    #
    # === Parameters
    #
    # [width (Integer)] the width to scale the image to
    # [height (Integer)] the height to scale the image to
    # [background (List, nil)] the color of the background as a RGB, like [0, 255, 255], nil indicates transparent
    # [gravity (String)] how to position the image
    # [alpha (Boolean, nil)] pad the image with the alpha channel if supported
    # [combine_options (Hash)] additional vips options to apply before resizing
    #
    # === Yields
    #
    # [Vips::Image] additional manipulations to perform
    #
    def resize_and_pad(width, height, background=nil, gravity='centre', alpha=nil, combine_options: {})
      width, height = resolve_dimensions(width, height)

      vips! do |builder|
        builder.resize_and_pad(width, height, background: background, gravity: gravity, alpha: alpha)
          .apply(combine_options)
      end
    end

    ##
    # Crop the image to the contents of a box positioned at [left] and [top], with the dimensions given
    # by [width] and [height]. The original image bottom/right edge is preserved if the cropping box falls
    # outside the image bounds.
    #
    # === Parameters
    #
    # [left (integer)] left edge of area to extract
    # [top (integer)] top edge of area to extract
    # [width (Integer)] width of area to extract
    # [height (Integer)] height of area to extract
    #
    # === Yields
    #
    # [Vips::Image] additional manipulations to perform
    #
    def crop(left, top, width, height, combine_options: {})
      width, height = resolve_dimensions(width, height)
      width = vips_image.width - left if width + left > vips_image.width
      height = vips_image.height - top if height + top > vips_image.height

      vips! do |builder|
        builder.crop(left, top, width, height)
          .apply(combine_options)
      end
    end

    ##
    # Returns the width of the image in pixels.
    #
    # === Returns
    #
    # [Integer] the image's width in pixels
    #
    def width
      vips_image.width
    end

    ##
    # Returns the height of the image in pixels.
    #
    # === Returns
    #
    # [Integer] the image's height in pixels
    #
    def height
      vips_image.height
    end

    # Process the image with vip, using the ImageProcessing gem. This
    # method will build a "convert" vips command and execute it on the
    # current image.
    #
    # === Gotcha
    #
    # This method assumes that the object responds to +current_path+.
    # Any class that this module is mixed into must have a +current_path+ method.
    # CarrierWave::Uploader does, so you won't need to worry about this in
    # most cases.
    #
    # === Yields
    #
    # [ImageProcessing::Builder] use it to define processing to be performed
    #
    # === Raises
    #
    # [CarrierWave::ProcessingError] if processing failed.
    def vips!
      builder = ImageProcessing::Vips.source(current_path)
      builder = yield(builder)

      result = builder.call
      result.close

      FileUtils.mv result.path, current_path

      if File.extname(result.path) != File.extname(current_path)
        move_to = current_path.chomp(File.extname(current_path)) + File.extname(result.path)
        file.content_type = Marcel::Magic.by_path(move_to).try(:type)
        file.move_to(move_to, permissions, directory_permissions)
      end
    rescue ::Vips::Error
      message = I18n.translate(:"errors.messages.processing_error")
      raise CarrierWave::ProcessingError, message
    end

  private

    def resolve_dimensions(*dimensions)
      dimensions.map do |value|
        next value unless value.instance_of?(Proc)
        value.arity >= 1 ? value.call(self) : value.call
      end
    end

    def vips_image
      ::Vips::Image.new_from_buffer(read, "")
    end

  end # Vips
end # CarrierWave
