// module load samtools boost
// g++ -g -O3 -Wall -I$BOOST_DIR/include -I$SAMTOOLS_DIR/include/bam
// -L$SAMTOOLS_DIR/lib -o jgi_summarize_bam_contig_depths
// jgi_summarize_bam_contig_depths.cpp -lpthread -lz -lbam -fopenmp
// g++ -g -O3 -Wall -I$BOOST_DIR/include -I$SAMTOOLS_DIR/include/bam
// -L$SAMTOOLS_DIR/lib -o jgi_summarize_bam_contig_depths
// jgi_summarize_bam_contig_depths.cpp -lpthread -lz -lbam -fopenmp

#include <cassert>
#include <condition_variable>
#include <mutex>

#include "version.h"
#include "jgi_summarize_bam_contig_depths.h"

#include "IOThreadBuffer.h"
#include "KseqReader.h"
#include "RunningStats.h"
#include "SafeOfstream.hpp"

ThreadBlocker tb;

static struct option long_options[] = {{"help", 0, 0, 0},
                                       {"outputDepth", 1, 0, 0},
                                       {"percentIdentity", 1, 0, 0},
                                       {"pairedContigs", 1, 0, 0},
                                       {"unmappedFastq", 1, 0, 0},
                                       {"referenceFasta", 1, 0, 0},
                                       {"shredLength", 1, 0, 0},
                                       {"shredDepth", 1, 0, 0},
                                       {"minContigLength", 1, 0, 0},
                                       {"minContigDepth", 1, 0, 0},
                                       {"minMapQual", 1, 0, 0},
                                       {"weightMapQual", 1, 0, 0},
                                       {"noIntraDepthVariance", 0, 0, 0},
                                       {"showDepth", 0, 0, 0},
                                       {"includeEdgeBases", 0, 0, 0},
                                       {"maxEdgeBases", 1, 0, 0},
                                       {"outputReadStats", 1, 0, 0},
                                       {"outputGC", 1, 0, 0},
                                       {"gcWindow", 1, 0, 0},
                                       {"outputKmers", 1, 0, 0},
                                       {"checkpoint", 0, 0, 0}};

void usage() {
  fprintf(
      stderr,
      "jgi_summarize_bam_contig_depths %s %s\n"
      "Usage: jgi_summarize_bam_contig_depths <options> sortedBam1 [ "
      "sortedBam2 ...]\n"
      "where options include:\n"
      "\t--outputDepth       arg  The file to put the contig by bam depth "
      "matrix (default: STDOUT)\n"
      "\t--checkpoint             The write checkpoints for every bam "
      "processed (default: off)\n"
      "\t--percentIdentity   arg  The minimum end-to-end %% identity of "
      "qualifying reads (default: 97)\n"
      "\t--pairedContigs     arg  The file to output the sparse matrix of "
      "contigs which paired reads span (default: none)\n"
      "\t--unmappedFastq     arg  The prefix to output unmapped reads from "
      "each bam file suffixed by 'bamfile.bam.fastq.gz'\n"
      "\t--noIntraDepthVariance   Do not include variance from mean depth "
      "along the contig\n"
      "\t--showDepth              Output a .depth file per bam for each contig "
      "base\n"
      "\t--minMapQual        arg  The minimum mapping quality necessary to "
      "count the read as mapped (default: 0)\n"
      "\t--weightMapQual     arg  Weight per-base depth based on the MQ of the "
      "read (i.e uniqueness) (default: 0.0 (disabled))\n"
      "\t--includeEdgeBases       When calculating depth & variance, include "
      "the 1-readlength edges (off by default)\n"
      "\t--maxEdgeBases           When calculating depth & variance, and not "
      "--includeEdgeBases, the maximum length (default:75)\n"
      "\t--referenceFasta    arg  The reference file.  (It must be the same "
      "fasta that bams used)\n"
      "\nOptions that require a --referenceFasta\n"
      "\t--outputGC          arg  The file to print the gc coverage histogram\n"
      "\t--gcWindow          arg  The sliding window size for GC calculations\n"
      "\t--outputReadStats   arg  The file to print the per read statistics\n"
      "\t--outputKmers       arg  The file to print the perfect kmer counts\n"
      "\nOptions to control shredding contigs that are under represented by "
      "the reads\n"
      "\t--shredLength       arg  The maximum length of the shreds\n"
      "\t--shredDepth        arg  The depth to generate overlapping shreds\n"
      "\t--minContigLength   arg  The mimimum length of contig to include for "
      "mapping and shredding\n"
      "\t--minContigDepth    arg  The minimum depth along contig at which to "
      "break the contig\n"
      "\n", MetaBAT_VERSION, MetaBAT_BUILD_DATE);
}

void abortMe(string msg) {
  cerr << msg << endl;
  exit(1);
}

bool file_exists(const string fname) {
  ifstream ifs(fname, std::ios_base::binary);
  return ifs.is_open();
}

string get_basename(const string fname) {
  string basename(fname);
  auto pos = basename.find_last_of('/');
  if (pos != string::npos)
    basename = basename.substr(pos + 1);
  pos = basename.find_last_of('.');
  if (pos != string::npos)
    basename = basename.substr(0, pos);
  return basename;
}

void printDepthTableHeader(ostream &of, const vector<string> &names,
                           bool intraDepthVariance) {
  of << "contigName\tcontigLen\ttotalAvgDepth";
  for (int bamIdx = 0; bamIdx < (int)names.size(); bamIdx++) {
    string shortName = get_basename(names[bamIdx]);
    of << "\t" << shortName;
    if (intraDepthVariance) {
      of << "\t" << shortName << "-var";
    }
  }
  of << "\n";
}

class ContigDepth {
public:
  int numSamples, contigLen;
  bool includeEdgeBases;
  int maxEdgeBases;
  double mean, variance;
  double weightedSum = 0.0;
  uint64_t totalCorrectedLength = 0;
  vector<double> means;
  vector<double> variances;
  ContigDepth(int numSamples, int contigLen, bool includeEdgeBases,
              int maxEdgeBases, bool intraDepthVariance)
      : numSamples(numSamples), contigLen(contigLen),
        includeEdgeBases(includeEdgeBases),
        maxEdgeBases(maxEdgeBases), mean{}, variance{}, weightedSum{},
        totalCorrectedLength{}, means{}, variances{} {
    means.reserve(numSamples);
    if (intraDepthVariance)
      variances.resize(numSamples);
  }
  void addDepth(int averageReadSize, CountType &contigDepth) {
    if (!variances.empty())
      throw;
    auto correctedLen = getCorrectedLen(averageReadSize);
    totalCorrectedLength += correctedLen;
    double mean = contigDepth / correctedLen;
    if (mean < 0.0 || correctedLen < 0) {
      std::stringstream ss;
      ss << "WARNING: calculated a negative mean=" << mean << ". correctedLen=" << correctedLen << " contigDepth=" << contigDepth << "\nPlease report this bam file to MetaBAT under Issue #166\n";
      std::cerr << ss.str() << std::flush;
      contigDepth = 0;
      mean = 0;
    }
    if (mean > 1000000) {
      std::stringstream ss;
      ss << "WARNING: calculated a huge mean=" << mean << ". correctedLen=" << correctedLen << " contigDepth=" << contigDepth << "\nPlease report this bam file to MetaBAT under Issue #166\n";
      std::cerr << ss.str() << std::flush;
      contigDepth = 0;
      mean = 0;
    }

    weightedSum += mean * correctedLen;
    means.push_back(mean);
  }
  void addDepth(int averageReadSize, VarianceType &variance) {
    if (variances.empty())
      throw;
    auto correctedLen = getCorrectedLen(averageReadSize);
    if (variance.mean < 0.0 || variance.variance < 0.0 || correctedLen < 0) {
      std::stringstream ss;
      ss << "WARNING: received a negative mean=" << variance.mean << " or variance=" << variance.variance << ". correctedLen=" << correctedLen << "\nPlease report this bam file to MetaBAT under Issue #166\n";
      std::cerr << ss.str() << std::flush;
      variance.mean = 0;
      variance.variance = 0;
    }
    if (variance.mean > 1000000 ) {
      std::stringstream ss;
      ss << "WARNING: received a huge mean=" << variance.mean << " or variance=" << variance.variance << ". correctedLen=" << correctedLen << "\nPlease report this bam file to MetaBAT under Issue #166\n";
      std::cerr << ss.str() << std::flush;
      variance.mean = 0;
      variance.variance = 0;
    }
    totalCorrectedLength += correctedLen;
    variances[means.size()] = variance.variance;
    means.push_back(variance.mean);
    weightedSum += variance.mean * correctedLen;
  }
  int getCorrectedLen(int averageReadSize) {
    int x = 2 * std::min(maxEdgeBases, averageReadSize / 3);
    int correctedLen =
        (includeEdgeBases |
         (x >= contigLen))
            ? contigLen
            : contigLen - x;
    assert(correctedLen > 0);
    return correctedLen;
  }
  double getTotalCorrectedDepth() const {
    double totalCorrectedDepth = 0.0;
    if (totalCorrectedLength > 0.0) {
      totalCorrectedDepth = weightedSum / totalCorrectedLength;
    }
    totalCorrectedDepth *= numSamples; // scale back up because we did not
                                       // divide by avgCorrectedLength
    return totalCorrectedDepth;
  }
  bool printDepth(ostream &of, string name, float minContigDepth) {
    auto totalAverageDepth = getTotalCorrectedDepth();
    std::string nameonly = name;
    int pos = nameonly.find_first_of(" \t");
    nameonly = nameonly.substr(0, pos);
    of << nameonly << "\t" << contigLen << "\t" << (float)totalAverageDepth;
    for (int i = 0; i < numSamples; i++) {
      if (variances.size()) {
        of << "\t" << (float)means[i] << "\t" << (float)variances[i];
      } else {
        of << "\t" << (float)means[i];
      }
    }
    of << "\n";
    return totalAverageDepth >= minContigDepth;
  }
};

void printDepthTable(ostream &of, const vector<string> &names,
                     bool intraDepthVariance,
                     CountTypeMatrix &bamContigDepths,
                     const vector<int> &averageReadSize, float percentIdentity,
                     VarianceTypeMatrix &bamContigVariances,
                     bam_header_t *header, BoolVector &contigLengthPass,
                     BoolVector &contigDepthPass, float minContigDepth,
                     bool includeEdgeBases = false, int maxEdgeBases = 0) {

  printDepthTableHeader(of, names, intraDepthVariance);

  for (int32_t contigIdx = 0; contigIdx < header->n_targets; contigIdx++) {
    if (!contigLengthPass[contigIdx])
      continue;

    int contigLen = header->target_len[contigIdx];
    ContigDepth contigDepth(names.size(), contigLen, includeEdgeBases,
                            maxEdgeBases, intraDepthVariance);
    for (int bamIdx = 0; bamIdx < (int)names.size(); bamIdx++) {
      if (intraDepthVariance)
        contigDepth.addDepth(averageReadSize[bamIdx],
                             bamContigVariances[bamIdx][contigIdx]);
      else
        contigDepth.addDepth(averageReadSize[bamIdx],
                             bamContigDepths[bamIdx].get()[contigIdx]);
    }
    contigDepthPass[contigIdx] = contigDepth.printDepth(
        of, header->target_name[contigIdx], minContigDepth);
  }
}

void printPairedContigs(ostream &of, const int numThreads,
                        std::vector<PairedCountTypeMatrix> &bamPairedContigs,
                        bam_header_t *header) {
  of << "contigIdx\tcontigIdxMate\tAvgCoverage\n";
  for (int32_t contigIdx = 0; contigIdx < header->n_targets; contigIdx++) {
    float len = header->target_len[contigIdx];
    PairedCountType sums;
    for (int threadNum = 0; threadNum < numThreads; threadNum++) {
      PairedCountTypeMatrix &pairedContigs = bamPairedContigs[threadNum];
      PairedCountType &pairedCounts = pairedContigs[contigIdx];
      if (pairedCounts.empty())
        continue;

      for (PairedCountType::const_iterator it = pairedCounts.begin();
           it != pairedCounts.end(); it++) {
        sums[it->first] += it->second;
      }
    }
    for (PairedCountType::const_iterator it = sums.begin(); it != sums.end();
         it++) {
      of << contigIdx << "\t" << it->first << "\t" << it->second / len << "\n";
    }
  }
}

int getGC(const char *seq, int len, bool isbam = false) {
  int count = 0, gc = 0;
  for (int i = 0; i < len; i++) {
    switch (isbam ? bam_nt16_rev_table[bam1_seqi((const uint8_t *)seq, i)]
                  : seq[i]) {
    case 'G':
    case 'C':
    case 'g':
    case 'c':
      gc++;
    case 'A':
    case 'T':
    case 'a':
    case 't':
      count++;
      break;
    }
  }
  if (count > 0) {
    return (int)((100.0 * (float)gc / (float)count) + 0.5);
  } else {
    return 0;
  }
}

int getGC(const bam1_t *b) {
  return getGC((const char *)bam1_seq(b), b->core.l_qseq, true);
}

ostream &writeReadStatsHeader(ostream &os) {
  ReadStatistics::writeHeader(os);
  os << "\tReadGC\tMappedGC\n";
  os.flush();
  return os;
}

ostream &writeReadStats(ostream &os, const bam1_t *b, ReadStatistics readstats,
                        const char *refseq) {
  if ((b->core.flag & BAM_FUNMAP) == BAM_FUNMAP)
    return os;

  stringstream &ss = IOThreadBuffer::getMyBuffer(os);
  readstats.write(ss) << "\t" << getGC(b) << "\t"
                      << getGC(refseq + b->core.pos, readstats.alignlen, false)
                      << "\n";

  return os;
}

void writeUnmapedFastqFile(BamFile &myBam, string unmappedFastqFile,
                           BamHeaderT header, BoolVector &contigLengthPass,
                           BoolVector &contigDepthPass,
                           gzipFileBufPtr &bamUnmappedFastqof,
                           NameBamMap &bamReadIds) {
  cerr << "Sequestering reads to unmappedFastq files for " << myBam.getBamFile()
       << endl;
  // write any reads from low depth contigs

  cerr << "Sequestering reads on low abundance contigs for "
       << myBam.getBamName() << endl;
  for (int32_t contigIdx = 0; contigIdx < header->n_targets; contigIdx++) {
    if (contigLengthPass[contigIdx] && (!contigDepthPass[contigIdx])) {
      // for each bam find and write all reads on these newly failed contigs

      ostream os(bamUnmappedFastqof.get());
      BamUtils::writeFastqByContig(os, bamReadIds, myBam, contigIdx);
    }
  }

  cerr << "Sequestering read mates not yet output for " << myBam.getBamName()
       << endl;
  // for each bam write all orphans...

  string name = unmappedFastqFile + "-" + myBam.getBamName() + "-single.fastq";
  {

    SafeOfstream singles(name.c_str());
    ostream pairs(bamUnmappedFastqof.get());
    BamUtils::writeFastqOrphans(pairs, singles, bamReadIds, myBam);
    cerr << "Closing pair and single unmaps for " << myBam.getBamName() << endl;
    // close the two files now
    bamUnmappedFastqof.reset();
    cerr << "Closed pair and single unmaps for " << myBam.getBamName() << endl;
  }
  struct stat filestatus;
  stat(name.c_str(), &filestatus);

  if (filestatus.st_size <= 20) {
    unlink(name.c_str());
  } else {
    cerr << "Additional orphaned reads are output as singles to: " << name
         << endl;
  }

  cerr << "Freeing up memory for " << myBam.getBamName() << endl;
  bamReadIds.clear();
}

int main(int argc, char *argv[]) {

  // set and parse options
  float percentIdentity = (float)97 / (float)100.0;
  string outputTableFile, pairedContigsFile, unmappedFastqFile,
      referenceFastaFile;
  string outputReadStatsFile, outputGCFile, outputKmersFile;
  int gcWindow = 100;
  int shredLength = 16000, shredDepth = 5, minContigLength = 1, minMapQual = 0;
  float weightMapQual = 0.0;
  bool normalizeWeightMapQual = false;
  float minContigDepth = 0;
  bool intraDepthVariance = true;
  bool showDepth = false;
  bool includeEdgeBases = false;
  int maxEdgeBases = 75;
  bool checkpoint = false;

  while (1) {
    int option_index = 0;
    int c = getopt_long(argc, argv, "h", long_options, &option_index);
    if (c == -1)
      break;
    switch (c) {
    case 0:
      if (strcmp(long_options[option_index].name, "help") == 0) {
        usage();
        exit(0);
      } else if (strcmp(long_options[option_index].name, "percentIdentity") ==
                 0) {
        percentIdentity = atoi(optarg) / 100.0;
        cerr << "Minimum percent identity for a mapped read: "
             << percentIdentity << endl;
      } else if (strcmp(long_options[option_index].name, "outputDepth") == 0) {
        outputTableFile = optarg;
        cerr << "Output depth matrix to " << outputTableFile << endl;
      } else if (strcmp(long_options[option_index].name, "checkpoint") == 0) {
        checkpoint = true;
      } else if (strcmp(long_options[option_index].name, "outputReadStats") ==
                 0) {
        outputReadStatsFile = optarg;
        cerr << "Output Read Stats file to " << outputReadStatsFile << endl;
      } else if (strcmp(long_options[option_index].name, "outputGC") == 0) {
        outputGCFile = optarg;
        cerr << "Output GC stats file to " << outputGCFile << endl;
      } else if (strcmp(long_options[option_index].name, "gcWindow") == 0) {
        gcWindow = atoi(optarg);
        cerr << "GC sliding window: " << gcWindow << endl;
      } else if (strcmp(long_options[option_index].name, "outputKmers") == 0) {
        outputKmersFile = optarg;
        cerr << "Output Perfect Kmers file to " << outputKmersFile << endl;
      } else if (strcmp(long_options[option_index].name, "pairedContigs") ==
                 0) {
        pairedContigsFile = optarg;
        cerr << "Output pairedContigs lower triangle to " << pairedContigsFile
             << endl;
      } else if (strcmp(long_options[option_index].name, "unmappedFastq") ==
                 0) {
        unmappedFastqFile = optarg;
        cerr << "Output Unmapped Fastq to " << unmappedFastqFile << endl;
      } else if (strcmp(long_options[option_index].name, "referenceFasta") ==
                 0) {
        referenceFastaFile = optarg;
        cerr << "Reference fasta file " << referenceFastaFile << endl;
      } else if (strcmp(long_options[option_index].name, "shredLength") == 0) {
        shredLength = atoi(optarg);
        cerr << "shredLength: " << shredLength << endl;
      } else if (strcmp(long_options[option_index].name, "shredDepth") == 0) {
        shredDepth = atoi(optarg);
        cerr << "shredDepth: " << shredDepth << endl;
      } else if (strcmp(long_options[option_index].name, "minContigLength") ==
                 0) {
        minContigLength = atoi(optarg);
        cerr << "minContigLength: " << minContigLength << endl;
      } else if (strcmp(long_options[option_index].name, "minMapQual") == 0) {
        minMapQual = atoi(optarg);
        cerr << "minMapQual: " << minMapQual << endl;
      } else if (strcmp(long_options[option_index].name, "weightMapQual") ==
                 0) {
        weightMapQual = atof(optarg);
        cerr << "weightMapQual: " << weightMapQual << endl;
      } else if (strcmp(long_options[option_index].name, "minContigDepth") ==
                 0) {
        minContigDepth = atof(optarg);
        cerr << "minContigDepth: " << minContigDepth << endl;
      } else if (strcmp(long_options[option_index].name,
                        "noIntraDepthVariance") == 0) {
        intraDepthVariance = false;
        cerr << "Calculating intra contig depth variance\n";
      } else if (strcmp(long_options[option_index].name, "showDepth") == 0) {
        showDepth = true;
        cerr << "Outputing a .depth file for each bam\n";
      } else if (strcmp(long_options[option_index].name, "includeEdgeBases") ==
                 0) {
        includeEdgeBases = true;
        cerr << "Edge bases will be included in all calculations\n";
      } else if (strcmp(long_options[option_index].name, "maxEdgeBases") == 0) {
        maxEdgeBases = atoi(optarg);
        cerr << "Edge bases will be included up to " << maxEdgeBases
             << " bases\n";
      } else {
        usage();
        cerr << "Unrecognized option: " << long_options[option_index].name
             << endl;
        exit(1);
      }
      break;
    default:
      usage();
      exit(0);
    };
  }
  if (argc - optind < 1) {
    usage();
    if (argc - optind < 1)
      cerr << "You must specify one or more bam files\n\n";
    return 0;
  }
  CheckRead::_edgeBases() = maxEdgeBases;

  cerr << "jgi_summarize_bam_contig_depths " << MetaBAT_VERSION << " "
       << MetaBAT_BUILD_DATE << endl;
  cerr << "Running with " << omp_get_max_threads()
       << " threads to save memory you can reduce the number of threads with "
          "the OMP_NUM_THREADS variable"
       << endl;
  cerr << "Output matrix to "
       << (outputTableFile.empty() ? "STDOUT" : outputTableFile.c_str())
       << endl;
  if (checkpoint)
    cerr << "Writing checkpoints as each bam has been processed" << endl;

  // assign names and allocate samfile handles
  BamFileVector bams;
  StringVector bamFilePaths;
  for (int i = optind; i < argc; i++) {
    bamFilePaths.push_back(argv[i]);
  }
  int num_bams = bamFilePaths.size();
  int largest_contig = 0;

  bool store_sequences = !outputGCFile.empty() | !outputKmersFile.empty() |
                         !outputReadStatsFile.empty();
  std::vector<string> referenceSequences;
  std::vector<int> referenceSequenceLens;
  std::vector<string> referenceSequenceNames;
  if (!referenceFastaFile.empty()) {
    cout << "Reading reference fasta file: " << referenceFastaFile << endl;
    KseqReader reference(referenceFastaFile);
    while (reference.hasNext()) {
      auto seq = reference.getSeq();
      auto sz = seq.length();
      referenceSequenceLens.push_back(sz);
      if (sz > largest_contig)
        largest_contig = sz;
      if (store_sequences)
        referenceSequences.push_back(seq);
      referenceSequenceNames.push_back(reference.getName());
    }
    cout << "... " << referenceSequenceLens.size() << " sequences" << endl;
    if (referenceSequenceLens.empty()) {
      cerr << "ERROR: the reference was empty!: " << referenceFastaFile << endl;
      exit(1);
    }
  }

  SafeOfstream *readStats = NULL;
  if (!outputReadStatsFile.empty()) {
    readStats = new SafeOfstream(outputReadStatsFile.c_str());
    writeReadStatsHeader(*readStats);
  }

  std::vector<float> refGC;
  ReadGCStats readGCStats;

  std::vector<std::vector<uint8_t>> refGCWindows;
  if (!outputGCFile.empty() && !referenceSequences.empty()) {
    refGC.resize(101, 0);
    readGCStats.resize(101, RunningStats());
    refGCWindows.resize(referenceSequences.size());
    for (long i = 0; i < (long)referenceSequences.size(); i++) {
      std::vector<uint8_t> &refGCs = refGCWindows[i];
      refGCs.reserve(referenceSequences[i].length() - gcWindow + 1);
      for (int j = 0; j < (int)referenceSequences[i].length() - gcWindow; j++) {
        int gc = getGC(referenceSequences[i].c_str() + j, gcWindow, false);
        refGCs.push_back(gc);
      }
    }
  }

  // open first bam
  bams.resize(num_bams);
  BamFile &firstBam = bams[0];
  BamHeaderT header;
  bool preprocess_complete = false;
  if (checkpoint) {
    cerr << "Opening the first bam file for a baseline header: "
         << bamFilePaths[0] << endl;
    firstBam = BamUtils::openBam(bamFilePaths[0], false);
    header = firstBam.header;
  } else {
    cerr << "Opening all bam files and validating headers" << endl;
    header = BamUtils::openBamsAndConsolidateHeaders(bamFilePaths, bams, false);
    preprocess_complete = true;
  }

  assert(header.get() != NULL);
  BoolVector contigLengthPass, contigDepthPass;
  contigLengthPass.resize(header->n_targets);
  contigDepthPass.resize(header->n_targets, true);
  if (!referenceSequenceLens.empty() &&
      header->n_targets != (long)referenceSequenceLens.size()) {
    cerr << "Error: referenceFile: " << referenceFastaFile
         << " is not the same as in the bam headers! (targets: "
         << header->n_targets << " from the bam vs "
         << referenceSequenceLens.size() << " from the ref)" << endl;
    if (referenceSequenceLens.empty())
      cerr << "no reference was loaded for " << referenceFastaFile << endl;
    exit(1);
  }
  for (int32_t i = 0; i < header->n_targets; i++) {
    if ((int)header->target_len[i] >= minContigLength) {
      contigLengthPass[i] = true;
    } else {
      contigLengthPass[i] = false;
    }
    if (!referenceSequenceLens.empty() &&
        header->target_len[i] != referenceSequenceLens[i]) {
      cerr << "Error: referenceFile: " << referenceFastaFile << " contig " << i
           << " is not the same as in the bam headers (bam reports "
           << header->target_name[i] << " with " << header->target_len[i]
           << " len, reference loaded " << referenceSequenceNames[i] << " with "
           << referenceSequenceLens[i] << " len)! " << endl;
      exit(1);
    }
  }

  // make vector of unmappedFastq files, to reuse
  std::vector<gzipFileBufPtr> bamUnmappedFastqof;
  bamUnmappedFastqof.resize(num_bams);

  // allocate memory for depth and optionally variance matrixes
  CountTypeMatrix bamContigDepths;
  bamContigDepths.resize(num_bams);
  vector<int> averageReadSize;
  averageReadSize.resize(num_bams, 0);

  VarianceTypeMatrix bamContigVariances;
  if (intraDepthVariance) {
    bamContigVariances.resize(num_bams);
    if (!checkpoint) {
      VarianceType dummy;
      for (int i = 0; i < (int)num_bams; i++) {
        bamContigVariances[i].resize(header->n_targets, dummy);
      }
    }
  }

  std::vector<NameBamMap> bamReadIds;
  bamReadIds.resize(num_bams);

  int numThreads = 1;
#pragma omp parallel
  {
    if (omp_get_thread_num() == 0)
      numThreads = omp_get_num_threads();
#pragma omp for reduction(max : largest_contig)
    for (int64_t i = 0; i < (int64_t)header->n_targets; i++)
      if (header->target_len[i] > largest_contig)
        largest_contig = header->target_len[i];
  }
  if (numThreads > (int)num_bams) {
    numThreads = num_bams;
    omp_set_num_threads(numThreads);
  }
  std::vector<PairedCountTypeMatrix> bamPairedContigs;
  bamPairedContigs.resize(numThreads);

  if (!pairedContigsFile.empty()) {
    cerr << "Allocating pairedContigs matrix: "
         << (numThreads * header->n_targets * sizeof(PairedCountType) / 1024 /
             1024)
         << " MB over " << numThreads << " threads" << endl;

#pragma omp parallel for schedule(static, 1)
    for (int threadNum = 0; threadNum < numThreads; threadNum++) {

      PairedCountTypeMatrix &pairedContigs = bamPairedContigs[threadNum];
      PairedCountType empty;
      pairedContigs.resize(header->n_targets, empty);
      assert((int)pairedContigs.size() == header->n_targets);
      for (int64_t i = 0; i < (int64_t)header->n_targets; i++) {
        if (!pairedContigs[i].empty())
          throw;
      }
    }
  }

  cerr << "Processing bam files with largest_contig=" << largest_contig << endl;
  bool hasAnyPairedContigs = false;

  // preallocate all forking for unmapped fastq, so we do not fork when memory
  // is tight
  if (!unmappedFastqFile.empty()) {
#pragma omp parallel for
    for (int bamIdx = 0; bamIdx < (int)num_bams; bamIdx++) {
      string name =
          unmappedFastqFile + "-" + bamFilePaths[bamIdx] + ".fastq.gz";
      bamUnmappedFastqof[bamIdx] = gzipOutputFile(name);
      cerr << "Outputting any unmapped reads to " << name << endl;
    }
  }
  MappedKmersStats *ourMappedKmersStats = NULL;
  if (!outputKmersFile.empty() && !referenceSequences.empty()) {
    ourMappedKmersStats = new MappedKmersStats();
  }
  DepthCounts noDepthCounts{};
  vector<DepthCounts> workingDepthCounts(omp_get_max_threads());
#pragma omp parallel for schedule(dynamic, 1)
  for (int threadIdx = 0; threadIdx < workingDepthCounts.size(); threadIdx++)
    workingDepthCounts[threadIdx].resetBaseCounts(largest_contig + 1,
                                                  weightMapQual > 0.0);

  std::mutex m;
  std::condition_variable cv;
  bool isSorted = true;
  CountTypeMatrix threadWorkingContigDepths(omp_get_max_threads());
  VarianceTypeMatrix threadWorkingVariance(omp_get_max_threads());

  int minAvgRead = maxEdgeBases * 3;
  vector<string> partialFiles(num_bams);
#pragma omp parallel for schedule(dynamic, 1)
  for (int bamIdx = 0; bamIdx < (int)num_bams; bamIdx++) {

    int threadNum = omp_get_thread_num();
    string baseName = get_basename(bamFilePaths[bamIdx]);

    if (checkpoint && bamIdx == 0) {
      // block processing all bam file alignments until preprocessing of contig
      // name 2 id map
      std::lock_guard<std::mutex> lk(m);
#pragma omp critical(OUTPUT)
      cerr << "Thread " << threadNum << " preprocessing contig2id map" << endl;
      BamUtils::preprocessContigNames(header);
      preprocess_complete = true;
      cv.notify_all();
    }

    string checkpoint_depth = partialFiles[bamIdx] =
        outputTableFile + ".partial-" + baseName + ".data";
    if (checkpoint && file_exists(checkpoint_depth)) {
#pragma omp critical(OUTPUT)
      cerr << "partial output for bam " << bamIdx << ": " << baseName
           << " already checkpointed." << endl;
      continue;
    }

    BamFile &myBam = bams[bamIdx];
#pragma omp critical(OUTPUT)
    cerr << "Thread " << threadNum
         << " opening and reading the header for file: " << bamFilePaths[bamIdx]
         << endl;
    if (bamIdx > 0)
      myBam = BamUtils::openBam(bamFilePaths[bamIdx], false, header);
#pragma omp critical(OUTPUT)
    cerr << "Thread " << threadNum
         << " opened the file: " << bamFilePaths[bamIdx] << endl;
    if (checkpoint && !preprocess_complete) {
#pragma omp critical(OUTPUT)
      cerr << "Thread " << threadNum
           << " waiting for preprocessing to complete before reading "
              "alignments in : "
           << bamFilePaths[bamIdx] << endl;
      std::unique_lock<std::mutex> lk(m);
      if (!preprocess_complete) {
        cv.wait(lk, [&preprocess_complete] { return preprocess_complete; });
      }
    }
    ostream *unmappedFastq = NULL;
    if (!unmappedFastqFile.empty()) {
      unmappedFastq = new std::ostream(bamUnmappedFastqof[bamIdx].get());
    }

#pragma omp critical(OUTPUT)
    cerr << "Thread " << threadNum << " processing bam " << bamIdx << ": "
         << myBam.getBamName() << endl;

    // initialize and allocate memory structures
    if (!checkpoint)
      bamContigDepths[bamIdx].swap(threadWorkingContigDepths[threadNum]);
    if (!bamContigDepths[bamIdx])
      bamContigDepths[bamIdx].reset(new CountType[header->n_targets]);
    else
      memset(bamContigDepths[bamIdx].get(), 0,
             header->n_targets * sizeof(CountType));
    if (!bamContigDepths[bamIdx]) {
      cerr << "Could not allocate enough memory to track depth per contig"
           << endl;
      exit(1);
    }

    if (intraDepthVariance) {
      assert(bamContigVariances.size() > bamIdx);
      if (!checkpoint)
        bamContigVariances[bamIdx].swap(threadWorkingVariance[threadNum]);
      if (bamContigVariances[bamIdx].empty()) {
        VarianceType dummy{};
        bamContigVariances[bamIdx].resize(header->n_targets, dummy);
      } else {
        for (auto &x : bamContigVariances[bamIdx])
          x.reset();
      }
    }
    CountType *contigDepths = bamContigDepths[bamIdx].get();
    PairedCountTypeMatrix &pairedContigs = bamPairedContigs[threadNum];
    NameBamMap &readIds = bamReadIds[bamIdx];
    NameBamMap *tempMates = NULL; // new BamNameTrackingChooser();

    int lastMinPos = -1;

    DepthCounts &depthCounts = workingDepthCounts[omp_get_thread_num()];
    std::shared_ptr<std::ofstream> depthFile;
    if (intraDepthVariance || !readGCStats.empty()) {
      depthCounts.resetBaseCounts(largest_contig + 1, weightMapQual > 0.0);
      if (showDepth) {
        depthFile.reset(
            new SafeOfstream(string(myBam.getFilePath() + ".depth").c_str()));
      }
    }

    MappedKmersStats *mappedKmersStats = NULL;
    if (!outputKmersFile.empty() && !referenceSequences.empty()) {
      mappedKmersStats = new MappedKmersStats();
    }
    bam1_t *b = myBam.getBamCache().getBam(),
           *lastBam = myBam.getBamCache().getBam();
    int bytesRead = 0, lastTid = -1, lastPos = 0;
    int64_t readSizes = 0, readCounts = 0, readsWellMapped = 0;
    int &avgRead = averageReadSize[bamIdx];

    CheckRead *check = new CheckRead(avgRead, contigLengthPass, header);
    readIds.setTrackNamer(check); // BamNameMap now manages this memory

    // read the bam file
    while (true) {
      std::swap(b, lastBam);
#ifdef LEGACY_SAMTOOLS
      bytesRead = samread(myBam, b);
      if (bytesRead <= 0)
        break;
#else
      bytesRead = sam_read1(myBam, header.get(), b);
      if (bytesRead < 0)
        break;
#endif

      int32_t tid = b->core.tid;
      int32_t pos = b->core.pos;
      readSizes += b->core.l_qseq;
      readCounts++;
      if (tid >= 0) {
        if (lastTid > tid || (lastTid == tid && lastPos > pos)) {
#pragma omp critical(BAM_WARN_UNSORTED)
          cerr << "ERROR: the bam file '" << myBam.getBamName()
               << "' is not sorted!" << endl;
          isSorted = false;
          break;
        }
      }

      if (!isSorted) // to show all unsorted bams at once
        break;

      assert(tid == -1 || lastTid <= tid); // ensure this is a sorted bam!
      if (check->unsupportedRead(b)) {
        continue;
      }

      bool printed = false;
      if (unmappedFastq != NULL) {
        if (check->failedMapping(b)) {
          // write, no name tracking
          BamUtils::writeFastqOrStorePair(*unmappedFastq, b, readIds);
          printed = true;
        }
      }
      if ((b->core.flag & BAM_FUNMAP) == BAM_FUNMAP ||
          b->core.qual < minMapQual) {
        // exclude this read from counts
        continue;
      }

      if (mappedKmersStats != NULL && !referenceSequences.empty()) {
        mappedKmersStats->addKmerStats(b, referenceSequences);
      }

      avgRead =
          readSizes / readCounts; // running avg.  recalculate after every read
      if (depthCounts && lastTid != tid) {
        // calculate statistics for the previous contig

        if (lastTid >= 0) {
          if (intraDepthVariance)
            bamContigVariances[bamIdx][lastTid] = calculateVarianceContig(
                header.get(), lastTid, depthCounts,
                includeEdgeBases ? 0 : std::min(maxEdgeBases, avgRead / 3),
                weightMapQual, normalizeWeightMapQual);
          if (!readGCStats.empty() && depthCounts &&
              contigDepths[lastTid] / header->target_len[lastTid] >
                  minContigDepth) {
            addGCCounts(readGCStats, refGCWindows[lastTid], gcWindow,
                        depthCounts);
          }
          if (showDepth)
            *depthFile << getContigDepthByBase(
                header.get(), lastTid, depthCounts,
                includeEdgeBases ? 0 : std::min(maxEdgeBases, avgRead / 3),
                weightMapQual);
          while ((tid >= 0 && lastTid < tid) ||
                 (tid < 0 && lastTid < header->n_targets)) {
            if (++lastTid == tid || lastTid >= header->n_targets) {
              lastTid--;
              lastPos = 0;
              break;
            }
            if (intraDepthVariance)
              bamContigVariances[bamIdx][lastTid] = VarianceType();
            if (showDepth) {
              depthCounts.resetBaseCounts(header->target_len[lastTid],
                                          weightMapQual > 0.0);
              *depthFile << getContigDepthByBase(
                  header.get(), lastTid, depthCounts,
                  includeEdgeBases ? 0 : std::min(maxEdgeBases, avgRead / 3),
                  weightMapQual);
            }
          }
        }
      } // finish stats for previous contigs

      if (depthCounts && lastTid != tid && tid >= 0) {
        depthCounts.resetBaseCounts(header->target_len[tid],
                                    weightMapQual > 0.0);
      }
      if (lastTid != tid || lastMinPos < pos - 2500) {
        if (lastTid != tid) {
          lastMinPos = -1;
        } else {
          lastMinPos = pos - 2000;
        }
        if (tempMates != NULL)
          tempMates->erase(tid, lastMinPos);
      }
      lastTid = tid;
      lastPos = pos;

      if ((b->core.flag & BAM_FUNMAP) == 0) {
        // check the end soft clip
        if (!CheckRead::checkEnd(b, header.get())) {
          b = CheckRead::fixEndClip(b, header.get());
        }
        // This read is mapped, apply its depth
        ReadStatistics rs;
        const char *refseq = referenceSequences.empty() || b->core.tid < 0 ||
                                     ((b->core.flag & BAM_FUNMAP) == BAM_FUNMAP)
                                 ? NULL
                                 : referenceSequences[b->core.tid].data();
        CountType overlapAdjusted,
            overlapRaw = caldepth(b, noDepthCounts, -1, refseq, 0,
                                  &rs); // just quickly calculate the overlap
        if (readStats != NULL && !referenceSequences.empty() &&
            b->core.tid >= 0) {
          writeReadStats(*readStats, b, rs,
                         referenceSequences[b->core.tid].data());
        }

        bool failedPctId = rs.getPctId() < percentIdentity;
        // validate this read is a good match
        if (failedPctId) {
          if (unmappedFastq != NULL && !printed) {
            // write & track the name
            if ((b->core.flag & BAM_FPAIRED) == BAM_FPAIRED) {
              string baseName = BamUtils::getBaseName(b);
              if (tempMates != NULL) {
                NameBamMap::iterator it = tempMates->find(baseName);
                if (it != tempMates->end()) {
                  BamUtils::writeFastq(*unmappedFastq, b, it->second);
                  tempMates->erase(it);
                  readIds.erase(baseName);
                  printed = true;
                }
              }
            }
            if (!printed) {
              BamUtils::writeFastqOrStorePair(*unmappedFastq, b, readIds);
              printed = true;
            }
          }
          continue;
        }

        if (depthCounts || !includeEdgeBases) {
          // now apply the overlap to baseCounts and adjust for edges
          overlapAdjusted = caldepth(
              b, depthCounts, header->target_len[tid], refseq,
              includeEdgeBases ? 0 : std::min(maxEdgeBases, avgRead / 3));
        } else {
          overlapAdjusted = overlapRaw;
        }
        contigDepths[tid] += overlapAdjusted;
        readsWellMapped++;

        // check for edge read
        if (unmappedFastq != NULL && !printed && check->edgeRead(b)) {
          // print out any edge reads, no name tracking
          BamUtils::writeFastqOrStorePair(*unmappedFastq, b, readIds);
          printed = true;
        }

        if (unmappedFastq != NULL &&
            (b->core.flag & BAM_FPAIRED) == BAM_FPAIRED) {

          string baseName = BamUtils::getBaseName(b);
          // check for mate edge effects
          if (!printed && check->edgeMateRead(b)) {
            // mate is edge and will be printed, not name tracking

            if (tempMates != NULL) {
              NameBamMap::iterator it = tempMates->find(baseName);
              if (it != tempMates->end()) {
                BamUtils::writeFastq(*unmappedFastq, b, it->second);
                tempMates->erase(it);
                readIds.erase(baseName);
                printed = true;
              }
            }
            if (!printed) {
              BamUtils::writeFastqOrStorePair(*unmappedFastq, b, readIds);
              printed = true;
            }
          }
          if (!printed) {
            // print if the pair is already stored to print
            bool stored = BamUtils::writeFastqOrStorePair(*unmappedFastq, b,
                                                          readIds, true);
            printed = true;
            if (!stored && tempMates != NULL) {
              tempMates->insert(baseName, b);
            }
          }
        } // if unmappedFastq && unmapped

        if (!pairedContigsFile.empty() &&
            (b->core.flag & (BAM_FPAIRED | BAM_FMUNMAP)) == BAM_FPAIRED &&
            tid >= 0 && b->core.mtid >= 0) {
          assert(tid < (int)pairedContigs.size());
          PairedCountType &pairedCounts = pairedContigs[tid];
          PairedCountType::iterator it = pairedCounts.find(b->core.mtid);
          if (it != pairedCounts.end()) {
            it->second += overlapRaw;
          } else {
            pairedCounts.insert(
                it, PairedCountType::value_type(b->core.mtid, overlapRaw));
          }

          hasAnyPairedContigs = true;
        }
      } // if mapped
      if (tempMates != NULL)
        delete tempMates;

    } // while read the bam file

    if (unmappedFastq != NULL)
      delete unmappedFastq;

    if (mappedKmersStats != NULL && ourMappedKmersStats != NULL) {
#pragma omp critical(MAPPED_KMERS_STATS)
      { *ourMappedKmersStats += *mappedKmersStats; }
      delete mappedKmersStats;
    }

#pragma omp critical(OUTPUT)
    cerr << "Thread " << threadNum << " finished reading bam " << bamIdx << ": "
         << myBam.getBamName() << endl;

    if (!isSorted) // skip
      continue;

    myBam.getBamCache().putBam(b);
    myBam.getBamCache().putBam(lastBam);

    // calculate the statistics for the last contig
    if (intraDepthVariance) {
      while (lastTid >= 0 && lastTid < header->n_targets) {
        assert(bamContigVariances.size() > bamIdx &&
               bamContigVariances[bamIdx].size() > lastTid);
        bamContigVariances[bamIdx][lastTid] = calculateVarianceContig(
            header.get(), lastTid, depthCounts,
            includeEdgeBases ? 0 : std::min(maxEdgeBases, avgRead / 3),
            weightMapQual, normalizeWeightMapQual);
        if (!readGCStats.empty() && depthCounts &&
            contigDepths[lastTid] / header->target_len[lastTid] >
                minContigDepth) {
          addGCCounts(readGCStats, refGCWindows[lastTid], gcWindow,
                      depthCounts);
        }

        if (showDepth)
          *depthFile << getContigDepthByBase(
              header.get(), lastTid, depthCounts,
              includeEdgeBases ? 0 : std::min(maxEdgeBases, avgRead / 3),
              weightMapQual);
        lastTid++;
        lastPos = 0;
        if (lastTid < header->n_targets) {
          depthCounts.resetBaseCounts(header->target_len[lastTid],
                                      weightMapQual > 0.0);
        }
      }
    } // intraDepthVariance

    if (checkpoint) {
#pragma omp critical(OUTPUT)
      cerr << "Writing partial output for bam " << bamIdx << ": "
           << myBam.getBamName() << " as " << checkpoint_depth << endl;
      string tmp = checkpoint_depth + ".tmp";
      ofstream out(tmp);
      assert(contigDepths == bamContigDepths[bamIdx].get());

      for (int i = 0; i < header->n_targets; i++) {
        if (intraDepthVariance) {
          auto &bcv = bamContigVariances[bamIdx];
          auto &cv = bcv[i];
          out.write((const char *)&cv, sizeof(VarianceType));
        } else {
          out.write((const char *)(contigDepths + i), sizeof(*contigDepths));
        }
      }
      threadWorkingContigDepths[threadNum].swap(bamContigDepths[bamIdx]);
      if (intraDepthVariance) {
        auto &bcv = bamContigVariances[bamIdx];
        threadWorkingVariance[threadNum].swap(bcv);
      }
      out.close();
      if (rename(tmp.c_str(), checkpoint_depth.c_str()) != 0) {
        cerr << "Could not rename " << tmp << " to " << checkpoint_depth << "! "
             << strerror(errno);
        exit(1);
      }
    } // checkpoint

#pragma omp critical(OUTPUT)
    cerr << "Thread " << threadNum << " finished: " << myBam.getBamName()
         << " with " << readCounts << " reads and " << readsWellMapped
         << " readsWellMapped (" << 100. * readsWellMapped / readCounts << "%)"
         << endl;
    if (readStats != NULL) {
      IOThreadBuffer::flush(*readStats);
    }
    if (!unmappedFastqFile.empty()) {
      writeUnmapedFastqFile(myBam, unmappedFastqFile, header, contigLengthPass,
                            contigDepthPass, bamUnmappedFastqof[bamIdx],
                            bamReadIds[bamIdx]);
    }
    if (bamIdx > 0) {
#pragma omp critical(OUTPUT)
      cerr << "Closing bam " << bamIdx << ": " << myBam.getBamFile() << endl;
      myBam.close();
    }
    if (minAvgRead > avgRead) minAvgRead = avgRead;
  } // foreach bamIdx
  workingDepthCounts.clear();
  if (!isSorted) {
    cerr << "Please execute 'samtools sort' on unsorted input bam files and "
            "try again!"
         << endl;
    exit(1);
  }

  if (readStats != NULL) {
    IOThreadBuffer::close(*readStats);
    delete readStats;
    readStats = NULL;
  }

  if (ourMappedKmersStats != NULL && !outputKmersFile.empty()) {
    SafeOfstream outkmers(outputKmersFile.c_str());
    ourMappedKmersStats->writeHeader(outkmers);
    ourMappedKmersStats->write(outkmers);
    delete ourMappedKmersStats;
    ourMappedKmersStats = NULL;
  }

  // output the matrix
  streambuf *buf;
  ofstream of;
  if (!outputTableFile.empty() || outputTableFile.compare("-") != 0) {
    of.open(outputTableFile.c_str());
    buf = of.rdbuf();
  } else {
    buf = cout.rdbuf();
  }
  ostream out(buf);
  if (checkpoint) {
    cerr << "All partial depths are written, combining partials ionto "
         << outputTableFile << endl;
    printDepthTableHeader(out, bamFilePaths, intraDepthVariance);
    vector<ifstream> partials;
    partials.reserve(partialFiles.size());
    for (auto &checkpoint_file : partialFiles) {
      partials.emplace_back(checkpoint_file);
    }
    for (int contigIdx = 0; contigIdx < header->n_targets; contigIdx++) {
      if (!contigLengthPass[contigIdx])
        continue;

      auto contigLen = header->target_len[contigIdx];
      ContigDepth contigDepth(partials.size(), contigLen, includeEdgeBases,
                              std::min(maxEdgeBases , minAvgRead / 3), intraDepthVariance);
      int bamIdx = 0;
      for (auto &in : partials) {
        if (!in.good())
          throw;
        if (intraDepthVariance) {
          VarianceType variance;
          in.read((char *)&variance, sizeof(VarianceType));
          contigDepth.addDepth(averageReadSize[bamIdx], variance);
        } else {
          CountType depth;
          in.read((char *)&depth, sizeof(CountType));
          contigDepth.addDepth(averageReadSize[bamIdx], depth);
        }
        if (!in.eof() && !in.good())
          throw;
        bamIdx++;
      }
      contigDepthPass[contigIdx] = contigDepth.printDepth(
          out, header->target_name[contigIdx], minContigDepth);
    }
  } else {
    cerr << "Creating depth matrix file: " << outputTableFile << endl;

    printDepthTable(out, bamFilePaths, intraDepthVariance, bamContigDepths,
                    averageReadSize, percentIdentity, bamContigVariances,
                    header.get(), contigLengthPass, contigDepthPass,
                    minContigDepth, includeEdgeBases, std::min(maxEdgeBases, minAvgRead / 3));
  }
  out.flush();
  of.close();

  bamContigDepths.clear();
  bamContigVariances.clear();

  if (checkpoint)
    for (auto &checkpoint_file : partialFiles)
      unlink(checkpoint_file.c_str());

  if (!referenceFastaFile.empty() && !unmappedFastqFile.empty()) {
    string shredFileName = unmappedFastqFile + "-contigShreds.fasta";
    SafeOfstream shredsOf(shredFileName.c_str());
    cerr << "Outputing shredded contigs to " << shredFileName << endl;
    KseqReader reference(referenceFastaFile);
    int32_t contigIdx = 0;
    while (reference.hasNext()) {
      if (contigLengthPass[contigIdx] && contigDepthPass[contigIdx]) {
        shredFasta(shredsOf, reference.getName(), reference.getSeq(),
                   shredLength, shredDepth, shredLength * 0.10);
      }
      contigIdx++;
    }
  }

  if (!outputGCFile.empty() && !refGCWindows.empty()) {
    for (int contigIdx = 0; contigIdx < (int)refGCWindows.size(); contigIdx++) {
      if (contigDepthPass[contigIdx]) {
        for (int j = 0; j < (int)refGCWindows[contigIdx].size(); j++) {
          refGC[refGCWindows[contigIdx][j]]++;
        }
      }
    }
    SafeOfstream outGC(outputGCFile.c_str());
    outGC << "GC\tRef\tReads\tCoverage\tMean\tVariance\n";
    for (int i = 0; i <= 100; i++) {
      outGC << i << "\t" << refGC[i] << "\t"
            << readGCStats[i].mean() * readGCStats[i].size() << "\t"
            << (refGC[i] > 0
                    ? readGCStats[i].mean() * readGCStats[i].size() / refGC[i]
                    : 0)
            << "\t" << readGCStats[i].mean() << "\t"
            << readGCStats[i].variance() << "\n";
    }
  }

  // output pairedContigs lowerTriangle
  if (!pairedContigsFile.empty() && hasAnyPairedContigs) {
    cerr << "Creating pairedContigs matrix file: " << pairedContigsFile << endl;

    SafeOfstream of(pairedContigsFile.c_str());
    printPairedContigs(of, numThreads, bamPairedContigs, header.get());
  }
  if (!pairedContigsFile.empty() && !hasAnyPairedContigs) {
    cerr << "The data files have no paired contigs to report" << endl;
  }

  cerr << "Closing last bam file" << endl;
  // close the last bam file
  bams[0].close();
  cerr << "Finished" << endl;

  return 0;
}
