//
//  TATAppDelegate.m
//  TadsTerp
//
//  Created by Rune Berg on 12/02/14.
//  Copyright (c) 2014 Rune Berg. All rights reserved.
//

#import "osxtads_support.h"
#import "XTConstants.h"
#import "XTAppDelegate.h"
#import "XTPrefs.h"
#import "XTAbout.h"
#import "XTGameWindowController.h"
#import "XTPrefsWindowController.h"
#import "XTAboutWindowController.h"
#import "XTFileUrlStringValueTransformer.h"
#import "XTLogger.h"
#import "XTFileUtils.h"
#import "XTUIUtils.h"
#import "XTFileNameHelper.h"
#import "XTNotifications.h"
#import "XTRecentGameFiles.h"
#import "XTOpenForTestingController.h"
#import "XTLoggingClassesInitializer.h"
#import "XTLogConsoleHelper.h"


@interface XTAppDelegate ()

@property XTPrefs *prefs;
@property XTAbout *about;
@property XTFileNameHelper *fileNameHelper;
@property NSMutableDictionary *gameWindowControllerByVMThreadName;
@property XTGameWindowController *newestGameWindowController;
@property XTCommandHistory *previousGameCommandHistory;
@property XTPrefsWindowController *prefsWindowController;
@property XTAboutWindowController *aboutWindowController;

@property (weak) IBOutlet NSMenu *fileMenu;
@property IBOutlet NSMenuItem *openGameFileForCaptureMenuItem; // strong, because we dynamically hide/show it by removing it from its parent menu
@property IBOutlet NSMenuItem *dividerMenuItem;                // strong, because we dynamically hide/show it by removing it from its parent menu
@property IBOutlet NSMenuItem *openLogConsoleMenuItem;         // strong, because we dynamically hide/show it by removing it from its parent menu

@property NSArray *tadsGameFileExtensions;

@property BOOL modalPanelIsOpen;
@property NSUInteger countApplicationDidBecomeActive;

@property BOOL isReloadingGame;

@property XTRunGameParams *lastUsedRunGameParams;

@end


@implementation XTAppDelegate

static XTPrefs *staticPrefs; // for early loading of log levels
static XTLogger* logger;

- (id)init
{
	self = [super init];
	if (self != nil) {
		_about = [XTAbout new];
		_prefs = [XTPrefs prefs];
		_about = [XTAbout about];
		_fileNameHelper = [XTFileNameHelper helper];
		_gameWindowControllerByVMThreadName = [NSMutableDictionary dictionary];
		_newestGameWindowController = nil;
		_previousGameCommandHistory = nil;
		_prefsWindowController = nil;
		_modalPanelIsOpen = NO;
		_countApplicationDidBecomeActive = 0;
		_tadsGameFileExtensions = @[@"gam", @"t3"];
		_isReloadingGame = NO;
		_lastUsedRunGameParams = nil;
	}
	return self;
}

+ (void)initialize
{
	//TODO? might need to mv these:
	staticPrefs = [XTPrefs prefs];
	[staticPrefs cleanupUnused];
	[staticPrefs restoreFromPersisted];
	[staticPrefs persist];

	logger = [XTLogger loggerForClass:[XTAppDelegate class]];
	
	// Register custom value transformers
	
	[NSValueTransformer setValueTransformer:[XTFileUrlStringValueTransformer new]
									forName:@"XTFileUrlStringValueTransformer"];
}

- (id<XTGameRunnerProtocol>)getGameRunner
{
	XT_DEF_SELNAME;
	
	NSThread *vmThread = [NSThread currentThread];
	NSString *vmThreadName = vmThread.name;
	if (vmThreadName == nil) {
		XT_ERROR_0(@"vmThreadName == nil");
	}
	return self.gameWindowControllerByVMThreadName[vmThreadName];
}

/*------------------------------------------------------------------------------------
 *  Application lifecycle functions
 */

- (void)applicationWillFinishLaunching:(NSNotification *)aNotification
{
	XT_TRACE_ENTRY;
	
	osxtads_init();
}

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
	XT_TRACE_ENTRY;

	[self setupReceptionOfAppLevelNotifications];
	
	XTRecentGameFiles *recentGameFiles = [XTRecentGameFiles recentGameFiles];
	[recentGameFiles restoreFromPersisted];
	[self refreshMyOpenRecentMenu];
	
	[self handleNotifyPrefsChanged:nil];
	
	// We don't need or want tab support in our windows, so:
	[NSWindow setAllowsAutomaticWindowTabbing:NO];

	// Create prefsWindowController early - it's needed to capture game directory etc.
	// even if the prefs window hasn't been opened
	if (self.prefsWindowController == nil) {
		self.prefsWindowController = [XTPrefsWindowController controllerWithPrefs:self.prefs];
	}
}

- (void)applicationDidBecomeActive:(NSNotification *)aNotification
{
	XT_TRACE_ENTRY;

	self.countApplicationDidBecomeActive += 1;
	
	if (self.countApplicationDidBecomeActive == 1) {
		[self hideFontPanelIfVisible];
		[self hideColorPanelIfVisible]; // can't reliably do this in applicationDidFinishLaunching :-(
		
		if (self.prefs.askForGameFileOnTerpStart.value.boolValue) {
			// Async notification to ask for a game file:
			NSNotification *myNotification = [NSNotification notificationWithName:XTadsNotifyAutoAskForGameFile object:self];
			[[NSNotificationQueue defaultQueue] enqueueNotification:myNotification postingStyle:NSPostASAP];
		}
	}
}

/*------------------------------------------------------------------------------------
 *  Menu event handlers
 */

- (IBAction)openAbout:(id)sender {
	
	XT_TRACE_ENTRY;
	
	if (self.aboutWindowController == nil) {
		self.aboutWindowController = [XTAboutWindowController controllerWithAbout:self.about];
	}
	[self.aboutWindowController showWindow:self];
}

- (IBAction)openPreferences:(id)sender
{
	XT_TRACE_ENTRY;
	
	[self.prefsWindowController showWindow:self];
}

- (IBAction)openGameFile:(id)sender
{
	XT_TRACE_ENTRY;
	
	[self notePreviousGameCommandHistory];

	if (! [self quitCurrentlyRunningGame]) {
		return;
	}

	NSURL *defaultDir = [self.fileNameHelper findDefaultDirectoryForFileType:XTadsFileNameDialogFileTypeGame];
	NSOpenPanel* panel = [XTUIUtils makeFileOpenPanelWithTitle:XT_DIALOG_TITLE_GAME_FILE
											 allowedExtensions: @[XT_FILE_EXTENSION_TADS2_GAME, XT_FILE_EXTENSION_TADS3_GAME]
													defaultDir:defaultDir];
	
	[XTNotifications notifyModalPanelOpened:self];
	[panel beginWithCompletionHandler:^(NSInteger result){
		[XTNotifications notifyModalPanelClosed:self];
		if (result == NSFileHandlingPanelOKButton) {
			XTRunGameParams *runGameParams = [XTRunGameParams new];
			runGameParams.gameFileUrl = [[panel URLs] objectAtIndex:0];
			if ([self checkValidGameFile:runGameParams.gameFileUrl]) {
				[self runGameFile:runGameParams];
			}
		}
	}];
}

- (IBAction)openGameFileForCapture:(id)sender
{
	[self notePreviousGameCommandHistory];
	
	if (! [self quitCurrentlyRunningGame]) {
		return;
	}

	[XTNotifications notifyModalPanelOpened:self];

	XTOpenForTestingController *wc = [XTOpenForTestingController controllerWithParams:self.lastUsedRunGameParams];
	[NSApp runModalForWindow:wc.window];
	[wc close];
	
	XTRunGameParams *runGameParams = wc.runGameParams;
	if (runGameParams.shouldOpen) {
		if ([self checkValidGameFile:runGameParams.gameFileUrl]) {
			if (runGameParams.keepSelectionsForNextTime) {
				self.lastUsedRunGameParams = runGameParams;
			} else {
				self.lastUsedRunGameParams = nil;
			}
			[self runGameFile:runGameParams];
		}
	}

	[XTNotifications notifyModalPanelClosed:self];
}

- (IBAction)openRecentMenu:(id)sender
{
	// When bound to menu item it triggers validateUserInterfaceItem for that menu, so that we can enable/disable it.
	// Apart from that, nothing to do here.
}

- (IBAction)myOpenRecentMenu:(id)sender
{
	// When bound to menu item it triggers validateUserInterfaceItem for that menu, so that we can enable/disable it.
	// Apart from that, nothing to do here.
}

- (IBAction)myClearRecentFiles:(id)sender
{
	XTRecentGameFiles *recentGameFiles = [XTRecentGameFiles recentGameFiles];
	[recentGameFiles removeAllEntries];
	[self refreshMyOpenRecentMenu];
}

- (IBAction)openLogConsole:(id)sender
{
	[XTLogConsoleHelper openLogConsole];
}

- (IBAction)reloadGameFile:(id)sender
{
	XT_TRACE_ENTRY;
	
	if (self.isReloadingGame) {
		XT_WARN_0(@"self.isReloadingGame == YES");
		return;
	}
	self.isReloadingGame = YES;
	
	[self notePreviousGameCommandHistory];

	if (! [self quitCurrentlyRunningGameForRestart]) {
		// user cancelled via confirmation dialog
		//XT_TRACE_0(@"! quitCurrentlyRunningGameForRestart");
		self.isReloadingGame = NO;
		return;
	}

	XTRunGameParams *runGameParams = [XTRunGameParams new];
	runGameParams.gameFileUrl = self.newestGameWindowController.gameFileUrl;
	if ([self checkValidGameFile:runGameParams.gameFileUrl]) {
		[self runGameFile:runGameParams];
	} else {
		XT_WARN_1(@"gameFileUrl %@ not legal", runGameParams.gameFileUrl);
	}
	
	XT_TRACE_0(@"done");
}

// Event handler, programmatically set by refreshMyOpenRecentMenu.
// Called when user selects an actual recent game menu item.
- (void)myOpenRecent:(id)sender
{
	XT_DEF_SELNAME;
	
	[self notePreviousGameCommandHistory];

	if ([sender isKindOfClass:[NSMenuItem class]]) {
		NSMenuItem *menuItem = sender;
		id ro = menuItem.representedObject;
		if ([ro isKindOfClass:[XTRecentGameFilesEntry class]]) {
			XTRecentGameFilesEntry *entry = ro;
			NSURL *gameFileUrl = entry.gameFileUrl;
			[self application:[NSApplication sharedApplication] openFile:[gameFileUrl path]];
		} else {
			XT_ERROR_0(@"menu item's representedObject was not a XTRecentGameFilesEntry");
		}
	} else {
		XT_ERROR_0(@"sender was not a NSMenuItem");
	}
}

/*------------------------------------------------------------------------------------
 *  Delegate and overrides
 */

// Called when using Finder to open a file with XTads
- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"%@", filename);

	[self notePreviousGameCommandHistory];
	
	if (self.modalPanelIsOpen) {
		XT_TRACE_0(@"-> NO - had modal dialog open");
		return NO;
	}
	
	XTRunGameParams *runGameParams = [XTRunGameParams new];
	runGameParams.gameFileUrl = [NSURL fileURLWithPath:filename];
	
	if (! [self checkValidGameFile:runGameParams.gameFileUrl]) {
		return NO;
	}
	
	if ([self quitCurrentlyRunningGame]) {
		[self runGameFile:runGameParams];
	}
	
	XT_TRACE_0(@"-> YES");
	
	return YES;
}

- (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)anItem
{
	//XT_DEF_SELNAME;
	//NSString *actionSelName = NSStringFromSelector(anItem.action);
	//XT_TRACE_1(@"got action %@", actionSelName);
	
	if (self.newestGameWindowController.isSleeping) {
		// allow nothing if game VM thread is sleeping
		//XT_WARN_0(@"-> 0 (VM thread is sleeping)");
		return NO;
	}
	
	BOOL res = YES;
	NSString *title = @"(not a menu item)";
	
	NSObject *anItemObj = (NSObject *)anItem;
	BOOL isRestartGame = (anItem.action == @selector(reloadGameFile:));
	BOOL isOpenGame = (anItem.action == @selector(openGameFile:));
	BOOL isOpenGameForCapture = (anItem.action == @selector(openGameFileForCapture:));
	BOOL isOpenARecentGame = (anItem.action == @selector(myOpenRecentMenu:));
	
	//XT_TRACE_3(@"isRestartGame=%d isOpenGame=%d isOpenARecentGame=%d", isRestartGame, isOpenGame, isOpenARecentGame);
	
	if ([anItemObj isKindOfClass:[NSMenuItem class]]) {
		title = ((NSMenuItem *)anItem).title;
		//XT_TRACE_1(@"got menu item \"%@\"", title);
	} else if ([anItemObj isKindOfClass:[NSMenu class]]) {
		// never get here. whatever.
		title = ((NSMenu *)anItem).title;
		//XT_TRACE_1(@"got menU \"%@\"", title);
	}
	
	// disable "Restart" menu item if...
	if (isRestartGame) {
		if (self.modalPanelIsOpen) {
			//XT_TRACE_0(@"not allowed to restart - modalPanelIsOpen");
			res = NO;
		} else if (self.isReloadingGame) {
			res = NO;
		} else if (self.newestGameWindowController == nil) {
			//XT_TRACE_0(@"not allowed to restart - gameWindowController == nil");
			res = NO;
		} else if (! [self.newestGameWindowController canLoadAndStartGameFile]) {
			//XT_TRACE_0(@"not allowed to restart - ! canLoadAndStartGameFile");
			res = NO;
		} else {
			//XT_TRACE_0(@"allowed to restart");
		}
	}
	
	// disable "Open" | "Open for capture/replay" menu items if...
	if (res && (isOpenGame || isOpenGameForCapture)) {
		if (self.modalPanelIsOpen) {
			res = NO;
		} else if (self.isReloadingGame) {
			res = NO;
		} else if (self.newestGameWindowController == nil) {
			res = YES;
		} else if (! [self.newestGameWindowController canLoadAndStartGameFile]) {
			res = NO;
		}
	}
	
	// disable "Open Recent" if...
	if (res && isOpenARecentGame) {
		if (self.modalPanelIsOpen) {
			res = NO;
		} else if (self.isReloadingGame) {
			res = NO;
		} else if (self.newestGameWindowController == nil) {
			res = YES;
		} else if (! [self.newestGameWindowController canLoadAndStartGameFile]) {
			res = NO;
		}
	}
	
	//XT_TRACE_2(@"(menu) \"%@\" -> %d", title, res);
	
	return res;
}

/*------------------------------------------------------------------------------------
 *  Misc.
 */

- (void)refreshMyOpenRecentMenu
{
	XT_DEF_SELNAME;

	NSMenu *mainMenu = [[NSApplication sharedApplication] mainMenu];
	NSMenuItem *fileMenuItem = [mainMenu itemWithTitle:@"File"];
	NSMenu *fileSubMenu = [fileMenuItem submenu];
	NSMenuItem *myOpenRecentMenuItem = [fileSubMenu itemWithTag:98765];  // 98765 is set in the xib
	NSMenu *myOpenRecentSubMenu = [myOpenRecentMenuItem submenu];
	
	[myOpenRecentSubMenu removeAllItems];
	
	XTRecentGameFiles *recentGameFiles = [XTRecentGameFiles recentGameFiles];
	NSUInteger countRecentGameFiles = [recentGameFiles numberOfEntries];
	
	for (NSUInteger i = 0; i < countRecentGameFiles; i++) {		
		XTRecentGameFilesEntry *entry = [recentGameFiles entryAtIndex:i];
		if ([XTFileUtils isExistingFileUrl:entry.gameFileUrl]) {
			NSString *gameTitle = entry.gameTitle;
			NSMenuItem *item = [[NSMenuItem new] initWithTitle:gameTitle action:@selector(myOpenRecent:) keyEquivalent:@""];
			item.representedObject = entry;
			item.tag = i;
			[myOpenRecentSubMenu addItem:item];
		} else {
			XT_TRACE_1(@"Recent game file %@ does not exist - skipped from menu", entry.gameFileUrl);
		}
	}
	
	if (countRecentGameFiles >= 1) {
		[myOpenRecentSubMenu addItem:[NSMenuItem separatorItem]];
	}
	
	NSMenuItem *clearItem = [[NSMenuItem new] initWithTitle:@"Clear Menu" action:@selector(myClearRecentFiles:) keyEquivalent:@""];
	[myOpenRecentSubMenu addItem:clearItem];

	[recentGameFiles persist];
}

- (BOOL)quitCurrentlyRunningGame
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"enter");
	
	BOOL res = YES;
	
	if (self.newestGameWindowController != nil) {
		if (self.prefs.askForConfirmationOnGameOpenIfGameRunning.value.boolValue) {
			res = [self.newestGameWindowController confirmQuitGameIfRunning:@"A game is already running. Do you really want to quit it and load another?"];
		}
		if (res) {
			res = [self.newestGameWindowController quitGameIfRunning];
				//TODO reconsider assignment - see quitCurrentlyRunningGameForRestart
			[self.newestGameWindowController closeWindow];
			[self asyncNotifySelfThatGameWindowClosed:self.newestGameWindowController];
		}
	}
	
	return res;
}

- (BOOL)quitCurrentlyRunningGameForRestart
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"enter");

	BOOL res = YES;
	
	if (self.newestGameWindowController != nil) {
		if (self.prefs.askForConfirmationOnGameRestart.value.boolValue) {
			res = [self.newestGameWindowController confirmQuitGameIfRunning:@"A game is already running. Do you really want to quit and restart it?"];
		}
		if (res) {
			/*res =*/ [self.newestGameWindowController quitGameIfRunning];
			[self.newestGameWindowController closeWindow];
			[self asyncNotifySelfThatGameWindowClosed:self.newestGameWindowController];
		}
	} else {
		XT_WARN_0(@"found no active game window controller");
	}
	
	return res;
}

- (void)asyncNotifySelfThatGameWindowClosed:(XTGameWindowController *)gameWindowController
{
	// Async notification to self, so that VM thread gets time finish

	//XT_DEF_SELNAME;
	//XT_TRACE_0(@"enter");
	
	NSNotification *myNotification = [NSNotification notificationWithName:XTadsNotifyGameWindowClosed
																   object:gameWindowController];
	[[NSNotificationQueue defaultQueue] enqueueNotification:myNotification postingStyle:NSPostWhenIdle];
}

- (void)noteRecentGameFileOpened:(NSURL *)gameFileUrl
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"%@", gameFileUrl);
	
	[[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL:gameFileUrl];
		// doesn't really work... state lost after terp restarted, hence homwgrown solution with XTRecentGameFiles
	
	XTRecentGameFiles *recentGameFiles = [XTRecentGameFiles recentGameFiles];
	XTRecentGameFilesEntry *entry = [XTRecentGameFilesEntry entryWithGameFileUrl:gameFileUrl gameTitle:nil];
	[recentGameFiles addNewestEntry:entry];
	
	[self refreshMyOpenRecentMenu];
}

- (BOOL)checkValidGameFile:(NSURL *)fileUrl
{
	BOOL res = NO;
	
	if (fileUrl != nil) {
		if ([XTFileUtils isExistingFileUrl:fileUrl]) {
			if ([XTFileUtils fileUrl:fileUrl hasExtensionIn:self.tadsGameFileExtensions]) {
				res = YES;
			}
		}
	}
	if (! res) {
		NSString *fileName = XTADS_FILESYSTEM_C_STRING_TO_NSSTRING([fileUrl fileSystemRepresentation]);
		NSString *msgText = [NSString stringWithFormat:@"\"%@\" is not a valid TADS game file. Please use existing files that end in \".gam\" or \".t3\"", fileName];
		[XTUIUtils showModalErrorDialogInWindow:nil messageText:msgText];
	}
	
	return res;
}

- (void)notePreviousGameCommandHistory
{
	self.previousGameCommandHistory = nil;
	if (self.prefs.keepCommandHistoryWhenStartingNewGame.value.boolValue) {
		if (self.newestGameWindowController != nil) {
			self.previousGameCommandHistory = self.newestGameWindowController.commandHistory;
		}
	}
}

- (void)runGameFile:(XTRunGameParams *)runGameParams
{
	//XT_DEF_SELNAME;
	
	XTGameWindowController *gwc = [XTGameWindowController controller];
	[gwc loadAndStartGameFile:runGameParams];
	
	if (self.previousGameCommandHistory != nil) {
		[self.previousGameCommandHistory resetHasBeenAccessed];
		gwc.commandHistory = self.previousGameCommandHistory;
	}
	NSString *vmThreadName = [gwc getVmThreadName];
	[self.gameWindowControllerByVMThreadName setObject:gwc forKey:vmThreadName];
	self.newestGameWindowController = gwc;

	[self.fileNameHelper noteUsedDirectory:runGameParams.gameFileUrl forFileType:XTadsFileNameDialogFileTypeGame];
	
	[self noteRecentGameFileOpened:runGameParams.gameFileUrl];
		//TODO this indirectly triggers sync w/ prefs (for colours etc.), but that should be done more explicitly after game's started
	
	if (runGameParams.saveFileUrl != nil) {
		[self.fileNameHelper noteUsedDirectory:runGameParams.saveFileUrl forFileType:XTadsFileNameDialogFileTypeSavedGame];
	}
	
	if (runGameParams.commandScriptAction != XT_CMD_SCRIPT_ACTION_NONE)	{
		[self.fileNameHelper noteUsedDirectory:runGameParams.commandScriptFileUrl forFileType:XTadsFileNameDialogFileTypeCommandScript];
	}
	
	if (runGameParams.captureTranscript) {
		[self.fileNameHelper noteUsedDirectory:runGameParams.transcriptFileUrl forFileType:XTadsFileNameDialogFileTypeTranscript];
	}
}

- (void)hideFontPanelIfVisible
{
	if ([[NSFontPanel sharedFontPanel] isVisible])
		[[NSFontPanel sharedFontPanel] orderOut:self];
}

- (void)hideColorPanelIfVisible
{
	XT_DEF_SELNAME;

	if ([[NSColorPanel sharedColorPanel] isVisible]) {
		[[NSColorPanel sharedColorPanel] orderOut:self];
		XT_TRACE_0(@"closed panel");
	}
}

//---  App level notifications  ------------------------------------------------------------

- (void)setupReceptionOfAppLevelNotifications
{
	XT_TRACE_ENTRY;
	
	[[NSNotificationCenter defaultCenter] addObserver:self
											 selector:@selector(handleNotifyGameWindowClosed:)
												 name:XTadsNotifyGameWindowClosed
											   object:nil]; // nil means "for any sender"
	
	[[NSNotificationCenter defaultCenter] addObserver:self
											 selector:@selector(handleNotifyModalPanelOpened:)
												 name:XTadsNotifyModalPanelOpened
											   object:nil]; // nil means "for any sender"
	
	[[NSNotificationCenter defaultCenter] addObserver:self
											 selector:@selector(handleNotifyModalPanelClosed:)
												 name:XTadsNotifyModalPanelClosed
											   object:nil]; // nil means "for any sender"
	
	[[NSNotificationCenter defaultCenter] addObserver:self
											 selector:@selector(handleNotifyPrefsWindowClosed:)
												 name:XTadsNotifyPrefsWindowClosed
											   object:nil]; // nil means "for any sender"
	
	[[NSNotificationCenter defaultCenter] addObserver:self
											 selector:@selector(handleNotifyAboutWindowClosed:)
												 name:XTadsNotifyAboutWindowClosed
											   object:nil]; // nil means "for any sender"
	
	[[NSNotificationCenter defaultCenter] addObserver:self
											 selector:@selector(handleNotifyAutoAskForGameFile:)
												 name:XTadsNotifyAutoAskForGameFile
											   object:nil]; // nil means "for any sender"
	
	[[NSNotificationCenter defaultCenter] addObserver:self
											 selector:@selector(handleNotifyPrefsChanged:)
												 name:XTadsNotifyPrefsChanged
											   object:nil]; // nil means "for any sender"
}

- (void)handleNotifyGameWindowClosed:(NSNotification *)notification
{
	XT_TRACE_ENTRY;

	XTGameWindowController *gwc = (XTGameWindowController *)notification.object;
	
	if (gwc != nil) {
		if (! gwc.hasExitedVmThread) {
			gwc.countNotifyMainThreadThatGameWindowClosed += 1;
			if ((gwc.countNotifyMainThreadThatGameWindowClosed % 800) == 0) {
				XT_WARN_0(@"! gwc.hasExitedVmThread - reposting notification");
			}
			[self asyncNotifySelfThatGameWindowClosed:gwc];
			return;
		}
		[gwc cleanupAtVmThreadExit];
		if (gwc == self.newestGameWindowController) {
			self.newestGameWindowController = nil;
			//XT_TRACE_0(@"after nilling newestGameWindowController (== gwc)");
		} else {
			// replaced game window with another - ok
			int brkpt = 1;
			//XT_TRACE_0(@"gwc != self.newestGameWindowController");
		}
		NSString *vmThreadName = [gwc getVmThreadName];
		if (vmThreadName != nil) {
			[self.gameWindowControllerByVMThreadName removeObjectForKey:vmThreadName];
			//XT_TRACE_1(@"after removing gwc for thread %@ from map", vmThreadName);
		} else {
			XT_ERROR_0(@"gwc.tadsEventLoopThread.name was nil");
		}
		gwc.tadsEventLoopThread = nil;
	} else {
		XT_ERROR_0(@"gwc was nil");
	}
	
	self.isReloadingGame = NO;
	
	XT_TRACE_1(@"exit, isReloadingGame=%lu", self.isReloadingGame);
}

- (void)handleNotifyModalPanelOpened:(NSNotification *)notification
{
	XT_TRACE_ENTRY;
	
	self.modalPanelIsOpen = YES;
}

- (void)handleNotifyModalPanelClosed:(NSNotification *)notification
{
	XT_TRACE_ENTRY;
	
	self.modalPanelIsOpen = NO;
}

- (void)handleNotifyPrefsWindowClosed:(NSNotification *)notification
{
	XT_TRACE_ENTRY;
	
	[self hideFontPanelIfVisible];
	[self hideColorPanelIfVisible];
}

- (void)handleNotifyAboutWindowClosed:(NSNotification *)notification
{
	XT_TRACE_ENTRY;
}

- (void)handleNotifyAutoAskForGameFile:(NSNotification *)notification
{
	XT_TRACE_ENTRY;

	[self openGameFile:nil];
}

- (void)handleNotifyPrefsChanged:(NSNotification *)notification
{
	BOOL devMode = self.prefs.enableDevelopmentModeFeatures.value.boolValue;

	[self insertOrRemoveFileMenuItem:self.openGameFileForCaptureMenuItem atIndex:1 devMode:devMode];
	[self insertOrRemoveFileMenuItem:self.dividerMenuItem atIndex:(self.fileMenu.numberOfItems) devMode:devMode];
	[self insertOrRemoveFileMenuItem:self.openLogConsoleMenuItem atIndex:(self.fileMenu.numberOfItems) devMode:devMode];
}

- (void) insertOrRemoveFileMenuItem:(NSMenuItem *)menuItem
							atIndex:(NSInteger)index
							devMode:(BOOL)devMode
{
	NSInteger idx = [self.fileMenu indexOfItem:menuItem];
	BOOL haveMenuItem = (idx != -1);
	
	if (devMode) {
		if (! haveMenuItem) {
			[self.fileMenu insertItem:menuItem atIndex:index];
		}
	} else {
		if (haveMenuItem) {
			[self.fileMenu removeItem:menuItem];
		}
	}
}

//---------------------------------------------------------------

/* Activate when/if needed:

// Returns the directory the application uses to store the Core Data store file. This code uses a directory named "runeberg.TadsTerp" in the user's Application Support directory.
- (NSURL *)applicationFilesDirectory
{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *appSupportURL = [[fileManager URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] lastObject];
    return [appSupportURL URLByAppendingPathComponent:@"runeberg.TadsTerp"];
}

// Creates if necessary and returns the managed object model for the application.
- (NSManagedObjectModel *)managedObjectModel
{
    if (_managedObjectModel) {
        return _managedObjectModel;
    }
	
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"TadsTerp" withExtension:@"momd"];
    _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    return _managedObjectModel;
}

// Returns the persistent store coordinator for the application. This implementation creates and return a coordinator, having added the store for the application to it. (The directory for the store is created, if necessary.)
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (_persistentStoreCoordinator) {
        return _persistentStoreCoordinator;
    }
    
    NSManagedObjectModel *mom = [self managedObjectModel];
    if (!mom) {
        [logger error:@"%@:%@ No model to generate a store from", [self class], NSStringFromSelector(_cmd)];
        return nil;
    }
    
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *applicationFilesDirectory = [self applicationFilesDirectory];
    NSError *error = nil;
    
    NSDictionary *properties = [applicationFilesDirectory resourceValuesForKeys:@[NSURLIsDirectoryKey] error:&error];
    
    if (!properties) {
        BOOL ok = NO;
        if ([error code] == NSFileReadNoSuchFileError) {
            ok = [fileManager createDirectoryAtPath:[applicationFilesDirectory path] withIntermediateDirectories:YES attributes:nil error:&error];
        }
        if (!ok) {
            [[NSApplication sharedApplication] presentError:error];
            return nil;
        }
    } else {
        if (![properties[NSURLIsDirectoryKey] boolValue]) {
            // Customize and localize this error.
            NSString *failureDescription = [NSString stringWithFormat:@"Expected a folder to store application data, found a file (%@).", [applicationFilesDirectory path]];
            
            NSMutableDictionary *dict = [NSMutableDictionary dictionary];
            [dict setValue:failureDescription forKey:NSLocalizedDescriptionKey];
            error = [NSError errorWithDomain:@"YOUR_ERROR_DOMAIN" code:101 userInfo:dict];
            
            [[NSApplication sharedApplication] presentError:error];
            return nil;
        }
    }
    
    NSURL *url = [applicationFilesDirectory URLByAppendingPathComponent:@"TadsTerp.storedata"];
    NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
    if (![coordinator addPersistentStoreWithType:NSXMLStoreType configuration:nil URL:url options:nil error:&error]) {
        [[NSApplication sharedApplication] presentError:error];
        return nil;
    }
    _persistentStoreCoordinator = coordinator;
    
    return _persistentStoreCoordinator;
}

// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) 
- (NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext) {
        return _managedObjectContext;
    }
    
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (!coordinator) {
        NSMutableDictionary *dict = [NSMutableDictionary dictionary];
        [dict setValue:@"Failed to initialize the store" forKey:NSLocalizedDescriptionKey];
        [dict setValue:@"There was an error building up the data file." forKey:NSLocalizedFailureReasonErrorKey];
        NSError *error = [NSError errorWithDomain:@"YOUR_ERROR_DOMAIN" code:9999 userInfo:dict];
        [[NSApplication sharedApplication] presentError:error];
        return nil;
    }
    _managedObjectContext = [[NSManagedObjectContext alloc] init];
    [_managedObjectContext setPersistentStoreCoordinator:coordinator];

    return _managedObjectContext;
}

// Returns the NSUndoManager for the application. In this case, the manager returned is that of the managed object context for the application.
- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window
{
    return [[self managedObjectContext] undoManager];
}

// Performs the save action for the application, which is to send the save: message to the application's managed object context. Any encountered errors are presented to the user.
- (IBAction)saveAction:(id)sender
{
    NSError *error = nil;
    
    if (![[self managedObjectContext] commitEditing]) {
        [logger error:@"%@:%@ unable to commit editing before saving", [self class], NSStringFromSelector(_cmd)];
    }
    
    if (![[self managedObjectContext] save:&error]) {
        [[NSApplication sharedApplication] presentError:error];
    }
}
*/

- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
	XT_DEF_SELNAME;

	if (self.newestGameWindowController != nil) {
		
		if (self.newestGameWindowController.isSleeping) {
			XT_TRACE_0(@"not while game VM thread is sleeping");
			return NSTerminateCancel;
		}

		BOOL quitGameConfirmed = YES;
		if (self.prefs.askForConfirmationOnGameQuit.value.boolValue) {
			quitGameConfirmed = [self.newestGameWindowController confirmQuitTerpIfGameRunning:@"A game is currently running. Do you really want to quit XTads?"];
		}
		if (! quitGameConfirmed) {
			return NSTerminateCancel;
		}
	}

	/*
	// Save changes in the application's managed object context before the application terminates.
    
    if (!_managedObjectContext) {
        return NSTerminateNow;
    }
    
    if (![[self managedObjectContext] commitEditing]) {
        [logger error:@"%@:%@ unable to commit editing to terminate", [self class], NSStringFromSelector(_cmd)];
        return NSTerminateCancel;
    }
    
    if (![[self managedObjectContext] hasChanges]) {
        return NSTerminateNow;
    }
    
    NSError *error = nil;
    if (![[self managedObjectContext] save:&error]) {

        // Customize this code block to include application-specific recovery steps.              
        BOOL result = [sender presentError:error];
        if (result) {
            return NSTerminateCancel;
        }

        NSString *question = NSLocalizedString(@"Could not save changes while quitting. Quit anyway?", @"Quit without saves error question message");
        NSString *info = NSLocalizedString(@"Quitting now will lose any changes you have made since the last successful save", @"Quit without saves error question info");
        NSString *quitButton = NSLocalizedString(@"Quit anyway", @"Quit anyway button title");
        NSString *cancelButton = NSLocalizedString(@"Cancel", @"Cancel button title");
        NSAlert *alert = [[NSAlert alloc] init];
        [alert setMessageText:question];
        [alert setInformativeText:info];
        [alert addButtonWithTitle:quitButton];
        [alert addButtonWithTitle:cancelButton];

        NSInteger answer = [alert runModal];
        
        if (answer == NSAlertAlternateReturn) {
            return NSTerminateCancel;
        }
    }
	*/

    return NSTerminateNow;
}


@end
