module Mongoid
  module EmbeddedCopy
    extend ActiveSupport::Concern

    class_methods do
      def embeds_copy(name, opts = {})
        klass = opts[:class_name] || name.to_s.camelize
        embedded_class = EmbeddedCopy.embedded_class(self, opts, klass)
        EmbeddedCopy.for(klass, opts.merge({:in => self, inverse_of: name}))
        mongoid_options = Mongoid::Relations::Embedded::One.valid_options +
          Mongoid::Relations::Options::COMMON
        embeds_one name, opts.merge({class_name: embedded_class}).slice(*mongoid_options)

        define_method("#{name}_with_copy=") do |value|
          value = embedded_class.constantize.new(value) if value.class.name == klass
          self.public_send("#{name}_without_copy=", value)
        end
        alias_method_chain "#{name}=", :copy
      end
    end

    def self.embedded_class(klass, opts, prefix = nil)
      embedded_class = opts[:embedded_class] || "CopyFor#{klass}".gsub('::', '')
      [prefix, embedded_class].compact.join('::')
    end

    def self.for(klass, opts = {})
      klass = klass.constantize if klass.is_a?(String)
      raise ArgumentError.new('Requires :in options to specify the document in which this one is embedded') if !opts.has_key?(:in)
      raise ArgumentError.new("Can't create an embedded copy of non-mongoid document class #{klass}") if !(klass < Mongoid::Document)
      skipped = Array(opts[:skip]).map{|f| f.to_s}
      skipped.push('deleted_at')

      embed_opts = opts[:in]
      embed_opts = {class_name: embed_opts.to_s, as: embed_opts.to_s.underscore} if embed_opts.is_a?(Class)
      embed_opts[:inverse_of] = opts[:inverse_of]
      embedded_class = self.embedded_class(embed_opts[:class_name], opts)
      embed_name = embed_opts.delete(:as)
      skipped.push(embed_name).push("#{embed_name}_id")

      return if klass.const_defined?(embedded_class) && [klass, embedded_class].join('::').constantize.respond_to?(:original_class)
      klass.send(:include, equality_module_for(klass))

      document_module = Module.new do
        extend ActiveSupport::Concern

        included do
          include Mongoid::Document
          include Mongoid::EmbeddedCopy.equality_module_for(klass)

          class_attribute :original_class, instance_writer: false
          class_attribute :skipped_attributes, instance_writer: false
          class_attribute :embed_name

          self.original_class = klass
          self.embed_name = embed_name
          self.skipped_attributes = skipped

          def initialize(*attrs)
            if attrs.first.is_a?(original_class)
              attrs = attrs.first.attributes.to_h.dup
              skipped_attributes.each {|n| attrs.delete(n) }
            end
            super(attrs)
          end

          embedded_in embed_name, embed_opts

          klass.fields.each do |name, f|
            next if skipped.include?(name) || name == '_id'
            options = f.options.dup
            options.delete(:klass)
            field name, options

            if opts[:update_original]
              define_method("#{name}_with_update_original=") do |value|
                load_original.set(name => value)
                public_send("#{name}_without_update_original=", value)
              end
              alias_method_chain "#{name}=", :update_original
            end
          end
        end

        def load_original
          @original ||= original_class.find(id)
        end
      end

      if klass.const_defined?(embedded_class)
        klass.const_get(embedded_class).send(:include, document_module)
      else
        klass.const_set(embedded_class, Class.new do
          include document_module
        end)
      end
    end

    def self.equality_module_for(klass)
      Module.new do
        extend ActiveSupport::Concern

        included do
          class_attribute :acts_as_method, instance_writer: false
          self.acts_as_method = "acts_as_#{klass.to_s.underscore}"

          define_method(acts_as_method) { true }
        end

        def ==(rhs)
          if rhs.respond_to?(acts_as_method) && rhs.public_send(acts_as_method)
            id == rhs.id
          else
            super
          end
        end
        alias_method :eql?, :==
      end
    end
  end
end