# encoding: utf-8

require 'mongoid'
require 'mongoid-grid_fs'
require 'carrierwave'
require 'carrierwave/validations/active_model'

module CarrierWave
  module Mongoid
    include CarrierWave::Mount

    module CarrierWave3
      def mount_uploader(column, uploader=nil, options={}, &block)
        field options[:mount_on] || column

        super
      end

      def mount_uploaders(column, uploader = nil, options = {}, &block)
        field (options[:mount_on] || column), type: Array, default: []

        super
      end

    private

      def mount_base(column, uploader=nil, options={}, &block)
        super

        alias_method :read_uploader, :read_attribute
        alias_method :write_uploader, :write_attribute
        public :read_uploader
        public :write_uploader

        include CarrierWave::Validations::ActiveModel

        validates_integrity_of column if uploader_option(column.to_sym, :validate_integrity)
        validates_processing_of column if uploader_option(column.to_sym, :validate_processing)
        validates_download_of column if uploader_option(column.to_sym, :validate_download)

        after_save :"store_#{column}!"
        before_save :"write_#{column}_identifier"
        after_update :"remove_previously_stored_#{column}"
        after_save :"reset_previous_changes_for_#{column}"
        after_update :"mark_remove_#{column}_false"
        after_destroy :"remove_#{column}!"

        mod = Module.new
        prepend mod
        mod.class_eval <<-RUBY, __FILE__, __LINE__+1
        def serializable_hash(options=nil)
          hash = {}

          except = options && options[:except] && Array.wrap(options[:except]).map(&:to_s)
          only   = options && options[:only]   && Array.wrap(options[:only]).map(&:to_s)

          self.class.uploaders.each do |column, uploader|
            if (!only && !except) || (only && only.include?(column.to_s)) || (except && !except.include?(column.to_s))
              next if _mounter(column.to_sym).uploaders.blank?
              hash[column.to_s] = _mounter(column.to_sym).#{options[:multiple] ? "uploaders.map(&:serializable_hash)" : "uploaders[0].serializable_hash"}
            end
          end
          super(options).merge(hash)
        end

        # Reset cached mounter on record reload
        def reload(*)
          @_mounters = nil
          super
        end

        # Reset cached mounter on record dup
        def initialize_dup(other)
          old_uploaders = _mounter(:"#{column}").uploaders
          super
          @_mounters[:"#{column}"] = nil
          # The attribute needs to be cleared to prevent it from picked up as identifier
          write_attribute(_mounter(:#{column}).serialization_column, nil)
          _mounter(:"#{column}").cache(old_uploaders)
        end

        def write_#{column}_identifier
          return unless has_attribute?(_mounter(:#{column}).serialization_column)
          super
        end
      RUBY
      end
    end

    module CarrierWavePre3
      ##
      # See +CarrierWave::Mount#mount_uploader+ for documentation
      #
      def mount_uploader(column, uploader=nil, options={}, &block)
        field options[:mount_on] || column

        super

        alias_method :read_uploader, :read_attribute
        alias_method :write_uploader, :write_attribute
        public :read_uploader
        public :write_uploader

        include CarrierWave::Validations::ActiveModel

        validates_integrity_of  column if uploader_option(column.to_sym, :validate_integrity)
        validates_processing_of column if uploader_option(column.to_sym, :validate_processing)

        after_save :"store_#{column}!"
        before_save :"write_#{column}_identifier"
        after_destroy :"remove_#{column}!"
        if Gem::Version.new(CarrierWave::VERSION) >= Gem::Version.new("1.0.beta")
          before_update :"store_previous_changes_for_#{column}"
        else
          before_update :"store_previous_model_for_#{column}"
        end
        after_save :"remove_previously_stored_#{column}"

        class_eval <<-RUBY, __FILE__, __LINE__+1
          def #{column}=(new_file)
            column = _mounter(:#{column}).serialization_column

            # We're using _new_ and _old_ placeholder values to force Mongoid to
            # recognize changes in embedded documents. Before we assign these
            # values, we need to store the original file name in case we need to
            # delete it when document is saved.
            previous_uploader_value = read_uploader(column)
            @_previous_uploader_value_for_#{column} = previous_uploader_value

            # mongoid won't upload a new file if there was no file previously.
            write_uploader(column, '_old_') if self.persisted? && read_uploader(column).nil?

            send(:"\#{column}_will_change!")
            super
          end

          def remove_#{column}=(arg)
            if ['1', true].include?(arg)
              column = _mounter(:#{column}).serialization_column
              send(:"\#{column}_will_change!")
            end
            super
          end

          def remove_#{column}!
            super unless respond_to?(:paranoid?) && paranoid? && flagged_for_destroy?
          end

          # Overrides Mongoid's default dirty behavior to instead work more like
          # ActiveRecord's. Mongoid doesn't deem an attribute as changed unless
          # the new value is different than the original. Given that CarrierWave
          # caches files before save, it's necessary to know that there's a
          # pending change even though the attribute value itself might not
          # reflect that yet.
          def #{column}_changed?
            changed_attributes.has_key?("#{column}")
          end

          # The default Mongoid attribute_will_change! method is not enough
          # when we want to upload a new file in an existing embedded document.
          # The custom version of that method forces the callbacks to be
          # ran and so does the upload.
          def #{column}_will_change!
            changed_attributes["#{column}"] = '_new_'
          end

          # Since we had to use tricks with _old_ and _new_ values to properly
          # track changes in embedded documents, we need to overwrite this method
          # to remove the original file if it was replaced with a new one that
          # had a different name.
          if Gem::Version.new(CarrierWave::VERSION) >= Gem::Version.new("1.0.beta")
            def remove_previously_stored_#{column}
              before, after = @_previous_changes_for_#{column}
              # Don't delete if the files had the same name
              return if before.nil? && after.nil?
              # Proceed to remove the file, use the original name instead of '_new_'
              before = @_previous_uploader_value_for_#{column} || before
              _mounter(:#{column}).remove_previous([before], [after])
            end
          end

          # CarrierWave 1.1 references ::ActiveRecord constant directly which
          # will fail in projects without ActiveRecord. We need to overwrite this
          # method to avoid it.
          # See https://github.com/carrierwaveuploader/carrierwave/blob/07dc4d7bd7806ab4b963cf8acbad73d97cdfe74e/lib/carrierwave/mount.rb#L189
          def store_previous_changes_for_#{column}
            @_previous_changes_for_#{column} = changes[_mounter(:#{column}).serialization_column]
          end

          def find_previous_model_for_#{column}
            if self.embedded?
              ancestors =
                if self.respond_to?(:_association) # Mongoid >= 7.0.0.beta
                  [[ self._association.key, self._parent ]].tap { |x| x.unshift([ x.first.last._association.key, x.first.last._parent ]) while x.first.last.embedded? }
                elsif self.respond_to?(:__metadata) # Mongoid >= 4.0.0.beta1 < 7.0.0.beta
                  [[ self.__metadata.key, self._parent ]].tap { |x| x.unshift([ x.first.last.__metadata.key, x.first.last._parent ]) while x.first.last.embedded? }
                else # Mongoid < 4.0.0.beta1
                  [[ self.metadata.key, self._parent ]].tap { |x| x.unshift([ x.first.last.metadata.key, x.first.last._parent ]) while x.first.last.embedded? }
                end
              first_parent = ancestors.first.last
              reloaded_parent = first_parent.class.unscoped.find(first_parent.to_key.first)
              association = ancestors.inject(reloaded_parent) { |parent,(key,ancestor)| (parent.is_a?(Array) ? parent.find(ancestor.to_key.first) : parent).send(key) }
              association.is_a?(Array) ? association.find(to_key.first) : association
            else
              self.class.unscoped.for_ids(to_key).first
            end
          end

          def serializable_hash(options=nil)
            hash = {}

            except = options && options[:except] && Array.wrap(options[:except]).map(&:to_s)
            only   = options && options[:only]   && Array.wrap(options[:only]).map(&:to_s)

            self.class.uploaders.each do |column, uploader|
              if (!only && !except) || (only && only.include?(column.to_s)) || (except && !except.include?(column.to_s))
                if Gem::Version.new(CarrierWave::VERSION) >= Gem::Version.new("1.0.beta")
                  next if _mounter(column.to_sym).uploaders.blank?
                  hash[column.to_s] = _mounter(column.to_sym).uploaders[0].serializable_hash
                else
                  hash[column.to_s] = _mounter(column.to_sym).uploader.serializable_hash
                end
              end
            end
            super(options).merge(hash)
          end

          # Reset cached mounter on mongoid reload
          def reload
            @_mounters = nil
            super
          end
        RUBY
      end

      if Gem::Version.new(CarrierWave::VERSION) >= Gem::Version.new('1.0.beta')
        def mount_uploaders(column, uploader = nil, options = {}, &block)
          field (options[:mount_on] || column), type: Array, default: []

          super

          alias_method :read_uploader, :read_attribute
          alias_method :write_uploader, :write_attribute
          public :read_uploader
          public :write_uploader

          include CarrierWave::Validations::ActiveModel

          validates_integrity_of column if uploader_option(column.to_sym, :validate_integrity)
          validates_processing_of column if uploader_option(column.to_sym, :validate_processing)

          before_update :"store_previous_changes_for_#{column}"
          before_save :"write_#{column}_identifier"
          after_save :"store_#{column}!"
          after_save :"remove_previously_stored_#{column}"
          after_destroy :"remove_#{column}!"

          class_eval <<-RUBY, __FILE__, (__LINE__ + 1)
            def #{column}=(new_files)
              column = _mounter(:#{column}).serialization_column

              previous_uploader_value = read_uploader(column)
              @_previous_uploader_value_for_#{column} = previous_uploader_value

              write_uploader(column, []) if self.persisted? && read_uploader(column).nil?

              send(:"\#{column}_will_change!")

              super
            end

            def #{column}_changed?
              changed_attributes.has_key?("#{column}")
            end

            def remove_#{column}=(value)
              if ['1', true].include?(value)
                column = _mounter(:#{column}).serialization_column

                send(:"\#{column}_will_change!")
              end

              super
            end

            # The default Mongoid attribute_will_change! method is not enough
            # when we want to upload a new file in an existing embedded document.
            # The custom version of that method forces the callbacks to be
            # ran and so does the upload.
            def #{column}_will_change!
              changed_attributes["#{column}"] = ['_new_']
            end

            def remove_previously_stored_#{column}
              before, after = @_previous_changes_for_#{column}
              # Don't delete if the files had the same name
              return if before.nil? && after.nil?
              # Proceed to remove the file, use the original name instead of '_new_'
              before = @_previous_uploader_value_for_#{column} || before
              _mounter(:#{column}).remove_previous(Array.wrap(before), Array.wrap(after))
            end

            def serializable_hash(options = nil)
              hash = {}

              except = options && options[:except] && Array.wrap(options[:except]).map(&:to_s)
              only = options && options[:only] && Array.wrap(options[:only]).map(&:to_s)

              self.class.uploaders.each do |column, _uploader|
                if (!only && !except) || (only && only.include?(column.to_s)) || (except && !except.include?(column.to_s))
                  next if _mounter(column.to_sym).uploaders.blank?
                  hash[column.to_s] = _mounter(column.to_sym).uploaders.map(&:serializable_hash)
                end
              end

              super(options).merge(hash)
            end

            def store_previous_changes_for_#{column}
              @_previous_changes_for_#{column} = changes[_mounter(:#{column}).serialization_column]
            end
          RUBY
        end
      end
    end

    if Gem::Version.new(CarrierWave::VERSION) >= Gem::Version.new("3.0.beta")
      include CarrierWave3
    else
      include CarrierWavePre3
    end
  end # Mongoid
end # CarrierWave

CarrierWave::Storage.autoload :GridFS, 'carrierwave/storage/grid_fs'

class CarrierWave::Uploader::Base
  add_config :grid_fs_access_url

  configure do |config|
    config.storage_engines[:grid_fs] = "CarrierWave::Storage::GridFS"
  end
end

if Gem::Version.new("3.0.beta") <= Gem::Version.new(CarrierWave::VERSION) &&
  Gem::Version.new(CarrierWave::VERSION) < Gem::Version.new("3.1.1")
  # Monkey patch to work around https://github.com/carrierwaveuploader/carrierwave/issues/2770
  CarrierWave::SanitizedFile.prepend Module.new {
    def read(*args)
      if args.empty?
        super
      else
        if is_path?
          @file = File.open(path, "rb")
        elsif @file.is_a?(CarrierWave::Uploader::Base)
          @file = StringIO.new(@file.read)
        end

        @file.read(*args)
      end
    end
  }
end

Mongoid::Document::ClassMethods.send(:include, CarrierWave::Mongoid)
