/* Scheduler.m - scheduler for Popup.app Copyright (C) 2003, 2004 Rob Burns December,13 2003 This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111 USA. */ #ifndef __APPLE__ #include // for fabs macro #endif #include "Scheduler.h" #include "StackModel.h" #include "QuizController.h" #include "Constants.h" @implementation Scheduler - (id) init { NSMutableDictionary *temp; if( (self = [super init]) ) { state = [[NSMutableArray alloc] init]; temp = [NSMutableDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt: -1], @"Slot", @"All Cards", @"Title", [NSNumber numberWithInt: 0], @"Cards", nil]; [state addObject: temp]; temp = [NSMutableDictionary dictionaryWithContentsOfFile: [[NSBundle mainBundle] pathForResource: @"Schedule" ofType: @"plist"]]; schedule = [[NSMutableArray alloc] initWithArray: [temp objectForKey: @"Schedule"]]; cardStack = nil; nextQuiz = nil; clock = nil; return self; } return nil; } // **************** // Property methods // **************** - (NSArray *) state; { return state; } - (void) setState: (NSArray *) aState { NSDate *tempD; int i; // outside counter variable (state array) int j; // inside counter variable (individual Times arrays) int x; // holder for intermediate integers RELEASE(state); state = [[NSMutableArray alloc] initWithArray: aState]; // we now have the task of going through the state array and changing // strings and times to NSNumbers and NSDates respectively :( for( i = 0; i < [state count]; i++ ) { x = [[[state objectAtIndex: i] objectForKey: @"Slot"] intValue]; [[state objectAtIndex: i] setObject: [NSNumber numberWithInt: x] forKey: @"Slot"]; x = [[[state objectAtIndex: i] objectForKey: @"Cards"] intValue]; [[state objectAtIndex: i] setObject: [NSNumber numberWithInt: x] forKey: @"Cards"]; if ( [[state objectAtIndex: i] objectForKey: @"Time"] != nil ) { tempD = [NSDate dateWithString: [[state objectAtIndex: i] objectForKey: @"Time"]]; [[state objectAtIndex: i] setObject: tempD forKey: @"Time"]; } if( [[state objectAtIndex: i] objectForKey: @"Times"] != nil ) { #define TEMPA [[state objectAtIndex: i] objectForKey: @"Times"] for( j = 0; j < [TEMPA count]; j++ ) { x = [[[TEMPA objectAtIndex: j] objectForKey: @"Slot"] intValue]; [[TEMPA objectAtIndex: j] setObject: [NSNumber numberWithInt: x] forKey: @"Slot"]; x = [[[TEMPA objectAtIndex: j] objectForKey: @"Cards"] intValue]; [[TEMPA objectAtIndex: j] setObject: [NSNumber numberWithInt: x] forKey: @"Cards"]; tempD = [NSDate dateWithString: [[TEMPA objectAtIndex: j] objectForKey: @"Time"]]; [[TEMPA objectAtIndex: j] setObject: tempD forKey: @"Time"]; } #undef TEMPA } } [self updateNextQuizDate]; if( nextQuiz ) { clock = [NSTimer scheduledTimerWithTimeInterval: [nextQuiz timeIntervalSinceNow] target: self selector: @selector(quizAlert:) userInfo: nil repeats: NO]; } } - (void) setCardStack: (StackModel *)aStack { cardStack = nil; cardStack = aStack; } - (NSMutableDictionary *) entryForCard: (CardModel *)aCard { int index = [aCard slot] + 1; NSMutableDictionary *slot = [state objectAtIndex: index]; NSMutableDictionary *quiz = nil; NSDate *time = [aCard time]; int i=0; if( time == nil ) { return [state objectAtIndex: index]; } else { for(i = 0; i < [[slot objectForKey: @"Times"] count]; i++ ) { quiz = [[slot objectForKey: @"Times"] objectAtIndex: i]; if( [[quiz objectForKey: @"Time"] isEqualToDate: time] ) return quiz; } } return nil; } - (void) setNumberOfCards: (int) number inEntry: (NSMutableDictionary *) anEntry { [anEntry setObject: [NSNumber numberWithInt: number] forKey: @"Cards"]; } - (void) incrementCardsInEntry: (NSMutableDictionary *)anEntry { int slot = 0; int oldCount = [[anEntry objectForKey: @"Cards"] intValue]; [anEntry setObject: [NSNumber numberWithInt: oldCount+1] forKey: @"Cards"]; if( [anEntry objectForKey: @"Title"] == nil ) // need to update the slot { slot = [[anEntry objectForKey: @"Slot"] intValue]; oldCount = [[[state objectAtIndex: slot+1] objectForKey: @"Cards"] intValue]; [[state objectAtIndex: slot+1] setObject: [NSNumber numberWithInt: oldCount+1] forKey: @"Cards"]; } } - (void) decrementCardsInEntry: (NSMutableDictionary *)anEntry { int slot = 0; int oldCount = [[anEntry objectForKey: @"Cards"] intValue]; [anEntry setObject: [NSNumber numberWithInt: oldCount-1] forKey: @"Cards"]; if( [anEntry objectForKey: @"Title"] == nil ) // need to update the slot { slot = [[anEntry objectForKey: @"Slot"] intValue]; oldCount = [[[state objectAtIndex: slot+1] objectForKey: @"Cards"] intValue]; [[state objectAtIndex: slot+1] setObject: [NSNumber numberWithInt: oldCount-1] forKey: @"Cards"]; } } - (BOOL) hasSlotZero { if( [state count] > 1 ) return YES; else return NO; } - (NSArray *) schedule { return schedule; } - (NSArray *) stateFileRep { NSEnumerator *sEnum = [state objectEnumerator]; NSMutableArray *sArr = [NSMutableArray arrayWithCapacity: 1]; NSDictionary *sEntry = nil; NSMutableDictionary *tSlot = nil; NSEnumerator *tEnum = nil; NSMutableArray *tArr = nil; NSDictionary *tEntry = nil; while( (sEntry = [sEnum nextObject]) ) { tSlot = [NSMutableDictionary dictionaryWithCapacity: 1]; [tSlot setObject: [[sEntry objectForKey: @"Slot"] stringValue] forKey: @"Slot"]; [tSlot setObject: [sEntry objectForKey: @"Title"] forKey: @"Title"]; [tSlot setObject: [[sEntry objectForKey: @"Cards"] stringValue] forKey: @"Cards"]; if( [sEntry objectForKey: @"Time"] ){ [tSlot setObject: [[sEntry objectForKey: @"Time"] description] forKey: @"Time"]; } tEnum = [[sEntry objectForKey: @"Times"] objectEnumerator]; tArr = [NSMutableArray arrayWithCapacity: 1]; while( (tEntry = [tEnum nextObject]) ) { [tArr addObject: [NSDictionary dictionaryWithObjectsAndKeys: [[tEntry objectForKey: @"Slot"] stringValue], @"Slot", [[tEntry objectForKey: @"Time"] description], @"Time", [[tEntry objectForKey: @"Cards"] stringValue], @"Cards", nil]]; } if( [[sEntry objectForKey: @"Slot"] intValue] > 0 ) [tSlot setObject: tArr forKey: @"Times"]; [sArr addObject: tSlot]; } return [NSArray arrayWithArray: sArr]; } // ************** // Action methods // ************** - (void) processQuiz: (QuizController *)aQuiz { NSDate *now = [NSDate date]; NSDate *newTime; NSDate *oldTime; NSMutableDictionary *scheduleEntry; int i, x; int curSlot; int nextSlot; int curStateSlot; int nextStateSlot; NSTimeInterval incrTime; NSArray *achievementList; NSDictionary *tempD; // to create the history entry achievementList = [NSArray arrayWithArray: [aQuiz achievementList]]; NSAssert( [achievementList count] == [[cardStack filteredCards] count], @"achievement/filter mismatch. this is a problem"); x = [[[cardStack filteredCards] objectAtIndex: 0] intValue]; // use the first card to determine the attributes of the entire bunch scheduleEntry = [self entryForCard: [[cardStack cards] objectAtIndex: x]]; curSlot = [[[cardStack cards] objectAtIndex: x] slot]; nextSlot = curSlot + 1; curStateSlot = curSlot + 1; // to account for the pesky 'all cards' item nextStateSlot = nextSlot + 1; if( [[[cardStack cards] objectAtIndex: x] time] != nil ) oldTime = [NSDate dateWithString: [[[[cardStack cards] objectAtIndex: x] time] description]]; else oldTime = nil; incrTime = [[[schedule objectAtIndex: nextSlot-1] objectForKey: @"Time"] doubleValue]*60; // use nextSlot-1 as there is no 'time zero' in the schedule newTime = [[NSDate alloc] initWithTimeInterval: incrTime sinceDate: now]; // this for loop is the actual processing of the cards. // the rest of this method is all clean up. and should // probably be refactored out into some other methods for( i = 0; i < [achievementList count]; i++ ) { x = [[[cardStack filteredCards] objectAtIndex: i] intValue]; if( nextSlot > [schedule count] ) { [[[cardStack cards] objectAtIndex: x] setSlot: NOSLOT]; [[[cardStack cards] objectAtIndex: x] setTime: nil]; } else { if( [[achievementList objectAtIndex: i] intValue] == PASSED ) { [[[cardStack cards] objectAtIndex: x] setSlot: nextSlot]; [[[cardStack cards] objectAtIndex: x] setTime: newTime]; tempD = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt: curSlot], @"Slot", now, @"Time", [NSNumber numberWithInt: PASSED], @"Result", nil]; [[[cardStack cards] objectAtIndex: x] addHistoryEntry: tempD]; } else if( [[achievementList objectAtIndex: i] intValue] == FAILED ) { [[[cardStack cards] objectAtIndex: x] setSlot: 0]; [[[cardStack cards] objectAtIndex: x] setTime: nil]; tempD = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt: curSlot], @"Slot", now, @"Time", [NSNumber numberWithInt: FAILED], @"Result", nil]; [[[cardStack cards] objectAtIndex: x] addHistoryEntry: tempD]; } } } // if we weren't quizing at time zero, we need to remove the old Times entry from the state if( curSlot != 0 || oldTime != nil ) { x = [[[state objectAtIndex: curStateSlot] objectForKey: @"Times"] count]; for( i = 0; i < x; i++ ) { if( [[[[[state objectAtIndex: curStateSlot] objectForKey: @"Times"] objectAtIndex: i] objectForKey: @"Time"] isEqualToDate: oldTime] ) { [[[state objectAtIndex: curStateSlot] objectForKey: @"Times"] removeObjectAtIndex: i]; break; } } // recalculate the number of cards in the old slot here x = 0; for( i = 0; i < [[[state objectAtIndex: curStateSlot] objectForKey: @"Times"] count]; i++ ) { x = x + [[[[[state objectAtIndex: curStateSlot] objectForKey: @"Times"] objectAtIndex: i] objectForKey: @"Cards"] intValue]; } [[state objectAtIndex: curStateSlot] setObject: [NSNumber numberWithInt: x] forKey: @"Cards"]; // and get set the new nearest time, if there is one if( [[[state objectAtIndex: curStateSlot] objectForKey: @"Times"] count] > 0 ) [[state objectAtIndex: curStateSlot] setObject: [[[[state objectAtIndex: curStateSlot] objectForKey: @"Times"] objectAtIndex: 0] objectForKey: @"Time"] forKey: @"Time"]; else [[state objectAtIndex: curStateSlot] removeObjectForKey: @"Time"]; } // now we need to generate a new Times entry, and place it in the correct slot. // creating a new slot if necessary. but only if we are not at the end of the // schedule, and if there are actually cards to put in the new slot if( nextSlot < [schedule count] && [aQuiz correctCount] > 0) { NSMutableDictionary *temp = [NSMutableDictionary dictionaryWithCapacity: 1]; [temp setObject: [NSNumber numberWithInt: nextSlot] forKey: @"Slot"]; [temp setObject: newTime forKey: @"Time"]; [temp setObject: [NSNumber numberWithInt: [aQuiz correctCount]] forKey: @"Cards"]; if( (nextSlot) >= [state count]-1 ) // -1 to account for the 'all cards' entry in in the state array { NSMutableDictionary *tempTwo = [NSMutableDictionary dictionaryWithCapacity: 1]; [tempTwo setObject: [NSNumber numberWithInt: nextSlot] forKey: @"Slot"]; [tempTwo setObject: [[schedule objectAtIndex: nextSlot-1] objectForKey: @"Title"] forKey: @"Title"]; // use nextSlot-1 as there is no 'time zero' in the schedule [tempTwo setObject: [NSNumber numberWithInt: [aQuiz correctCount]] forKey: @"Cards"]; [tempTwo setObject: [NSMutableArray arrayWithCapacity: 1] forKey: @"Times"]; [state addObject: tempTwo]; } [[[state objectAtIndex: nextStateSlot] objectForKey: @"Times"] addObject: temp]; // set the new nearest time, if there is one if( [[[state objectAtIndex: nextStateSlot] objectForKey: @"Times"] count] > 0 ) [[state objectAtIndex: nextStateSlot] setObject: [[[[state objectAtIndex: nextStateSlot] objectForKey: @"Times"] objectAtIndex: 0] objectForKey: @"Time"] forKey: @"Time"]; else [[state objectAtIndex: nextStateSlot] removeObjectForKey: @"Time"]; // recalculate the number of cards in the slot x = 0; for( i = 0; i < [[[state objectAtIndex: nextStateSlot] objectForKey: @"Times"] count]; i++ ) { x = x + [[[[[state objectAtIndex: nextStateSlot] objectForKey: @"Times"] objectAtIndex: i] objectForKey: @"Cards"] intValue]; } [[state objectAtIndex: nextStateSlot] setObject: [NSNumber numberWithInt: x] forKey: @"Cards"]; [self updateNextQuizDate]; if( nextQuiz && (clock == nil) ) { clock = [NSTimer scheduledTimerWithTimeInterval: [nextQuiz timeIntervalSinceNow] target: self selector: @selector(quizAlert:) userInfo: nil repeats: NO]; } } } - (void) updateNextQuizDate { int i, j; BOOL flag=NO; NSDate* tempD = [NSDate distantFuture]; NSDate* nowD = [NSDate dateWithTimeIntervalSinceNow: 10]; if( [state count] > 1 ) { for( i = 2; i < [state count]; i++ ) // i is 2, skip "all cards" and slot 0 { for( j = 0; j < [[[state objectAtIndex: i] objectForKey: @"Times"] count]; j++ ) { NSDate *selD = (NSDate *)[[[[state objectAtIndex: i] objectForKey: @"Times"] objectAtIndex: j] objectForKey: @"Time"]; if( [[tempD laterDate: selD] isEqual: tempD] && [[selD laterDate: nowD] isEqual: selD] ) { tempD = [NSDate dateWithString: [selD description]]; flag = YES; } } } if(flag) ASSIGN(nextQuiz, tempD); else nextQuiz=nil; } } - (void) createSlotZero { NSMutableDictionary *temp; if( [state count] == 1 ) { temp = [NSMutableDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt: 0], @"Slot", @"Time Zero", @"Title", [NSNumber numberWithInt: 0], @"Cards", nil]; [state addObject: temp]; } } - (void) quizAlert: (id) sender { int i; if( [state count] > 1 ) { for( i = 2; i < [state count]; i++ ) // i is 2, skip "all cards" and slot 0 { if( fabs([[[state objectAtIndex: i] objectForKey: @"Time"] timeIntervalSinceDate: nextQuiz]) < 1 ) { [[NSNotificationCenter defaultCenter] postNotificationName: @"ScheduledQuizAlertNotification" object: [state objectAtIndex: i]]; } } [clock invalidate]; clock = nil; [self updateNextQuizDate]; if( nextQuiz && (clock == nil) ) { clock = [NSTimer scheduledTimerWithTimeInterval: [nextQuiz timeIntervalSinceNow] target: self selector: @selector(quizAlert:) userInfo: nil repeats: NO]; } } } // ***************************************** // NSOutlineView DataSource Protocol Methods // ***************************************** - (id) outlineView: (NSOutlineView *) outlineView child: (int) index ofItem: (id) item { if( !item ) { return [state objectAtIndex: index]; } if( [item objectForKey: @"Times"] != nil ) { return [[item objectForKey: @"Times"] objectAtIndex: index]; } return nil; } - (BOOL) outlineView: (NSOutlineView *) outlineView isItemExpandable: (id) item { if( [[item objectForKey: @"Times"] count] > 0 ) return YES; return NO; } - (int) outlineView: (NSOutlineView *) outlineView numberOfChildrenOfItem: (id) item { if( item == nil ) return [state count]; if( [item objectForKey: @"Times"] ) return [[item objectForKey: @"Times"] count]; return 0; } - (id) outlineView: (NSOutlineView *) outlineView objectValueForTableColumn: (NSTableColumn *) tableColumn byItem: (id)item { if( [[tableColumn identifier] isEqualToString: @"TimeSlots"] ) { if( [item objectForKey: @"Title"] ) return [item objectForKey: @"Title"]; if( [item objectForKey: @"Time"] ) return [[item objectForKey: @"Time"] dateWithCalendarFormat: @"%a, %b %d, %I:%M %p" timeZone: nil] ; } if( [[tableColumn identifier] isEqualToString: @"Cards"] ) { return [item objectForKey: @"Cards"]; } return nil; } @end