# frozen_string_literal: true

# Copyright 2018 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/bigtable/mutation_entry"
require "google/cloud/bigtable/row"
require "google/cloud/bigtable/rows_mutator"
require "google/cloud/bigtable/read_modify_write_rule"
require "google/cloud/bigtable/status"

module Google
  module Cloud
    module Bigtable
      ##
      # # MutationOperations
      #
      # Collection of mutations APIs.
      #
      #   * Mutate single row
      #   * Mutate multiple rows
      #   * Read modify and write row atomically on the server
      #   * Check and mutate row
      #
      module MutationOperations
        ##
        # Mutates a row atomically. Cells in the row are left
        # unchanged unless explicitly changed by the mutations.
        # Changes to be atomically applied to the specified row. Entries are applied
        # in order, meaning that earlier mutations can be masked by later mutations.
        # Must contain at least one mutation and at most 100,000.
        #
        # @param entry [Google::Cloud::Bigtable::MutationEntry]
        #   Mutation entry with row key and list of mutations.
        # @return [Boolean]
        # @example Single mutation on row.
        #   require "google/cloud/bigtable"
        #
        #   bigtable = Google::Cloud::Bigtable.new
        #
        #   table = bigtable.table "my-instance", "my-table"
        #
        #   entry = table.new_mutation_entry "user-1"
        #   entry.set_cell "cf1", "field1", "XYZ"
        #   table.mutate_row entry
        #
        # @example Multiple mutations on row.
        #   require "google/cloud/bigtable"
        #
        #   bigtable = Google::Cloud::Bigtable.new
        #
        #   table = bigtable.table "my-instance", "my-table"
        #
        #   entry = table.new_mutation_entry "user-1"
        #   entry.set_cell(
        #     "cf1",
        #     "field1",
        #     "XYZ",
        #     timestamp: (Time.now.to_f * 1_000_000).round(-3) # microseconds
        #   ).delete_cells "cf2", "field02"
        #
        #   table.mutate_row entry
        #
        def mutate_row entry
          service.mutate_row path, entry.row_key, entry.mutations, app_profile_id: @app_profile_id
          true
        end

        ##
        # Mutates multiple rows in a batch. Each individual row is mutated
        # atomically as in #{mutate_row}, but the entire batch is not executed
        # atomically.
        #
        # @param entries [Array<Google::Cloud::Bigtable::MutationEntry>]
        #   The row keys and corresponding mutations to be applied in bulk.
        #   Each entry is applied as an atomic mutation, but the entries may be
        #   applied in arbitrary order (even between entries for the same row).
        #   At least one entry must be specified, and in total the entries can
        #   contain a maximum of 100,000 mutations.
        # @return [Array<Google::Cloud::Bigtable::V2::MutateRowsResponse::Entry>]
        #
        # @example
        #   require "google/cloud/bigtable"
        #
        #   bigtable = Google::Cloud::Bigtable.new
        #
        #   table = bigtable.table "my-instance", "my-table"
        #
        #   entries = []
        #   entries << table.new_mutation_entry("row-1").set_cell("cf1", "field1", "XYZ")
        #   entries << table.new_mutation_entry("row-2").set_cell("cf1", "field1", "ABC")
        #   responses = table.mutate_rows entries
        #
        #   responses.each do |response|
        #     puts response.status.description
        #   end
        #
        def mutate_rows entries
          statuses = RowsMutator.new(self, entries).apply_mutations
          statuses.map { |s| Response.from_grpc s }
        end

        ##
        # Modifies a row atomically on the server. The method reads the latest
        # existing timestamp and value from the specified columns and writes a new
        # entry based on pre-defined read/modify/write rules. The new value for the
        # timestamp is the greater of the existing timestamp or the current server
        # time. The method returns the new contents of all modified cells.
        #
        # @param key [String]
        #   The row key of the row to which the read/modify/write rules should be applied.
        # @param rules [Google::Cloud::Bigtable::ReadModifyWriteRule,
        #   Array<Google::Cloud::Bigtable::ReadModifyWriteRule>]
        #   Rules specifying how the specified row's contents are to be transformed
        #   into writes. Entries are applied in order, meaning that earlier rules will
        #   affect the results of later ones.
        # @return [Google::Cloud::Bigtable::Row]
        # @example Apply multiple modification rules.
        #   require "google/cloud/bigtable"
        #
        #   bigtable = Google::Cloud::Bigtable.new
        #   table = bigtable.table "my-instance", "my-table"
        #
        #   rule_1 = table.new_read_modify_write_rule "cf", "field01"
        #   rule_1.append "append-xyz"
        #
        #   rule_2 = table.new_read_modify_write_rule "cf", "field01"
        #   rule_2.increment 1
        #
        #   row = table.read_modify_write_row "user01", [rule_1, rule_2]
        #
        #   puts row.cells
        #
        # @example Apply single modification rules.
        #   require "google/cloud/bigtable"
        #
        #   bigtable = Google::Cloud::Bigtable.new
        #   table = bigtable.table "my-instance", "my-table"
        #
        #   rule = table.new_read_modify_write_rule("cf", "field01").append("append-xyz")
        #
        #   row = table.read_modify_write_row "user01", rule
        #
        #   puts row.cells
        #
        def read_modify_write_row key, rules
          res_row = service.read_modify_write_row(
            path,
            key,
            Array(rules).map(&:to_grpc),
            app_profile_id: @app_profile_id
          ).row
          row = Row.new res_row.key

          res_row.families.each do |family|
            family.columns.each do |column|
              column.cells.each do |cell|
                row_cell = Row::Cell.new(
                  family.name,
                  column.qualifier,
                  cell.timestamp_micros,
                  cell.value,
                  cell.labels
                )
                row.cells[family.name] << row_cell
              end
            end
          end
          row
        end

        ##
        # Mutates a row atomically based on the output of a predicate reader filter.
        #
        # NOTE: Condition predicate filter is not supported.
        #
        # @param key [String] Row key.
        #   The row key of the row to which the conditional mutation should be applied.
        # @param predicate [SimpleFilter, ChainFilter, InterleaveFilter] Predicate filter.
        #   The filter to be applied to the contents of the specified row. Depending
        #   on whether or not any results are yielded, either +true_mutations+ or
        #   +false_mutations+ will be executed. If unset, checks that the row contains
        #   any values.
        # @param on_match [Google::Cloud::Bigtable::MutationEntry] Mutation entry
        #   applied to predicate filter match.
        #   Changes to be atomically applied to the specified row if +predicate_filter+
        #   yields at least one cell when applied to +row_key+. Entries are applied in
        #   order, meaning that earlier mutations can be masked by later ones.
        #   Must contain at least one entry if +false_mutations+ is empty and at most
        #   100,000 entries.
        # @param otherwise [Google::Cloud::Bigtable::MutationEntry] Mutation entry applied
        #   when predicate filter does not match.
        #   Changes to be atomically applied to the specified row if +predicate_filter+
        #   does not yield any cells when applied to +row_key+. Entries are applied in
        #   order, meaning that earlier mutations can be masked by later ones.
        #   Must contain at least one entry if +true_mutations+ is empty and at most
        #   100,000 entries.
        # @return [Boolean]
        #   Predicate match or not status.
        #
        # @example
        #   require "google/cloud/bigtable"
        #
        #   bigtable = Google::Cloud::Bigtable.new
        #   table = bigtable.table "my-instance", "my-table"
        #
        #   predicate_filter = Google::Cloud::Bigtable::RowFilter.key "user-10"
        #   on_match_mutations = Google::Cloud::Bigtable::MutationEntry.new
        #   on_match_mutations.set_cell(
        #     "cf1",
        #     "field1",
        #     "XYZ",
        #     timestamp: (Time.now.to_f * 1_000_000).round(-3) # microseconds
        #   ).delete_cells "cf2", "field02"
        #
        #   otherwise_mutations = Google::Cloud::Bigtable::MutationEntry.new
        #   otherwise_mutations.delete_from_family "cf3"
        #
        #   predicate_matched = table.check_and_mutate_row(
        #     "user01",
        #     predicate_filter,
        #     on_match: on_match_mutations,
        #     otherwise: otherwise_mutations
        #   )
        #
        #   if predicate_matched
        #     puts "All predicates matched"
        #   end
        #
        def check_and_mutate_row key, predicate, on_match: nil, otherwise: nil
          true_mutations = on_match.mutations if on_match
          false_mutations = otherwise.mutations if otherwise
          response = service.check_and_mutate_row(
            path,
            key,
            predicate_filter: predicate&.to_grpc,
            true_mutations:   true_mutations,
            false_mutations:  false_mutations,
            app_profile_id:   @app_profile_id
          )
          response.predicate_matched
        end

        ##
        # Creates a mutation entry instance.
        #
        # @param row_key [String] Row key. Optional.
        #   The row key of the row to which the mutation should be applied.
        # @return [Google::Cloud::Bigtable::MutationEntry]
        #
        # @example
        #   require "google/cloud/bigtable"
        #
        #   bigtable = Google::Cloud::Bigtable.new
        #   table = bigtable.table "my-instance", "my-table"
        #
        #   entry = table.new_mutation_entry "row-key-1"
        #
        #   # Without row key
        #   entry = table.new_mutation_entry
        #
        def new_mutation_entry row_key = nil
          Google::Cloud::Bigtable::MutationEntry.new row_key
        end

        ##
        # Create a read/modify/write rule to append or increment the value
        # of the cell qualifier.
        #
        # @param family [String]
        #   The name of the column family to which the read/modify/write rule should be applied.
        # @param qualifier [String]
        #   The qualifier of the column to which the read/modify/write rule should be applied.
        # @return [Google::Cloud::Bigtable::ReadModifyWriteRule]
        #
        # @example Create rule to append to qualifier value.
        #   require "google/cloud/bigtable"
        #
        #   bigtable = Google::Cloud::Bigtable.new
        #   table = bigtable.table "my-instance", "my-table"
        #   rule = table.new_read_modify_write_rule "cf", "qualifier-1"
        #   rule.append "append-xyz"
        #
        # @example Create rule to increment qualifier value.
        #   require "google/cloud/bigtable"
        #
        #   bigtable = Google::Cloud::Bigtable.new
        #   table = bigtable.table "my-instance", "my-table"
        #   rule = table.new_read_modify_write_rule "cf", "qualifier-1"
        #   rule.increment 100
        #
        def new_read_modify_write_rule family, qualifier
          Google::Cloud::Bigtable::ReadModifyWriteRule.new family, qualifier
        end

        ##
        # # MutationEntry::Response
        #
        # Represents a response message from BigtableService.MutateRows.
        #
        # @attr [Integer] index The index into the original request's `entries`
        #   list of the Entry for which a result is being reported.
        # @attr [Google::Cloud::Bigtable::Status] The result of the request
        #   Entry identified by `index`. Depending on how requests are batched
        #   during execution, it is possible for one Entry to fail due to an
        #   error with another Entry. In the event that this occurs, the same
        #   error will be reported for both entries.
        #
        # @example
        #   require "google/cloud/bigtable"
        #
        #   bigtable = Google::Cloud::Bigtable.new
        #
        #   table = bigtable.table "my-instance", "my-table"
        #
        #   entries = []
        #   entries << table.new_mutation_entry("row-1").set_cell("cf1", "field1", "XYZ")
        #   entries << table.new_mutation_entry("row-2").set_cell("cf1", "field1", "ABC")
        #   responses = table.mutate_rows entries
        #
        #   responses.each do |response|
        #     puts response.status.description
        #   end
        #
        class Response
          attr_reader :index
          attr_reader :status

          ##
          # @private Creates a MutationEntry::Response object.
          def initialize index, status
            @index = index
            @status = status
          end

          ##
          # @private New MutationEntry::Response from a
          # Google::Cloud::Bigtable::V2::MutateRowsResponse::Entry object.
          def self.from_grpc grpc
            new grpc.index, Status.from_grpc(grpc.status)
          end
        end
      end
    end
  end
end
