# frozen_string_literal: true

require 'mongoid/association/constrainable'
require 'mongoid/association/options'

module Mongoid
  module Association

    # This module provides behaviors shared between Association types.
    #
    # @since 7.0
    module Relatable
      include Constrainable
      include Options

      # The options shared between all association types.
      #
      # @return [ Array<Symbol> ] The shared options.
      #
      # @since 7.0
      SHARED_OPTIONS = [
                         :class_name,
                         :inverse_of,
                         :validate,
                         :extend
                       ].freeze

      # The primary key default.
      #
      # @return [ String ] The primary key field default.
      #
      # @since 7.0
      PRIMARY_KEY_DEFAULT = '_id'.freeze

      # The name of the association.
      #
      # @return [ Symbol ] The name of the association.
      #
      # @since 7.0
      attr_reader :name

      # The options on this association.
      #
      # @return [ Hash ] The options.
      #
      # @since 7.0
      attr_reader :options

      # Initialize the Association.
      #
      # @param [ Class ] _class The class of the model who owns this association.
      # @param [ Symbol ] name The name of the association.
      # @param [ Hash ] opts The association options.
      # @param [ Block ] block The optional block.
      #
      # @since 7.0
      def initialize(_class, name, opts = {}, &block)
        @owner_class = _class
        @name = name
        @options = opts
        @extension = nil

        @module_path = _class.name ? _class.name.split('::')[0..-2].join('::') : ''
        @module_path << '::' unless @module_path.empty?

        create_extension!(&block)
        validate!
      end

      # Compare this association to another.
      #
      # @return [ Object ] The object to compare to this association.
      #
      # @since 7.0
      def ==(other)
        relation_class_name == other.relation_class_name &&
          inverse_class_name == other.inverse_class_name &&
            name == other.name &&
              options == other.options
      end

      # Get the callbacks for a given type.
      #
      # @param [ Symbol ] callback_type The type of callback type.
      #
      # @return [ Array<Proc, Symbol> ] A list of the callbacks, either method
      #   names or Procs.
      #
      # @since 7.0
      def get_callbacks(callback_type)
        Array(options[callback_type])
      end

      # Get the type setter.
      # @note Only relevant for polymorphic associations that take the :as option.
      #
      # @return [ String ] The type setter method.
      #
      # @since 7.0
      def type_setter
        @type_setter ||= type.__setter__
      end

      # Whether trying to bind an object using this association should raise
      # an error.
      #
      # @param [ Document ] doc The document to be bound.
      #
      # @return [ true, false ] Whether the document can be bound.
      #
      # @since 7.0
      def bindable?(doc); false; end

      # Get the inverse names.
      #
      # @param [ Object ] other The other model class or model object to use when
      #   determining inverses.
      #
      # @return [ Array<Symbol> ] The list of inverse names.
      #
      # @since 7.0
      def inverses(other = nil)
        return [ inverse_of ] if inverse_of
        if polymorphic?
          polymorphic_inverses(other)
        else
          determine_inverses(other)
        end
      end

      # Get the inverse's association metadata.
      #
      # @param [ Object ] other The other model class or model object to use when
      #   determining inverses.
      #
      # @return [ Association ] The inverse's association metadata.
      #
      # @since 7.0
      def inverse_association(other = nil)
        (other || relation_class).relations[inverse(other)]
      end

      # Get the inverse type.
      #
      # @return [ nil ] Default is nil for an association.
      #
      # @since 7.0
      def inverse_type; end

      # The class name of the association object(s).
      #
      # @return [ String ] The association objects' class name.
      #
      # @since 7.0
      def relation_class_name
        @class_name ||= @options[:class_name] || ActiveSupport::Inflector.classify(name_with_module)
      end
      alias :class_name :relation_class_name

      # The class of the association object(s).
      #
      # @return [ String ] The association objects' class.
      #
      # @since 7.0
      def klass
        @klass ||= relation_class_name.constantize
      end
      alias :relation_class :klass

      # The class name of the object owning this association.
      #
      # @return [ String ] The owning objects' class name.
      #
      # @since 7.0
      def inverse_class_name
        @inverse_class_name ||= @owner_class.name
      end

      # The class of the object owning this association.
      #
      # @return [ String ] The owning objects' class.
      #
      # @since 7.0
      def inverse_class
        @owner_class
      end
      alias :inverse_klass :inverse_class

      # The foreign key field if this association stores a foreign key.
      # Otherwise, the primary key.
      #
      # @return [ Symbol, String ] The primary key.
      #
      # @since 7.0
      def key
        stores_foreign_key? ? foreign_key : primary_key
      end

      # The name of the setter on this object for assigning an associated object.
      #
      # @return [ String ] The setter name.
      #
      # @since 7.0
      def setter
        @setter ||= "#{name}="
      end

      # The name of the inverse setter method.
      #
      # @return [ String ] The name of the inverse setter.
      #
      # @since 7.0
      def inverse_setter(other = nil)
        @inverse_setter ||= "#{inverses(other).first}=" unless inverses(other).blank?
      end

      # The name of the foreign key setter method.
      #
      # @return [ String ] The name of the foreign key setter.
      #
      # @since 7.0
      def foreign_key_setter
        # note: You can't check if this association stores foreign key
        # See HasOne and HasMany binding, they referenced foreign_key_setter
        @foreign_key_setter ||= "#{foreign_key}=" if foreign_key
      end

      # The atomic path for this association.
      #
      # @return [  Mongoid::Atomic::Paths::Root ] The atomic path object.
      #
      # @since 7.0
      def path(document)
        relation.path(document)
      end

      # Gets the setter for the field that sets the type of document on a
      # polymorphic association.
      #
      # @example Get the inverse type setter.
      #   association.inverse_type_setter
      #
      # @return [ String ] The name of the setter.
      #
      # @since 7.0
      def inverse_type_setter
        @inverse_type_setter ||= inverse_type.__setter__
      end

      # Get the name of the method to check if the foreign key has changed.
      #
      # @example Get the foreign key check method.
      #   association.foreign_key_check
      #
      # @return [ String ] The foreign key check.
      #
      # @since 7.0
      def foreign_key_check
        @foreign_key_check ||= "#{foreign_key}_changed?" if (stores_foreign_key? && foreign_key)
      end

      # Create an association proxy object using the owner and target.
      #
      # @param [ Document ] owner The document this association hangs off of.
      # @param [ Document, Array<Document> ] target The target (parent) of the
      #   association.
      #
      # @return [ Proxy ]
      #
      # @since 7.0
      def create_relation(owner, target)
        relation.new(owner, target, self)
      end

      # Whether the dependent method is destructive.
      #
      # @return [ true, false ] If the dependent method is destructive.
      #
      # @since 7.0
      def destructive?
        @destructive ||= !!(dependent && (dependent == :delete_all || dependent == :destroy))
      end

      # Get the counter cache column name.
      #
      # @return [ String ] The counter cache column name.
      #
      # @since 7.0
      def counter_cache_column_name
        @counter_cache_column_name ||= (@options[:counter_cache].is_a?(String) ||
            @options[:counter_cache].is_a?(Symbol)) ?
            @options[:counter_cache] : "#{inverse || inverse_class_name.demodulize.underscore.pluralize}_count"
      end

      # Get the extension.
      #
      # @return [ Module ] The extension module, if one has been defined.
      #
      # @since 7.0
      def extension
        @extension ||= @options[:extend]
      end

      # Get the inverse name.
      #
      # @return [ Symbol ] The inverse name.
      #
      # @since 7.0
      def inverse(other = nil)
        candidates = inverses(other)
        candidates.detect { |c| c } if candidates
      end

      # Whether the associated object(s) should be validated.
      #
      # @return [ true, false ] If the associated object(s)
      #   should be validated.
      #
      # @since 7.0
      def validate?
        @validate ||= if @options[:validate].nil?
                        validation_default
                      else
                        !!@options[:validate]
                      end
      end

      private

      def name_with_module
        @module_path + name.to_s.capitalize
      end

      # Gets the model classes with inverse associations of this model. This is used to determine
      # the classes on the other end of polymorphic associations with models.
      def inverse_association_classes
        Mongoid::Config.models.map { |m| inverse_association(m) }.compact.map(&:inverse_class)
      end

      def setup_index!
        @owner_class.index(index_spec, background: true) if indexed?
      end

      def define_touchable!
        if touchable?
          Touchable.define_touchable!(self)
        end
      end

      def define_autosaver!
        if autosave?
          Association::Referenced::AutoSave.define_autosave!(self)
        end
      end

      def define_builder!
        Association::Builders.define_builder!(self)
      end

      def define_creator!
        Association::Builders.define_creator!(self)
      end

      def define_getter!
        Association::Accessors.define_getter!(self)
      end

      def define_setter!
        Association::Accessors.define_setter!(self)
      end

      def define_existence_check!
        Association::Accessors.define_existence_check!(self)
      end

      def define_ids_getter!
        Association::Accessors.define_ids_getter!(self)
      end

      def define_ids_setter!
        Association::Accessors.define_ids_setter!(self)
      end

      def define_counter_cache_callbacks!
        if counter_cached?
          Association::Referenced::CounterCache.define_callbacks!(self)
        end
      end

      def define_dependency!
        if dependent
          Association::Depending.define_dependency!(self)
        end
      end

      def validate!
        @options.keys.each do |opt|
          unless self.class::VALID_OPTIONS.include?(opt)
            raise Errors::InvalidRelationOption.new(@owner_class, name, opt, self.class::VALID_OPTIONS)
          end
        end

        [name, "#{name}?".to_sym, "#{name}=".to_sym].each do |n|
          if Mongoid.destructive_fields.include?(n)
            raise Errors::InvalidRelation.new(@owner_class, n)
          end
        end
      end

      def polymorph!
        if polymorphic?
          @owner_class.polymorphic = true
        end
      end

      def create_extension!(&block)
        if block
          extension_module_name = "#{@owner_class.to_s.demodulize}#{name.to_s.camelize}RelationExtension"
          silence_warnings do
            @owner_class.const_set(extension_module_name, Module.new(&block))
          end
          @extension = "#{@owner_class}::#{extension_module_name}".constantize
        end
      end

      def default_inverse
        @default_inverse ||= klass.relations[inverse_klass.name.underscore]
      end
    end
  end
end
