# frozen_string_literal: true
# rubocop:todo all

module Mongoid
  module Fields

    # Represents a BSON document field definition which stores
    # a foreign key that references the ID of another document.
    # Used for association behavior.
    class ForeignKey < Standard

      # Adds the atomic changes for this type of resizable field.
      #
      # @example Add the atomic changes.
      #   field.add_atomic_changes(doc, "key", {}, [], [])
      #
      # @todo: Refactor, big time.
      #
      # @param [ Document ] document The document to add to.
      # @param [ String ] name The name of the field.
      # @param [ String ] key The atomic location of the field.
      # @param [ Hash ] mods The current modifications.
      # @param [ Array ] new_elements The new elements to add.
      # @param [ Array ] old_elements The old elements getting removed.
      def add_atomic_changes(document, name, key, mods, new_elements, old_elements)
        old = (old_elements || [])
        new = (new_elements || [])
        if new.length > old.length
          if new.first(old.length) == old
            document.atomic_array_add_to_sets[key] = new.drop(old.length)
          else
            mods[key] = document.attributes[name]
          end
        elsif new.length < old.length
          pulls = old - new
          if new == old - pulls
            document.atomic_array_pulls[key] = pulls
          else
            mods[key] = document.attributes[name]
          end
        elsif new != old
          mods[key] = document.attributes[name]
        end
      end

      # Is this field a foreign key?
      #
      # @example Is the field a foreign key?
      #   field.foreign_key?
      #
      # @return [ true | false ] If the field is a foreign key.
      def foreign_key?
        true
      end

      # Evolve the object into an id compatible object.
      #
      # @example Evolve the object.
      #   field.evolve(object)
      #
      # @param [ Object ] object The object to evolve.
      #
      # @return [ Object ] The evolved object.
      def evolve(object)
        if object_id_field? || object.is_a?(Document)
          if association.polymorphic?
            association.convert_to_foreign_key(object)
          elsif object.is_a?(Document) && object.respond_to?(association.primary_key)
            primary_key_field.evolve(object.send(association.primary_key))
          else
            object.__evolve_object_id__
          end
        else
          related_id_field.evolve(object)
        end
      end

      # Does this field do lazy default evaluation?
      #
      # @example Is the field lazy?
      #   field.lazy?
      #
      # @return [ true | false ] If the field is lazy.
      def lazy?
        type.resizable?
      end

      # Mongoize the object into the Mongo friendly value.
      #
      # @example Mongoize the object.
      #   field.mongoize(object)
      #
      # @param [ Object ] object The object to Mongoize.
      #
      # @return [ Object ] The mongoized object.
      def mongoize(object)
        if type.resizable? || object_id_field?
          mongoize_foreign_key(object)
        else
          related_id_field.mongoize(object)
        end
      end

      # Is the field a BSON::ObjectId?
      #
      # @example Is the field a BSON::ObjectId?
      #   field.object_id_field?
      #
      # @return [ true | false ] If the field is a BSON::ObjectId.
      def object_id_field?
        @object_id_field ||=
            association.polymorphic? ? true : association.klass.using_object_ids?
      end

      # Returns true if an array, false if not.
      #
      # @example Is the field resizable?
      #   field.resizable?
      #
      # @return [ true | false ] If the field is resizable.
      def resizable?
        type.resizable?
      end

      private

      # Convert the provided object to a Mongo-friendly foreign key.
      #
      # @example Convert the object to a foreign key.
      #   mongoize_foreign_key(object)
      #
      # @param [ Object ] object The object to convert.
      #
      # @return [ Object ] The converted object.
      def mongoize_foreign_key(object)
        if type == Array || type == Set
          object = object.to_a if type == Set || object.is_a?(Set)

          if object.resizable?
            object.blank? ? object : association.convert_to_foreign_key(object)
          else
            object.blank? ? [] : association.convert_to_foreign_key(Array(object))
          end
        elsif !(object.nil? || object == '')
          association.convert_to_foreign_key(object)
        end
      end

      # Evaluate the default proc. In some cases we need to instance exec,
      # in others we don't.
      #
      # @example Eval the default proc.
      #   field.evaluate_default_proc(band)
      #
      # @param [ Document ] doc The document.
      #
      # @return [ Object ] The called proc.
      def evaluate_default_proc(doc)
        serialize_default(default_val[])
      end

      # Get the id field of the association.
      #
      # @api private
      #
      # @example Get the related id field.
      #   field.related_id_field
      #
      # @return [ Fields::Standard ] The field.
      def related_id_field
        @related_id_field ||= association.klass.fields["_id"]
      end

      def primary_key_field
        @primary_key_field ||= association.klass.fields[association.primary_key]
      end

      # This is used when default values need to be serialized. Most of the
      # time just return the object.
      #
      # @api private
      #
      # @example Serialize the default value.
      #   field.serialize_default(obj)
      #
      # @param [ Object ] object The default.
      #
      # @return [ Object ] The serialized default.
      def serialize_default(object); object; end
    end
  end
end
