module CarrierWave

  ##
  # If a Class is extended with this module, it gains the mount_uploader
  # method, which is used for mapping attributes to uploaders and allowing
  # easy assignment.
  #
  # You can use mount_uploader with pretty much any class, however it is
  # intended to be used with some kind of persistent storage, like an ORM.
  # If you want to persist the uploaded files in a particular Class, it
  # needs to implement a `read_uploader` and a `write_uploader` method.
  #
  module Mount

    ##
    # === Returns
    #
    # [Hash{Symbol => CarrierWave}] what uploaders are mounted on which columns
    #
    def uploaders
      @uploaders ||= superclass.respond_to?(:uploaders) ? superclass.uploaders.dup : {}
    end

    def uploader_options
      @uploader_options ||= superclass.respond_to?(:uploader_options) ? superclass.uploader_options.dup : {}
    end

    ##
    # Return a particular option for a particular uploader
    #
    # === Parameters
    #
    # [column (Symbol)] The column the uploader is mounted at
    # [option (Symbol)] The option, e.g. validate_integrity
    #
    # === Returns
    #
    # [Object] The option value
    #
    def uploader_option(column, option)
      if uploader_options[column].has_key?(option)
        uploader_options[column][option]
      else
        uploaders[column].send(option)
      end
    end

    ##
    # Mounts the given uploader on the given column. This means that assigning
    # and reading from the column will upload and retrieve files. Supposing
    # that a User class has an uploader mounted on image, you can assign and
    # retrieve files like this:
    #
    #     @user.image # => <Uploader>
    #     @user.image.store!(some_file_object)
    #
    #     @user.image.url # => '/some_url.png'
    #
    # It is also possible (but not recommended) to omit the uploader, which
    # will create an anonymous uploader class.
    #
    # Passing a block makes it possible to customize the uploader. This can be
    # convenient for brevity, but if there is any significant logic in the
    # uploader, you should do the right thing and have it in its own file.
    #
    # === Added instance methods
    #
    # Supposing a class has used +mount_uploader+ to mount an uploader on a column
    # named +image+, in that case the following methods will be added to the class:
    #
    # [image]                   Returns an instance of the uploader only if anything has been uploaded
    # [image=]                  Caches the given file
    #
    # [image_url]               Returns the url to the uploaded file
    #
    # [image_cache]             Returns a string that identifies the cache location of the file
    # [image_cache=]            Retrieves the file from the cache based on the given cache name
    #
    # [remote_image_url]        Returns previously cached remote url
    # [remote_image_url=]       Retrieve the file from the remote url
    #
    # [remove_image]            An attribute reader that can be used with a checkbox to mark a file for removal
    # [remove_image=]           An attribute writer that can be used with a checkbox to mark a file for removal
    # [remove_image?]           Whether the file should be removed when store_image! is called.
    #
    # [store_image!]            Stores a file that has been assigned with +image=+
    # [remove_image!]           Removes the uploaded file from the filesystem.
    #
    # [image_integrity_error]   Returns an error object if the last file to be assigned caused an integrity error
    # [image_processing_error]  Returns an error object if the last file to be assigned caused a processing error
    # [image_download_error]    Returns an error object if the last file to be remotely assigned caused a download error
    #
    # [image_identifier]        Reads out the identifier of the file
    #
    # === Parameters
    #
    # [column (Symbol)]                   the attribute to mount this uploader on
    # [uploader (CarrierWave::Uploader)]  the uploader class to mount
    # [options (Hash{Symbol => Object})]  a set of options
    # [&block (Proc)]                     customize anonymous uploaders
    #
    # === Options
    #
    # [:mount_on => Symbol] if the name of the column to be serialized to differs you can override it using this option
    # [:ignore_integrity_errors => Boolean] if set to true, integrity errors will result in caching failing silently
    # [:ignore_processing_errors => Boolean] if set to true, processing errors will result in caching failing silently
    #
    # === Examples
    #
    # Mounting uploaders on different columns.
    #
    #     class Song
    #       mount_uploader :lyrics, LyricsUploader
    #       mount_uploader :alternative_lyrics, LyricsUploader
    #       mount_uploader :file, SongUploader
    #     end
    #
    # This will add an anonymous uploader with only the default settings:
    #
    #     class Data
    #       mount_uploader :csv
    #     end
    #
    # this will add an anonymous uploader overriding the store_dir:
    #
    #     class Product
    #       mount_uploader :blueprint do
    #         def store_dir
    #           'blueprints'
    #         end
    #       end
    #     end
    #
    def mount_uploader(column, uploader=nil, options={}, &block)
      mount_base(column, uploader, options, &block)

      mod = Module.new
      include mod
      mod.class_eval <<-RUBY, __FILE__, __LINE__+1

        def #{column}
          _mounter(:#{column}).uploaders[0] ||= _mounter(:#{column}).blank_uploader
        end

        def #{column}=(new_file)
          _mounter(:#{column}).cache([new_file])
        end

        def #{column}_url(*args)
          #{column}.url(*args)
        end

        def #{column}_cache
          _mounter(:#{column}).cache_names[0]
        end

        def #{column}_cache=(cache_name)
          _mounter(:#{column}).cache_names = [cache_name]
        end

        def remote_#{column}_url
          [_mounter(:#{column}).remote_urls].flatten[0]
        end

        def remote_#{column}_url=(url)
          _mounter(:#{column}).remote_urls = [url]
        end

        def remote_#{column}_request_header=(header)
          _mounter(:#{column}).remote_request_headers = [header]
        end

        def write_#{column}_identifier
          return if frozen?
          mounter = _mounter(:#{column})

          if mounter.remove?
            write_uploader(mounter.serialization_column, nil)
          elsif mounter.identifiers.first
            write_uploader(mounter.serialization_column, mounter.identifiers.first)
          end
        end

        def #{column}_identifier
          _mounter(:#{column}).read_identifiers[0]
        end

        def store_previous_changes_for_#{column}
          attribute_changes = ::ActiveRecord.version.to_s.to_f >= 5.1 ? saved_changes : changes
          @_previous_changes_for_#{column} = attribute_changes[_mounter(:#{column}).serialization_column]
        end

        def remove_previously_stored_#{column}
          before, after = @_previous_changes_for_#{column}
          _mounter(:#{column}).remove_previous([before], [after])
        end
      RUBY
    end

    ##
    # Mounts the given uploader on the given array column. This means that
    # assigning and reading from the array column will upload and retrieve
    # multiple files. Supposing that a User class has an uploader mounted on
    # images, and that images can store an array, for example it could be a
    # PostgreSQL JSON column. You can assign and retrieve files like this:
    #
    #     @user.images # => []
    #     @user.images = [some_file_object]
    #     @user.images # => [<Uploader>]
    #
    #     @user.images[0].url # => '/some_url.png'
    #
    # It is also possible (but not recommended) to omit the uploader, which
    # will create an anonymous uploader class.
    #
    # Passing a block makes it possible to customize the uploader. This can be
    # convenient for brevity, but if there is any significant logic in the
    # uploader, you should do the right thing and have it in its own file.
    #
    # === Added instance methods
    #
    # Supposing a class has used +mount_uploaders+ to mount an uploader on a column
    # named +images+, in that case the following methods will be added to the class:
    #
    # [images]                  Returns an array of uploaders for each uploaded file
    # [images=]                 Caches the given files
    #
    # [images_urls]             Returns the urls to the uploaded files
    #
    # [images_cache]            Returns a string that identifies the cache location of the files
    # [images_cache=]           Retrieves the files from the cache based on the given cache name
    #
    # [remote_image_urls]       Returns previously cached remote urls
    # [remote_image_urls=]      Retrieve files from the given remote urls
    #
    # [remove_images]           An attribute reader that can be used with a checkbox to mark the files for removal
    # [remove_images=]          An attribute writer that can be used with a checkbox to mark the files for removal
    # [remove_images?]          Whether the files should be removed when store_image! is called.
    #
    # [store_images!]           Stores all files that have been assigned with +images=+
    # [remove_images!]          Removes the uploaded file from the filesystem.
    #
    # [images_integrity_error]  Returns an error object if the last files to be assigned caused an integrity error
    # [images_processing_error] Returns an error object if the last files to be assigned caused a processing error
    # [images_download_error]   Returns an error object if the last files to be remotely assigned caused a download error
    #
    # [image_identifiers]       Reads out the identifiers of the files
    #
    # === Parameters
    #
    # [column (Symbol)]                   the attribute to mount this uploader on
    # [uploader (CarrierWave::Uploader)]  the uploader class to mount
    # [options (Hash{Symbol => Object})]  a set of options
    # [&block (Proc)]                     customize anonymous uploaders
    #
    # === Options
    #
    # [:mount_on => Symbol] if the name of the column to be serialized to differs you can override it using this option
    # [:ignore_integrity_errors => Boolean] if set to true, integrity errors will result in caching failing silently
    # [:ignore_processing_errors => Boolean] if set to true, processing errors will result in caching failing silently
    #
    # === Examples
    #
    # Mounting uploaders on different columns.
    #
    #     class Song
    #       mount_uploaders :lyrics, LyricsUploader
    #       mount_uploaders :alternative_lyrics, LyricsUploader
    #       mount_uploaders :files, SongUploader
    #     end
    #
    # This will add an anonymous uploader with only the default settings:
    #
    #     class Data
    #       mount_uploaders :csv_files
    #     end
    #
    # this will add an anonymous uploader overriding the store_dir:
    #
    #     class Product
    #       mount_uploaders :blueprints do
    #         def store_dir
    #           'blueprints'
    #         end
    #       end
    #     end
    #
    def mount_uploaders(column, uploader=nil, options={}, &block)
      mount_base(column, uploader, options, &block)

      mod = Module.new
      include mod
      mod.class_eval <<-RUBY, __FILE__, __LINE__+1

        def #{column}
          _mounter(:#{column}).uploaders
        end

        def #{column}=(new_files)
          _mounter(:#{column}).cache(new_files)
        end

        def #{column}_urls(*args)
          _mounter(:#{column}).urls(*args)
        end

        def #{column}_cache
          names = _mounter(:#{column}).cache_names
          names.to_json if names.present?
        end

        def #{column}_cache=(cache_name)
          _mounter(:#{column}).cache_names = JSON.parse(cache_name) if cache_name.present?
        end

        def remote_#{column}_urls
          _mounter(:#{column}).remote_urls
        end

        def remote_#{column}_urls=(urls)
          _mounter(:#{column}).remote_urls = urls
        end

        def remote_#{column}_request_headers=(headers)
          _mounter(:#{column}).remote_request_headers = headers
        end

        def write_#{column}_identifier
          return if frozen?
          mounter = _mounter(:#{column})

          if mounter.remove?
            write_uploader(mounter.serialization_column, nil)
          elsif mounter.identifiers.any?
            write_uploader(mounter.serialization_column, mounter.identifiers)
          end
        end

        def #{column}_identifiers
          _mounter(:#{column}).read_identifiers
        end

        def store_previous_changes_for_#{column}
          attribute_changes = ::ActiveRecord.version.to_s.to_f >= 5.1 ? saved_changes : changes
          @_previous_changes_for_#{column} = attribute_changes[_mounter(:#{column}).serialization_column]
        end

        def remove_previously_stored_#{column}
          _mounter(:#{column}).remove_previous(*@_previous_changes_for_#{column})
        end
      RUBY
    end

    private

    def mount_base(column, uploader=nil, options={}, &block)
      include CarrierWave::Mount::Extension

      uploader = build_uploader(uploader, &block)
      uploaders[column.to_sym] = uploader
      uploader_options[column.to_sym] = options

      # Make sure to write over accessors directly defined on the class.
      # Simply super to the included module below.
      class_eval <<-RUBY, __FILE__, __LINE__+1
        def #{column}; super; end
        def #{column}=(new_file); super; end
      RUBY

      mod = Module.new
      include mod
      mod.class_eval <<-RUBY, __FILE__, __LINE__+1

        def #{column}?
          _mounter(:#{column}).present?
        end

        def remove_#{column}
          _mounter(:#{column}).remove
        end

        def remove_#{column}!
          _mounter(:#{column}).remove!
        end

        def remove_#{column}=(value)
          _mounter(:#{column}).remove = value
        end

        def remove_#{column}?
          _mounter(:#{column}).remove?
        end

        def store_#{column}!
          _mounter(:#{column}).store!
        end

        def #{column}_integrity_error
          _mounter(:#{column}).integrity_error
        end

        def #{column}_processing_error
          _mounter(:#{column}).processing_error
        end

        def #{column}_download_error
          _mounter(:#{column}).download_error
        end

        def mark_remove_#{column}_false
          _mounter(:#{column}).remove = false
        end
      RUBY
    end

    def build_uploader(uploader, &block)
      return uploader if uploader && !block_given?

      uploader = Class.new(uploader || CarrierWave::Uploader::Base)
      const_set("Uploader#{uploader.object_id}".tr('-', '_'), uploader)

      if block_given?
        uploader.class_eval(&block)
        uploader.recursively_apply_block_to_versions(&block)
      end

      uploader
    end

    module Extension

      ##
      # overwrite this to read from a serialized attribute
      #
      def read_uploader(column); end

      ##
      # overwrite this to write to a serialized attribute
      #
      def write_uploader(column, identifier); end

    private

      def _mounter(column)
        # We cannot memoize in frozen objects :(
        return Mounter.new(self, column) if frozen?
        @_mounters ||= {}
        @_mounters[column] ||= Mounter.new(self, column)
      end

    end # Extension

  end # Mount
end # CarrierWave
