/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */
package org.apache.blur.manager.writer;

import static org.apache.blur.metrics.MetricsConstants.BLUR;
import static org.apache.blur.metrics.MetricsConstants.ORG_APACHE_BLUR;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;

import org.apache.blur.analysis.FieldManager;
import org.apache.blur.lucene.search.IndexSearcherCloseable;
import org.apache.blur.server.ShardContext;
import org.apache.blur.server.TableContext;
import org.apache.blur.thrift.BException;
import org.apache.blur.thrift.MutationHelper;
import org.apache.blur.thrift.generated.BlurException;
import org.apache.blur.thrift.generated.Column;
import org.apache.blur.thrift.generated.FetchRecordResult;
import org.apache.blur.thrift.generated.Record;
import org.apache.blur.thrift.generated.RecordMutation;
import org.apache.blur.thrift.generated.RecordMutationType;
import org.apache.blur.thrift.generated.Row;
import org.apache.blur.thrift.generated.RowMutation;
import org.apache.blur.thrift.generated.RowMutationType;
import org.apache.blur.utils.BlurConstants;
import org.apache.blur.utils.RowDocumentUtil;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.StringField;
import org.apache.lucene.index.AtomicReader;
import org.apache.lucene.index.AtomicReaderContext;
import org.apache.lucene.index.DocsEnum;
import org.apache.lucene.index.Fields;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.Terms;
import org.apache.lucene.index.TermsEnum;
import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.util.BytesRef;

import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import com.yammer.metrics.core.MetricName;

public class MutatableAction extends IndexAction {

  private static final Meter _writeRecordsMeter;
  private static final Meter _writeRowMeter;

  static {
    MetricName metricName1 = new MetricName(ORG_APACHE_BLUR, BLUR, "Write Records/s");
    MetricName metricName2 = new MetricName(ORG_APACHE_BLUR, BLUR, "Write Row/s");
    _writeRecordsMeter = Metrics.newMeter(metricName1, "Records/s", TimeUnit.SECONDS);
    _writeRowMeter = Metrics.newMeter(metricName2, "Row/s", TimeUnit.SECONDS);
  }

  static abstract class BaseRecordMutatorIterator implements Iterable<Record> {

    private final Iterable<Record> _iterable;
    private final Map<String, Record> _records;

    public BaseRecordMutatorIterator(Iterable<Record> iterable, List<Record> records) {
      _iterable = iterable;
      _records = new TreeMap<String, Record>();
      for (Record r : records) {
        _records.put(r.getRecordId(), r);
      }
    }

    protected abstract Record handleRecordMutate(Record existingRecord, Record newRecord);

    @Override
    public Iterator<Record> iterator() {
      final Iterator<Record> iterator = _iterable.iterator();
      return new Iterator<Record>() {

        private SortedSet<String> _needToBeApplied = new TreeSet<String>(_records.keySet());
        private boolean _append = false;

        @Override
        public boolean hasNext() {
          boolean hasNext = iterator.hasNext();
          if (hasNext) {
            return true;
          }
          if (areAllApplied()) {
            return false; // Already applied changes, finished.
          }
          _append = true;
          return true; // Still need to add new records.
        }

        private boolean areAllApplied() {
          return _needToBeApplied.size() == 0;
        }

        @Override
        public Record next() {
          if (_append) {
            String first = _needToBeApplied.first();
            _needToBeApplied.remove(first);
            return _records.get(first);
          }
          Record record = iterator.next();
          String recordId = record.getRecordId();
          Record newRecord = _records.get(recordId);
          if (newRecord != null) {
            record = handleRecordMutate(record, newRecord);
            _needToBeApplied.remove(recordId);
          }
          return record;
        }

        @Override
        public void remove() {
          throw new RuntimeException("Not Supported.");
        }

      };
    }
  }

  static class UpdateRow extends InternalAction {

    static abstract class UpdateRowAction {
      abstract IterableRow performAction(IterableRow row);
    }

    private final List<UpdateRowAction> _actions = new ArrayList<UpdateRowAction>();

    private UpdateRowAction _deleteRecordsAction;
    private final Set<String> _deleteRecordsActionRecordsIdToDelete = new HashSet<String>();
    private UpdateRowAction _appendColumnsAction;
    private List<Record> _appendColumnsActionRecords = new ArrayList<Record>();
    private UpdateRowAction _replaceColumnsAction;
    private List<Record> _replaceColumnsActionRecords = new ArrayList<Record>();
    private UpdateRowAction _replaceRecordAction;
    private List<Record> _replaceRecordActionRecords = new ArrayList<Record>();

    private final String _rowId;
    private final TableContext _tableContext;
    private final FieldManager _fieldManager;

    UpdateRow(String rowId, TableContext tableContext) {
      _rowId = rowId;
      _tableContext = tableContext;
      _fieldManager = _tableContext.getFieldManager();
    }

    void deleteRecord(final String recordId) {
      if (_deleteRecordsAction == null) {
        _deleteRecordsAction = new UpdateRowAction() {
          @Override
          IterableRow performAction(IterableRow row) {
            if (row == null) {
              return null;
            } else {
              return new IterableRow(row.getRowId(), new DeleteRecordIterator(row,
                  _deleteRecordsActionRecordsIdToDelete));
            }
          }
        };
        _actions.add(_deleteRecordsAction);
      }
      _deleteRecordsActionRecordsIdToDelete.add(recordId);
    }

    static class DeleteRecordIterator implements Iterable<Record> {

      private final Set<String> _recordsIdToDelete;
      private final Iterable<Record> _iterable;

      public DeleteRecordIterator(Iterable<Record> iterable, Set<String> recordsIdToDelete) {
        _recordsIdToDelete = recordsIdToDelete;
        _iterable = iterable;
      }

      @Override
      public Iterator<Record> iterator() {
        final GenericPeekableIterator<Record> iterator = GenericPeekableIterator.wrap(_iterable.iterator());
        return new Iterator<Record>() {

          @Override
          public boolean hasNext() {
            Record record = iterator.peek();
            if (record == null) {
              return false;
            }
            if (_recordsIdToDelete.contains(record.getRecordId())) {
              iterator.next();// Eat the delete
              return hasNext();// Move to the next record
            }
            return iterator.hasNext();
          }

          @Override
          public Record next() {
            return iterator.next();
          }

          @Override
          public void remove() {
            throw new RuntimeException("Not Supported.");
          }
        };
      }

    }

    void appendColumns(final Record record) {
      if (_appendColumnsAction == null) {
        _appendColumnsAction = new UpdateRowAction() {
          @Override
          IterableRow performAction(IterableRow row) {
            if (row == null) {
              return new IterableRow(_rowId, _appendColumnsActionRecords);
            } else {
              return new IterableRow(row.getRowId(), new AppendColumnsIterator(row, _appendColumnsActionRecords));
            }
          }
        };
        _actions.add(_appendColumnsAction);
      }
      _appendColumnsActionRecords.add(record);
    }

    static class AppendColumnsIterator extends BaseRecordMutatorIterator {

      public AppendColumnsIterator(Iterable<Record> iterable, List<Record> records) {
        super(iterable, records);
      }

      @Override
      protected Record handleRecordMutate(Record existingRecord, Record newRecord) {
        for (Column column : newRecord.getColumns()) {
          existingRecord.addToColumns(column);
        }
        return existingRecord;
      }

    }

    void replaceColumns(final Record record) {
      if (_replaceColumnsAction == null) {
        _replaceColumnsAction = new UpdateRowAction() {
          @Override
          IterableRow performAction(IterableRow row) {
            if (row == null) {
              return new IterableRow(_rowId, _replaceColumnsActionRecords);
            } else {
              return new IterableRow(row.getRowId(), new ReplaceColumnsIterator(row, _replaceColumnsActionRecords));
            }
          }
        };
        _actions.add(_replaceColumnsAction);
      }
      _replaceColumnsActionRecords.add(record);
    }

    static class ReplaceColumnsIterator extends BaseRecordMutatorIterator {

      public ReplaceColumnsIterator(Iterable<Record> iterable, List<Record> records) {
        super(iterable, records);
      }

      @Override
      protected Record handleRecordMutate(Record existingRecord, Record newRecord) {
        return replaceColumns(existingRecord, newRecord);
      }

    }

    protected static Record replaceColumns(Record existing, Record newRecord) {
      Map<String, List<Column>> existingColumns = getColumnMap(existing.getColumns());
      Map<String, List<Column>> newColumns = getColumnMap(newRecord.getColumns());
      existingColumns.putAll(newColumns);
      Record record = new Record();
      record.setFamily(existing.getFamily());
      record.setRecordId(existing.getRecordId());
      record.setColumns(toList(existingColumns.values()));
      return record;
    }

    private static List<Column> toList(Collection<List<Column>> values) {
      ArrayList<Column> list = new ArrayList<Column>();
      for (List<Column> v : values) {
        list.addAll(v);
      }
      return list;
    }

    private static Map<String, List<Column>> getColumnMap(List<Column> columns) {
      Map<String, List<Column>> columnMap = new TreeMap<String, List<Column>>();
      for (Column column : columns) {
        String name = column.getName();
        List<Column> list = columnMap.get(name);
        if (list == null) {
          list = new ArrayList<Column>();
          columnMap.put(name, list);
        }
        list.add(column);
      }
      return columnMap;
    }

    void replaceRecord(final Record record) {
      if (_replaceRecordAction == null) {
        _replaceRecordAction = new UpdateRowAction() {
          @Override
          IterableRow performAction(IterableRow row) {
            if (row == null) {
              // New Row
              return new IterableRow(_rowId, _replaceRecordActionRecords);
            } else {
              // Existing Row
              return new IterableRow(row.getRowId(), new ReplaceRecordIterator(row, _replaceRecordActionRecords));
            }
          }
        };
        _actions.add(_replaceRecordAction);
      }
      _replaceRecordActionRecords.add(record);
    }

    static class ReplaceRecordIterator extends BaseRecordMutatorIterator {

      public ReplaceRecordIterator(Iterable<Record> iterable, List<Record> records) {
        super(iterable, records);
      }

      @Override
      protected Record handleRecordMutate(Record existingRecord, Record newRecord) {
        return newRecord;
      }

    }

    @Override
    void performAction(IndexSearcherCloseable searcher, IndexWriter writer) throws IOException {
      IterableRow iterableRow = getIterableRow(_rowId, searcher);
      for (UpdateRowAction action : _actions) {
        iterableRow = action.performAction(iterableRow);
      }
      Term term = createRowId(_rowId);
      if (iterableRow != null) {
        RecordToDocumentIterable docsToUpdate = new RecordToDocumentIterable(iterableRow, _fieldManager);
        Iterator<Iterable<Field>> iterator = docsToUpdate.iterator();
        final GenericPeekableIterator<Iterable<Field>> gpi = GenericPeekableIterator.wrap(iterator);
        if (gpi.peek() != null) {
          writer.updateDocuments(term, wrapPrimeDoc(new Iterable<Iterable<Field>>() {
            @Override
            public Iterator<Iterable<Field>> iterator() {
              return gpi;
            }
          }));
        } else {
          writer.deleteDocuments(term);
        }
        _writeRecordsMeter.mark(docsToUpdate.count());
      }
      _writeRowMeter.mark();
    }

    private static class AtomicReaderTermsEnum {
      AtomicReader _atomicReader;
      TermsEnum _termsEnum;

      AtomicReaderTermsEnum(AtomicReader atomicReader, TermsEnum termsEnum) {
        _atomicReader = atomicReader;
        _termsEnum = termsEnum;
      }
    }

    private IterableRow getIterableRow(String rowId, IndexSearcherCloseable searcher) throws IOException {
      IndexReader indexReader = searcher.getIndexReader();
      BytesRef rowIdRef = new BytesRef(rowId);
      List<AtomicReaderTermsEnum> possibleRowIds = new ArrayList<AtomicReaderTermsEnum>();
      for (AtomicReaderContext atomicReaderContext : indexReader.leaves()) {
        AtomicReader atomicReader = atomicReaderContext.reader();
        Fields fields = atomicReader.fields();
        if (fields == null) {
          continue;
        }
        Terms terms = fields.terms(BlurConstants.ROW_ID);
        if (terms == null) {
          continue;
        }
        TermsEnum termsEnum = terms.iterator(null);
        if (!termsEnum.seekExact(rowIdRef, true)) {
          continue;
        }
        // need atomic read as well...
        possibleRowIds.add(new AtomicReaderTermsEnum(atomicReader, termsEnum));
      }
      if (possibleRowIds.isEmpty()) {
        return null;
      }
      return new IterableRow(rowId, getRecords(possibleRowIds));
    }

    private Iterable<Record> getRecords(final List<AtomicReaderTermsEnum> possibleRowIds) {
      return new Iterable<Record>() {
        @Override
        public Iterator<Record> iterator() {
          final List<DocsEnum> docsEnums = new ArrayList<DocsEnum>();
          for (AtomicReaderTermsEnum atomicReaderTermsEnum : possibleRowIds) {
            try {
              docsEnums.add(atomicReaderTermsEnum._termsEnum.docs(atomicReaderTermsEnum._atomicReader.getLiveDocs(),
                  null));
            } catch (IOException e) {
              throw new RuntimeException(e);
            }
          }
          return new Iterator<Record>() {

            private int _index = 0;
            private boolean _nextCalled;
            private int _docId;

            @Override
            public boolean hasNext() {
              try {
                if (_nextCalled) {
                  if (_docId == DocIdSetIterator.NO_MORE_DOCS) {
                    return false;
                  }
                  return true;
                }
                while (true) {
                  if (_index >= docsEnums.size()) {
                    _nextCalled = true;
                    _docId = DocIdSetIterator.NO_MORE_DOCS;
                    return false;
                  }
                  DocsEnum docsEnum = docsEnums.get(_index);
                  int docId = docsEnum.nextDoc();
                  if (docId != DocIdSetIterator.NO_MORE_DOCS) {
                    _nextCalled = true;
                    _docId = docId;
                    return true;
                  }
                  _index++;
                }
              } catch (IOException e) {
                throw new RuntimeException(e);
              }
            }

            @Override
            public Record next() {
              _nextCalled = false;
              AtomicReaderTermsEnum atomicReaderTermsEnum = possibleRowIds.get(_index);
              try {
                Document document = atomicReaderTermsEnum._atomicReader.document(_docId);
                FetchRecordResult fetchRecordResult = RowDocumentUtil.getRecord(document);
                return fetchRecordResult.getRecord();
              } catch (IOException e) {
                throw new RuntimeException(e);
              }
            }

            @Override
            public void remove() {
              throw new RuntimeException("Not Supported.");
            }
          };
        }
      };
    }

    Iterable<Iterable<Field>> wrapPrimeDoc(final Iterable<Iterable<Field>> iterable) {
      return new Iterable<Iterable<Field>>() {

        @Override
        public Iterator<Iterable<Field>> iterator() {
          final Iterator<Iterable<Field>> iterator = iterable.iterator();
          return new Iterator<Iterable<Field>>() {

            private boolean _first = true;

            @Override
            public boolean hasNext() {
              return iterator.hasNext();
            }

            @Override
            public Iterable<Field> next() {
              Iterable<Field> fields = iterator.next();
              if (_first) {
                _first = false;
                return addPrimeDocField(fields);
              } else {
                return fields;
              }
            }

            private Iterable<Field> addPrimeDocField(Iterable<Field> fields) {
              return new IterablePlusOne<Field>(new StringField(BlurConstants.PRIME_DOC, BlurConstants.PRIME_DOC_VALUE,
                  Store.NO), fields);
            }

            @Override
            public void remove() {
              throw new RuntimeException("Not Supported.");
            }

          };
        }
      };
    }

  }

  static abstract class InternalAction {
    abstract void performAction(IndexSearcherCloseable searcher, IndexWriter writer) throws IOException;
  }

  private final List<InternalAction> _actions = new ArrayList<InternalAction>();
  private final Map<String, UpdateRow> _rowUpdates = new HashMap<String, UpdateRow>();
  private final FieldManager _fieldManager;
  private final TableContext _tableContext;

  public MutatableAction(ShardContext context) {
    _tableContext = context.getTableContext();
    _fieldManager = _tableContext.getFieldManager();
  }

  public void deleteRow(final String rowId) {
    _actions.add(new InternalAction() {
      @Override
      void performAction(IndexSearcherCloseable searcher, IndexWriter writer) throws IOException {
        writer.deleteDocuments(createRowId(rowId));
        _writeRowMeter.mark();
      }
    });
  }

  public void replaceRow(final Row row) {
    _actions.add(new InternalAction() {
      @Override
      void performAction(IndexSearcherCloseable searcher, IndexWriter writer) throws IOException {
        List<List<Field>> docs = RowDocumentUtil.getDocs(row, _fieldManager);
        Term rowId = createRowId(row.getId());
        writer.updateDocuments(rowId, docs);
        _writeRecordsMeter.mark(docs.size());
        _writeRowMeter.mark();
      }
    });
  }

  public void deleteRecord(final String rowId, final String recordId) {
    UpdateRow updateRow = getUpdateRow(rowId);
    updateRow.deleteRecord(recordId);
  }

  public void replaceRecord(final String rowId, final Record record) {
    UpdateRow updateRow = getUpdateRow(rowId);
    updateRow.replaceRecord(record);
  }

  public void appendColumns(final String rowId, final Record record) {
    UpdateRow updateRow = getUpdateRow(rowId);
    updateRow.appendColumns(record);
  }

  public void replaceColumns(final String rowId, final Record record) {
    UpdateRow updateRow = getUpdateRow(rowId);
    updateRow.replaceColumns(record);
  }

  @Override
  public void performMutate(IndexSearcherCloseable searcher, IndexWriter writer) throws IOException {
    try {
      for (InternalAction internalAction : _actions) {
        internalAction.performAction(searcher, writer);
      }
    } finally {
      _actions.clear();
    }
  }

  public static Term createRowId(String id) {
    return new Term(BlurConstants.ROW_ID, id);
  }

  public static Term createRecordId(String id) {
    return new Term(BlurConstants.RECORD_ID, id);
  }

  private synchronized UpdateRow getUpdateRow(String rowId) {
    UpdateRow updateRow = _rowUpdates.get(rowId);
    if (updateRow == null) {
      updateRow = new UpdateRow(rowId, _tableContext);
      _rowUpdates.put(rowId, updateRow);
      _actions.add(updateRow);
    }
    return updateRow;
  }

  @Override
  public void doPreCommit(IndexSearcherCloseable indexSearcher, IndexWriter writer) {

  }

  @Override
  public void doPostCommit(IndexWriter writer) {

  }

  @Override
  public void doPreRollback(IndexWriter writer) {

  }

  @Override
  public void doPostRollback(IndexWriter writer) {

  }

  public void mutate(RowMutation mutation) {
    RowMutationType type = mutation.rowMutationType;
    switch (type) {
    case REPLACE_ROW:
      Row row = MutationHelper.getRowFromMutations(mutation.rowId, mutation.recordMutations);
      replaceRow(row);
      break;
    case UPDATE_ROW:
      doUpdateRowMutation(mutation, this);
      break;
    case DELETE_ROW:
      deleteRow(mutation.rowId);
      break;
    default:
      throw new RuntimeException("Not supported [" + type + "]");
    }
  }

  private void doUpdateRowMutation(RowMutation mutation, MutatableAction mutatableAction) {
    String rowId = mutation.getRowId();
    for (RecordMutation recordMutation : mutation.getRecordMutations()) {
      RecordMutationType type = recordMutation.recordMutationType;
      Record record = recordMutation.getRecord();
      switch (type) {
      case DELETE_ENTIRE_RECORD:
        mutatableAction.deleteRecord(rowId, record.getRecordId());
        break;
      case APPEND_COLUMN_VALUES:
        mutatableAction.appendColumns(rowId, record);
        break;
      case REPLACE_ENTIRE_RECORD:
        mutatableAction.replaceRecord(rowId, record);
        break;
      case REPLACE_COLUMNS:
        mutatableAction.replaceColumns(rowId, record);
        break;
      default:
        throw new RuntimeException("Unsupported record mutation type [" + type + "]");
      }
    }
  }

  public void mutate(List<RowMutation> mutations) {
    for (int i = 0; i < mutations.size(); i++) {
      mutate(mutations.get(i));
    }
  }

  public static List<RowMutation> reduceMutates(List<RowMutation> mutations) throws BlurException {
    Map<String, RowMutation> mutateMap = new TreeMap<String, RowMutation>();
    for (RowMutation mutation : mutations) {
      if (mutation.getRowId() == null) {
        throw new BException("Mutation has null rowid [{0}]", mutation);
      }
      RowMutation rowMutation = mutateMap.get(mutation.getRowId());
      if (rowMutation != null) {
        mutateMap.put(mutation.getRowId(), merge(rowMutation, mutation));
      } else {
        mutateMap.put(mutation.getRowId(), mutation);
      }
    }
    return new ArrayList<RowMutation>(mutateMap.values());
  }

  private static RowMutation merge(RowMutation mutation1, RowMutation mutation2) throws BlurException {
    RowMutationType rowMutationType1 = mutation1.getRowMutationType();
    RowMutationType rowMutationType2 = mutation2.getRowMutationType();
    if (!rowMutationType1.equals(rowMutationType2)) {
      throw new BException(
          "RowMutation conflict, cannot perform 2 different operations on the same row in the same batch. [{0}] [{1}]",
          mutation1, mutation2);
    }
    if (rowMutationType1.equals(RowMutationType.DELETE_ROW)) {
      // Since both are trying to delete the same row, just pick one and move
      // on.
      return mutation1;
    } else if (rowMutationType1.equals(RowMutationType.REPLACE_ROW)) {
      throw new BException(
          "RowMutation conflict, cannot perform 2 different REPLACE_ROW mutations on the same row in the same batch. [{0}] [{1}]",
          mutation1, mutation2);
    } else {
      // Now this is a row update, so try to merge the record mutations
      List<RecordMutation> recordMutations1 = mutation1.getRecordMutations();
      List<RecordMutation> recordMutations2 = mutation2.getRecordMutations();
      List<RecordMutation> mergedRecordMutations = merge(recordMutations1, recordMutations2);
      mutation1.setRecordMutations(mergedRecordMutations);
      return mutation1;
    }
  }

  private static List<RecordMutation> merge(List<RecordMutation> recordMutations1, List<RecordMutation> recordMutations2)
      throws BException {
    Map<String, RecordMutation> recordMutationMap = new TreeMap<String, RecordMutation>();
    merge(recordMutations1, recordMutationMap);
    merge(recordMutations2, recordMutationMap);
    return new ArrayList<RecordMutation>(recordMutationMap.values());
  }

  private static void merge(List<RecordMutation> recordMutations, Map<String, RecordMutation> recordMutationMap)
      throws BException {
    for (RecordMutation recordMutation : recordMutations) {
      Record record = recordMutation.getRecord();
      String recordId = record.getRecordId();
      RecordMutation existing = recordMutationMap.get(recordId);
      if (existing != null) {
        recordMutationMap.put(recordId, merge(recordMutation, existing));
      } else {
        recordMutationMap.put(recordId, recordMutation);
      }
    }
  }

  private static RecordMutation merge(RecordMutation recordMutation1, RecordMutation recordMutation2) throws BException {
    RecordMutationType recordMutationType1 = recordMutation1.getRecordMutationType();
    RecordMutationType recordMutationType2 = recordMutation2.getRecordMutationType();
    if (!recordMutationType1.equals(recordMutationType2)) {
      throw new BException(
          "RecordMutation conflict, cannot perform 2 different operations on the same record in the same row in the same batch. [{0}] [{1}]",
          recordMutation1, recordMutation2);
    }

    if (recordMutationType1.equals(RecordMutationType.DELETE_ENTIRE_RECORD)) {
      // Since both are trying to delete the same record, just pick one and move
      // on.
      return recordMutation1;
    } else if (recordMutationType1.equals(RecordMutationType.REPLACE_ENTIRE_RECORD)) {
      throw new BException(
          "RecordMutation conflict, cannot perform 2 different replace record operations on the same record in the same row in the same batch. [{0}] [{1}]",
          recordMutation1, recordMutation2);
    } else if (recordMutationType1.equals(RecordMutationType.REPLACE_COLUMNS)) {
      throw new BException(
          "RecordMutation conflict, cannot perform 2 different replace columns operations on the same record in the same row in the same batch. [{0}] [{1}]",
          recordMutation1, recordMutation2);
    } else {
      Record record1 = recordMutation1.getRecord();
      Record record2 = recordMutation2.getRecord();
      String family1 = record1.getFamily();
      String family2 = record2.getFamily();

      if (isSameFamily(family1, family2)) {
        record1.getColumns().addAll(record2.getColumns());
        return recordMutation1;
      } else {
        throw new BException("RecordMutation conflict, cannot merge records with different family. [{0}] [{1}]",
            recordMutation1, recordMutation2);
      }
    }
  }

  private static boolean isSameFamily(String family1, String family2) {
    if (family1 == null && family2 == null) {
      return true;
    }
    if (family1 != null && family1.equals(family2)) {
      return true;
    }
    return false;
  }
}