# frozen_string_literal: true
# rubocop:todo all

module Mongoid
  class Criteria

    # Mixin module included in Mongoid::Criteria which adds
    # functionality related to default query scopes and named
    # scopes.
    module Scopable

      # Applies the default scope to the criteria.
      #
      # @example Apply the default scope.
      #   criteria.apply_default_scope
      #
      # @return [ Criteria ] The criteria.
      def apply_default_scope
        klass.without_default_scope do
          merge!(klass.default_scoping.call)
        end
        self.scoping_options = true, false
      end

      # Applies a scope to the current criteria.
      #
      # This method does not modify the receiver but it may return a new
      # object or the receiver depending on the argument: if the +scope+
      # argument is nil, the receiver is returned without modification,
      # otherwise a new criteria object is returned.
      #
      # @param [ Proc | Symbol | Criteria | nil ] scope The scope to apply.
      #
      # @return [ Criteria ] The criteria with the scope applied.
      #
      # @api private
      def apply_scope(scope)
        case scope
        when Proc
          instance_exec(&scope)
        when Symbol
          send(scope)
        when Criteria
          merge(scope)
        else
          self
        end
      end

      # Given another criteria, remove the other criteria's scoping from this
      # criteria.
      #
      # @example Remove the scoping.
      #   criteria.remove_scoping(other)
      #
      # @param [ Criteria ] other The other criteria.
      #
      # @return [ Criteria ] The criteria with scoping removed.
      def remove_scoping(other)
        if other
          reject_matching(other, :selector, :options)
          other.inclusions.each do |meta|
            inclusions.delete_one(meta)
          end
        end
        self
      end

      # Forces the criteria to be scoped, unless it's inside an unscoped block.
      #
      # @example Force the criteria to be scoped.
      #   criteria.scoped(skip: 10)
      #
      # @param [ Hash ] options Additional query options.
      #
      # @return [ Criteria ] The scoped criteria.
      def scoped(options = nil)
        crit = clone
        crit.options.merge!(options || {})
        if klass.default_scopable? && !scoped?
          crit.apply_default_scope
        end
        crit
      end

      # Has the criteria had the default scope applied?
      #
      # @example Is the default scope applied?
      #   criteria.scoped?
      #
      # @return [ true | false ] If the default scope is applied.
      def scoped?
        !!(defined?(@scoped) ? @scoped : nil)
      end

      # Clears all scoping from the criteria.
      #
      # @example Clear all scoping from the criteria.
      #   criteria.unscoped
      #
      # @return [ Criteria ] The unscoped criteria.
      def unscoped
        crit = clone
        unless unscoped?
          crit.scoping_options = false, true
          crit.selector.clear; crit.options.clear
        end
        crit
      end

      # Is the criteria unscoped?
      #
      # @example Is the criteria unscoped?
      #   criteria.unscoped?
      #
      # @return [ true | false ] If the criteria is force unscoped.
      def unscoped?
        !!(defined?(@unscoped) ? @unscoped : nil)
      end

      # Get the criteria scoping options, as a pair (scoped, unscoped).
      #
      # @example Get the scoping options.
      #   criteria.scoping_options
      #
      # @return [ Array ] Scoped, unscoped.
      def scoping_options
        [ (defined?(@scoped) ? @scoped : nil), (defined?(@unscoped) ? @unscoped : nil) ]
      end

      # Set the criteria scoping options, as a pair (scoped, unscoped).
      #
      # @example Set the scoping options.
      #   criteria.scoping_options = true, false
      #
      # @param [ Array ] options Scoped, unscoped.
      #
      # @return [ Array ] The new scoping options.
      def scoping_options=(options)
        @scoped, @unscoped = options
      end

      # Get the criteria with the default scope applied, if the default scope
      # is able to be applied. Cases in which it cannot are: If we are in an
      # unscoped block, if the criteria is already forced unscoped, or the
      # default scope has already been applied.
      #
      # @example Get the criteria with the default scope.
      #   criteria.with_default_scope
      #
      # @return [ Criteria ] The criteria.
      def with_default_scope
        crit = clone
        if klass.default_scopable? && !unscoped? && !scoped?
          crit.apply_default_scope
        end
        crit
      end

      private

      def reject_matching(other, *methods)
        methods.each do |method|
          send(method).reject! do |key, value|
            other.send(method).has_key?(key) && other.send(method)[key] == value
          end
        end
      end
    end
  end
end
