public Question PopQuestion(IQuestionKey keyToUseForReference)
            {
                ResolveDeletionsAndAdditions();

                PhraseCustomization questionToInsert;

                try
                {
                    questionToInsert = AdditionsAndInsertions.First();
                }
                catch (Exception e)
                {
                    throw new InvalidOperationException("PopQuestion should only be called for a customization that is not tied to any other question and has " +
                                                        "at least one insertion or addition.", e);
                }
                AdditionsAndInsertions.RemoveAt(0);
                var newQ = new Question(keyToUseForReference.ScriptureReference, keyToUseForReference.StartRef, keyToUseForReference.EndRef,
                                        questionToInsert.ModifiedPhrase ?? questionToInsert.OriginalPhrase, questionToInsert.Answer);

                SetExcludedAndModified(newQ);
                if (AllAnswers != null && AllAnswers.Any())                 // Note: If there are any, there are at least 2
                {
                    newQ.Answers = AllAnswers.ToArray();
                }
                return(newQ);
            }
            public void Add(PhraseCustomization pc)
            {
                switch (pc.Type)
                {
                case PhraseCustomization.CustomizationType.AdditionAfter:
                case PhraseCustomization.CustomizationType.InsertionBefore:
                    if (AdditionsAndInsertions.Any(a => a.ModifiedPhrase == pc.ModifiedPhrase || a.Type == pc.Type))
                    {
                        m_isResolved = false;
                    }
                    AdditionsAndInsertions.Add(pc);
                    break;

                case PhraseCustomization.CustomizationType.Deletion:
                    Deletions.Add(pc);
                    if (Deletions.Count > 1)
                    {
                        m_isResolved = false;
                    }
                    break;

                case PhraseCustomization.CustomizationType.Modification:
                    if (Modification != null)
                    {
                        throw new InvalidOperationException("Only one modified version of a question/phrase is permitted. Question/phrase '" + pc.OriginalPhrase +
                                                            "' has already been modified as '" + Modification.ModifiedPhrase + "'. Value of subsequent modification attempt was: '" +
                                                            pc.ModifiedPhrase + "'.");
                    }
                    Modification = pc;
                    break;
                }
            }
            public bool IsInsertionAtOrBeforeReference(IQuestionKey keyToUseForReference, IEnumerable <Question> existingQuestionsInCategory, bool inclusive)
            {
                var addition = AdditionsAndInsertions.FirstOrDefault();

                return(addition != null && addition.Key.IsAtOrBeforeReference(keyToUseForReference, inclusive) &&
                       !existingQuestionsInCategory.Any(q => q.Matches(addition.Key)));
            }
            private void FinishResolvingIfNoMorePairsCanBeDeleted()
            {
                if (Deletions.Any() && AdditionsAndInsertions.Count > Deletions.Count)
                {
                    return;
                }
                if (AdditionsAndInsertions.Count <= 1)
                {
                    if (Deletions.Count > 1)
                    {
                        // This should probably be an exception, but maybe we can recover...
                        Deletions.RemoveRange(1, Deletions.Count - 1);
                        Debug.Fail($"There were more deletions than additions for {Deletions.Single().Key}.");
                    }
                }
                else if (Deletions.Count <= 1)
                {
                    // REVIEW: We're assuming that the first (remaining) addition is the "base" one (i.e., any other non-duplicates will
                    // be hanging off of it as an insertion or addition). If this is not true, we'll need to look through the list to find
                    // the first one whose OriginalPhrase is not the ModifiedPhrase of any other addition/insertion in the list.
                    var baseAddition = AdditionsAndInsertions[0].ModifiedPhrase;
                    var i            = 0;
                    while (i + 1 < AdditionsAndInsertions.Count)
                    {
                        // We assume that earlier ones in the list are older versions whose answers are less likely to be the most desirable
                        // one, so we delete ealier ones first so that the last one survives (and its answer will be inserted first in the
                        // list. Sadly, this is probably the best we can do.
                        var iNewBase = AdditionsAndInsertions.FindIndex(i + 1, a => a.ModifiedPhrase == baseAddition);
                        if (iNewBase < 0)
                        {
                            break;
                        }
                        RemoveAddition(i);
                        i = iNewBase;
                    }
                }
                else
                {
                    return;
                }

                if (AllAnswers.Any())
                {
                    var bestAnswer = AdditionsAndInsertions.First().Answer;
                    if (!String.IsNullOrWhiteSpace(bestAnswer) && !AllAnswers.Any(a => a.Contains(bestAnswer)))
                    {
                        AllAnswers.Insert(0, bestAnswer);
                    }
                    else if (AllAnswers.Count == 1)
                    {
                        AdditionsAndInsertions.First().Answer = AllAnswers[0];
                        AllAnswers = null;
                    }
                }

                m_isResolved = true;
            }
            private void RemoveAddition(int iAdditionToRemove)
            {
                var answer = AdditionsAndInsertions[iAdditionToRemove].Answer;

                AdditionsAndInsertions.RemoveAt(iAdditionToRemove);
                if (!String.IsNullOrWhiteSpace(answer) && !AllAnswers.Any(a => a.Contains(answer)) && !AdditionsAndInsertions.Where(b => b.Answer != null).Any(c => c.Answer.Contains(answer)))
                {
                    AllAnswers.Add(answer);
                }
            }
            private void ResolveDeletionsAndAdditions()
            {
                if (m_isResolved)
                {
                    return;
                }

                AllAnswers = new List <string>();

                FinishResolvingIfNoMorePairsCanBeDeleted();

                int iDel = 0;

                // Pass 1: Exact match between deletion and addition on ModifiedPhrase
                while (!m_isResolved && iDel < Deletions.Count)
                {
                    iDel = Deletions.Skip(iDel).IndexOf(d => !String.IsNullOrEmpty(d.ModifiedPhrase));
                    if (iDel < 0)
                    {
                        break;
                    }
                    // Prefer to delete additions that don't have answers
                    int iAdditionToRemove = AdditionsAndInsertions.IndexOf(a => a.ModifiedPhrase == Deletions[iDel].ModifiedPhrase && String.IsNullOrEmpty(a.Answer));
                    if (iAdditionToRemove < 0)
                    {
                        iAdditionToRemove = AdditionsAndInsertions.IndexOf(a => a.ModifiedPhrase == Deletions[iDel].ModifiedPhrase);
                    }
                    if (iAdditionToRemove >= 0)
                    {
                        RemoveDeletionAndAdditionPair(iDel, iAdditionToRemove);
                    }
                }
                iDel = 0;
                var iDelOrig = iDel;

                // Pass 2: Neither deletion nor addition have ModifiedPhrase set
                while (!m_isResolved && iDel < Deletions.Count)
                {
                    iDel = Deletions.Skip(iDel).IndexOf(d => String.IsNullOrEmpty(d.ModifiedPhrase));
                    if (iDel < 0)
                    {
                        break;
                    }
                    // Prefer to delete additions that don't have answers
                    int iAdditionToRemove = AdditionsAndInsertions.IndexOf(a => (String.IsNullOrEmpty(a.ModifiedPhrase) || a.ModifiedPhrase == a.OriginalPhrase) && String.IsNullOrEmpty(a.Answer));
                    if (iAdditionToRemove < 0)
                    {
                        iAdditionToRemove = AdditionsAndInsertions.IndexOf(a => String.IsNullOrEmpty(a.ModifiedPhrase) || a.ModifiedPhrase == a.OriginalPhrase);
                    }
                    if (iAdditionToRemove >= 0)
                    {
                        RemoveDeletionAndAdditionPair(iDel, iAdditionToRemove);
                    }
                    else if (iDel == iDelOrig)
                    {
                        break;
                    }
                    iDelOrig = iDel;
                }
                // Pass 3: Pair 'em up and blow 'em away
                while (!m_isResolved)
                {
                    // Prefer to delete additions that don't have answers
                    int iAdditionToRemove = AdditionsAndInsertions.IndexOf(a => String.IsNullOrEmpty(a.Answer));
                    if (iAdditionToRemove < 0)
                    {
                        iAdditionToRemove = 0;
                    }
                    RemoveDeletionAndAdditionPair(0, iAdditionToRemove);
                }
            }
            public void ApplyToQuestion(Question question)
            {
                ResolveDeletionsAndAdditions();

                SetExcludedAndModified(question);

                var duplicate = AdditionsAndInsertions.SingleOrDefault(q => q.OriginalPhrase == q.ModifiedPhrase && q.OriginalPhrase == question.PhraseInUse);

                if (duplicate != null)
                {
                    var newAnswer = duplicate.Answer;
                    if (newAnswer.Length == 0)
                    {
                        newAnswer = null;
                    }
                    // Adding an exactly identical question.
                    if (question.IsExcluded)
                    {
                        // Okay, so this is a true replacement, but if the replacement doesn't have an answer, it probably doesn't make sense, so let's ignore it
                        if (newAnswer == null)
                        {
                            question.IsExcluded = false;
                            AdditionsAndInsertions.Clear();
                        }
                    }
                    else
                    {
                        if (newAnswer != null)
                        {
                            // We don't allow exact duplicate questions, so this can't be treated as a real addition. We're just changing the answer.
                            if (question.Answers.Length == 0)
                            {
                                question.Answers = new[] { newAnswer }
                            }
                            ;
                            else
                            {
                                question.Answers[0] = newAnswer;
                            }
                        }
                        AdditionsAndInsertions.Remove(duplicate);
                    }
                }
                // TODO: Support adding (not just replacing) answers?
                // TODO Support modifying (and replacing?) notes.

                var insertion = AdditionsAndInsertions.SingleOrDefault(a => a.Type == PhraseCustomization.CustomizationType.InsertionBefore);

                if (insertion != null)
                {
                    question.InsertedQuestionBefore = new Question(question.ScriptureReference, question.StartRef, question.EndRef,
                                                                   insertion.ModifiedPhrase, insertion.Answer);
                }

                var addition = AdditionsAndInsertions.SingleOrDefault(a => a.Type == PhraseCustomization.CustomizationType.AdditionAfter);

                if (addition != null)
                {
                    question.AddedQuestionAfter = new Question(question.ScriptureReference, question.StartRef, question.EndRef,
                                                               addition.ModifiedPhrase, addition.Answer);
                }
            }