/*
 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 "WordConsolidation.h"
#import "DictionaryDocument.h"
#import "OAMacros.h"
#import "Accessors.h"
#import "ConnTracker.h"
#import "OAReference.h"
#import "OADebug.h"
#import "OACoreData.h"

#pragma mark -
#pragma mark WordConsolidation

// Use this trick to allow initWithDictionaryDocument: to invoke
// methods not specified externally.
@interface WordConsolidation (Internal)

- (void) loadExtantWords;
- (NSMutableDictionary*) textHashForWord:(NSManagedObject*) word;

@end

@implementation WordConsolidation

- initWithDictionaryDocument:(DictionaryDocument*)dictionary
{
    if ((self = [super init])) {
        _dictionary = dictionary; // no need to hold ref, would create cycles
        _managedObjectContext = [_dictionary managedObjectContext];
        NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
        [center addObserver:self 
                   selector:@selector(objectsChanged:) 
                       name:NSManagedObjectContextObjectsDidChangeNotification
                     object:_managedObjectContext];
        
        // give ourselves big old hashtables to store words in
        _wordHash = [[NSMutableDictionary alloc] initWithCapacity:10*1024];
        _textHashes = [[NSMutableDictionary alloc] initWithCapacity:32];
        
        [self loadExtantWords];
    }
    return self;
}
@end

@implementation WordConsolidation (Internal)

- (void) dealloc
{
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center removeObserver:self];
    [_wordHash release];
    [_textHashes release];
    
    [super dealloc];
}

- (NSMutableDictionary*) textHashForWord:(NSManagedObject*) word
{
    NSManagedObject *lang = [word languageValue];
    NSMutableDictionary *dict = [_textHashes objectForPointer:lang];
    if (dict == nil) {
        dict = [[[NSMutableDictionary alloc] initWithCapacity:10*1024] autorelease];
        [_textHashes setObject:dict forPointer:lang];
    }
    return dict;
}

- (void) loadExtantWords
{
    @try {
        OAIndent (@"WordConsolidation: loadExtantWords");
        NSArray *result = [_managedObjectContext allObjectsOfEntityType:@"Word"];
        if (result != nil) {
            FOREACH (NSManagedObject *, word, result) {
                // ignore words associated w/ articles
                if ([word articleValue])
                    continue;
                
                NSNumber *ptr = [word idValue];
                NSString *text = [word textValue];
                NSMutableDictionary *textHash = [self textHashForWord:word];
                
                OALog (@"Word: %@ (Ptr=%@)", text, ptr);
                
                [_wordHash setObject:text forKey:ptr];
                [textHash setObject:word forKey:text];
            } ENDFOREACH;
        }
    }
    @finally {
        OAUndent ();
    }
}

static void redirectConnections (NSSet *ourconns,
                                 NSManagedObject *newword, 
                                 SEL setsel,
                                 NSSet *connections,
                                 BOOL usefromword)
{
    FOREACH (NSManagedObject *, conn, connections) {
        // Check whether we are already connected to this word in this way, and
        // if so continue
        ConnTracker *ct = [[[ConnTracker alloc] initWithConnection:conn useFromWord:usefromword] autorelease];
        if ([ourconns containsObject:ct])
            continue;
        
        // Otherwise, we want to modify this connection to point at us, not the
        // old word
        OALog (@"Redirecting connection %@", ct);
        [conn performSelector:setsel withObject:newword];
    } ENDFOREACH;
}

BOOL isEligibleWord (NSManagedObject *word)
{
    NSString *entname = [[word entity] name];
    if (![entname isEqualToString:@"Word"])
        return NO; // we only monitor Words for changes
    if (![word readyForUseValue]) {
        OALog (@"Word not ready for use: ignoring");
        return NO; // ignore any updates until the word has been fully initialized
    }
    if ([word articleValue]) {
        OALog (@"Article word: ignoring");
        return NO; // ignore words associated w/ articles.  They don't follow the rules.
    }
    return YES;
}

- (void) insertWord:(NSManagedObject*)word withText:(NSString*)text withPointer:(NSNumber*)ptr
{
    NSMutableDictionary *textHash = [self textHashForWord:word];
    NSManagedObject *oldword = [textHash objectForKey:text];
    if (oldword != nil) {
        NSNumber *oldid = [oldword idValue];
        OALog (@"Overwriting existing word #%@ with same text.", oldid);
        [_wordHash removeObjectForKey:oldid];
    }
    
    [textHash setObject:word forKey:text];
    [_wordHash setObject:text forKey:ptr];
}

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

    @try {
        OAIndent (@"WordConsolidation observeObjects: %d updated objects, %d deleted, %d inserted", 
                  [updkeys count], [delkeys count], [addkeys count]);
        
        // Remove those words that were deleted from the hash table.
        FOREACH (NSManagedObject *, word, delkeys) {
            if (!isEligibleWord (word))
                continue;

            NSNumber *ptr = [word idValue];
            NSString *text = [_wordHash objectForKey:ptr];
            if (text) {
                OALog (@"Purging Word: %@ (Ptr=%@)", text, ptr);                
                [_wordHash removeObjectForKey:ptr];
                [[self textHashForWord:word] removeObjectForKey:text];
            }
        } ENDFOREACH;

        // Finally, process inserted words.  These are only eliglble, basically,
        // on an UNDO; otherwise, they will not be marked as readyForUse.  Because
        // of this, we know we can simply overwrite the old entry in the hashtable
        // for the text of this new word.  This is because that old entry will soon
        // be modified by an undo event which will change its text back to its
        // original value; since we have removed it from the hashtable, this will
        // cause it to simply be inserted back.  We know there will be no conflict
        // because there was no conflict before the change occurred, and the 
        // undo is simply moving us back to that state.
        FOREACH (NSManagedObject *, word, addkeys) {
            if (!isEligibleWord (word)) 
                continue;
            
            NSNumber *ptr = [word idValue];
            NSString *text = [word textValue];
            
            //OALog (@"Inserting Word: %@, #%@", text, ptr);
            [self insertWord:word withText:text withPointer:ptr];
        } ENDFOREACH;
        
        // For those words that we're updated, check whether we need
        // to adjust any connections.
        FOREACH (NSManagedObject *, word, updkeys) {
            if (!isEligibleWord (word))
                continue;

            NSNumber *ptr = [word idValue];
            NSString *text = [word textValue];
            
            @try {
                OAIndent (@"Processing updated word %@,#%@",
                          text, ptr);
            
                if ([text length] == 0) {
                    OALog (@"Empty text??: ignoring");
                    continue; // ignore empty text; shouldn't happen
                }
                
                // Find out what text we were previously associated with.
                // If there was no previous word with this ID, then the
                // AddWords system should have guaranteed that its text is
                // unique.
                NSString *prevtext = [_wordHash objectForKey:ptr];
                if (prevtext == nil) {
                    OALog (@"New word: should be no conflict");
                    NSAssert ([[self textHashForWord:word] objectForKey:text] == nil,
                              @"Word we've never seen before conflicts with existing");
                    [self insertWord:word withText:text withPointer:ptr];
                    continue;
                }
                
                // Check whether the text has changed from the last time we
                // saw it.  If not, something else we are not interested in
                // has changed.
                if ([prevtext isEqualToString:text]) {
                    OALog (@"Text unchanged since last update.  Ignoring.");
                    NSAssert ([[self textHashForWord:word] objectForKey:text] == word,
                              @"Text hashtable out of sync with ptr hashtable");
                    continue;
                }
                
                NSMutableDictionary *textHash = [self textHashForWord:word];
                                
                // Otherwise, purge the old word from our tables.  It will be
                // re-inserted below.
                OALog (@"Text changed from %@.  Purging from tables under old id.", 
                       prevtext);
                [_wordHash removeObjectForKey:ptr];
                [textHash removeObjectForKey:prevtext];
                
                // Now, find out if there is an owner for our new text
                NSManagedObject *owner = [textHash objectForKey:text];
                if (owner) {
                    // There is an owner, and it's not us!  Our job is to eliminate
                    // that word and merge its connections to us.
                    
                    OALog (@"Old owner #%@ identified.  Removing.", [owner idValue]);
                    
                    // First go through our existing connections.  We want to make a set
                    // so that we can quickly look up and see if we are already
                    // connected to a given word through a given relationship.
                    NSMutableSet *ourconns = [NSMutableSet set];
                    FOREACH (NSManagedObject *, conn, [word connectionsFromValue]) {
                        ConnTracker *ct = [[[ConnTracker alloc] initWithConnection:conn useFromWord:NO] autorelease];
                        [ourconns addObject:ct];
                        OALog (@"Existing connection %@", ct);
                    } ENDFOREACH;
                    
                    // Go through each connection from the old word: if we are not already connected in the same
                    // fashion to the same word, then redirect the old word's connection to us.  
                    redirectConnections (ourconns, word, @selector (setFromValue:), [owner connectionsFromValue], NO);
                    redirectConnections (ourconns, word, @selector (setToValue:), [owner connectionsToValue], YES);
                    
                    // Purge the old word from our hashtables now, before the deleted object notification.
                    // This is because we will be putting the new word in its place.
                    NSNumber *oldptr = [owner idValue];
                    [_wordHash removeObjectForKey:oldptr];
                    [textHash removeObjectForKey:text];
                    
                    // Delete the old word.  Hopefully, this will not delete the old word's connections since we have 
                    // redirected them.
                    [_managedObjectContext deleteObject:owner];
                }
                
                // We are owner of this word now.
                OALog (@"Adding to hashtables text=%@ ptr=%@", text, ptr);
                [textHash setObject:word forKey:text];
                [_wordHash setObject:text forKey:ptr];
            }
            @finally {
                OAUndent ();
            }
        } ENDFOREACH;
    } 
    @finally {
        OAUndent ();
    }
}

@end
