1/*
2	File:		MBCGameInfo.mm
3	Contains:	Managing information about the current game
4	Copyright:	© 2003-2011 by Apple Inc., all rights reserved.
5
6	IMPORTANT: This Apple software is supplied to you by Apple Computer,
7	Inc.  ("Apple") in consideration of your agreement to the following
8	terms, and your use, installation, modification or redistribution of
9	this Apple software constitutes acceptance of these terms.  If you do
10	not agree with these terms, please do not use, install, modify or
11	redistribute this Apple software.
12
13	In consideration of your agreement to abide by the following terms,
14	and subject to these terms, Apple grants you a personal, non-exclusive
15	license, under Apple's copyrights in this original Apple software (the
16	"Apple Software"), to use, reproduce, modify and redistribute the
17	Apple Software, with or without modifications, in source and/or binary
18	forms; provided that if you redistribute the Apple Software in its
19	entirety and without modifications, you must retain this notice and
20	the following text and disclaimers in all such redistributions of the
21	Apple Software.  Neither the name, trademarks, service marks or logos
22	of Apple Inc. may be used to endorse or promote products
23	derived from the Apple Software without specific prior written
24	permission from Apple.  Except as expressly stated in this notice, no
25	other rights or licenses, express or implied, are granted by Apple
26	herein, including but not limited to any patent rights that may be
27	infringed by your derivative works or by other works in which the
28	Apple Software may be incorporated.
29
30	The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
31	MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
32	THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND
33	FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS
34	USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
35
36	IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT,
37	INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
38	PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
39	PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE,
40	REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE,
41	HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING
42	NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN
43	ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
44*/
45
46#import "MBCGameInfo.h"
47#import "MBCController.h"
48#import "MBCPlayer.h"
49#import "MBCDocument.h"
50#import "MBCUserDefaults.h"
51
52#import <GameKit/GameKit.h>
53
54
55#include <sys/types.h>
56#include <regex.h>
57#include <algorithm>
58
59NSArray *   sVoices;
60
61@implementation MBCGameInfo
62
63@synthesize document = fDocument;
64@synthesize moveList = fMoveList;
65@synthesize whiteEditable, blackEditable;
66
67NSString * kMBCShowMoveInTitle  = @"MBCShowMoveInTitle";
68//
69// Obsolete: Parsing the human name serves no purpose and carries some risk
70//
71NSString * kMBCHumanFirst		= @"MBCHumanFirst";
72NSString * kMBCHumanLast		= @"MBCHumanLast";
73
74+ (void)initialize
75{
76	NSUserDefaults * 	userDefaults = [NSUserDefaults standardUserDefaults];
77	NSString * humanName;
78
79	//
80	// Deal with legacy user name default representation
81	//
82	if ([userDefaults stringForKey:kMBCHumanFirst]) {
83		humanName = [NSString stringWithFormat:@"%@ %@",
84							  [userDefaults stringForKey:kMBCHumanFirst],
85							  [userDefaults stringForKey:kMBCHumanLast]];
86		[userDefaults removeObjectForKey:kMBCHumanFirst];
87		[userDefaults removeObjectForKey:kMBCHumanLast];
88	} else
89		humanName = NSFullUserName();
90
91	//
92	// Get the city we might be in.
93	//
94	NSString *		city 	= @"?";
95	NSString *		country	= @"?";
96
97	NSString * event =
98		[NSLocalizedString(@"casual_game", @"Casual Game") retain];
99
100	NSDictionary * defaults =
101		[NSDictionary
102			dictionaryWithObjectsAndKeys:
103				 humanName, kMBCHumanName,
104			          city, kMBCGameCity,
105   			       country, kMBCGameCountry,
106			         event, kMBCGameEvent,
107			nil];
108	[userDefaults registerDefaults: defaults];
109    sVoices = [[NSSpeechSynthesizer availableVoices] retain];
110}
111
112const int kNumFixedMenuItems = 2;
113
114- (void)loadVoiceMenu:(id)menu
115{
116    for (NSString * voiceIdentifier in sVoices)
117        [menu addItemWithTitle:[[NSSpeechSynthesizer attributesForVoice:voiceIdentifier] objectForKey:NSVoiceName]];
118}
119
120- (NSString *)voiceAtIndex:(NSUInteger)menuIndex
121{
122    if (menuIndex < kNumFixedMenuItems)
123        return nil;
124    else
125        return [sVoices objectAtIndex:menuIndex-kNumFixedMenuItems];
126}
127
128- (NSUInteger)indexForVoice:(NSString *)voiceId
129{
130    return [voiceId length] ? [sVoices indexOfObject:voiceId]+kNumFixedMenuItems : 0;
131}
132
133- (NSString *) localizedStyleName:(NSString *)name
134{
135	NSString * loc = NSLocalizedString(name, @"");
136
137	return loc;
138}
139
140- (NSString *) unlocalizedStyleName:(NSString *)name
141{
142	NSString * unloc = [fStyleLocMap objectForKey:name];
143
144	return unloc ? unloc : name;
145}
146
147- (void)awakeFromNib
148{
149    [self loadVoiceMenu:fPrimaryVoiceMenu];
150    [self loadVoiceMenu:fAlternateVoiceMenu];
151
152	fStyleLocMap = [[NSMutableDictionary alloc] init];
153    [fBoardStyle removeAllItems];
154    [fPieceStyle removeAllItems];
155
156	NSFileManager *	fileManager = [NSFileManager defaultManager];
157	NSString	  * stylePath	=
158    [[[NSBundle mainBundle] resourcePath]
159     stringByAppendingPathComponent:@"Styles"];
160	NSEnumerator  * styles 		=
161    [[fileManager contentsOfDirectoryAtPath:stylePath error:nil] objectEnumerator];
162	while (NSString * style = [styles nextObject]) {
163		NSString * locStyle = [self localizedStyleName:style];
164		[fStyleLocMap setObject:style forKey:locStyle];
165		NSString * s = [stylePath stringByAppendingPathComponent:style];
166		if ([fileManager fileExistsAtPath:
167             [s stringByAppendingPathComponent:@"Board.plist"]]
168            )
169			[fBoardStyle addItemWithTitle:locStyle];
170		if ([fileManager fileExistsAtPath:
171             [s stringByAppendingPathComponent:@"Piece.plist"]]
172            )
173			[fPieceStyle addItemWithTitle:locStyle];
174	}
175}
176
177- (void) setPlayerAlias:(NSString *)playerID forKey:(NSString *)key
178{
179    if (!playerID)
180        playerID = [fDocument nonLocalPlayerID];
181    if (playerID)
182        [GKPlayer loadPlayersForIdentifiers:[NSArray arrayWithObject:playerID]
183            withCompletionHandler:^(NSArray *players, NSError *error) {
184                if (!error) {
185                    [fDocument setObject:[[players objectAtIndex:0] alias] forKey:key];
186                    [self updateMoves:nil];
187                }
188            }];
189}
190
191- (void) removeChessObservers
192{
193    if (!fHasObservers)
194        return;
195
196    NSNotificationCenter * notificationCenter = [NSNotificationCenter defaultCenter];
197    [notificationCenter removeObserver:self name:MBCEndMoveNotification object:nil];
198    [notificationCenter removeObserver:self name:MBCTakebackNotification object:nil];
199    [notificationCenter removeObserver:self name:MBCIllegalMoveNotification object:nil];
200    [notificationCenter removeObserver:self name:MBCGameEndNotification object:nil];
201
202    fHasObservers = NO;
203}
204
205- (void)dealloc
206{
207    [self removeChessObservers];
208    [super dealloc];
209}
210
211- (void) startGame:(MBCVariant)variant playing:(MBCSide)sideToPlay
212{
213    [self removeChessObservers];
214    NSNotificationCenter * notificationCenter = [NSNotificationCenter defaultCenter];
215	[notificationCenter
216     addObserver:self
217     selector:@selector(updateMoves:)
218     name:MBCEndMoveNotification
219     object:fDocument];
220	[notificationCenter
221     addObserver:self
222     selector:@selector(takeback:)
223     name:MBCTakebackNotification
224     object:fDocument];
225	[notificationCenter
226     addObserver:self
227     selector:@selector(updateMoves:)
228     name:MBCIllegalMoveNotification
229     object:fDocument];
230	[notificationCenter
231     addObserver:self
232     selector:@selector(gameEnd:)
233     name:MBCGameEndNotification
234     object:fDocument];
235    fHasObservers = YES;
236
237    //
238    // Fill in missing properties
239    //
240    NSUserDefaults *        defaults    =   [NSUserDefaults standardUserDefaults];
241    NSString *              human       =   [defaults stringForKey:kMBCHumanName];
242    NSString *              engine      =   NSLocalizedString(@"engine_player", @"Computer");
243    NSMutableDictionary *   props       =   fDocument.properties;
244    BOOL                    gameCenter  =   [fDocument remoteSide] != kNeitherSide;
245
246    [self setWhiteEditable: !gameCenter && SideIncludesWhite(sideToPlay)];
247    [self setBlackEditable: !gameCenter && SideIncludesBlack(sideToPlay)];
248
249    if (![props objectForKey:@"White"])
250        if (gameCenter)
251            [self setPlayerAlias:[props objectForKey:@"WhitePlayerID"] forKey:@"White"];
252        else if (SideIncludesWhite(sideToPlay))
253            [fDocument setObject:human forKey:@"White"];
254        else
255            [fDocument setObject:engine forKey:@"White"];
256    if (![props objectForKey:@"Black"])
257        if (gameCenter)
258            [self setPlayerAlias:[props objectForKey:@"BlackPlayerID"] forKey:@"Black"];
259        else if (SideIncludesBlack(sideToPlay))
260            [fDocument setObject:human forKey:@"Black"];
261        else
262            [fDocument setObject:engine forKey:@"Black"];
263
264    NSDate * now	= [NSDate date];
265    if (![props objectForKey:@"StartDate"])
266        [fDocument setObject:[now descriptionWithCalendarFormat:@"%Y.%m.%d" timeZone:nil locale:nil]
267                      forKey:@"StartDate"];
268    if (![props objectForKey:@"StartTime"])
269        [fDocument setObject:[now descriptionWithCalendarFormat:@"%H:%M:%S" timeZone:nil locale:nil]
270                      forKey:@"StartTime"];
271    if (![props objectForKey:@"Result"])
272        [fDocument setObject:@"*" forKey:@"Result"];
273    if (![props objectForKey:@"City"])
274        if (gameCenter)
275            [fDocument setObject:NSLocalizedString(@"cloud_city", @"Game Center") forKey:@"City"];
276        else
277            [fDocument setObject:[defaults stringForKey:kMBCGameCity] forKey:@"City"];
278    if (![props objectForKey:@"Country"])
279        if (gameCenter)
280            [fDocument setObject:NSLocalizedString(@"cloud_country", @"The Cloud") forKey:@"Country"];
281        else
282            [fDocument setObject:[defaults stringForKey:kMBCGameCountry] forKey:@"Country"];
283    if (![props objectForKey:@"Event"])
284        [fDocument setObject:[defaults stringForKey:kMBCGameEvent] forKey:@"Event"];
285
286	fRows	= 0;
287
288    [self updateMoves:nil];
289}
290
291- (void) updateMoves:(NSNotification *)notification
292{
293	[self willChangeValueForKey:@"gameTitle"];
294    NSDictionary * props = fDocument.properties;
295    if (![props objectForKey:@"White"])
296        [self setPlayerAlias:[props objectForKey:@"WhitePlayerID"] forKey:@"White"];
297    if (![props objectForKey:@"Black"])
298        [self setPlayerAlias:[props objectForKey:@"BlackPlayerID"] forKey:@"Black"];
299	[fMoveList reloadData];
300	[fMoveList setNeedsDisplay:YES];
301
302	int rows = [self numberOfRowsInTableView:fMoveList];
303	if (rows != fRows) {
304		fRows = rows;
305		[fMoveList scrollRowToVisible:rows-1];
306	}
307	[self didChangeValueForKey:@"gameTitle"];
308}
309
310- (void) takeback:(NSNotification *)notification
311{
312    [fDocument setObject:@"*" forKey:@"Result"];
313
314	[self updateMoves:notification];
315}
316
317- (void) gameEnd:(NSNotification *)notification
318{
319    dispatch_async(dispatch_get_main_queue(), ^{
320        MBCMove *    move 	= reinterpret_cast<MBCMove *>([notification userInfo]);
321
322        switch (move->fCommand) {
323        case kCmdWhiteWins:
324            [fDocument setObject:@"1-0" forKey:@"Result"];
325            break;
326        case kCmdBlackWins:
327            [fDocument setObject:@"0-1" forKey:@"Result"];
328            break;
329        case kCmdDraw:
330            [fDocument setObject:@"1/2-1/2" forKey:@"Result"];
331            break;
332        default:
333            return;
334        }
335        [self updateMoves:notification];
336    });
337}
338
339- (NSString *)outcome
340{
341    NSString * result = [fDocument objectForKey:@"Result"];
342	if ([result isEqual:@"1-0"])
343		return NSLocalizedString(@"white_win_msg", @"White wins");
344	if ([result isEqual:@"0-1"])
345		return NSLocalizedString(@"black_win_msg", @"Black wins");
346	else if ([result isEqual:@"1/2-1/2"])
347		return NSLocalizedString(@"draw_msg", @"Draw");
348
349    return nil;
350}
351
352- (void)editProperties:(NSWindow *)sheet modalForWindow:(NSWindow *)window
353{
354    fEditedProperties = [[NSMutableDictionary alloc] init];
355
356	[NSApp beginSheet:sheet
357       modalForWindow:window
358        modalDelegate:self
359       didEndSelector:@selector(didEndSheet:returnCode:contextInfo:)
360          contextInfo:nil];
361}
362
363- (IBAction) editInfo:(id)sender
364{
365    [self editProperties:fEditSheet modalForWindow:[sender window]];
366}
367
368- (void)editPreferencesForWindow:(NSWindow *)window
369{
370    [fPrimaryVoiceMenu selectItemAtIndex:[self indexForVoice:[fDocument objectForKey:kMBCDefaultVoice]]];
371    [fAlternateVoiceMenu selectItemAtIndex:[self indexForVoice:[fDocument objectForKey:kMBCAlternateVoice]]];
372    [fBoardStyle selectItemWithTitle:[self localizedStyleName:[fDocument objectForKey:kMBCBoardStyle]]];
373    [fPieceStyle selectItemWithTitle:[self localizedStyleName:[fDocument objectForKey:kMBCPieceStyle]]];
374
375    [self editProperties:fPrefsSheet modalForWindow:window];
376}
377
378- (id)valueForUndefinedKey:(NSString *)key
379{
380    return [fDocument objectForKey:key];
381}
382
383- (void)setValue:(id)value forUndefinedKey:(NSString *)key
384{
385    if (![fEditedProperties objectForKey:key]) {
386        id oldValue = [fDocument objectForKey:key];
387        if (!oldValue)
388            oldValue = [NSNull null];
389        [fEditedProperties setObject:oldValue forKey:key];
390    }
391
392    [fDocument setValue:value forKey:key];
393}
394
395+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
396{
397    if (isupper([key characterAtIndex:0]))
398        return [NSSet setWithObject:[@"document." stringByAppendingString:key]];
399    else
400        return [NSSet set];
401}
402
403- (void) didEndSheet:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)ctx
404{
405    [sheet orderOut:self];
406}
407
408- (IBAction) cancelProperties:(id)sender
409{
410    //
411    // Restore all the values that were changed
412    //
413	[NSApp endSheet:[sender window]];
414    for (NSString * prop in fEditedProperties)
415        [fDocument setObject:[fEditedProperties objectForKey:prop] forKey:prop];
416    [fEditedProperties release];
417}
418
419- (IBAction) updateProperties:(id)sender
420{
421	[self willChangeValueForKey:@"gameTitle"];
422    NSUserDefaults * 	defaults 	= [NSUserDefaults standardUserDefaults];
423
424    //
425    // Update defaults
426    //
427    for (NSString * edited in fEditedProperties) {
428        id val = [fDocument objectForKey:edited];
429        if ([edited isEqual:@"White"]
430         || ([edited isEqual:@"Black"] && ![fEditedProperties objectForKey:@"White"])
431        )
432            [defaults setObject:val forKey:kMBCHumanName];
433        else if ([edited isEqual:@"City"] ||[edited isEqual:@"Country"] || [edited isEqual:@"Event"])
434            [defaults setObject:val forKey:[@"MBCGame" stringByAppendingString:edited]];
435        else if ([[edited substringToIndex:3] isEqual:@"MBC"])
436            [defaults setObject:val forKey:edited];
437    }
438    if ([fEditedProperties objectForKey:kMBCSearchTime])
439        [fDocument updateSearchTime];
440
441	[NSApp endSheet:[sender window]];
442    [fEditedProperties release];
443	[self didChangeValueForKey:@"gameTitle"];
444}
445
446- (IBAction) updateVoices:(id)sender;
447{
448    NSString * voice    = [self voiceAtIndex:[sender indexOfSelectedItem]];
449    NSString * pvoice   = voice ? voice : @"";
450    if ([sender tag])
451        [self setValue:pvoice forKey:kMBCAlternateVoice];
452    else
453        [self setValue:pvoice forKey:kMBCDefaultVoice];
454
455    NSSpeechSynthesizer *   selectedSynth = [[NSSpeechSynthesizer alloc] initWithVoice:voice];
456    NSString *              demoText      =
457        [[NSSpeechSynthesizer attributesForVoice:[selectedSynth voice]]
458         objectForKey:NSVoiceDemoText];
459    if (demoText)
460        [selectedSynth startSpeakingString:demoText];
461}
462
463- (IBAction) updateStyles:(id)sender;
464{
465	NSString *			boardStyle	=
466        [self unlocalizedStyleName:[fBoardStyle titleOfSelectedItem]];
467	NSString *			pieceStyle  =
468        [self unlocalizedStyleName:[fPieceStyle titleOfSelectedItem]];
469
470	[self setValue:boardStyle forKey:kMBCBoardStyle];
471	[self setValue:pieceStyle forKey:kMBCPieceStyle];
472}
473
474//
475//     If there is no document anymore, fBoard can't be trusted either
476//
477- (void)setDocument:(MBCDocument *)document
478{
479    fDocument = document;
480    if (!fDocument)
481        fBoard = nil;
482}
483
484- (int)numberOfRowsInTableView:(NSTableView *)aTableView
485{
486	return ([fBoard numMoves]+1) / 2;
487}
488
489- (id)tableView:(NSTableView *)v objectValueForTableColumn:(NSTableColumn *)col row:(int)row
490{
491	NSString * 		ident 	= [col identifier];
492	if ([ident isEqual:@"Move"]) {
493		return [NSNumber numberWithInt:row+1];
494	} else {
495		NSArray * identComp = [ident componentsSeparatedByString:@"."];
496		return [[fBoard move: row*2+[[identComp objectAtIndex:0] isEqual:@"Black"]]
497				valueForKey:[identComp objectAtIndex:1]];
498	}
499    return nil;
500}
501
502- (BOOL)tableView:(NSTableView *)aTableView shouldSelectRow:(NSInteger)rowIndex
503{
504	return NO; /* Disallow all selections */
505}
506
507- (NSString *)describeMove:(int)move
508{
509    NSDictionary * localization = nil;
510
511    if (NSURL * url = [[NSBundle mainBundle] URLForResource:@"Spoken" withExtension:@"strings"
512                                    subdirectory:nil]
513    )
514        localization = [NSDictionary dictionaryWithContentsOfURL:url];
515    NSString * nr_fmt =
516        NSLocalizedStringFromTable(@"move_table_nr", @"Spoken", @"Move %d");
517    NSString * text = [NSString localizedStringWithFormat:nr_fmt, move];
518    int white = (move-1)*2;
519    if (white < [fBoard numMoves])
520        text = [text stringByAppendingFormat:@"\n\n%@",
521                [fBoard extStringFromMove:[fBoard move:white]
522                         withLocalization:localization]];
523    int black = move*2 - 1;
524    if (black < [fBoard numMoves])
525        text = [text stringByAppendingFormat:@"\n\n%@",
526                [fBoard extStringFromMove:[fBoard move:black]
527                         withLocalization:localization]];
528
529    return text;
530}
531
532- (NSString *)gameTitle
533{
534    if (!fDocument || [fDocument brandNewGame])
535        return @"";
536
537	NSString * 		move;
538	int		 		numMoves = [fBoard numMoves];
539
540	if (numMoves && [[NSUserDefaults standardUserDefaults] boolForKey:kMBCShowMoveInTitle]) {
541		NSNumber * moveNum = [NSNumber numberWithInt:(numMoves+1)/2];
542		NSString * moveStr = [NSNumberFormatter localizedStringFromNumber:moveNum
543															  numberStyle:NSNumberFormatterDecimalStyle];
544		move = 	[NSString localizedStringWithFormat:NSLocalizedString(@"title_move_line_fmt", @"%@. %@%@"),
545						  moveStr, numMoves&1 ? @"":@"\xE2\x80\xA6 ",
546						  [[fBoard lastMove] localizedText]];
547    } else if ([[fDocument objectForKey:@"Request"] isEqual:@"Takeback"]) {
548        move = NSLocalizedString(@"takeback_msg", @"Takeback requested");
549	} else if (!numMoves) {
550		move =	NSLocalizedString(@"new_msg", @"New Game");
551	} else if (numMoves & 1) {
552		move = 	NSLocalizedString(@"black_move_msg", @"Black to move");
553	} else {
554		move = 	NSLocalizedString(@"white_move_msg", @"White to move");
555	}
556
557	if (NSString * outcome = [self outcome])
558		move = outcome;
559	NSString * title =
560		[NSString localizedStringWithFormat:NSLocalizedString(@"game_title_fmt", @"%@ - %@   (%@)"),
561				  [fDocument objectForKey:@"White"], [fDocument objectForKey:@"Black"], move];
562
563    return title;
564}
565
566@end
567
568// Local Variables:
569// mode:ObjC
570// End:
571