/// <summary>
        /// Read in the current transition groups's precursors and try to modernize the XML so that it's in terms of a common neutral
        /// molecule with adducts, while preserving the precursor mz values
        /// </summary>
        /// <param name="peptide">Neutral molecule associated with current reader position, may be replaced during this call</param>
        /// <returns>Reader positioned at end of current precursor opening, but using cached and possibly altered XML</returns>
        public XmlReader Read(ref Peptide peptide)
        {
            if (_precursorRawDetails.Count == 0)
            {
                return(ReadAhead.CreateXmlReader()); // No precursors to process
            }

            // Find a formula for the parent molecule, if possible
            ProposeMoleculeWithCommonFormula(peptide);

            // Deal with mass-only declarations if no formulas were found
            if (string.IsNullOrEmpty(ProposedMolecule.Formula))
            {
                return(HandleMassOnlyDeclarations(ref peptide));
            }

            // Check to see if description already makes sense
            if (_precursorRawDetails.TrueForAll(d =>
                                                !Adduct.IsNullOrEmpty(d._nominalAdduct) &&
                                                Math.Abs(d._declaredMz - d._nominalAdduct.MzFromNeutralMass(ProposedMolecule.MonoisotopicMass)) <= MzToler))
            {
                return(UpdatePeptideAndInsertAdductsInXML(ref peptide, _precursorRawDetails.Select(d => d._nominalAdduct)));
            }

            // See if there are simple adducts that work with common formula
            if (DeriveAdductsForCommonFormula(peptide))
            {
                // Found a molecule that works for all precursors that declared a formula (though that may not be all precursors, users create highly variable documents)
                return(UpdatePeptideAndInsertAdductsInXML(ref peptide, _precursorRawDetails.Select(d => d._proposedAdduct)));
            }

            // Done adjusting parent molecule, must come up with rest of adducts to make mz and base molecule agree
            return(ConsiderMassShiftAdducts(ref peptide));
        }
        private XmlReader ConsiderMassShiftAdducts(ref Peptide peptide)
        {
            var commonFormula = ProposedMolecule.Formula;
            var molMass       = ProposedMolecule.MonoisotopicMass;

            foreach (var d in _precursorRawDetails.Where(d => Adduct.IsNullOrEmpty(d._proposedAdduct)))
            {
                if (!Adduct.IsNullOrEmpty(d._nominalAdduct) && Math.Abs(d._declaredMz - d._nominalAdduct.MzFromNeutralMass(molMass)) <= MzToler)
                {
                    d._proposedAdduct = d._nominalAdduct;
                }
            }

            // Final attempt at assigning adducts in the event that no adjustment to neutral molecule has worked out
            foreach (var d in _precursorRawDetails.Where(d => Adduct.IsNullOrEmpty(d._proposedAdduct)))
            {
                // So far we haven't hit on a common molecules + adducts scenario that explains the given mz
                // We're forced to add weird mass shifts to adducts
                for (var retry = 0; retry < 4; retry++)
                {
                    Adduct adduct;
                    switch (retry)
                    {
                    case 0:     // Try [M+H] style
                        adduct = Adduct.ProtonatedFromFormulaDiff(d._formulaUnlabeled, commonFormula, d._declaredCharge)
                                 .ChangeIsotopeLabels(d._labels);
                        break;

                    case 1:     // Try [M+] style
                        adduct = Adduct.FromFormulaDiff(d._formulaUnlabeled, commonFormula, d._declaredCharge)
                                 .ChangeIsotopeLabels(d._labels);
                        break;

                    default:
                        adduct = retry == 2
                                ? Adduct.ProtonatedFromFormulaDiff(d._formulaUnlabeled, commonFormula,
                                                                   d._declaredCharge)                            // Try [M1.2345+H] style
                                : Adduct.FromFormulaDiff(d._formulaUnlabeled, commonFormula, d._declaredCharge); // Try [M1.2345+] style
                        var ionMass         = adduct.MassFromMz(d._declaredMz, MassType.Monoisotopic);
                        var unexplainedMass = ionMass - molMass;
                        adduct = adduct.ChangeIsotopeLabels(unexplainedMass, _mzDecimalPlaces);
                        break;
                    }
                    if (Math.Abs(d._declaredMz - adduct.MzFromNeutralMass(molMass)) <= MzToler)
                    {
                        d._proposedAdduct = adduct;
                        break;
                    }
                }
            }
            Assume.IsTrue(_precursorRawDetails.TrueForAll(d => !Adduct.IsNullOrEmpty(d._proposedAdduct)),
                          "Unable to to deduce adducts and common molecule for " + peptide); // Not L10N

            // We found a satisfactory set of adducts and neutral molecule, update the "peptide" and
            // modify the readahead buffer with the new adduct info
            return(UpdatePeptideAndInsertAdductsInXML(ref peptide, _precursorRawDetails.Select(d => d._proposedAdduct)));
        }
        private bool DeriveAdductsForCommonFormula(Peptide peptide)
        {
            // Try to come up with a set of adducts to common formula that explain the declared mz values
            var success = false;

            if (_precursorRawDetails.TrueForAll(d => Adduct.IsNullOrEmpty(d._nominalAdduct)))
            {
                // No explicit adducts, just charges.
                // Start with the most common scenario, which is that the user meant (de)protonation
                // See if we can arrive at a common formula ( by adding or removing H) that works with all charges as (de)protonations
                // N.B. the parent molecule may well be completely unrelated to the children, as users were allowed to enter anything they wanted
                var commonFormula          = ProposedMolecule.Formula;
                var precursorsWithFormulas = _precursorRawDetails.Where(d => !string.IsNullOrEmpty(d._formulaUnlabeled)).ToList();
                foreach (var detail in precursorsWithFormulas)
                {
                    var revisedCommonFormula = Molecule.AdjustElementCount(commonFormula, BioMassCalc.H, -detail._declaredCharge);
                    var adjustedMolecule     = new CustomMolecule(revisedCommonFormula, peptide.CustomMolecule.Name);
                    var mass = adjustedMolecule.MonoisotopicMass;
                    if (precursorsWithFormulas.TrueForAll(d =>
                    {
                        d._proposedAdduct = Adduct.ProtonatedFromFormulaDiff(d._formulaUnlabeled, revisedCommonFormula, d._declaredCharge)
                                            .ChangeIsotopeLabels(d._labels);
                        return(Math.Abs(d._declaredMz - d._proposedAdduct.MzFromNeutralMass(mass)) <= MzToler);
                    }))
                    {
                        ProposedMolecule = adjustedMolecule;
                        success          = true;
                        break;
                    }
                }
                success &= _precursorRawDetails.All(d => !Adduct.IsNullOrEmpty(d._proposedAdduct));
                if (!success)
                {
                    foreach (var d in _precursorRawDetails)
                    {
                        d._proposedAdduct = Adduct.EMPTY;
                    }
                }
            }
            return(success);
        }
        private void ProposeMoleculeWithCommonFormula(Peptide peptide)
        {
            // Examine any provided formulas (including parent molecule and/or precursor ions) and find common basis
            ProposedMolecule = peptide.CustomMolecule;
            var precursorsWithFormulas = _precursorRawDetails.Where(d => !string.IsNullOrEmpty(d._formulaUnlabeled)).ToList();
            var parentFormula          = peptide.CustomMolecule.UnlabeledFormula;
            var commonFormula          = string.IsNullOrEmpty(parentFormula)
                ? BioMassCalc.MONOISOTOPIC.FindFormulaIntersectionUnlabeled(
                precursorsWithFormulas.Select(p => p._formulaUnlabeled))
                : parentFormula;

            // Check for consistent and correctly declared precursor formula+adduct
            var precursorsWithFormulasAndAdducts = precursorsWithFormulas.Where(d => !Adduct.IsNullOrEmpty(d._nominalAdduct)).ToList();

            if (precursorsWithFormulasAndAdducts.Any() &&
                precursorsWithFormulas.All(
                    d => d._formulaUnlabeled.Equals(precursorsWithFormulasAndAdducts[0]._formulaUnlabeled)))
            {
                commonFormula = precursorsWithFormulasAndAdducts[0]._formulaUnlabeled;
            }

            if (!string.IsNullOrEmpty(commonFormula))
            {
                var parentComposition = Molecule.ParseExpression(commonFormula);
                // Check for children proposing to label more atoms than parent provides, adjust parent as needed
                foreach (var precursor in _precursorRawDetails.Where(d => d._labels != null))
                {
                    foreach (var kvpIsotopeCount in precursor._labels)
                    {
                        var unlabeled = BioMassCalc.UnlabeledFromIsotopeSymbol(kvpIsotopeCount.Key);
                        int parentCount;
                        parentComposition.TryGetValue(unlabeled, out parentCount);
                        if (kvpIsotopeCount.Value > parentCount)
                        {
                            // Child proposes to label more of an atom than the parent possesses (seen in the wild) - update the parent
                            commonFormula =
                                Molecule.AdjustElementCount(commonFormula, unlabeled, kvpIsotopeCount.Value - parentCount);
                            parentComposition = Molecule.ParseExpression(commonFormula);
                        }
                    }
                }
                if (!Equals(peptide.CustomMolecule.Formula, commonFormula))
                {
                    ProposedMolecule = new CustomMolecule(commonFormula, peptide.CustomMolecule.Name);
                }
            }
        }