module CarrierWave

  ##
  # This module simplifies manipulation with RMagick by providing a set
  # of convenient helper methods. If you want to use them, you'll need to
  # require this file:
  #
  #     require 'carrierwave/processing/rmagick'
  #
  # And then include it in your uploader:
  #
  #     class MyUploader < CarrierWave::Uploader::Base
  #       include CarrierWave::RMagick
  #     end
  #
  # You can now use the provided helpers:
  #
  #     class MyUploader < CarrierWave::Uploader::Base
  #       include CarrierWave::RMagick
  #
  #       process :resize_to_fit => [200, 200]
  #     end
  #
  # Or create your own helpers with the powerful manipulate! method. Check
  # out the RMagick docs at https://rmagick.github.io/ for more
  # info
  #
  #     class MyUploader < CarrierWave::Uploader::Base
  #       include CarrierWave::RMagick
  #
  #       process :do_stuff => 10.0
  #
  #       def do_stuff(blur_factor)
  #         manipulate! do |img|
  #           img = img.sepiatone
  #           img = img.auto_orient
  #           img = img.radial_blur(blur_factor)
  #         end
  #       end
  #     end
  #
  # === Note
  #
  # You should be aware how RMagick handles memory. manipulate! takes care
  # of freeing up memory for you, but for optimum memory usage you should
  # use destructive operations as much as possible:
  #
  # DON'T DO THIS:
  #     img = img.resize_to_fit
  #
  # DO THIS INSTEAD:
  #     img.resize_to_fit!
  #
  # Read this for more information why:
  #
  # http://rubyforge.org/forum/forum.php?thread_id=1374&forum_id=1618
  #
  module RMagick
    extend ActiveSupport::Concern

    included do
      begin
        require "rmagick"
      rescue LoadError
        begin
          require "RMagick"
        rescue LoadError => e
          e.message << " (You may need to install the rmagick gem)"
          raise e
        end
      end

      prepend Module.new {
        def initialize(*)
          super
          @format = nil
        end
      }
    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=::Magick::CenterGravity)
        process :resize_to_fill => [width, height, gravity]
      end

      def resize_and_pad(width, height, background=:transparent, gravity=::Magick::CenterGravity)
        process :resize_and_pad => [width, height, background, gravity]
      end

      def resize_to_geometry_string(geometry_string)
        process :resize_to_geometry_string => [geometry_string]
      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 even https://rmagick.github.io/magick.html#formats
    #
    # === Parameters
    #
    # [format (#to_s)] an abbreviation of the format
    #
    # === Yields
    #
    # [Magick::Image] additional manipulations to perform
    #
    # === Examples
    #
    #     image.convert(:png)
    #
    def convert(format)
      manipulate!(:format => format)
      @format = format
    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
    #
    # === Yields
    #
    # [Magick::Image] additional manipulations to perform
    #
    def resize_to_limit(width, height)
      width = dimension_from width
      height = dimension_from height
      manipulate! do |img|
        geometry = Magick::Geometry.new(width, height, 0, 0, Magick::GreaterGeometry)
        new_img = img.change_geometry(geometry) do |new_width, new_height|
          img.resize(new_width, new_height)
        end
        destroy_image(img)
        new_img = yield(new_img) if block_given?
        new_img
      end
    end

    ##
    # From the RMagick documentation: "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."
    #
    # See even https://rmagick.github.io/image3.html#resize_to_fit
    #
    # === Parameters
    #
    # [width (Integer)] the width to scale the image to
    # [height (Integer)] the height to scale the image to
    #
    # === Yields
    #
    # [Magick::Image] additional manipulations to perform
    #
    def resize_to_fit(width, height)
      width = dimension_from width
      height = dimension_from height
      manipulate! do |img|
        img.resize_to_fit!(width, height)
        img = yield(img) if block_given?
        img
      end
    end

    ##
    # From the RMagick documentation: "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."
    #
    # See even https://rmagick.github.io/image3.html#resize_to_fill
    #
    # === Parameters
    #
    # [width (Integer)] the width to scale the image to
    # [height (Integer)] the height to scale the image to
    #
    # === Yields
    #
    # [Magick::Image] additional manipulations to perform
    #
    def resize_to_fill(width, height, gravity=::Magick::CenterGravity)
      width = dimension_from width
      height = dimension_from height
      manipulate! do |img|
        img.crop_resized!(width, height, gravity)
        img = yield(img) if block_given?
        img
      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).
    #
    # === Parameters
    #
    # [width (Integer)] the width to scale the image to
    # [height (Integer)] the height to scale the image to
    # [background (String, :transparent)] the color of the background as a hexcode, like "#ff45de"
    # [gravity (Magick::GravityType)] how to position the image
    #
    # === Yields
    #
    # [Magick::Image] additional manipulations to perform
    #
    def resize_and_pad(width, height, background=:transparent, gravity=::Magick::CenterGravity)
      width = dimension_from width
      height = dimension_from height
      manipulate! do |img|
        img.resize_to_fit!(width, height)
        filled = ::Magick::Image.new(width, height) { |image| image.background_color = background == :transparent ? 'rgba(255,255,255,0)' : background.to_s }
        filled.composite!(img, gravity, ::Magick::OverCompositeOp)
        destroy_image(img)
        filled = yield(filled) if block_given?
        filled
      end
    end

    ##
    # Resize the image per the provided geometry string.
    #
    # === Parameters
    #
    # [geometry_string (String)] the proportions in which to scale image
    #
    # === Yields
    #
    # [Magick::Image] additional manipulations to perform
    #
    def resize_to_geometry_string(geometry_string)
      manipulate! do |img|
        new_img = img.change_geometry(geometry_string) do |new_width, new_height|
          img.resize(new_width, new_height)
        end
        destroy_image(img)
        new_img = yield(new_img) if block_given?
        new_img
      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
    #
    # [Magick::Image] additional manipulations to perform
    #
    def crop(left, top, width, height, combine_options: {})
      width = dimension_from width
      height = dimension_from height

      manipulate! do |img|
        img.crop!(left, top, width, height)
        img = yield(img) if block_given?
        img
      end
    end

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

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

    ##
    # Manipulate the image with RMagick. This method will load up an image
    # and then pass each of its frames to the supplied block. It will then
    # save the image to disk.
    #
    # === 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
    #
    # [Magick::Image] manipulations to perform
    # [Integer] Frame index if the image contains multiple frames
    # [Hash] options, see below
    #
    # === Options
    #
    # The options argument to this method is also yielded as the third
    # block argument.
    #
    # Currently, the following options are defined:
    #
    # ==== :write
    # A hash of assignments to be evaluated in the block given to the RMagick write call.
    #
    # An example:
    #
    #      manipulate! do |img, index, options|
    #        options[:write] = {
    #          :quality => 50,
    #          :depth => 8
    #        }
    #        img
    #      end
    #
    # This will translate to the following RMagick::Image#write call:
    #
    #     image.write do |img|
    #       self.quality = 50
    #       self.depth = 8
    #     end
    #
    # ==== :read
    # A hash of assignments to be given to the RMagick read call.
    #
    # The options available are identical to those for write, but are passed in directly, like this:
    #
    #     manipulate! :read => { :density => 300 }
    #
    # ==== :format
    # Specify the output format. If unset, the filename extension is used to determine the format.
    #
    # === Raises
    #
    # [CarrierWave::ProcessingError] if manipulation failed.
    #
    def manipulate!(options={}, &block)
      cache_stored_file! if !cached?

      read_block = create_info_block(options[:read])
      image = ::Magick::Image.read(current_path, &read_block)
      frames = ::Magick::ImageList.new

      image.each_with_index do |frame, index|
        frame = yield(*[frame, index, options].take(block.arity)) if block_given?
        frames << frame if frame
      end
      frames.append(true) if block_given?

      write_block = create_info_block(options[:write])

      if options[:format] || @format
        frames.write("#{options[:format] || @format}:#{current_path}", &write_block)
        move_to = current_path.chomp(File.extname(current_path)) + ".#{options[:format] || @format}"
        file.content_type = Marcel::Magic.by_path(move_to).try(:type)
        file.move_to(move_to, permissions, directory_permissions)
      else
        frames.write(current_path, &write_block)
      end

      destroy_image(frames)
    rescue ::Magick::ImageMagickError
      raise CarrierWave::ProcessingError, I18n.translate(:"errors.messages.processing_error")
    end

  private

    def create_info_block(options)
      return nil unless options
      proc do |img|
        options.each do |k, v|
          if v.is_a?(String) && (matches = v.match(/^["'](.+)["']/))
            CarrierWave.deprecator.warn "Passing quoted strings like #{v} to #manipulate! is deprecated, pass them without quoting."
            v = matches[1]
          end
          img.public_send(:"#{k}=", v)
        end
      end
    end

    def destroy_image(image)
      image.try(:destroy!)
    end

    def dimension_from(value)
      return value unless value.instance_of?(Proc)
      value.arity >= 1 ? value.call(self) : value.call
    end

    def rmagick_image
      ::Magick::Image.from_blob(self.read).first
    end

  end # RMagick
end # CarrierWave
