# 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

        ##
        # @private Creates a new ReadOnlyTransaction instance.
        # Takes a Service instead of project and Credentials.
        #
        def initialize service
          @service = service
          reset!
          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] query The Query object with the search criteria.
        # @param [String] namespace The namespace the query is to run within.
        #
        # @return [Google::Cloud::Datastore::Dataset::QueryResults]
        #
        # @example
        #   require "google/cloud/datastore"
        #
        #   datastore = Google::Cloud::Datastore.new
        #
        #   query = datastore.query("Task").
        #     where("done", "=", false)
        #   datastore.read_only_transaction do |tx|
        #     tasks = tx.run query
        #   end
        #
        def run query, namespace: 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,
                                        transaction: @id
          Dataset::QueryResults.from_grpc query_res, service, namespace,
                                          query.to_grpc.dup
        end
        alias run_query run

        ##
        # 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
          @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"
        #   query = datastore.query("Task").
        #     ancestor(task_list_key)
        #
        #   tx = datastore.transaction
        #   task_list = tx.find task_list_key
        #   if task_list
        #     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"
        #   query = datastore.query("Task").
        #     ancestor(task_list_key)
        #
        #   tx = datastore.transaction
        #   task_list = tx.find task_list_key
        #   if task_list
        #     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

        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
