text: //
  // $Id$
  //
  //
  // Original author: Darren Kessner <darren@proteowizard.org>
  //
  // Copyright 2008 Spielberg Family Center for Applied Proteomics
  //   Cedars-Sinai Medical Center, Los Angeles, California  90048
  //
  // 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 
  //
  // 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.
  //
  
  
  #include "pwiz_tools/common/FullReaderList.hpp"
  #include "pwiz/data/msdata/MSDataFile.hpp"
  #include "pwiz/data/msdata/MSDataMerger.hpp"
  #include "pwiz/data/msdata/IO.hpp"
  #include "pwiz/data/msdata/SpectrumInfo.hpp"
  #include "pwiz/utility/misc/IterationListener.hpp"
  #include "pwiz/utility/misc/IntegerSet.hpp"
  #include "pwiz/analysis/spectrum_processing/SpectrumListFactory.hpp"
  #include "pwiz/analysis/chromatogram_processing/ChromatogramListFactory.hpp"
  #include "pwiz/Version.hpp"
  #include "pwiz/data/msdata/Version.hpp"
  #include "pwiz/analysis/Version.hpp"
  #include "boost/program_options.hpp"
  #include "boost/format.hpp"
  #include "pwiz/utility/misc/Filesystem.hpp"
  #include "pwiz/utility/misc/Std.hpp"
  #include <boost/filesystem/detail/utf8_codecvt_facet.hpp>
  
  using namespace pwiz::cv;
  using namespace pwiz::data;
  using namespace pwiz::msdata;
  using namespace pwiz::analysis;
  using namespace pwiz::util;
  
  ostream* os_ = &cout;
  
  
  /// Holds the results of the parseCommandLine function. 
  struct Config : public Reader::Config
  {
      vector<string> filenames;
      vector<string> filters;
      vector<string> chromatogramFilters;
      string outputPath;
      string extension;
      string outputFile;
      bool verbose;
      MSDataFile::WriteConfig writeConfig;
      string contactFilename;
      bool merge;
      IntegerSet runIndexSet;
      bool stripLocationFromSourceFiles;
  
      Config()
          : outputPath("."), verbose(false), merge(false)
      {
          simAsSpectra = false;
          srmAsSpectra = false;
          combineIonMobilitySpectra = false;
          unknownInstrumentIsError = true;
          stripLocationFromSourceFiles = false;
      }
  
      string outputFilename(const string& inputFilename, const MSData& inputMSData) const;
  };
  
  
  string Config::outputFilename(const string& filename, const MSData& msd) const
  {
      string runId = msd.run.id;
  
      // if necessary, adjust runId so it makes a suitable filename
      if (!outputFile.empty())
      {
          runId = outputFile;
      }
      if (runId.empty())
          runId = bfs::basename(filename);
      else
      {
          string extension = bal::to_lower_copy(bfs::extension(runId));
          if (extension == ".mzml" ||
              extension == ".mzxml" ||
              extension == ".xml" ||
              extension == ".mgf" ||
              extension == ".ms1" ||
              extension == ".cms1" ||
              extension == ".ms2" ||
              extension == ".cms2" ||
              extension == ".mz5")
              runId = bfs::basename(runId);
      }
  
      // this list is for Windows; it's a superset of the POSIX list
      string illegalFilename = "\\/*:?<>|\"";
      for(char& c : runId)
          if (illegalFilename.find(c) != string::npos)
              c = '_';
  
      bfs::path newFilename = runId + extension;
      bfs::path fullPath = bfs::path(outputPath) / newFilename;
      return fullPath.string(); 
  }
  
  
  ostream& operator<<(ostream& os, const Config& config)
  {
      os << "format: " << config.writeConfig << endl;
      os << "outputPath: " << config.outputPath << endl;
      os << "extension: " << config.extension << endl; 
      os << "contactFilename: " << config.contactFilename << endl;
      os << "runIndexSet: " << config.runIndexSet << endl;
      os << endl;
  
      os << "spectrum list filters:\n  ";
      copy(config.filters.begin(), config.filters.end(), ostream_iterator<string>(os,"\n  "));
      os << endl;
  
      os << "chromatogram list filters:\n  ";
      copy(config.chromatogramFilters.begin(), config.chromatogramFilters.end(), ostream_iterator<string>(os, "\n  "));
      os << endl;
  
      os << "filenames:\n  ";
      copy(config.filenames.begin(), config.filenames.end(), ostream_iterator<string>(os,"\n  "));
      os << endl;
  
      return os;
  }
  
  static double string_to_double( const std::string& str )
  {
  	errno = 0;
  	const char* stringToConvert = str.c_str();
  	const char* endOfConversion = stringToConvert;
  	double value = STRTOD( stringToConvert, const_cast<char**>(&endOfConversion) );
  	if( value == 0.0 && stringToConvert == endOfConversion ) // error: conversion could not be performed
  		throw bad_lexical_cast(); // not a double
  	if (*endOfConversion)
  		throw bad_lexical_cast(); // started out as a double but became something else - like "10foo.raw"
  	return value;
  }
  
  /// Parses command line arguments to setup execution parameters, and
  /// contructs help text. Inputs are the arguments to main. A filled
  /// Config object is returned.
  Config parseCommandLine(int argc, char** argv)
  {
      namespace po = boost::program_options;
  
      ostringstream usage;
      usage << "Usage: msconvert [options] [filemasks]\n"
            << "Convert mass spec data file formats.\n"
  #ifndef _MSC_VER
            << "\n"
            << "Note: the use of mass spec vendor DLLs is not enabled in this \n"
            << "(non-MSVC) build, this means no Thermo, Bruker, Waters etc input.\n"
  #endif
            << "\n"
            << "Return value: # of failed files.\n"
            << "\n";
          
      Config config;
      string filelistFilename;
      string configFilename;
  
      bool format_text = false;
      bool format_mzML = false;
      bool format_mzXML = false;
      bool format_MGF = false;
      bool format_MS1 = false;
      bool format_CMS1 = false;
      bool format_MS2 = false;
      bool format_CMS2 = false;
      bool format_mz5 = false;
      bool precision_32 = false;
      bool precision_64 = false;
      bool mz_precision_32 = false;
      bool mz_precision_64 = false;
      bool intensity_precision_32 = false;
      bool intensity_precision_64 = false;
      bool noindex = false;
      bool zlib = false;
      bool gzip = false;
      bool ms_numpress_all = false; // if true, use this numpress compression with default tolerance
      double ms_numpress_linear = -1; // if >= 0, use this numpress linear compression with this tolerance
  	string ms_numpress_linear_str; // input as text, to help with the "msconvert --numpresslinear foo.raw" case
      string ms_numpress_linear_default = (boost::format("%4.2g") % BinaryDataEncoder_default_numpressLinearErrorTolerance).str();
      bool ms_numpress_pic = false; // if true, use this numpress Pic compression
      double ms_numpress_slof = -1; // if >= 0, use this numpress slof compression with this tolerance
      double ms_numpress_linear_abs_tolerance = -1; // if >= 0, use this numpress linear compression with this absolute Th tolerance
  	string ms_numpress_linear_abs_tolerance_str; // input as text
  	string ms_numpress_linear_abs_tolerance_str_default("-1"); // input as text
  	string ms_numpress_slof_str; // input as text, to help with the "msconvert --numpressslof foo.raw" case
      string ms_numpress_slof_default = (boost::format("%4.2g") % BinaryDataEncoder_default_numpressSlofErrorTolerance).str();
      string runIndexSet;
      bool detailedHelp = false;
  
      pair<int, int> consoleBounds = get_console_bounds(); // get platform-specific console bounds, or default values if an error occurs
  
      po::options_description od_config("Options", consoleBounds.first);
      od_config.add_options()
          ("filelist,f",
              po::value<string>(&filelistFilename),
              ": specify text file containing filenames")
          ("outdir,o",
              po::value<string>(&config.outputPath)->default_value(config.outputPath),
              ": set output directory ('-' for stdout) [.]")
          ("config,c", 
              po::value<string>(&configFilename),
              ": configuration file (optionName=value)")
          ("outfile",
              po::value<string>(&config.outputFile)->default_value(config.outputFile),
           ": Override the name of output file.")
          ("ext,e",
              po::value<string>(&config.extension)->default_value(config.extension),
              ": set extension for output files [mzML|mzXML|mgf|txt"
  #ifndef WITHOUT_MZ5
              "|mz5"
  #endif
              "]")
          ("mzML",
              po::value<bool>(&format_mzML)->zero_tokens(),
              ": write mzML format [default]")
          ("mzXML",
              po::value<bool>(&format_mzXML)->zero_tokens(),
              ": write mzXML format")
  #ifndef WITHOUT_MZ5
          ("mz5",
              po::value<bool>(&format_mz5)->zero_tokens(),
              ": write mz5 format")
  #endif
          ("mgf",
              po::value<bool>(&format_MGF)->zero_tokens(),
              ": write Mascot generic format")
          ("text",
              po::value<bool>(&format_text)->zero_tokens(),
              ": write ProteoWizard internal text format")
          ("ms1",
              po::value<bool>(&format_MS1)->zero_tokens(),
              ": write MS1 format")
          ("cms1",
              po::value<bool>(&format_CMS1)->zero_tokens(),
              ": write CMS1 format")
          ("ms2",
              po::value<bool>(&format_MS2)->zero_tokens(),
              ": write MS2 format")
          ("cms2",
              po::value<bool>(&format_CMS2)->zero_tokens(),
              ": write CMS2 format")
          ("verbose,v",
              po::value<bool>(&config.verbose)->zero_tokens(),
              ": display detailed progress information")
          ("64",
              po::value<bool>(&precision_64)->zero_tokens(),
              ": set default binary encoding to 64-bit precision [default]")
          ("32",
              po::value<bool>(&precision_32)->zero_tokens(),
              ": set default binary encoding to 32-bit precision")
          ("mz64",
              po::value<bool>(&mz_precision_64)->zero_tokens(),
              ": encode m/z values in 64-bit precision [default]")
          ("mz32",
              po::value<bool>(&mz_precision_32)->zero_tokens(),
              ": encode m/z values in 32-bit precision")
          ("inten64",
              po::value<bool>(&intensity_precision_64)->zero_tokens(),
              ": encode intensity values in 64-bit precision")
          ("inten32",
              po::value<bool>(&intensity_precision_32)->zero_tokens(),
              ": encode intensity values in 32-bit precision [default]")
          ("noindex",
              po::value<bool>(&noindex)->zero_tokens(),
              ": do not write index")
          ("contactInfo,i",
              po::value<string>(&config.contactFilename),
              ": filename for contact info")
          ("zlib,z",
              po::value<bool>(&zlib)->zero_tokens(),
              ": use zlib compression for binary data")
          ("numpressLinear",
              po::value<std::string>(&ms_numpress_linear_str)->implicit_value(ms_numpress_linear_default),
              ": use numpress linear prediction compression for binary mz and rt data (relative accuracy loss will not exceed given tolerance arg, unless set to 0)")
          ("numpressLinearAbsTol",
              po::value<std::string>(&ms_numpress_linear_abs_tolerance_str)->implicit_value(ms_numpress_linear_abs_tolerance_str_default),
              ": desired absolute tolerance for linear numpress prediction (e.g. use 1e-4 for a mass accuracy of 0.2 ppm at 500 m/z, default uses -1.0 for maximal accuracy). Note: setting this value may substantially reduce file size, this overrides relative accuracy tolerance.")
          ("numpressPic",
              po::value<bool>(&ms_numpress_pic)->zero_tokens(),
              ": use numpress positive integer compression for binary intensities (absolute accuracy loss will not exceed 0.5)")
          ("numpressSlof",
              po::value<std::string>(&ms_numpress_slof_str)->implicit_value(ms_numpress_slof_default),
              ": use numpress short logged float compression for binary intensities (relative accuracy loss will not exceed given tolerance arg, unless set to 0)")
          ("numpressAll,n",
              po::value<bool>(&ms_numpress_all)->zero_tokens(),
              ": same as --numpressLinear --numpressSlof (see https://github.com/fickludd/ms-numpress for more info)")
          ("gzip,g",
              po::value<bool>(&gzip)->zero_tokens(),
              ": gzip entire output file (adds .gz to filename)")
          ("filter",
              po::value< vector<string> >(&config.filters),
              ": add a spectrum list filter")
          ("chromatogramFilter",
              po::value< vector<string> >(&config.chromatogramFilters),
              ": add a chromatogram list filter")
          ("merge",
              po::value<bool>(&config.merge)->zero_tokens(),
              ": create a single output file from multiple input files by merging file-level metadata and concatenating spectrum lists")
          ("runIndexSet",
              po::value<string>(&runIndexSet),
              ": for multi-run sources, select only the specified run indices")
          ("simAsSpectra",
              po::value<bool>(&config.simAsSpectra)->zero_tokens(),
              ": write selected ion monitoring as spectra, not chromatograms")
          ("srmAsSpectra",
              po::value<bool>(&config.srmAsSpectra)->zero_tokens(),
              ": write selected reaction monitoring as spectra, not chromatograms")
          ("combineIonMobilitySpectra",
              po::value<bool>(&config.combineIonMobilitySpectra)->zero_tokens(),
              ": write all drift bins/scans in a frame/block as one spectrum instead of individual spectra")
          ("acceptZeroLengthSpectra",
              po::value<bool>(&config.acceptZeroLengthSpectra)->zero_tokens(),
              ": some vendor readers have an efficient way of filtering out empty spectra, but it takes more time to open the file")
          ("ignoreUnknownInstrumentError",
              po::value<bool>(&config.unknownInstrumentIsError)->zero_tokens()->default_value(!config.unknownInstrumentIsError),
              ": if true, if an instrument cannot be determined from a vendor file, it will not be an error")
          ("stripLocationFromSourceFiles",
              po::value<bool>(&config.stripLocationFromSourceFiles)->zero_tokens(),
              ": if true, sourceFile elements will be stripped of location information, so the same file converted from different locations will produce the same mzML")
          ("help",
              po::value<bool>(&detailedHelp)->zero_tokens(),
              ": show this message, with extra detail on filter options")
          ;
  
      // handle positional arguments
  
      const char* label_args = "args";
  
      po::options_description od_args;
      od_args.add_options()(label_args, po::value< vector<string> >(), "");
  
      po::positional_options_description pod_args;
      pod_args.add(label_args, -1);
     
      po::options_description od_parse;
      od_parse.add(od_config).add(od_args);
  
      // parse command line
  
      po::variables_map vm;
      po::store(po::command_line_parser(argc, (char**)argv).
                options(od_parse).positional(pod_args).run(), vm);
      po::notify(vm);
  
      // negate unknownInstrumentIsError value since command-line parameter (ignoreUnknownInstrumentError) and the Config parameters use inverse semantics
      config.unknownInstrumentIsError = !config.unknownInstrumentIsError;
  
      if (!runIndexSet.empty())
          config.runIndexSet.parse(runIndexSet);
  
      // append options description to usage string
  
      usage << od_config;
  
      // extra usage
  
      usage << SpectrumListFactory::usage(detailedHelp, "run this command with --help to see more detail", consoleBounds.first) << endl;
  
      usage << "Examples:\n"
            << endl
            << "# convert data.RAW to data.mzML\n"
            << "msconvert data.RAW\n"
            << endl
            << "# convert data.RAW to data.mzXML\n"
            << "msconvert data.RAW --mzXML\n"
            << endl
            << "# put output file in my_output_dir\n"
            << "msconvert data.RAW -o my_output_dir\n"
            << endl
            << "# combining options to create a smaller mzML file, much like the old ReAdW converter program\n"
            << "msconvert data.RAW --32 --zlib --filter \"peakPicking true 1-\" --filter \"zeroSamples removeExtra\"\n"
            << endl
            << "# extract scan indices 5...10 and 20...25\n"
            << "msconvert data.RAW --filter \"index [5,10] [20,25]\"\n"
            << endl
            << "# extract MS1 scans only\n"
            << "msconvert data.RAW --filter \"msLevel 1\"\n"
            << endl
            << "# extract MS2 and MS3 scans only\n"
            << "msconvert data.RAW --filter \"msLevel 2-3\"\n"
            << endl
            << "# extract MSn scans for n>1\n"
            << "msconvert data.RAW --filter \"msLevel 2-\"\n"
            << endl
            << "# apply ETD precursor mass filter\n"
            << "msconvert data.RAW --filter ETDFilter\n"
            << endl
            << "# remove non-flanking zero value samples\n"
            << "msconvert data.RAW --filter \"zeroSamples removeExtra\"\n"
            << endl
            << "# remove non-flanking zero value samples in MS2 and MS3 only\n"
            << "msconvert data.RAW --filter \"zeroSamples removeExtra 2 3\"\n"
            << endl
            << "# add missing zero value samples (with 5 flanking zeros) in MS2 and MS3 only\n"
            << "msconvert data.RAW --filter \"zeroSamples addMissing=5 2 3\"\n"
            << endl
            << "# keep only HCD spectra from a decision tree data file\n"
            << "msconvert data.RAW --filter \"activation HCD\"\n"
            << endl
            << "# keep the top 42 peaks or samples (depending on whether spectra are centroid or profile):\n"
            << "msconvert data.RAW --filter \"threshold count 42 most-intense\"\n"
            << endl
            << "# multiple filters: select scan numbers and recalculate precursors\n"
            << "msconvert data.RAW --filter \"scanNumber [500,1000]\" --filter \"precursorRecalculation\"\n"
            << endl
            << "# multiple filters: apply peak picking and then keep the bottom 100 peaks:\n"
            << "msconvert data.RAW --filter \"peakPicking true 1-\" --filter \"threshold count 100 least-intense\"\n"
            << endl
            << "# multiple filters: apply peak picking and then keep all peaks that are at least 50% of the intensity of the base peak:\n"
            << "msconvert data.RAW --filter \"peakPicking true 1-\" --filter \"threshold bpi-relative .5 most-intense\"\n"
            << endl
            << "# use a configuration file\n"
            << "msconvert data.RAW -c config.txt\n"
            << endl
            << "# example configuration file\n"
            << "mzXML=true\n"
            << "zlib=true\n"
            << "filter=\"index [3,7]\"\n"
            << "filter=\"precursorRecalculation\"\n"
            << endl
            << endl
  
            << "Questions, comments, and bug reports:\n"
            << "https://github.com/ProteoWizard\n"
            << "support@proteowizard.org\n"
            << "\n"
            << "ProteoWizard release: " << pwiz::Version::str() << " (" << pwiz::Version::LastModified() << ")" << endl
            << "Build date: " << __DATE__ << " " << __TIME__ << endl;
  
      if ((argc <= 1) || detailedHelp)
          throw usage_exception(usage.str().c_str());
  
      // parse config file if required
  
      if (!configFilename.empty())
      {
          ifstream is(configFilename.c_str());
  
          if (is)
          {
              *os_ << "Reading configuration file " << configFilename << "\n\n";
              po::store(parse_config_file(is, od_config), vm);
              po::notify(vm);
          }
          else
          {
              *os_ << "Unable to read configuration file " << configFilename << "\n\n";
          }
      }
  
      // remember filenames from command line
  
      if (vm.count(label_args))
      {
          config.filenames = vm[label_args].as< vector<string> >();
  
          // expand the filenames by globbing to handle wildcards
          vector<bfs::path> globbedFilenames;
          for(const string& filename : config.filenames)
          {
              if (isHTTP(filename))
                  globbedFilenames.push_back(filename);
              else if (expand_pathmask(bfs::path(filename), globbedFilenames) == 0)
                  cout << "[msconvert] no files found matching \"" << filename << "\"" << endl;
          }
  
          config.filenames.clear();
          for(const bfs::path& filename : globbedFilenames)
              config.filenames.push_back(filename.string());
      }
  
      // parse filelist if required
  
      if (!filelistFilename.empty())
      {
          ifstream is(filelistFilename.c_str());
          while (is)
          {
              string filename;
              getline(is, filename);
              if (is) config.filenames.push_back(filename);
          }
      }
  
      // check stuff
  
  	if (ms_numpress_slof_str.length()) // was that a numerical arg to --numpressSlof, or a filename?
  	{
  		try 
  		{
  			ms_numpress_slof = string_to_double(ms_numpress_slof_str); 
  		}
  		catch(...) {
  			config.filenames.push_back(ms_numpress_slof_str); // actually that was a filename
              ms_numpress_slof = BinaryDataEncoder_default_numpressSlofErrorTolerance;
  		}
  	}
  	if (ms_numpress_linear_abs_tolerance_str.length()) // this argument needs to be numerical
  	{
  		ms_numpress_linear_abs_tolerance = string_to_double(ms_numpress_linear_abs_tolerance_str);
  	}
  	if (ms_numpress_linear_str.length()) // was that a numerical arg to --numpressLinear, or a filename?
  	{
  		try 
  		{
  			ms_numpress_linear = string_to_double(ms_numpress_linear_str); 
  		}
  		catch(...) {
  			config.filenames.push_back(ms_numpress_linear_str); // actually that was a filename
              ms_numpress_linear = BinaryDataEncoder_default_numpressLinearErrorTolerance;
  		}
  	}
  
      if (config.filenames.empty())
          throw user_error("[msconvert] No files specified.");
  
      int count = format_text + format_mzML + format_mzXML + format_MGF + format_MS2 + format_CMS2 + format_mz5;
      if (count > 1) throw user_error("[msconvert] Multiple format flags specified.");
      if (format_text) config.writeConfig.format = MSDataFile::Format_Text;
      if (format_mzML) config.writeConfig.format = MSDataFile::Format_mzML;
      if (format_mzXML) config.writeConfig.format = MSDataFile::Format_mzXML;
      if (format_MGF) config.writeConfig.format = MSDataFile::Format_MGF;
      if (format_MS1) config.writeConfig.format = MSDataFile::Format_MS1;
      if (format_CMS1) config.writeConfig.format = MSDataFile::Format_CMS1;
      if (format_MS2) config.writeConfig.format = MSDataFile::Format_MS2;
      if (format_CMS2) config.writeConfig.format = MSDataFile::Format_CMS2;
      if (format_mz5) config.writeConfig.format = MSDataFile::Format_MZ5;
  
      config.writeConfig.gzipped = gzip; // if true, file is written as .gz
  
      if (config.extension.empty())
      {
          switch (config.writeConfig.format)
          {
              case MSDataFile::Format_Text:
                  config.extension = ".txt";
                  break;
              case MSDataFile::Format_mzML:
                  config.extension = ".mzML";
                  break;
              case MSDataFile::Format_mzXML:
                  config.extension = ".mzXML";
                  break;
              case MSDataFile::Format_MGF:
                  config.extension = ".mgf";
                  break;
              case MSDataFile::Format_MS1:
                  config.extension = ".ms1";
                  break;
              case MSDataFile::Format_CMS1:
                  config.extension = ".cms1";
                  break;
              case MSDataFile::Format_MS2:
                  config.extension = ".ms2";
                  break;
              case MSDataFile::Format_CMS2:
                  config.extension = ".cms2";
                  break;
              case MSDataFile::Format_MZ5:
  #ifdef WITHOUT_MZ5
                  throw user_error("[msconvert] Not built with mz5 support."); 
  #endif
                  config.extension = ".mz5";
                  break;
              default:
                  throw user_error("[msconvert] Unsupported format."); 
          }
          if (config.writeConfig.gzipped) 
          {
              config.extension += ".gz";
          }
      }
  
      // precision defaults
  
      config.writeConfig.binaryDataEncoderConfig.precision = BinaryDataEncoder::Precision_64;
      config.writeConfig.binaryDataEncoderConfig.precisionOverrides[MS_m_z_array] = BinaryDataEncoder::Precision_64;
      config.writeConfig.binaryDataEncoderConfig.precisionOverrides[MS_intensity_array] = BinaryDataEncoder::Precision_32;
  
      // handle precision flags
  
      if (precision_32 && precision_64 ||
          mz_precision_32 && mz_precision_64 ||
          intensity_precision_32 && intensity_precision_64)
          throw user_error("[msconvert] Incompatible precision flags.");
  
      if (precision_32)
      {
          config.writeConfig.binaryDataEncoderConfig.precision
              = config.writeConfig.binaryDataEncoderConfig.precisionOverrides[MS_m_z_array]
              = config.writeConfig.binaryDataEncoderConfig.precisionOverrides[MS_intensity_array] 
              = BinaryDataEncoder::Precision_32;
      }
      else if (precision_64)
      {
          config.writeConfig.binaryDataEncoderConfig.precision
              = config.writeConfig.binaryDataEncoderConfig.precisionOverrides[MS_m_z_array]
              = config.writeConfig.binaryDataEncoderConfig.precisionOverrides[MS_intensity_array] 
              = BinaryDataEncoder::Precision_64;
      }
  
      if (mz_precision_32)
          config.writeConfig.binaryDataEncoderConfig.precisionOverrides[MS_m_z_array] = BinaryDataEncoder::Precision_32;
      if (mz_precision_64)
          config.writeConfig.binaryDataEncoderConfig.precisionOverrides[MS_m_z_array] = BinaryDataEncoder::Precision_64;
      if (intensity_precision_32)
          config.writeConfig.binaryDataEncoderConfig.precisionOverrides[MS_intensity_array] = BinaryDataEncoder::Precision_32;
      if (intensity_precision_64)
          config.writeConfig.binaryDataEncoderConfig.precisionOverrides[MS_intensity_array] = BinaryDataEncoder::Precision_64;
  
      // other flags
  
      if (noindex)
          config.writeConfig.indexed = false;
  
      if (zlib)
          config.writeConfig.binaryDataEncoderConfig.compression = BinaryDataEncoder::Compression_Zlib;
  
      if ((ms_numpress_slof>=0) && ms_numpress_pic)
          throw user_error("[msconvert] Incompatible compression flags 'numpressPic' and 'numpressSlof'.");
  
      if (ms_numpress_all && ms_numpress_pic)
          throw user_error("[msconvert] Incompatible compression flags 'numpressPic' and 'numpressAll'.");
  
      if (ms_numpress_all) {
          config.writeConfig.binaryDataEncoderConfig.numpressOverrides[MS_m_z_array] = BinaryDataEncoder::Numpress_Linear;
          config.writeConfig.binaryDataEncoderConfig.numpressOverrides[MS_time_array] = BinaryDataEncoder::Numpress_Linear;
          config.writeConfig.binaryDataEncoderConfig.numpressOverrides[MS_intensity_array] = BinaryDataEncoder::Numpress_Slof;
          config.writeConfig.binaryDataEncoderConfig.numpressLinearErrorTolerance = BinaryDataEncoder_default_numpressLinearErrorTolerance;
          config.writeConfig.binaryDataEncoderConfig.numpressSlofErrorTolerance = BinaryDataEncoder_default_numpressSlofErrorTolerance;
      }
      if (ms_numpress_pic) 
      {
          config.writeConfig.binaryDataEncoderConfig.numpressOverrides[MS_intensity_array] = BinaryDataEncoder::Numpress_Pic;
      }    
      if (ms_numpress_slof>=0) 
      {
          config.writeConfig.binaryDataEncoderConfig.numpressOverrides[MS_intensity_array] = BinaryDataEncoder::Numpress_Slof;
          config.writeConfig.binaryDataEncoderConfig.numpressSlofErrorTolerance = ms_numpress_slof;
      }
      if (ms_numpress_linear>=0) 
      {
          config.writeConfig.binaryDataEncoderConfig.numpressOverrides[MS_m_z_array] = BinaryDataEncoder::Numpress_Linear;
          config.writeConfig.binaryDataEncoderConfig.numpressOverrides[MS_time_array] = BinaryDataEncoder::Numpress_Linear;
          config.writeConfig.binaryDataEncoderConfig.numpressLinearErrorTolerance = ms_numpress_linear;
      }
      if (ms_numpress_linear_abs_tolerance>=0) 
      {
          config.writeConfig.binaryDataEncoderConfig.numpressOverrides[MS_m_z_array] = BinaryDataEncoder::Numpress_Linear;
          config.writeConfig.binaryDataEncoderConfig.numpressOverrides[MS_time_array] = BinaryDataEncoder::Numpress_Linear;
          config.writeConfig.binaryDataEncoderConfig.numpressLinearAbsMassAcc = ms_numpress_linear_abs_tolerance;
          // this overrides any relative guarantees as the user specifically wants an absolute mass error guarantee
          config.writeConfig.binaryDataEncoderConfig.numpressLinearErrorTolerance = 0;
      }
  
      return config;
  }
  
  
  void addContactInfo(MSData& msd, const string& contactFilename)
  {
      ifstream is(contactFilename.c_str());
      if (!is)
      {
          cerr << "unable to read contact info: " << contactFilename << endl; 
          return;
      }
  
      Contact contact;
      IO::read(is, contact);
      msd.fileDescription.contacts.push_back(contact);
  }
  
  
  void stripSourceFileLocation(MSData& msd)
  {
      for (const auto& sourceFilePtr : msd.fileDescription.sourceFilePtrs)
          sourceFilePtr->location = "file:///";
  }
  
  
  class UserFeedbackIterationListener : public IterationListener
  {
      std::streamoff longestMessage;
  
      public:
  
      UserFeedbackIterationListener()
      {
          longestMessage = 0;
      }
  
      virtual Status update(const UpdateMessage& updateMessage)
      {
          
          stringstream updateString;
          if (updateMessage.message.empty())
              updateString << updateMessage.iterationIndex + 1 << "/" << updateMessage.iterationCount;
          else
              updateString << updateMessage.message << ": " << updateMessage.iterationIndex + 1 << "/" << updateMessage.iterationCount;
  
          longestMessage = max(longestMessage, (std::streamoff) updateString.tellp());
          updateString << string(longestMessage - updateString.tellp(), ' '); // add whitespace to erase all of the previous line
          *os_ << updateString.str() << "\r" << flush;
  
          // spectrum and chromatogram lists both iterate; put them on different lines
          if (updateMessage.iterationIndex+1 == updateMessage.iterationCount)
              *os_ << endl;
          return Status_Ok;
      }
  };
  
  
  void calculateSourceFilePtrSHA1(const SourceFilePtr& sourceFilePtr)
  {
      calculateSourceFileSHA1(*sourceFilePtr);
  }
  
  
  /// Combines multiple input files into a single MSData object. Called
  /// when the --merge argument is present on the command line.
  int mergeFiles(const vector<string>& filenames, const Config& config, const ReaderList& readers)
  {
      vector<MSDataPtr> msdList;
      int failedFileCount = 0;
  
      ReaderList::Config readerConfig(config);
  
      // Each file is read in separately in MSData objects in the msdList list.
      for(const string& filename : filenames)
      {
          try
          {
              *os_ << "processing file: " << filename << endl;
              readers.read(filename, msdList, readerConfig);
          }
          catch (exception& e)
          {
              ++failedFileCount;
              cerr << "Error reading file " << filename << ":\n" << e.what() << endl;
          }
      }
  
      // handle progress updates if requested
  
      IterationListenerRegistry iterationListenerRegistry;
      // update on the first spectrum, the last spectrum, the 100th spectrum, the 200th spectrum, etc.
      const size_t iterationPeriod = 100;
      iterationListenerRegistry.addListener(IterationListenerPtr(new UserFeedbackIterationListener), iterationPeriod);
      IterationListenerRegistry* pILR = config.verbose ? &iterationListenerRegistry : 0;
  
      if (!config.runIndexSet.empty())
      {
          vector<MSDataPtr> msdListFiltered;
          for (int i = 0; i < msdList.size(); ++i)
              if (config.runIndexSet.contains(i))
                  msdListFiltered.push_back(msdList[i]);
          msdList.swap(msdListFiltered);
  
          if (msdList.empty())
              throw user_error("[msconvert] No runs correspond to the specified indices");
      }
  
      // MSDataMerger handles combining all files in msdList into a single MSDataFile object.
      try
      {
          MSDataMerger msd(msdList);
  
          *os_ << "calculating source file checksums" << endl;
          calculateSHA1Checksums(msd);
  
          if (!config.contactFilename.empty())
              addContactInfo(msd, config.contactFilename);
  
          SpectrumListFactory::wrap(msd, config.filters, pILR);
          ChromatogramListFactory::wrap(msd, config.chromatogramFilters, pILR);
  
          string outputFilename = config.outputFilename("merged-spectra", msd);
          *os_ << "writing output file: " << outputFilename << endl;
  
          if (config.stripLocationFromSourceFiles)
              stripSourceFileLocation(msd);
  
          if (config.outputPath == "-")
              MSDataFile::write(msd, cout, config.writeConfig);
          else
              MSDataFile::write(msd, outputFilename, config.writeConfig, pILR);
      }
      catch (exception& e)
      {
          failedFileCount = (int)filenames.size();
          cerr << "Error merging files: " << e.what() << endl;
      }
  
      return failedFileCount;
  }
  
  /// Handles the reading of a single input file. Called once for each
  /// input file when the --merge arguement is absent.
  void processFile(const string& filename, const Config& config, const ReaderList& readers)
  {
      // read in data file
  
      *os_ << "processing file: " << filename << endl;
  
      // handle progress updates if requested
  
      IterationListenerRegistry iterationListenerRegistry;
      // update on the first spectrum, the last spectrum, the 100th spectrum, the 200th spectrum, etc.
      //const size_t iterationPeriod = 100;
      iterationListenerRegistry.addListenerWithTimer(IterationListenerPtr(new UserFeedbackIterationListener), 0.5);
      IterationListenerRegistry* pILR = config.verbose ? &iterationListenerRegistry : 0;
  
      ReaderList::Config readerConfig(config);
  
      readerConfig.iterationListenerRegistry = pILR;
  
      vector<MSDataPtr> msdList;
      readers.read(filename, msdList, readerConfig);
  
      if (!config.runIndexSet.empty())
      {
          vector<MSDataPtr> msdListFiltered;
          for (int i = 0; i < msdList.size(); ++i)
              if (config.runIndexSet.contains(i))
                  msdListFiltered.push_back(msdList[i]);
          msdList.swap(msdListFiltered);
  
          if (msdList.empty())
              throw user_error("[msconvert] No runs correspond to the specified indices");
      }
  
      for (size_t i=0; i < msdList.size(); ++i)
      {
          MSData& msd = *msdList[i];
          try
          {
              *os_ << "calculating source file checksums" << endl;
              os_->flush();
              calculateSHA1Checksums(msd);
  
              // process the data 
  
              if (!config.contactFilename.empty())
                  addContactInfo(msd, config.contactFilename);
  
              SpectrumListFactory::wrap(msd, config.filters, pILR);
              ChromatogramListFactory::wrap(msd, config.chromatogramFilters, pILR);
  
              // write out the new data file
              string outputFilename = config.outputFilename(filename, msd);
              *os_ << "writing output file: " << outputFilename << endl;
  
              if (config.stripLocationFromSourceFiles)
                  stripSourceFileLocation(msd);
  
              if (config.outputPath == "-")
                  MSDataFile::write(msd, cout, config.writeConfig, pILR);
              else
              {
                  // String compare of filenames is case-sensitive, which is a problem on Windows. bfs::equivalent() fixes this.
                  if (!isHTTP(filename) && (filename == outputFilename || bfs::equivalent(filename, outputFilename)))
                  {
                      throw user_error("[msconvert] Output filepath is the same as input filepath");
                  }
                  MSDataFile::write(msd, outputFilename, config.writeConfig, pILR);
              }
          }
          catch (exception& e)
          {
              cerr << "Error writing run " << (i+1) << " in " << bfs::path(filename).leaf() << ":\n" << e.what() << endl;
          }
      }
      *os_ << endl;
  }
  
  
  /// Handles the high level logic of msconvert. Constructs the output
  /// directory, reads files into memory and writes them out consistent
  /// with the options in the supplied Config.
  int go(const Config& config)
  {
      *os_ << config;
  
      if (!bfs::exists(config.outputPath))
          boost::filesystem::create_directories(config.outputPath);
  
      FullReaderList readers;
  
      int failedFileCount = 0;
  
      if (config.merge)
          failedFileCount = mergeFiles(config.filenames, config, readers);
      else
      {
  
          for (vector<string>::const_iterator it=config.filenames.begin(); 
               it!=config.filenames.end(); ++it)
          {
              try
              {
                  processFile(*it, config, readers);
              }
              catch (exception& e)
              {
                  failedFileCount++;
                  *os_ << e.what() << endl;
                  *os_ << "Error processing file " << *it << "\n\n"; 
              }
          }
      }
  
      return failedFileCount;
  }
  
  
  int main(int argc, char* argv[])
  {
      bnw::args args(argc, argv);
  
      std::locale global_loc = std::locale();
      std::locale loc(global_loc, new bfs::detail::utf8_codecvt_facet);
      bfs::path::imbue(loc);
      cout.imbue(loc);
      cerr.imbue(loc);
  
      try
      {
          Config config = parseCommandLine(argc, argv);
  
          // if redirecting conversion to cout, use cerr for all console output
          if (config.outputPath == "-")
              os_ = &cerr;
  
          return go(config);
      }
      catch (usage_exception& e)
      {
          cerr << e.what() << endl;
          return 0;
      }
      catch (user_error& e)
      {
          cerr << e.what() << endl;
          return 1;
      }
      catch (boost::program_options::error& e)
      {
          cerr << "Invalid command-line: " << e.what() << endl;
          return 1;
      }
      catch (exception& e)
      {
          cerr << e.what() << endl;
      }
      catch (...)
      {
          cerr << "[" << argv[0] << "] Caught unknown exception.\n";
      }
  
      cerr << "Please report this error to support@proteowizard.org.\n"
           << "Attach the command output and this version information in your report:\n"
           << "\n"
           << "ProteoWizard release: " << pwiz::Version::str() << " (" << pwiz::Version::LastModified() << ")" << endl
           << "Build date: " << __DATE__ << " " << __TIME__ << endl;
  
      return 1;
  }
  
  