/*
 Flash: Flashcard program
 Copyright (C) 2005, Niko Matsakis (niko@alum.mit.edu)
 
 This program is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License
 as published by the Free Software Foundation; either version 2
 of the License, or (at your option) any later version.
 
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#import "QuizController.h"
#import "OACoreData.h"
#import "DictionaryDocument.h"
#import "Accessors.h"
#import "OATextField.h"
#import "Preferences.h"
#import "QuizGuess.h"
#import "QuizAnswer.h"
#import "QuizMatching.h"
#import "OACategories.h"
#import "OAKeyboardLayout.h"
#import "OADebug.h"
#import "OAMacros.h"

// Takes two NSManagedObjects representing Connections and sorts
// them by relationship.  We currently sort by the relationship
// name first, then by the "inverse" quality of the connection,
// then by the pointer if still tied.
static NSComparisonResult SortConnectionsByRelationship(id one, id two, void *v) {
    NSManagedObject *a = one, *b = two;
    
    // compare rel name
    NSString *relNames[2] = { [[a typeValue] nameValue], [[b typeValue] nameValue] };
    if (![relNames[0] isEqualToString:relNames[1]])
        return [relNames[0] compare:relNames[1]];
    
    // compare connection ptrs as last resort
    if (a < b)
        return NSOrderedAscending;
    if (a > b)
        return NSOrderedDescending;
    return NSOrderedSame;
}

// Sorts connections by the date of modification of the "from"
// word (in ascending order).
static NSComparisonResult SortConnectionsByDate(id one, id two, void *v) {
    NSManagedObject *a = one, *b = two;
    
    NSDate *dates[2] = { 
        [[a fromValue] creationDateValue],
        [[b fromValue] creationDateValue] };
    
    return [dates[0] compare:dates[1]];
}

@interface QuizController (Private)
- (void) prepareForNextGuess;
- (void) prepareForNextRepeat;
- (QuizAnswer*) currentAnswer;
- (void) prepareNextQuestion;
- (void) computeInitialMistakes;
- (void) computeUnlearnedConnections;
- (void) addGuess:(NSString*)uentry;
- (void) generateReviewConnections;
- (BOOL) isEligibleConnection:(NSManagedObject*)connection;
- (void) limitPreviousMistakes;
- (QuizQuestion*) extractItemAndMakeQuizQuestion:(id)connections extractSelector:(SEL)selector;
@end

@implementation QuizController

#pragma mark -
#pragma mark Accessors for key value observing

- (QuizQuestion*) question
{
    return a_question;
}

- (void) setQuestion:(QuizQuestion*)qw
{
    [a_question release];
    a_question = [qw retain];
}

- (NSArray*) guesses
{
    return a_guesses;
}

- (unsigned int) countOfGuesses
{
    return [a_guesses count];
}

- (QuizGuess *) createNewGuess:(NSString*)guess
{
    [self willChangeValueForKey:@"guesses"];
    QuizGuess *qg = [QuizGuess quizGuessWithGuess:guess];
    [a_guesses addObject:qg];
    [self didChangeValueForKey:@"guesses"];
    return qg;
}

- (void) setGuesses:(NSMutableArray*)array
{
    [a_guesses autorelease];
    a_guesses = [array retain];
}

- (void) purgeQuizQuestion:(QuizQuestion*)wq
{
    [self willChangeValueForKey:@"askedQuestions"];
    [a_askedQuestions removeObject:wq];
    [self didChangeValueForKey:@"askedQuestions"];
    
    [m_mistakesThisRound removeObject:wq];
    [m_previousMistakes removeObject:wq];
    [m_allQuestions removeObject:wq];
    [m_unlearnedConnections removeObject:wq];
    
    if ([self question] == wq)
        [self setQuestion:nil];
}

- (NSSet*) askedQuestions
{
    return a_askedQuestions;
}

- (void) addAskedQuestion:(QuizQuestion*)qq
{
    [self willChangeValueForKey:@"quizWords"];
    [a_askedQuestions addObject:qq];
    [self didChangeValueForKey:@"quizWords"];
}

- (QuizAnswer*) currentAnswer
{
    // The index of the current answer is determined by how many guesses
    // the user has made:
    int curAnswerIndex = [self countOfGuesses];
    return [[[self question] answers] objectAtIndex:curAnswerIndex];
}

#pragma mark -
#pragma mark Init, release, etc

- initWithDictionary:(DictionaryDocument*)document 
                user:(NSManagedObject*)user
            language:(NSManagedObject*)language
          quizLength:(int)quizLength
{
    if ((self = [super initWithWindowNibName:@"Quiz"])) {
        m_model = [[document managedObjectModel] retain];
        m_mctx = [[document managedObjectContext] retain];
        m_dictionary = document;
        m_language = [language retain];
        m_user = [user retain];
        a_askedQuestions = [[NSMutableSet alloc] init];
        m_mistakesThisRound = [[NSMutableSet alloc] init];
        m_previousMistakes = [[NSMutableArray alloc] init];
        m_allQuestions = [[NSMutableSet alloc] init];
        m_allConnectionsAsked = [[NSMutableSet alloc] init];
        m_quizLength = quizLength;
        m_nativeTongue = [[m_user nativeTongueValue] retain];

        [self computeInitialMistakes];
        
        // listen for updates to the managed object context:
        NSManagedObjectContext *mctx = [m_dictionary managedObjectContext];
        NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
        [center addObserver:self 
                   selector:@selector(objectsChanged:) 
                       name:NSManagedObjectContextObjectsDidChangeNotification
                     object:mctx];
    }
    return self;
}

- (void) dealloc
{
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center removeObserver:self];

    [m_unlearnedConnections release];
    [m_model release];
    [m_mctx release];
    [self setQuestion:nil];
    [self setGuesses:nil];
    [m_language release];
    [m_user release];
    [a_askedQuestions release];
    [m_mistakesThisRound release];
    [m_previousMistakes release];
    [m_allQuestions release];
    [m_nativeTongue release];
    [super dealloc];
}

- (void) windowDidLoad
{
    [self showWindow:self];
    [[self window] makeKeyAndOrderFront:self];
}

- (void) awakeFromNib
{
    // set up the NS
    NSTableColumn *col = [_guessesTable tableColumnWithIdentifier:@"correct"];
    NSLevelIndicatorCell *cell = [col dataCell];
    [cell setLevelIndicatorStyle:NSRatingLevelIndicatorStyle];
    [cell setMinValue:0];
    [cell setMaxValue:3];
    
    [self limitPreviousMistakes];
    [self prepareNextQuestion];
}

- (DictionaryDocument *)dictionary
{
    return m_dictionary;
}

- (NSManagedObject *)user
{
    return m_user;
}

#pragma mark -
#pragma mark Monitoring for deleted words

- (void) objectsChanged:(NSNotification*)notification
{
    //NSSet *newkeys = [[notification userInfo] objectForKey:NSInsertedObjectsKey];
    //NSSet *updkeys = [[notification userInfo] objectForKey:NSUpdatedObjectsKey];
    NSSet *delkeys = [[notification userInfo] objectForKey:NSDeletedObjectsKey];

    if ([delkeys count] == 0) {
        OALog (@"QuizController: no objects deleted, uninterested");
        return;
    }
    
    OAINDENTED(@"QuizController: objectsChanged") {
        // We are basically only interested in Connections getting deleted.
        // When objects get deleted, we scan through our list of QuizWords
        // and purge any that are based solely on outdated connections.
        
        [m_allConnectionsAsked filterInPlace:@selector(isNotDeletedObject)];
        [m_unlearnedConnections filterInPlace:@selector(isNotDeletedObject)];
        
        NSMutableSet *deadSet = [m_allQuestions filterSetWith:@selector(pruneAnswers)];
        FOREACH(QuizQuestion *, dead, deadSet) {
            [self purgeQuizQuestion:dead];
        } ENDFOREACH;
        
        if ([self question] == nil)
            // our current question is invalid!  need to select a new one
            [self prepareNextQuestion];
        
        // TODO: Still some uncertainties about what happens if you have extant
        // QuizGuess objects; some of them might need to be purged, etc.
        // TODO: purge from m_allConnectionsAsked, but seems harmless enough.
    } OAUNDENTED;
}

#pragma mark -
#pragma mark Manipulating user interface

// Sets the enabled/disable settings so as to enable
// the user to enter guesses
- (void) enableEntry
{
    [_userEntry setEnabled:YES];        
    [_yesButton setEnabled:NO];
    [_noButton setEnabled:NO];    
    [_userEntry setStringValue:@""];
    
    /* Select the user entry so that when the user types it ends up there */
    [[self window] makeFirstResponder:_userEntry];
}

// Sets the enabled/disable settings so as to allow
// the user to decide whether their guesses were
// correct or not
- (void) disableEntry
{
    [_entryLabel setStringValue:@""];
    [_userEntry setEnabled:NO];
    [_yesButton setEnabled:YES];
    [_noButton setEnabled:YES];
    
    // Return to the "dialog" keyboard
    Preferences *pref = [Preferences sharedPreferences];
    [[pref dialogKeyboard] set];
}

#pragma mark -
#pragma mark User Feedback

// When the user presses enter in the guess field,
// this method is invoked.  
- (IBAction)confirmGuess:(id)sender
{
    // Obtain the string the user typed, normalizing
    // white space, and clear the text entry field.
    NSString *uentry = [_userEntry stringValue];
    uentry = [uentry strip];
    [_userEntry setStringValue:@""];
    
    // Initially, the user is making guesses.  In this case,
    // we want to add the string as a guess until they has
    // made one guess for each anser.
    
    if ([uentry isEqualToString:@""]) {
        // empty string signals that the user gave up, so we just generate strings consisting of various
        // amounts of spaces for all remaining questions
        int anscnt = [[[self question] answers] count];
        for (int len = 0; [self countOfGuesses] < anscnt; len++) {
            NSString *str = [@"" stringByPaddingToLength:len withString:@" " startingAtIndex:0];            
            [self addGuess:str];
        }
    }
    else {
        [self addGuess:uentry];
    }
}

- (void) addGuess:(NSString*)uentry
{
    // Check for a previous guess with the same string.  That's not kosher.
    FOREACH (QuizGuess *, qg, [self guesses]) {
        if ([[qg guess] isEqualToString:uentry]) {
            OALog (@"guess already made");
            return;
        }
    } ENDFOREACH;
    
    // Create a new guess object for this guess.
    OALog (@"guess=%@", uentry);
    [self createNewGuess:uentry];

    // Still waiting for more guesses?
    NSArray *answers = [[self question] answers];
    int cnt = [self countOfGuesses];
    if (cnt < [answers count]) {
        OALog(@"Current # of answers: %d; Current # of guesses: %d", [answers count], cnt);
        [self prepareForNextGuess];
        return;
    }

    // If they have made a guess for everything, then match the guesses
    // till answers and disable the user entry box so they must select
    // YES or NO to indicate if they were correct.
    if (MatchGuessesAndAnswers(answers, [self guesses])) {
        // if the match was exact, consider it correct automatically
        [self yes:self];
    }
    else
        [self disableEntry];
}

- (IBAction)no:(id)sender
{
    OALog (@"================================================");
    OALog (@"Word answered incorrectly.");

    // On the first round, we mark words right or wrong.  After the first round, only words
    // the user got wrong are tested and they are already marked.
    if (m_round == 0)
        [[[self question] answers] makeObjectsPerformSelector:@selector(markWrongForUser:) withObject:m_user];
    
    // Add the question to our list of missed words from this word.
    [m_mistakesThisRound addObject:[self question]];
    
    // Go to the next question.
    [self prepareNextQuestion];
}

- (IBAction)yes:(id)sender
{
    OALog (@"================================================");
    OALog (@"Word answered correctly.");

    // If they got it correct on the first round, then update the learning
    // objects.  Otherwise, on the latter rounds, this is just a review and
    // the word remains marked as incorrect.
    if (m_round == 0)
        [[[self question] answers] makeObjectsPerformSelector:@selector(markCorrectForUser:) withObject:m_user];
    
    // Go to the next question.
    [self prepareNextQuestion];
}

#pragma mark -
#pragma mark Limiting Initial Set of Questions

- (void) limitPreviousMistakes {
    OALog (@"================================================");
    OALog (@"Limit Previous Mistakes");
    
    // TODO: make this amount configurable!!
    if ([m_expiredConnections count] > 22) {
        [m_howManyField setIntValue:[m_expiredConnections count]];    
        NSApplication *app = [NSApplication sharedApplication];
        [app runModalForWindow:m_howManyPanel];
        [m_howManyPanel orderOut:self];        
        
        int limit = [m_howManyField intValue];
        if (limit < 0) limit = 0;
        while (limit < [m_expiredConnections count]) {
            id obj = [m_expiredConnections randomObject];
            [m_expiredConnections removeObject:obj];
        }
        OALog(@"Limited previous mistakes to be of size %d", [m_expiredConnections count]);
    }
    
    m_countOfExpiredMistakesAtStartOfQuiz = [m_expiredConnections count];
}

- (IBAction) endLimit:sender {
    NSApplication *app = [NSApplication sharedApplication];
    [app stopModal];
}

#pragma mark -
#pragma mark Review Connections

- (NSArray*) reviewConnections
{
    return a_reviewConnections;
}

- (void) setReviewConnections:(NSArray*)reviewConnections
{
    [a_reviewConnections release];
    a_reviewConnections = [reviewConnections copy];
}

- (void) generateReviewConnections
{
    NSMutableArray *review = [NSMutableArray array];
    FOREACH (QuizQuestion *, qq, m_mistakesThisRound) {
        FOREACH (QuizAnswer *, qa, [qq answers]) {
            [review addObject:qa];
        } ENDFOREACH;
    } ENDFOREACH;
    [self setReviewConnections:review];
}

#pragma mark -
#pragma mark Changing between rounds

- (void) endRound
{
    // If no mistakes were made this round, we are all done.
    if ([m_mistakesThisRound count] == 0) {
        [[self window] orderOut:self];
        [m_dictionary removeWindowController:self];
        return;
    }
    
    // If the user made mistakes:
    
    // Display them so they can write them down.
    [self generateReviewConnections];
    
    // Prepare for the next round by moving all of the
    // mistakes that were made this round into the list of
    // previous mistakes.
    m_round++;
    [m_previousMistakes removeAllObjects];
    [m_previousMistakes addAllObjectsFrom:m_mistakesThisRound];
    [m_mistakesThisRound removeAllObjects];
    m_countOfPreviousMistakesAtStartOfRound = [m_previousMistakes count];

    // Run the review panel.
    NSApplication *app = [NSApplication sharedApplication];
    [app runModalForWindow:m_reviewPanel];
    [m_reviewPanel orderOut:self];
    
    
    // Prepare the first question of the next round.
    [self prepareNextQuestion];
}

- (void) endReview:outlet
{    
    NSApplication *app = [NSApplication sharedApplication];
    [app stopModal];
}

#pragma mark -
#pragma mark Progress Bar

- (void) updateProgressBar {
    int value, max_value;
    if (m_round == 0) {
        max_value = m_countOfExpiredMistakesAtStartOfQuiz + m_quizLength;
        value = (m_countOfExpiredMistakesAtStartOfQuiz - [m_expiredConnections count]);
        value += [m_mistakesThisRound count];
    }
    else {
        max_value = m_countOfPreviousMistakesAtStartOfRound;
        value = m_countOfPreviousMistakesAtStartOfRound - [m_previousMistakes count];
    }
    double percent = (double)value/(double)max_value*100.0;
    OALog(@"Value=%f Max Value=%f Percent=%f", value, max_value, percent);
    
    //[m_progressBar setMinValue:min_value];
    //[m_progressBar setMaxValue:max_value];
    [m_progressBar setDoubleValue:percent];
}

#pragma mark -
#pragma mark Next Question Preparation

- (void) prepareNextQuestion
{
    OALog (@"prepareNextQuestion ------------------------------\n");
    
    [self updateProgressBar];    
    
    // Depending on the current round, we draw the next question from one of 
    // three possible sources:
    //
    // Round 0 only:
    // 1. If there are expired connections, we create a quiz question from one of those.
    // 2. If there are unlearned connections, we create a quiz question from one of those.
    //
    // Rounds 1+:
    // 3. If there are incorrect questions from last round, we draw it from there.
    //
    // If all 3 sources are empty, then we have no more questions to ask and the round
    // ends!
    QuizQuestion *nextQuestion = nil;
    if (m_round == 0) {
        nextQuestion = [self extractItemAndMakeQuizQuestion:m_expiredConnections
                                            extractSelector:@selector(randomObject)];
        if (nextQuestion == nil && [m_mistakesThisRound count] < m_quizLength) {
            if (m_unlearnedConnections == nil) 
                [self computeUnlearnedConnections];
            nextQuestion = [self extractItemAndMakeQuizQuestion:m_unlearnedConnections
                                                extractSelector:@selector(lastObject)];
        }
    } else if ([m_previousMistakes count] != 0) {
        nextQuestion = [[[m_previousMistakes randomObject] retain] autorelease];
        [m_previousMistakes removeObject:nextQuestion];
    }
    
    // Check for the end of this round (and possibly the quiz as a whole)
    if (nextQuestion == nil) {
        [self endRound];
        return;
    }
    
    // The round is continuing, so select the next question word,
    // and add it to the list of words we added:
    [self addAskedQuestion:nextQuestion];
    
    // Reset the current question and list of guesses
    [self setQuestion:nextQuestion];
    [self setGuesses:[NSMutableArray arrayWithCapacity:[[nextQuestion answers] count]]];
    [_guessesTable reloadData];
    
    // Set the font of the prompt section; its text should be automatically
    // updated via bindings
    [_prompt setAlignment:NSCenterTextAlignment];
    NSFont *font = [_prompt font];
    NSFont *newfont = [NSFont fontWithName:[font fontName] size:48];
    [_prompt setFont:newfont];
    
    // Prepare for the user to make a guess
    [self prepareForNextGuess];
}

- (void) prepareForNextGuess
{
    QuizAnswer *curAnswer = [self currentAnswer];
    
    // Set the label next to it to describe the relationship we are asking
    // for:
    [_entryLabel setStringValue:[curAnswer relationshipDescription]];
    [_relationshipLabel setStringValue:[curAnswer relationshipDescription]];

    // Enable text entry, ensure we are using the right keyboard:
    [self enableEntry];
    [self becameFirstResponder:nil];
}

#pragma mark -
#pragma mark Keeping the right keyboard selected

// when we the guess field becomes first responder, we change the keyboard to
// be the right one for the language the answer is in
- (void) becameFirstResponder:(OATextField*)field
{
    if (![field isEnabled]) {        
        // sometimes this gets invoked after we've answered the last question.  We
        // just ignore it then because we are not enabled.
        OALog (@"ignoring first responder because not enabled");
        return;
    }
    
    OAINDENTED(@"becameFirstResponder") {
        /* Find keyboard appr for answer language and use it */
        if ([self question]) {
            QuizAnswer *curAnswer = [self currentAnswer];
            NSManagedObject *alang = [curAnswer answerLanguage];
            NSString *keylnm = [alang keyboardLayoutValue];
            if (keylnm && ![keylnm isEqual:@""]) {
                OAKeyboardLayout *keyl = [OAKeyboardLayout keyboardLayoutWithName:keylnm];
                if (keyl) {
                    if ([keyl set])
                        OALog (@"Set keyboard layout to %@", [keyl name]);
                }
            }
        }
        
        OALog (@"Keyboard layout is now: %@", [[OAKeyboardLayout currentKeyboardLayout] name]);
    } OAUNDENTED;
}

#pragma mark -
#pragma mark Word Selection

- (QuizQuestion*) makeQuizQuestion:(NSManagedObject*)qConnection
{    
    NSManagedObject *eqRel = [m_dictionary relationshipForEquivalence];
    
    // Was this connection already asked?
    if ([m_allConnectionsAsked containsObject:qConnection])
        return nil;
    
    // Determine whether to ask the connection normal or in reverse:
    //    Careful, if depending on the "promptWith" settings, some
    //    orientations may not be valid!
    BOOL pFrom = [qConnection booleanValueForKeyPath:@"from.promptWith"];
    BOOL pTo = [qConnection booleanValueForKeyPath:@"to.promptWith"];
    BOOL reverse;
    if (pFrom) {
        if (pTo)
            reverse = (random() % 2) != 0;
        else 
            reverse = NO;
    } else {
        if (pTo)
            reverse = YES;
        else
            return nil;
    }
            
    // create the QuizQuestion object:
    NSManagedObject *promptWord = (reverse ? [qConnection toValue] : [qConnection fromValue]);
    QuizQuestion *qq = [[[QuizQuestion alloc] initWithWord:promptWord dictionary:m_dictionary] autorelease];            
     
    // find the set of connections related to the promptWord:
    NSMutableArray *mulist = [NSMutableArray array];
    [mulist addAllObjectsFrom:[promptWord connectionsFromValue]];
    [mulist addAllObjectsFrom:[promptWord connectionsToValue]];
    [mulist sortUsingFunction:SortConnectionsByRelationship context:NULL];
    
    // add any connections which originate in the language we are trying to learn,
    // excluding those we have already quizzed:
    BOOL foundEquivalent=NO;
    FOREACH (NSManagedObject *, connection, mulist) {
        if ([[connection fromValue] languageValue] == m_language)
            if (![m_allConnectionsAsked containsObject:connection]) {
                [m_allConnectionsAsked addObject:connection];
                [qq addConnection:connection answerWord:nil];
                if ([connection typeValue] == eqRel)
                    foundEquivalent = YES;
            }
    } ENDFOREACH;
    
    // try to ensure that an "equivalent" connection is in there:
    if (!foundEquivalent) {
		NSMutableArray *newQuizConns = [[NSMutableArray alloc] init];
        FOREACH(QuizAnswer *, qa, [qq answers]) {
            NSManagedObject *otherWord = [qa answerWord];
            NSSet *eqConns = 
                [[otherWord connectionsFromValue] filteredSetUsingPredicateFormat:@"type == %@ AND to.language == %@", 
                    eqRel, m_nativeTongue]; 
            FOREACH(NSManagedObject *, eqConn, eqConns) {
                if (![m_allConnectionsAsked containsObject:eqConn]) {
                    [m_allConnectionsAsked addObject:eqConn];
					[newQuizConns addObject:eqConn];
                }
            } ENDFOREACH;
        } ENDFOREACH;
		
		FOREACH(NSManagedObject *, eqConn, newQuizConns) {
			[qq addConnection:eqConn answerWord:[eqConn toValue]];
		} ENDFOREACH;
		[newQuizConns release];
    }
    
    // we should have found at least qConnection to add, but maybe others:
    NSAssert3(![qq emptyAnswers], @"Found no available answers for connection %@-%@->%@",
              [[qConnection fromValue] textValue],
              [[qConnection typeValue] nameValue],
              [[qConnection toValue] textValue]);
        
    // add the finalized QuizQuestion* object to our list of "allQuestions".
    [m_allQuestions addObject:qq];    
    return qq;
}

- (QuizQuestion*) extractItemAndMakeQuizQuestion:(id)connections
                                 extractSelector:(SEL)selector
{
    QuizQuestion *res = nil;
    while (res == nil && [connections count] != 0) {
        NSManagedObject *conn = [[[connections performSelector:selector] retain] autorelease];
        [connections removeObject:conn];
        res = [self makeQuizQuestion:conn];
    }
    return res;
}

- (void) computeUnlearnedConnections
{
    OALog(@"Initializing m_unlearnedConnections");
            
    // Select all eliglble connections and screen out those that the user
    // has a Learning object for.  Note: if CoreData would allow us to say
    // "ALL learned.user != $USER", we wouldn't need to do this by hand.
    // Although the grammar allows such constructions, at runtime an exception
    // is thrown indicated it is not yet supported.
    NSPredicate *predObj = 
        [NSPredicate predicateWithFormat:@"from.language == %@", m_language];
    NSArray *allConnections = 
        [[m_dictionary managedObjectContext] objectsOfEntityType:@"Connection" 
                                               matchingPredicate:predObj];
    
    OALog(@"All eligible connections: %d", [allConnections count]);
    
    // Iterate through the connections and keep only those that have never been learned.
    m_unlearnedConnections = [[NSMutableArray alloc] init];
    NSPredicate *everLearned = [NSPredicate predicateWithFormat:@"ANY learned.user == %@", m_user];
    FOREACH (NSManagedObject *, conn, allConnections) {
        if (![everLearned evaluateWithObject:conn])
            [m_unlearnedConnections addObject:conn];
    } ENDFOREACH;
    
    [m_unlearnedConnections sortUsingFunction:SortConnectionsByDate context:nil];
}

- (void) computeInitialMistakes
{
    // Build up the list of mistakes that we start with.
    OAINDENTED(@"selectInitialMistakes") {
        // Execute the query and select all expired Learning objects.
        NSManagedObjectContext *mctx = [m_dictionary managedObjectContext];
        NSArray *eligibleLearning = 
            [mctx objectsOfEntityType:@"Learning" 
              matchingPredicateFormat:@"user == %@ and askNextOn < %@ and connection.from.language == %@",
                m_user, [NSDate date], m_language];
                                                           
        // Obtain the corresponding expired connections.
        m_expiredConnections = [NSMutableSet setWithArray:[eligibleLearning mapArrayWith:@selector(connectionValue)]];
        [m_expiredConnections retain];
        OALog(@"Expired connections: %d", [m_expiredConnections count]);
    } OAUNDENTED;
}

@end
