# Copyright 2014 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/datastore/dataset/lookup_results"
require "google/cloud/datastore/dataset/query_results"

module Google
  module Cloud
    module Datastore
      ##
      # # ReadOnlyTransaction
      #
      # Represents a read-only Datastore transaction that only allows reads.
      #
      # A read-only transaction cannot modify entities; in return they do not
      # contend with other read-write or read-only transactions. Using a
      # read-only transaction for transactions that only read data will
      # potentially improve throughput.
      #
      # See {Google::Cloud::Datastore::Dataset#transaction}
      #
      # @see https://cloud.google.com/datastore/docs/concepts/transactions
      #   Transactions
      #
      # @example
      #   require "google/cloud/datastore"
      #
      #   datastore = Google::Cloud::Datastore.new
      #
      #   task_list_key = datastore.key "TaskList", "default"
      #   query = datastore.query("Task").
      #     ancestor(task_list_key)
      #
      #   tasks = nil
      #
      #   datastore.read_only_transaction do |tx|
      #     task_list = tx.find task_list_key
      #     if task_list
      #       tasks = tx.run query
      #     end
      #   end
      #
      class ReadOnlyTransaction
        attr_reader :id

        ##
        # @private The Service object.
        attr_accessor :service

        ##
        # Reads entities at the given time.
        # This may not be older than 60 seconds.
        attr_reader :read_time

        ##
        # @private Creates a new ReadOnlyTransaction instance.
        # Takes a Service instead of project and Credentials.
        #
        # @param [Time] read_time Reads documents as they were at the given time.
        #   This may not be older than 270 seconds. Optional
        #
        def initialize service, read_time: nil
          @service = service
          reset!
          @read_time = read_time
          start
        end

        ##
        # Retrieve an entity by providing key information. The lookup is run
        # within the transaction.
        #
        # @param [Key, String] key_or_kind A Key object or `kind` string value.
        #
        # @return [Google::Cloud::Datastore::Entity, nil]
        #
        # @example
        #   require "google/cloud/datastore"
        #
        #   datastore = Google::Cloud::Datastore.new
        #
        #   task_list_key = datastore.key "TaskList", "default"
        #
        #   datastore.read_only_transaction do |tx|
        #     task_list = tx.find task_list_key
        #   end
        #
        def find key_or_kind, id_or_name = nil
          key = key_or_kind
          unless key.is_a? Google::Cloud::Datastore::Key
            key = Key.new key_or_kind, id_or_name
          end
          find_all(key).first
        end
        alias get find

        ##
        # Retrieve the entities for the provided keys. The lookup is run within
        # the transaction.
        #
        # @param [Key] keys One or more Key objects to find records for.
        #
        # @return [Google::Cloud::Datastore::Dataset::LookupResults]
        #
        # @example
        #   require "google/cloud/datastore"
        #
        #   datastore = Google::Cloud::Datastore.new
        #
        #   task_key1 = datastore.key "Task", 123456
        #   task_key2 = datastore.key "Task", 987654
        #
        #   datastore.read_only_transaction do |tx|
        #     tasks = tx.find_all task_key1, task_key2
        #   end
        #
        def find_all *keys
          ensure_service!
          lookup_res = service.lookup(*Array(keys).flatten.map(&:to_grpc),
                                      transaction: @id)
          Dataset::LookupResults.from_grpc lookup_res, service, nil, @id
        end
        alias lookup find_all

        ##
        # Retrieve entities specified by a Query. The query is run within the
        # transaction.
        #
        # @param [Query, GqlQuery] query The query with the search criteria.
        # @param [String] namespace The namespace the query is to run within.
        # @param [Hash, Google::Cloud::Datastore::V1::ExplainOptions] explain_options
        #   The options for query explanation. See {Google::Cloud::Datastore::V1::ExplainOptions}
        #   for details. Optional.
        #
        # @return [Google::Cloud::Datastore::Dataset::QueryResults]
        #
        # @example
        #   require "google/cloud/datastore"
        #
        #   datastore = Google::Cloud::Datastore.new
        #
        #   datastore.read_only_transaction do |tx|
        #     query = tx.query("Task").
        #       where("done", "=", false)
        #     tasks = tx.run query
        #   end
        #
        # @example Run the query with explain options:
        #   require "google/cloud/datastore"
        #
        #   datastore = Google::Cloud::Datastore.new
        #
        #   datastore.read_only_transaction do |tx|
        #     query = tx.query("Task")
        #     results = tx.run query, explain_options: { analyze: true }
        #
        #     # You must iterate through all pages of results to get the metrics.
        #     loop do
        #       break unless results.next?
        #       results = results.next
        #     end
        #
        #     if results.explain_metrics
        #       stats = results.explain_metrics.execution_stats
        #       puts "Read operations: #{stats.read_operations}"
        #     end
        #   end
        #
        # @example Run the query with explain options using a `Google::Cloud::Datastore::V1::ExplainOptions` object.
        #   require "google/cloud/datastore"
        #
        #   datastore = Google::Cloud::Datastore.new
        #
        #   datastore.read_only_transaction do |tx|
        #     query = tx.query("Task")
        #     explain_options = Google::Cloud::Datastore::V1::ExplainOptions.new
        #     results = tx.run query, explain_options: explain_options
        #
        #     # You must iterate through all pages of results to get the metrics.
        #     loop do
        #       break unless results.next?
        #       results = results.next
        #     end
        #
        #     if results.explain_metrics
        #       stats = results.explain_metrics.execution_stats
        #       puts "Read operations: #{stats.read_operations}"
        #     end
        #   end
        #
        def run query, namespace: nil, explain_options: nil
          ensure_service!
          unless query.is_a?(Query) || query.is_a?(GqlQuery)
            raise ArgumentError, "Cannot run a #{query.class} object."
          end
          query_res = service.run_query query.to_grpc, namespace,
                                        explain_options: explain_options,
                                        transaction: @id

          Dataset::QueryResults.from_grpc query_res, service, namespace,
                                          query.to_grpc.dup, nil, explain_options
        end
        alias run_query run

        ##
        # Retrieve aggregate query results specified by an AggregateQuery. The query is run within the
        # transaction.
        #
        # @param [AggregateQuery, GqlQuery] aggregate_query The Query object
        #   with the search criteria.
        # @param [String] namespace The namespace the query is to run within.
        #
        # @return [Google::Cloud::Datastore::Dataset::AggregateQueryResults]
        #
        # @example
        #   require "google/cloud/datastore"
        #
        #   datastore = Google::Cloud::Datastore.new
        #
        #   datastore.read_only_transaction do |tx|
        #     query = tx.query("Task")
        #               .where("done", "=", false)
        #     aggregate_query = query.aggregate_query
        #                            .add_count
        #     res = tx.run_aggregation aggregate_query
        #   end
        # @example Run the aggregate query with explain options:
        #   require "google/cloud/datastore"
        #
        #   datastore = Google::Cloud::Datastore.new
        #
        #   datastore.read_only_transaction do |tx|
        #     query = tx.query("Task")
        #     aggregate_query = query.aggregate_query.add_count aggregate_alias: "total"
        #     results = tx.run_aggregation aggregate_query, explain_options: { analyze: true }
        #
        #     if results.explain_metrics
        #       stats = results.explain_metrics.execution_stats
        #       puts "Read operations: #{stats.read_operations}"
        #     end
        #   end
        #
        def run_aggregation aggregate_query, namespace: nil, explain_options: nil
          ensure_service!
          unless aggregate_query.is_a?(AggregateQuery) || aggregate_query.is_a?(GqlQuery)
            raise ArgumentError, "Cannot run a #{aggregate_query.class} object."
          end

          aggregate_query_results = service.run_aggregation_query aggregate_query.to_grpc,
                                                                  namespace,
                                                                  transaction: @id,
                                                                  explain_options: explain_options

          Dataset::AggregateQueryResults.from_grpc aggregate_query_results, explain_options
        end

        ##
        # Begins a transaction.
        # This method is run when a new ReadOnlyTransaction is created.
        #
        def start
          raise TransactionError, "Transaction already opened." unless @id.nil?
          ensure_service!
          tx_res = service.begin_transaction read_only: true, read_time: @read_time
          @id = tx_res.transaction
        end
        alias begin_transaction start

        ##
        # Commits the transaction.
        #
        # @example
        #   require "google/cloud/datastore"
        #
        #   datastore = Google::Cloud::Datastore.new
        #
        #   task_list_key = datastore.key "TaskList", "default"
        #
        #   tx = datastore.transaction
        #   task_list = tx.find task_list_key
        #   if task_list
        #     query = tx.query("Task").
        #       ancestor(task_list_key)
        #     tasks = tx.run query
        #   end
        #   tx.commit
        #
        def commit
          if @id.nil?
            raise TransactionError, "Cannot commit when not in a transaction."
          end

          ensure_service!

          service.commit [], transaction: @id
          true
        end

        ##
        # Rolls back the transaction.
        #
        # @example
        #   require "google/cloud/datastore"
        #
        #   datastore = Google::Cloud::Datastore.new
        #
        #   task_list_key = datastore.key "TaskList", "default"
        #
        #   tx = datastore.transaction
        #   task_list = tx.find task_list_key
        #   if task_list
        #     query = tx.query("Task").
        #       ancestor(task_list_key)
        #     tasks = tx.run query
        #   end
        #   tx.rollback
        #
        def rollback
          if @id.nil?
            raise TransactionError, "Cannot rollback when not in a transaction."
          end

          ensure_service!
          service.rollback @id
          true
        end

        ##
        # Reset the transaction.
        # {ReadOnlyTransaction#start} must be called afterwards.
        def reset!
          @id = nil
        end

        ##
        # Create a new Query instance. This is a convenience method to make the
        # creation of Query objects easier.
        #
        # @param [String] kinds The kind of entities to query. This is optional.
        #
        # @return [Google::Cloud::Datastore::Query]
        #
        # @example
        #   require "google/cloud/datastore"
        #
        #   datastore = Google::Cloud::Datastore.new
        #
        #   datastore.read_only_transaction do |tx|
        #     query = tx.query("Task").
        #       where("done", "=", false)
        #     tasks = tx.run query
        #   end
        #
        def query *kinds
          query = Query.new
          query.kind(*kinds) unless kinds.empty?
          query
        end

        ##
        # Create a new GqlQuery instance. This is a convenience method to make
        # the creation of GqlQuery objects easier.
        #
        # @param [String] query The GQL query string.
        # @param [Hash] bindings Named bindings for the GQL query string, each
        #   key must match regex `[A-Za-z_$][A-Za-z_$0-9]*`, must not match
        #   regex `__.*__`, and must not be `""`. The value must be an `Object`
        #   that can be stored as an Entity property value, or a `Cursor`.
        #
        # @return [Google::Cloud::Datastore::GqlQuery]
        #
        # @example
        #   require "google/cloud/datastore"
        #
        #   datastore = Google::Cloud::Datastore.new
        #
        #   datastore.read_only_transaction do |tx|
        #     gql_query = tx.gql "SELECT * FROM Task WHERE done = @done",
        #                        done: false
        #     tasks = tx.run gql_query
        #   end
        #
        # @example The previous example is equivalent to:
        #   require "google/cloud/datastore"
        #
        #   datastore = Google::Cloud::Datastore.new
        #
        #   datastore.read_only_transaction do |tx|
        #     gql_query = Google::Cloud::Datastore::GqlQuery.new
        #     gql_query.query_string = "SELECT * FROM Task WHERE done = @done"
        #     gql_query.named_bindings = {done: false}
        #     tasks = tx.run gql_query
        #   end
        #
        def gql query, bindings = {}
          gql = GqlQuery.new
          gql.query_string = query
          gql.named_bindings = bindings unless bindings.empty?
          gql
        end

        ##
        # Create a new Key instance. This is a convenience method to make the
        # creation of Key objects easier.
        #
        # @param [Array<Array(String,(String|Integer|nil))>] path An optional
        #   list of pairs for the key's path. Each pair may include the key's
        #   kind (String) and an id (Integer) or name (String). This is
        #   optional.
        # @param [String] project The project of the Key. This is optional.
        # @param [String] namespace namespace kind of the Key. This is optional.
        #
        # @return [Google::Cloud::Datastore::Key]
        #
        # @example
        #   require "google/cloud/datastore"
        #
        #   datastore = Google::Cloud::Datastore.new
        #
        #   datastore.read_only_transaction do |tx|
        #     task_key = tx.key "Task", "sampleTask"
        #   end
        #
        # @example The previous example is equivalent to:
        #   require "google/cloud/datastore"
        #
        #   datastore = Google::Cloud::Datastore.new
        #
        #   datastore.read_only_transaction do |tx|
        #     task_key = Google::Cloud::Datastore::Key.new "Task", "sampleTask"
        #   end
        #
        # @example Create a key with a parent:
        #   require "google/cloud/datastore"
        #
        #   datastore = Google::Cloud::Datastore.new
        #
        #   datastore.read_only_transaction do |tx|
        #     key = tx.key [["TaskList", "default"], ["Task", "sampleTask"]]
        #     results = tx.find_all key
        #   end
        #
        # @example Create a key with multi-level ancestry:
        #   require "google/cloud/datastore"
        #
        #   datastore = Google::Cloud::Datastore.new
        #
        #   datastore.read_only_transaction do |tx|
        #     key = tx.key([
        #       ["User", "alice"],
        #       ["TaskList", "default"],
        #       ["Task", "sampleTask"]
        #     ])
        #     results = tx.find_all key
        #   end
        #
        # @example Create a key with a project and namespace:
        #   require "google/cloud/datastore"
        #
        #   datastore = Google::Cloud::Datastore.new
        #
        #   datastore.read_only_transaction do |tx|
        #     key = tx.key ["TaskList", "default"], ["Task", "sampleTask"],
        #                  project: "my-todo-project",
        #                  namespace: "example-ns"
        #     results = tx.find_all key
        #   end
        #
        def key *path, project: nil, namespace: nil
          path = path.flatten.each_slice(2).to_a # group in pairs
          kind, id_or_name = path.pop
          Key.new(kind, id_or_name).tap do |k|
            k.project = project
            k.namespace = namespace
            unless path.empty?
              k.parent = key path, project: project, namespace: namespace
            end
          end
        end

        protected

        ##
        # @private Raise an error unless an active connection to the service is
        # available.
        def ensure_service!
          raise "Must have active connection to service" unless service
        end
      end
    end
  end
end
