/**
   * Finalize, if appropriate, all derived data in recalibrationTables.
   *
   * <p>Called once after all calls to updateDataForRead have been issued.
   *
   * <p>Assumes that all of the principal tables (by quality score) have been completely updated,
   * and walks over this data to create summary data tables like by read group table.
   */
  public void finalizeData() {
    if (finalized) throw new IllegalStateException("FinalizeData() has already been called");

    // merge all of the thread-local tables
    finalRecalibrationTables = mergeThreadLocalRecalibrationTables();

    final NestedIntegerArray<RecalDatum> byReadGroupTable =
        finalRecalibrationTables.getReadGroupTable();
    final NestedIntegerArray<RecalDatum> byQualTable =
        finalRecalibrationTables.getQualityScoreTable();

    // iterate over all values in the qual table
    for (final NestedIntegerArray.Leaf<RecalDatum> leaf : byQualTable.getAllLeaves()) {
      final int rgKey = leaf.keys[0];
      final int eventIndex = leaf.keys[2];
      final RecalDatum rgDatum = byReadGroupTable.get(rgKey, eventIndex);
      final RecalDatum qualDatum = leaf.value;

      if (rgDatum == null) {
        // create a copy of qualDatum, and initialize byReadGroup table with it
        byReadGroupTable.put(new RecalDatum(qualDatum), rgKey, eventIndex);
      } else {
        // combine the qual datum with the existing datum in the byReadGroup table
        rgDatum.combine(qualDatum);
      }
    }

    finalized = true;
  }
  /**
   * Update the recalibration statistics using the information in recalInfo
   *
   * @param recalInfo data structure holding information about the recalibration values for a single
   *     read
   */
  @Requires("recalInfo != null")
  public void updateDataForRead(final ReadRecalibrationInfo recalInfo) {
    final GATKSAMRecord read = recalInfo.getRead();
    final ReadCovariates readCovariates = recalInfo.getCovariatesValues();
    final RecalibrationTables tables = getUpdatableRecalibrationTables();
    final NestedIntegerArray<RecalDatum> qualityScoreTable = tables.getQualityScoreTable();

    for (int offset = 0; offset < read.getReadBases().length; offset++) {
      if (!recalInfo.skip(offset)) {

        for (final EventType eventType : EventType.values()) {
          final int[] keys = readCovariates.getKeySet(offset, eventType);
          final int eventIndex = eventType.ordinal();
          final byte qual = recalInfo.getQual(eventType, offset);
          final double isError = recalInfo.getErrorFraction(eventType, offset);

          RecalUtils.incrementDatumOrPutIfNecessary(
              qualityScoreTable, qual, isError, keys[0], keys[1], eventIndex);

          for (int i = 2; i < covariates.length; i++) {
            if (keys[i] < 0) continue;

            RecalUtils.incrementDatumOrPutIfNecessary(
                tables.getTable(i), qual, isError, keys[0], keys[1], keys[i], eventIndex);
          }
        }
      }
    }
  }
  /**
   * Merge all of the thread local recalibration tables into a single one.
   *
   * <p>Reuses one of the recalibration tables to hold the merged table, so this function can only
   * be called once in the engine.
   *
   * @return the merged recalibration table
   */
  @Requires("! finalized")
  private RecalibrationTables mergeThreadLocalRecalibrationTables() {
    if (recalibrationTablesList.isEmpty()) {
      recalibrationTablesList.add(
          new RecalibrationTables(covariates, numReadGroups, maybeLogStream));
    }

    RecalibrationTables merged = null;
    for (final RecalibrationTables table : recalibrationTablesList) {
      if (merged == null)
        // fast path -- if there's only only one table, so just make it the merged one
        merged = table;
      else {
        merged.combine(table);
      }
    }

    return merged;
  }