# Copyright 2021 Google LLC
#
# 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
#
#     https://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 "google/cloud/firestore/v1"
require "google/cloud/firestore/query"
require "google/cloud/firestore/query_partition"

module Google
  module Cloud
    module Firestore
      ##
      # # CollectionGroup
      #
      # A collection group object is used for adding documents, getting
      # document references, and querying for documents, including with partitions.
      #
      # See {Client#col_group} and {Query}.
      #
      # @example
      #   require "google/cloud/firestore"
      #
      #   firestore = Google::Cloud::Firestore.new
      #
      #   # Get a collection group
      #   col_group = firestore.col_group "cities"
      #
      #   # Get and print all city documents
      #   col_group.get do |city|
      #     puts "#{city.document_id} has #{city[:population]} residents."
      #   end
      #
      class CollectionGroup < Query
        ##
        # Partitions a query by returning partition cursors that can be used to run the query in parallel. The returned
        # partition cursors are split points that can be used as starting/end points for the query results.
        #
        # @param [Integer] partition_count The desired maximum number of partition points. The number must be strictly
        #   positive. The actual number of partitions returned may be fewer.
        # @param [Time] read_time Reads documents as they were at the given time.
        #   This may not be older than 270 seconds. Optional
        #
        # @return [Array<QueryPartition>] An ordered array of query partitions.
        #
        # @example
        #   require "google/cloud/firestore"
        #
        #   firestore = Google::Cloud::Firestore.new
        #
        #   col_group = firestore.col_group "cities"
        #
        #   partitions = col_group.partitions 3
        #
        #   queries = partitions.map(&:to_query)
        #
        # @example partition with read time
        #   require "google/cloud/firestore"
        #
        #   firestore = Google::Cloud::Firestore.new
        #
        #   col_group = firestore.col_group "cities"
        #
        #   read_time = Time.now
        #
        #   partitions = col_group.partitions 3, read_time: read_time
        #
        #   queries = partitions.map(&:to_query)
        #
        def partitions partition_count, read_time: nil
          ensure_service!

          raise ArgumentError, "partition_count must be > 0" unless partition_count.positive?

          # Partition queries require explicit ordering by __name__.
          query_with_default_order = order "__name__"
          # Since we are always returning an extra partition (with en empty endBefore cursor), we reduce the desired
          # partition count by one.
          partition_count -= 1

          grpc_partitions = if partition_count.positive?
                              # Retrieve all pages, since cursor order is not guaranteed and they must be sorted.
                              list_all partition_count, query_with_default_order, read_time
                            else
                              [] # Ensure that a single, empty QueryPartition is returned.
                            end
          cursor_values = grpc_partitions.map do |cursor|
            # Convert each cursor to a (single-element) array of Google::Cloud::Firestore::DocumentReference.
            cursor.values.map do |value|
              Convert.value_to_raw value, client
            end
          end
          # Sort the values of the returned cursor, which right now should only contain a single reference value (which
          # needs to be sorted one component at a time).
          cursor_values.sort! do |a, b|
            a.first <=> b.first
          end

          start_at = nil
          results = cursor_values.map do |end_before|
            partition = QueryPartition.new query_with_default_order, start_at, end_before
            start_at = end_before
            partition
          end
          # Always add a final QueryPartition with an empty end_before value.
          results << QueryPartition.new(query_with_default_order, start_at, nil)
          results
        end

        ##
        # @private New Collection group object from a path.
        def self.from_collection_id parent_path, collection_id, client
          query = Google::Cloud::Firestore::V1::StructuredQuery.new(
            from: [
              Google::Cloud::Firestore::V1::StructuredQuery::CollectionSelector.new(
                collection_id: collection_id,
                all_descendants: true
              )
            ]
          )
          CollectionGroup.new query, parent_path, client
        end

        protected

        def list_all partition_count, query_with_default_order, read_time
          grpc_partitions = []
          token = nil
          loop do
            grpc = service.partition_query parent_path, query_with_default_order.query, partition_count,
                                           token: token, read_time: read_time
            grpc_partitions += Array(grpc.partitions)
            token = grpc.next_page_token
            token = nil if token == ""
            break unless token
          end
          grpc_partitions
        end
      end
    end
  end
end
