/// <summary>
        /// Executes the cross-link search for LC-IMS-TOF data.
        /// </summary>
        /// <param name="settings">Settings object to control parameters for cross-linking.</param>
        /// <param name="fastAFile">The FileInfo object for the FASTA file containg all protein sequences you want to search.</param>
        /// <param name="featureFile">The FileInfo object for the LC-IMS-MS features file, created by the LC-IMS-MS Feature Finder. (email [email protected] for more info)</param>
        /// <param name="peaksFile">The FileInfo object for the Isotopic Peaks file, created by DeconTools. (email [email protected] for more info)</param>
        /// <returns>An enumerable of CrossLinkResult objects.</returns>
        public static IList<CrossLinkResult> Execute(CrossLinkSettings settings, FileInfo fastAFile, FileInfo featureFile, FileInfo peaksFile)
        {
            IEnumerable<ISequence> sequenceEnumerable;
            List<LcImsMsFeature> featureList;
            List<IsotopicPeak> peakEnumerable;

            Console.WriteLine();

            try
            {

                // Read in FASTA File
                var fastAParser = new FastAParser(fastAFile.FullName);
                sequenceEnumerable = fastAParser.Parse();
                Console.WriteLine("FASTA file:     " + GetRelativePath(fastAFile.FullName));
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error reading the FASTA file: " + ex.Message);
                throw;
            }

            try
            {
                // Read in LC-IMS-MS Features
                featureList = LcImsMsFeatureReader.ReadFile(featureFile);
                Console.WriteLine("Features file:  " + GetRelativePath(featureFile.FullName));
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error reading the LCMSFeatures file: " + ex.Message);
                throw;
            }

            try
            {
                // Read in Isotopic Peaks (not Isotopic Profile)
                peakEnumerable = IsotopicPeakReader.ReadFile(peaksFile);
                Console.WriteLine("Peaks file:     " + GetRelativePath(peaksFile.FullName));
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error reading the Isotopic Peaks file: " + ex.Message);
                throw;
            }

            // Now call the executor that expects the opbjects instead of the file locations
            return Execute(settings, sequenceEnumerable, featureList, peakEnumerable);
        }
        static int Main(string[] args)
        {
            try
            {

                // Handles a bug in .NET console applications
                var handle = Process.GetCurrentProcess().MainWindowHandle;
                SetConsoleMode(handle, ENABLE_EXTENDED_FLAGS);

                // Get the version number
                var assembly = Assembly.GetExecutingAssembly();
                var assemblyVersion = assembly.GetName().Version.ToString();

                Console.WriteLine("CrossLinkingIMS Console Application Version " + assemblyVersion);

                var commandLineUtil = new CommandLineUtil(caseSensitiveSwitchNames: false);

                // Make sure we have parameters. If not, then show the user the proper syntax
                var doParametersExist = commandLineUtil.ParseCommandLine();
                if (!doParametersExist)
                {
                    ShowSyntax();
                    return -1;
                }

                // Get the Feature File Location
                string featureFileLocation;
                if (!commandLineUtil.RetrieveValueForParameter("f", out featureFileLocation))
                {
                    Console.WriteLine("-f switch is missing");
                    ShowSyntax();
                    return -2;
                }

                if (!FileExists(featureFileLocation))
                {
                    return -4;
                }

                var featureFile = new FileInfo(featureFileLocation);

                // Get the Peaks File Location
                string peaksFileLocation;
                if (!commandLineUtil.RetrieveValueForParameter("p", out peaksFileLocation))
                {
                    Console.WriteLine("-p switch is missing");
                    ShowSyntax();
                    return -2;
                }

                if (!FileExists(peaksFileLocation))
                {
                    return -4;
                }

                var peaksFile = new FileInfo(peaksFileLocation);

                // Get the FastA File Location
                string fastAFileLocation;
                if (!commandLineUtil.RetrieveValueForParameter("fasta", out fastAFileLocation))
                {
                    Console.WriteLine("-fasta switch is missing");
                    ShowSyntax();
                    return -2;
                }

                if (!FileExists(fastAFileLocation))
                {
                    return -4;
                }

                var fastAFile = new FileInfo(fastAFileLocation);

                // Get the PPM Mass Tolerance
                string massToleranceString;
                if (!commandLineUtil.RetrieveValueForParameter("ppm", out massToleranceString))
                {
                    Console.WriteLine("-ppm switch is missing");
                    ShowSyntax();
                    return -2;
                }

                var massTolerance = double.Parse(massToleranceString);

                // Get the Max Missed Cleavages
                string maxMissedCleavagesString;
                commandLineUtil.RetrieveValueForParameter("c", out maxMissedCleavagesString);

                if (string.IsNullOrEmpty(maxMissedCleavagesString))
                    maxMissedCleavagesString = "1";

                var maxMissedCleavages = int.Parse(maxMissedCleavagesString);

                // Get the Partially Tryptic Flag
                string trypticString;
                commandLineUtil.RetrieveValueForParameter("t", out trypticString);

                string staticDeltaMassString;
                var staticDeltaMass = 0.0;
                if (commandLineUtil.RetrieveValueForParameter("static", out staticDeltaMassString))
                {
                    double.TryParse(staticDeltaMassString, out staticDeltaMass);
                }

                var useC13 = true;
                var useN15 = true;

                if (commandLineUtil.IsParameterPresent("c13off"))
                    useC13 = false;

                if (commandLineUtil.IsParameterPresent("n15off"))
                    useN15 = false;

                if (!(useC13 || useN15) && Math.Abs(staticDeltaMass) < float.Epsilon)
                {
                    Console.WriteLine("If you use both -C13off and -N15off, you must use -Static:MassDiff to specify a static mass difference");
                    Thread.Sleep(1500);
                    return -3;
                }

                var settings = new CrossLinkSettings(massTolerance, maxMissedCleavages, trypticString, useC13, useN15, staticDeltaMass);

                // Get the Output File Location
                string outputFileLocation;
                if (!commandLineUtil.RetrieveValueForParameter("o", out outputFileLocation))
                {
                    if (featureFile.DirectoryName != null && featureFile.DirectoryName == peaksFile.DirectoryName)
                        outputFileLocation = Path.Combine(featureFile.DirectoryName, "crossLinkResults.csv");
                    else
                        outputFileLocation = "crossLinkResults.csv";
                }

                // Run the cross-linking application
                Console.WriteLine("Executing...");
                var crossLinkResults = CrossLinkingImsController.Execute(settings, fastAFile, featureFile, peaksFile);

                var outputFileInfo = new FileInfo(outputFileLocation);

                // Output the results
                Console.WriteLine("Outputting " + crossLinkResults.Count().ToString("#,##0") + " results to \n" + outputFileInfo.FullName);
                CrossLinkUtil.OutputCrossLinkResults(crossLinkResults, outputFileInfo);

                return 0;
            }
            catch (Exception ex)
            {
                Console.WriteLine();
                Console.WriteLine("=========================================");
                Console.WriteLine("Error: " + ex.Message);
                Console.WriteLine(ex.StackTrace);
                Console.WriteLine("=========================================");

                Thread.Sleep(2000);
                return -10;
            }
        }
        /// <summary>
        /// Executes the cross-link search for LC-IMS-TOF data.
        /// </summary>
        /// <param name="settings">Settings object to control parameters for cross-linking.</param>
        /// <param name="proteinSequenceEnumerable">IEnumerable of protein sequences, as a .NET Bio ISequence object.</param>
        /// <param name="featureList">List of LC-IMS-MS Features, as LcImsMsFeature.</param>
        /// <param name="peakList">List of Isotopic Peaks, as IsotopicPeak.</param>
        /// <returns>An enumerable of CrossLinkResult objects.</returns>
        public static IList<CrossLinkResult> Execute(
            CrossLinkSettings settings,
            IEnumerable<ISequence> proteinSequenceEnumerable,
            List<LcImsMsFeature> featureList,
            List<IsotopicPeak> peakList)
        {
            var massToleranceBase = settings.MassTolerance;
            var maxMissedCleavages = settings.MaxMissedCleavages;
            var digestionRule = settings.TrypticType;

            CrossLinkUtil.StaticDeltaMass = settings.StaticDeltaMass;
            CrossLinkUtil.UseC13 = settings.UseC13;
            CrossLinkUtil.UseN15 = settings.UseN15;

            Console.WriteLine();
            Console.WriteLine("Mass Tolerance:          " + massToleranceBase + " ppm");
            Console.WriteLine("Max missed cleavages:    " + maxMissedCleavages);
            Console.WriteLine("Digestion rule:          " + settings.TrypticType );
            Console.WriteLine("Delta mass uses C13:     " + settings.UseC13);
            Console.WriteLine("Delta mass uses N15:     " + settings.UseN15);
            Console.WriteLine("Static delta mass addon: " + settings.StaticDeltaMass + " Da");

            // Used for finding Isotopic Profiles in the data
                var msFeatureFinder = new BasicTFF();

            var crossLinkList = new List<CrossLink>();
            var lastProgress = DateTime.UtcNow;
            var proteinsProcessed = 0;

            // Create CrossLink objects from all of the protein sequences
            foreach (var proteinSequence in proteinSequenceEnumerable)
            {
                var proteinSequenceString = new string(proteinSequence.Select((a => (char)a)).ToArray());
                var proteinId = proteinSequence.ID;

                // Get a List of Peptides from the Protein Sequence
                var peptideList = SequenceUtil.DigestProtein(proteinSequenceString, digestionRule, maxMissedCleavages);

                // Find all possible cross links from the peptide list
                var crossLinkEnumerable = CrossLinkUtil.GenerateTheoreticalCrossLinks(peptideList, proteinSequenceString, proteinId);
                crossLinkList.AddRange(crossLinkEnumerable);

                proteinsProcessed++;
                if (DateTime.UtcNow.Subtract(lastProgress).TotalSeconds >= 15)
                {
                    lastProgress = DateTime.UtcNow;
                    Console.WriteLine("Creating cross linked peptide list; " + proteinsProcessed + " proteins processed");
                }

            }

            Console.WriteLine("Sorting cross-linked peptides");

            // Sort the CrossLinks by mass so that the results are ordered in a friendly way
            IEnumerable<CrossLink> orderedCrossLinkEnumerable = crossLinkList.OrderBy(x => x.Mass);

            // Sort Feature by mass so we can use binary search
            featureList = featureList.OrderBy(x => x.MassMonoisotopic).ToList();

            // Set up a Feature Comparer and Peak Comparer to use for binary search later on
            var featureComparer = new AnonymousComparer<LcImsMsFeature>((x, y) => x.MassMonoisotopic.CompareTo(y.MassMonoisotopic));
            var peakComparer = new AnonymousComparer<IsotopicPeak>((x, y) => x.ScanLc != y.ScanLc ? x.ScanLc.CompareTo(y.ScanLc) : x.ScanIms != y.ScanIms ? x.ScanIms.CompareTo(y.ScanIms) : x.Mz.CompareTo(y.Mz));

            // Sort the Isotopic Peaks by LC Scan, IMS Scan, and m/z to set them up for binary search later on
            peakList.Sort(peakComparer);

            var crossLinkResultList = new List<CrossLinkResult>();
            var totalCandidatePeptides = crossLinkList.Count;

            Console.WriteLine("Searching isotopic data vs. " + totalCandidatePeptides.ToString("#,##0") + " candidate cross-linked peptides");
            lastProgress = DateTime.UtcNow;
            var crosslinkCandidatesProcessed = 0;

            // Search the data for the existence of cross-links
            foreach (var crossLink in orderedCrossLinkEnumerable)
            {
                // Calculate mass tolerance to use for binary search
                var massTolerance = massToleranceBase * crossLink.Mass / GeneralConstants.PPM_DIVISOR;

                var lowFeature = new LcImsMsFeature { MassMonoisotopic = crossLink.Mass - massTolerance };
                var highFeature = new LcImsMsFeature { MassMonoisotopic = crossLink.Mass + massTolerance };

                var lowFeaturePosition = featureList.BinarySearch(lowFeature, featureComparer);
                var highFeaturePosition = featureList.BinarySearch(highFeature, featureComparer);

                lowFeaturePosition = lowFeaturePosition < 0 ? ~lowFeaturePosition : lowFeaturePosition;
                highFeaturePosition = highFeaturePosition < 0 ? ~highFeaturePosition : highFeaturePosition;

                // Iterate over all LC-IMS-MS Features that match the Unmodified cross-link mass
                for (var i = lowFeaturePosition; i < highFeaturePosition; i++)
                {
                    var feature = featureList[i];

                    // Search for a mass shift in each of the LC Scans the unmodified cross-link mass was found
                    for (var currentScanLc = feature.ScanLcStart; currentScanLc <= feature.ScanLcEnd; currentScanLc++)
                    {
                        var crossLinkResult = new CrossLinkResult(crossLink, feature, currentScanLc);

                        var candidatePeaks = PeakUtil.FindCandidatePeaks(peakList, feature.MzMonoisotopic, currentScanLc, feature.ScanImsRep);
                        var massShiftList = crossLink.MassShiftList;
                        var shiftedMassList = new List<double>();

                        // Calculate the shifted mass values that we want to search for
                        switch (massShiftList.Count)
                        {
                            case 1:
                                {
                                    var firstNewMass = feature.MassMonoisotopic + massShiftList[0];
                                    shiftedMassList.Add(firstNewMass);
                                }
                                break;
                            case 2:
                                {
                                    var firstNewMass = feature.MassMonoisotopic + massShiftList[0];
                                    var secondNewMass = feature.MassMonoisotopic + massShiftList[1];
                                    var thirdNewMass = feature.MassMonoisotopic + massShiftList[0] + massShiftList[1];

                                    shiftedMassList.Add(firstNewMass);
                                    shiftedMassList.Add(secondNewMass);
                                    shiftedMassList.Add(thirdNewMass);
                                }
                                break;
                        }

                        // Search for shifted mass values in Isotopic Peaks
                        foreach (var shiftedMass in shiftedMassList)
                        {
                            var shiftedMz = (shiftedMass / feature.ChargeState) + GeneralConstants.MASS_OF_PROTON;

                            // Create theoretical Isotopic Peaks that will later form a theoretical Isotopic Profile
                            var theoreticalPeakList = new List<MSPeak> { new MSPeak { XValue = shiftedMz, Height = 1 } };
                            for (double k = 1; k < 4; k++)
                            {
                                theoreticalPeakList.Add(new MSPeak { XValue = shiftedMz + (k * 1.003 / feature.ChargeState), Height = (float)(1.0 - (k / 4)) });
                                theoreticalPeakList.Add(new MSPeak { XValue = shiftedMz - (k * 1.003 / feature.ChargeState), Height = (float)(1.0 - (k / 4)) });
                            }

                            // Sort peaks by m/z
                            var sortPeaksQuery = from peak in theoreticalPeakList
                                                 orderby peak.XValue
                                                 select peak;

                            // Create a theoretical Isotopic Profile for DeconTools to search for
                            var isotopicProfile = new IsotopicProfile
                            {
                                MonoIsotopicMass = shiftedMass,
                                MonoPeakMZ = shiftedMz,
                                ChargeState = feature.ChargeState,
                                Peaklist = sortPeaksQuery.ToList()
                            };

                            // Search for the theoretical Isotopic Profile
                            var foundProfile = msFeatureFinder.FindMSFeature(candidatePeaks, isotopicProfile, massToleranceBase, false);

                            /*
                             * It is possible that the set mono pass of the previous theoretical distribution was the right-most peak of the actual distribution
                             * If so, we should be able to shift the theoretical distribution over to the left and find the actual distribution
                             */
                            if (foundProfile == null)
                            {
                                foreach (var msPeak in sortPeaksQuery)
                                {
                                    msPeak.XValue -= (1.003 / feature.ChargeState);
                                }

                                isotopicProfile = new IsotopicProfile
                                {
                                    MonoIsotopicMass = shiftedMass - 1.003,
                                    MonoPeakMZ = shiftedMz - (1.003 / feature.ChargeState),
                                    ChargeState = feature.ChargeState,
                                    Peaklist = sortPeaksQuery.ToList()
                                };

                                foundProfile = msFeatureFinder.FindMSFeature(candidatePeaks, isotopicProfile, massToleranceBase, false);
                            }

                            // Add to results, even if we did not find it.
                            var didFindProfile = foundProfile != null;
                            crossLinkResult.MassShiftResults.KvpList.Add(new KeyValuePair<double, bool>(shiftedMass, didFindProfile));
                        }

                        crossLinkResultList.Add(crossLinkResult);
                    }
                }

                crosslinkCandidatesProcessed++;
                if (DateTime.UtcNow.Subtract(lastProgress).TotalSeconds >= 10)
                {
                    lastProgress = DateTime.UtcNow;
                    var percentComplete = crosslinkCandidatesProcessed / (double)totalCandidatePeptides * 100;

                    Console.WriteLine("Searching isotopic data; " + percentComplete.ToString("0.0") + "% complete");
                }

            }

            return crossLinkResultList;
        }