# frozen_string_literal: true

# Copyright (C) 2014-2020 MongoDB Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require 'mongo/collection/view/builder'
require 'mongo/collection/view/immutable'
require 'mongo/collection/view/iterable'
require 'mongo/collection/view/explainable'
require 'mongo/collection/view/aggregation'
require 'mongo/collection/view/change_stream'
require 'mongo/collection/view/map_reduce'
require 'mongo/collection/view/readable'
require 'mongo/collection/view/writable'

module Mongo
  class Collection
    # Representation of a query and options producing a result set of documents.
    #
    # A +View+ can be modified using helpers. Helpers can be chained,
    # as each one returns a +View+ if arguments are provided.
    #
    # The query message is sent to the server when a "terminator" is called.
    # For example, when #each is called on a +View+, a Cursor object is
    # created, which then sends the query to the server.
    #
    # A +View+ is not created directly by a user. Rather, +View+
    # creates a +View+ when a CRUD operation is called and returns it to
    # the user to interact with.
    #
    # @note The +View+ API is semipublic.
    # @api semipublic
    class View
      extend Forwardable
      include Enumerable
      include Immutable
      include Iterable
      include Readable
      include Explainable
      include Writable

      # @return [ Collection ] The +Collection+ to query.
      attr_reader :collection

      # @return [ Hash ] The query filter.
      attr_reader :filter

      # Delegate necessary operations to the collection.
      def_delegators :collection,
                     :client,
                     :cluster,
                     :database,
                     :nro_write_with_retry,
                     :read_with_retry,
                     :read_with_retry_cursor,
                     :write_with_retry,
                     :write_concern_with_session

      # Delegate to the cluster for the next primary.
      def_delegators :cluster, :next_primary

      def_delegators :client, :tracer

      alias selector filter

      # @return [ Integer | nil | The timeout_ms value that was passed as an
      #   option to the view.
      #
      # @api private
      attr_reader :operation_timeout_ms

      # Compare two +View+ objects.
      #
      # @example Compare the view with another object.
      #   view == other
      #
      # @return [ true, false ] Equal if collection, filter, and options of two
      #   +View+ match.
      #
      # @since 2.0.0
      def ==(other)
        return false unless other.is_a?(View)

        collection == other.collection &&
          filter == other.filter &&
          options == other.options
      end
      alias eql? ==

      # A hash value for the +View+ composed of the collection namespace,
      # hash of the options and hash of the filter.
      #
      # @example Get the hash value.
      #   view.hash
      #
      # @return [ Integer ] A hash value of the +View+ object.
      #
      # @since 2.0.0
      def hash
        [ collection.namespace, options.hash, filter.hash ].hash
      end

      # Creates a new +View+.
      #
      # @example Find all users named Emily.
      #   View.new(collection, {:name => 'Emily'})
      #
      # @example Find all users named Emily skipping 5 and returning 10.
      #   View.new(collection, {:name => 'Emily'}, :skip => 5, :limit => 10)
      #
      # @example Find all users named Emily using a specific read preference.
      #   View.new(collection, {:name => 'Emily'}, :read => :secondary_preferred)
      #
      # @param [ Collection ] collection The +Collection+ to query.
      # @param [ Hash ] filter The query filter.
      # @param [ Hash ] options The additional query options.
      #
      # @option options [ true, false ] :allow_disk_use When set to true, the
      #   server can write temporary data to disk while executing the find
      #   operation. This option is only available on MongoDB server versions
      #   4.4 and newer.
      # @option options [ Integer ] :batch_size The number of documents to
      #   return in each response from MongoDB.
      # @option options [ Hash ] :collation The collation to use.
      # @option options [ String ] :comment Associate a comment with the query.
      # @option options [ :tailable, :tailable_await ] :cursor_type The type of cursor to use.
      # @option options [ Hash ] :explain Execute an explain with the provided
      #   explain options (known options are :verbose and :verbosity) rather
      #   than a find.
      # @option options [ Hash ] :hint Override the default index selection and
      #   force MongoDB to use a specific index for the query.
      # @option options [ Integer ] :limit Max number of documents to return.
      # @option options [ Integer ] :max_scan Constrain the query to only scan
      #   the specified number of documents. Use to prevent queries from
      #   running for too long. Deprecated as of MongoDB server version 4.0.
      # @option options [ Hash ] :projection The fields to include or exclude
      #   in the returned documents.
      # @option options [ Hash ] :read The read preference to use for the
      #   query. If none is provided, the collection's default read preference
      #   is used.
      # @option options [ Hash ] :read_concern The read concern to use for
      #   the query.
      # @option options [ true | false ] :show_disk_loc Return disk location
      #   info as a field in each doc.
      # @option options [ Integer ] :skip The number of documents to skip.
      # @option options [ true | false ] :snapshot Prevents returning a
      #   document more than once. Deprecated as of MongoDB server version 4.0.
      # @option options [ Hash ] :sort The key and direction pairs used to sort
      #   the results.
      # @option options [ :cursor_lifetime | :iteration ] :timeout_mode How to interpret
      #   :timeout_ms (whether it applies to the lifetime of the cursor, or per
      #   iteration).
      # @option options [ Integer ] :timeout_ms The operation timeout in milliseconds.
      #    Must be a non-negative integer. An explicit value of 0 means infinite.
      #    The default value is unset which means the value is inherited from
      #    the collection or the database or the client.
      #
      # @since 2.0.0
      def initialize(collection, filter = {}, options = {})
        validate_doc!(filter)

        filter = BSON::Document.new(filter)
        options = BSON::Document.new(options)

        @collection = collection
        @operation_timeout_ms = options.delete(:timeout_ms)

        validate_timeout_mode!(options)

        # This is when users pass $query in filter and other modifiers
        # alongside?
        query = filter.delete(:$query)
        # This makes modifiers contain the filter if filter wasn't
        # given via $query but as top-level keys, presumably
        # downstream code ignores non-modifier keys in the modifiers?
        modifiers = filter.merge(options.delete(:modifiers) || {})
        @filter = (query || filter).freeze
        @options = Operation::Find::Builder::Modifiers.map_driver_options(modifiers).merge!(options).freeze
      end

      # The timeout_ms value to use for this operation; either specified as an
      # option to the view, or inherited from the collection.
      #
      # @return [ Integer | nil ] the timeout_ms for this operation
      def timeout_ms
        operation_timeout_ms || collection.timeout_ms
      end

      # Get a human-readable string representation of +View+.
      #
      # @example Get the inspection.
      #   view.inspect
      #
      # @return [ String ] A string representation of a +View+ instance.
      #
      # @since 2.0.0
      def inspect
        "#<Mongo::Collection::View:0x#{object_id} namespace='#{collection.namespace}'" +
          " @filter=#{filter} @options=#{options}>"
      end

      # Get the write concern on this +View+.
      #
      # @example Get the write concern.
      #   view.write_concern
      #
      # @return [ Mongo::WriteConcern ] The write concern.
      #
      # @since 2.0.0
      def write_concern
        WriteConcern.get(options[:write_concern] || options[:write] || collection.write_concern)
      end

      # @return [ Hash ] timeout_ms value set on the operation level (if any),
      #   and/or timeout_ms that is set on collection/database/client level (if any).
      #
      # @api private
      def operation_timeouts(opts = {})
        {}.tap do |result|
          if opts[:timeout_ms] || operation_timeout_ms
            result[:operation_timeout_ms] = opts[:timeout_ms] || operation_timeout_ms
          else
            result[:inherited_timeout_ms] = collection.timeout_ms
          end
        end
      end

      private

      def initialize_copy(other)
        @collection = other.collection
        @options = other.options.dup
        @filter = other.filter.dup
      end

      def new(options)
        options = options.merge(timeout_ms: operation_timeout_ms) if operation_timeout_ms
        View.new(collection, filter, options)
      end

      def view
        self
      end

      def with_session(opts = {}, &block)
        client.with_session(@options.merge(opts), &block)
      end
    end
  end
end
