//
//  XTGameWindowController.m
//  TadsTerp
//
//  Created by Rune Berg on 14/03/14.
//  Copyright (c) 2014 Rune Berg. All rights reserved.
//

#import "XTGameWindowController.h"
#import "XTAppDelegate.h"
#import "XTTads3Entry.h"
#import "XTLogger.h"
#import "XTStringUtils.h"
#import "XTEventLoopBridge.h"
#import "XTBannerTextHandler.h"
#import "XTMainTextHandler.h"
#import "XTBottomBarHandler.h"
#import "XTOutputFormatterProtocol.h"
#import "XTBannerContainerView.h"
#import "XTMainTextView.h"
#import "XTTadsGameInfo.h"
#import "XTPrefs.h"
#import "XTFontManager.h"
#import "XTFileNameHelper.h"
#import "XTUIUtils.h"
#import "XTNotifications.h"
#import "XTTads2AppCtx.h"
#import "XTBannerTextHandler.h"
#import "os.h"
#import "appctx.h"
#import "trd.h"
#import "XTAllocDeallocCounter.h"
#import "XTCallOnMainThreadCounter.h"
#import "XTBannerCreateParams.h"
#import "XTRecentGameFiles.h"
#import "XTUserDefaultsUtils.h"
#import "XTRunGameParams.h"
#import "XTTableColumnWidthTracker.h"
#import "XTRequiredRectForTextCache.h"


@interface XTGameWindowController ()

@property NSUInteger instanceIndex;

@property XTRunGameParams *runGameParams;

@property (weak) IBOutlet NSView *outermostContainerView;
@property (weak) IBOutlet XTBannerContainerView *gameOutputContainerView;
@property (weak) NSScrollView *mainTextScrollView;
@property (unsafe_unretained) XTMainTextView *mainTextView;
@property (weak) IBOutlet NSTextField *keyPromptTextField;
@property (weak) IBOutlet NSTextField *parsingModeTextField;

@property NSDictionary *tads2EncodingsByInternalId;

@property XTTadsGameInfo *gameInfo;
@property volatile BOOL gameIsStarting;
	//TODO must cover stopping state too
@property BOOL windowWasClosedProgrammatically;

@property NSNumber *tads2EncodingSetByGame;
@property BOOL hasWarnedAboutFailedT2Decoding;
@property BOOL hasWarnedAboutFailedT2Encoding;

@property XTTads3Entry *tads3Entry;

@property XTMainTextHandler *mainTextHandler;
@property XTBottomBarHandler *bottomBarHandler;

@property NSMutableDictionary *bannersByHandle;
@property NSMutableDictionary *bannersByTagId;

@property volatile BOOL shuttingDownTadsEventLoopThread;
@property NSUInteger countTimesInTadsEventLoopThreadCancelledState;

@property XTEventLoopBridge *os_gets_EventLoopBridge;
@property XTEventLoopBridge *os_waitc_eventLoopBridge;
@property XTEventLoopBridge *os_event_eventLoopBridge;
@property XTEventLoopBridge *os_fileNameDialog_EventLoopBridge;

//TODO to out text handler?
@property BOOL pendingKeyFlag;
@property unichar pendingKey;

@property XTGameInputEvent *inputEvent; // shared between VM and UI threads

@property NSUInteger returnCodeFromInputDialogWithTitle;

@property NSURL* fileNameDialogUrl;

@property XTFontManager *fontManager;
@property XTPrefs *prefs;
@property XTFileNameHelper *fileNameHelper;

@property NSString *morePromptText;
@property NSString *pressAnyKeyPromptText;

@end


@implementation XTGameWindowController

static NSUInteger instanceCount = 0;

// Redefine to include instanceIndex
#undef XT_DEF_SELNAME
#define XT_DEF_SELNAME  NSString *selName = [NSString stringWithFormat:@"[%lu] %@", self.instanceIndex, NSStringFromSelector(_cmd)];

static XTLogger* logger;

// for XTGameRunnerProtocol:
@synthesize isSleeping = _isSleeping;

@synthesize gameIsT3 = _gameIsT3;


#include "XTGameWindowController_vmThreadFuncs.m"
#include "XTGameWindowController_vmThreadBannerApi.m"


+ (void)initialize
{
	logger = [XTLogger loggerForClass:[XTGameWindowController class]];
}

OVERRIDE_ALLOC_FOR_COUNTER
OVERRIDE_DEALLOC_FOR_COUNTER

+ (XTGameWindowController *)controller
{
	XTGameWindowController *gwc = [[XTGameWindowController alloc] initWithWindowNibName:@"XTGameWindowController"];
	return gwc;
}

- (NSURL *)gameFileUrl
{
	return self.runGameParams.gameFileUrl;
}

- (BOOL)isShuttingDownTadsEventLoopThread
{
	return _shuttingDownTadsEventLoopThread;
}

- (NSString *)getVmThreadName
{
	XT_DEF_SELNAME;
	
	NSString *vmThreadName = nil;
	if (self.tadsEventLoopThread == nil) {
		// april/april gets us here - why??
		XT_ERROR_0(@"vm thread is nil");
	} else {
		vmThreadName = self.tadsEventLoopThread.name;
		if (vmThreadName == nil) {
			XT_ERROR_0(@"vm thread name is nil");
		}
	}
	return vmThreadName;
}

- (BOOL)canLoadAndStartGameFile
{
	//XT_DEF_SELNAME;
	//XT_TRACE_2(@"_gameIsStarting=%d _shuttingDownTadsEventLoopThread=%d", _gameIsStarting, _shuttingDownTadsEventLoopThread);

	BOOL res = ((! _gameIsStarting) && (! _shuttingDownTadsEventLoopThread));
	return res;
}

- (BOOL)loadAndStartGameFile:(XTRunGameParams *)runGameParams
{
	_runGameParams = runGameParams;
	
	BOOL res = [self loadAndStartGameFile];
	return res;
}

- (BOOL)loadAndStartGameFile
{
	XT_DEF_SELNAME;
	
	if (! [self canLoadAndStartGameFile]) {
		XT_ERROR_0(@"! canLoadAndStartGameFile");
	}
	
	[self showWindow:self];
	
	[XTRequiredRectForTextCache reset];
	[[XTTableColumnWidthTracker tracker] clearAll];

	[self.mainTextHandler resetToDefaults];
	[self.bottomBarHandler resetToDefaults];

	self.gameIsStarting = NO;
	self.gameIsRunning = NO;
	self.gameIsT3 = NO;
	self.tads2EncodingSetByGame = nil;
	self.hasWarnedAboutFailedT2Decoding = NO;
	self.hasWarnedAboutFailedT2Encoding = NO;
	self.pendingKeyFlag = NO;
	self.isSleeping = NO;
	
	self.gameInfo = [XTTadsGameInfo gameInfoFromFile:[self getGameFilename]];

	if (self.gameInfo != nil) {
		if (self.gameInfo.gameTitle != nil && self.gameInfo.gameTitle.length >= 1) {
			NSMutableString *gameTitle = [NSMutableString stringWithString:self.gameInfo.gameTitle];
			self.mainTextHandler.gameTitle = gameTitle;
		}
	}
	
	BOOL res = YES; //TODO real value from startGame
	[self startGame];
	
	return res;
}

- (NSString *)getGameFilename
{
	const char* filename = [self.gameFileUrl fileSystemRepresentation];
	NSString *nsFilename = [NSString stringWithUTF8String:filename];
	return nsFilename;
}

- (XTCommandHistory *)commandHistory
{
	return self.mainTextHandler.commandHistory;
}

- (void)setCommandHistory:(XTCommandHistory *)commandHistory
{
	self.mainTextHandler.commandHistory = commandHistory;
}

- (BOOL)gameIsT3
{
	return _gameIsT3;
}

- (void)setGameIsT3:(BOOL)gameIsT3
{
	_gameIsT3 = gameIsT3;
	[self.mainTextHandler setIsForT3:gameIsT3];
	// Banners handlers get this set as they're created
}

- (void)windowWillClose:(NSNotification *)notification
{
	//XT_TRACE_ENTRY;
	
	if (! self.windowWasClosedProgrammatically) {
		// Window closed by user clicking Close icon or ditto keyboard shortcut
		// (as opposed to closed indirectly by user starting a new game or restarting game)
		//XT_TRACE_0(@"Window closed by user clicking Close icon or ditto keyboard shortcut");

		[self quitGameIfRunning];
		
		NSNotification *myNotification = [NSNotification notificationWithName:XTadsNotifyGameWindowClosed object:self];
		[[NSNotificationQueue defaultQueue] enqueueNotification:myNotification postingStyle:NSPostWhenIdle];
	}
}

- (BOOL)windowShouldClose:(id)sender
{
	//XT_DEF_SELNAME;
	
	BOOL res = YES;
	
	if (self.isSleeping) {
		// game VM thread is sleeping
		//XT_TRACE_0(@"disallow because game VM thread is sleeping");
		res = NO;
	} else if (self.prefs.askForConfirmationOnGameQuit.value.boolValue) {
		res = [self confirmQuitGameIfRunning:@"The game is still running. Do you really want to close the window and quit the game?"];
	}
	
	//XT_TRACE_1(@"-> %d", res);

	return res;
}

- (BOOL)confirmQuitGameIfRunning:(NSString *)informativeText
{
	XT_DEF_SELNAME;

	BOOL res = NO;
	
	if (self.gameIsRunning) {
		res = [XTUIUtils confirmAbortRunningGameInWindow:self.window
											 messageText:@"Quit Game?"
										 informativeText:informativeText
							   continuePlayingButtonText:@"Continue Playing"
								   quitPlayingButtonText:@"Quit Game"];

	} else {
		res = YES;
	}
	
	XT_TRACE_1(@"-> %d", res);
	
	return res;
}

- (BOOL)confirmQuitTerpIfGameRunning:(NSString *)informativeText
{
	XT_DEF_SELNAME;

	BOOL res = NO;
	
	if (self.gameIsRunning) {
		res = [XTUIUtils confirmAbortRunningGameInWindow:self.window
											 messageText:@"Quit XTads?"
										 informativeText:informativeText
							   continuePlayingButtonText:@"Continue Playing"
								   quitPlayingButtonText:@"Quit"];
		
	} else {
		res = YES;
	}

	XT_TRACE_1(@"-> %d", res);
	
	return res;
}

- (BOOL)quitGameIfRunning
{
	//XT_TRACE_ENTRY;
	
	if (! self.gameIsRunning) {
		//XT_TRACE_0(@"game not running");
		return YES;
	}
	
	if (! [self shutDownTadsEventLoopThread]) {
		//XT_TRACE_0(@"shutDownTadsEventLoopThread returned NO");
		return NO;
	}
	
	return YES;
}

- (void)closeWindow
{
	self.windowWasClosedProgrammatically = YES;
	[self close];
}

- (id)initWithWindow:(NSWindow *)window
{
    self = [super initWithWindow:window];
    if (self) {
        [self myCustomInit];
    }
    return self;
}

- (void)myCustomInit
{
	instanceCount += 1;
	_instanceIndex = instanceCount;
	
	_os_gets_EventLoopBridge = [XTEventLoopBridge bridgeWithName:@"os_gets_EventLoopBridge"];
	_os_waitc_eventLoopBridge = [XTEventLoopBridge bridgeWithName:@"os_waitc_eventLoopBridge"];
	_os_event_eventLoopBridge = [XTEventLoopBridge bridgeWithName:@"os_event_eventLoopBridge"];
	_os_fileNameDialog_EventLoopBridge = [XTEventLoopBridge bridgeWithName:@"os_fileNameForSaveDialog_EventLoopBridge"];
	
	_tads2EncodingsByInternalId = @{
		@"La1": [NSNumber numberWithUnsignedInteger:NSISOLatin1StringEncoding], /* ISO 8859-1 */
		@"La2": [NSNumber numberWithUnsignedInteger:NSISOLatin2StringEncoding], /* ISO 8859-2 */
		@"La3": [NSNumber numberWithUnsignedInteger:CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatin3)], /* ISO 8859-3 */
		@"La4": [NSNumber numberWithUnsignedInteger:CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatin4)], /* ISO 8859-4 */
		@"La5": [NSNumber numberWithUnsignedInteger:CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatinCyrillic)], /* ISO 8859-5 */
		@"La6": [NSNumber numberWithUnsignedInteger:CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatinArabic)], /* ISO 8859-6, =ASMO 708, =DOS CP 708 */
		@"La7": [NSNumber numberWithUnsignedInteger:CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatinGreek)], /* ISO 8859-7 */
		@"La8": [NSNumber numberWithUnsignedInteger:CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatinHebrew)], /* ISO 8859-8 */
		@"La9": [NSNumber numberWithUnsignedInteger:CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatin5)], /* ISO 8859-9 */
		@"La10": [NSNumber numberWithUnsignedInteger:CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatin6)], /* ISO 8859-10 */
		@"La11": [NSNumber numberWithUnsignedInteger:CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatinThai)], /* ISO 8859-11 */
		@"La13": [NSNumber numberWithUnsignedInteger:CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatin7)], /* ISO 8859-13 */
		@"La14": [NSNumber numberWithUnsignedInteger:CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatin8)], /* ISO 8859-14 */
		@"La15": [NSNumber numberWithUnsignedInteger:CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatin9)], /* ISO 8859-15 */
		@"La16": [NSNumber numberWithUnsignedInteger:CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatin10)], /* ISO 8859-16 */
		@"1251": [NSNumber numberWithUnsignedInteger:NSWindowsCP1251StringEncoding]
	};
	
	_countTimesInTadsEventLoopThreadCancelledState = 0;
	
	_tads2EncodingSetByGame = nil;
	_hasWarnedAboutFailedT2Decoding = NO;
	_hasWarnedAboutFailedT2Encoding = NO;
	_gameIsStarting = NO;
	_gameIsRunning = NO;
	_gameIsT3 = NO;
	_pendingKeyFlag = NO;
	_inputEvent = [XTGameInputEvent new];
	_isSleeping = NO;

	_bottomBarHandler = [XTBottomBarHandler new];
	
	_bannersByHandle = [NSMutableDictionary dictionary];
	_bannersByTagId = [NSMutableDictionary dictionary];
	[XTBannerTextHandler resetStaticState];
	
	_fontManager = [XTFontManager fontManager];
	_prefs = [XTPrefs prefs];
		//TODO bug! get a default-value-only instance here
	_fileNameHelper = [XTFileNameHelper helper];
	
	_morePromptText = @"More - press a key to continue...";
	_pressAnyKeyPromptText = @"Press a key...";
}

- (void)windowDidLoad
{
	//XT_DEF_SELNAME;
	
    [super windowDidLoad];

	self.window.delegate = self;
	
	[self setPositionAndSizeFromPrefs];
	[self setColorsFromPrefs];

	[self createRootBanner];
	
	//TODO call handler:
	self.mainTextView.string = @"";
	self.mainTextView.mainTextHandler = self.mainTextHandler;
	self.mainTextView.delegate = self;

	//TODO mv to classq
	[self.mainTextView setAutomaticTextReplacementEnabled:NO];
	[self.mainTextView setAutomaticQuoteSubstitutionEnabled:NO];

	//TODO mv
	self.mainTextHandler.textView = self.mainTextView;
	self.mainTextHandler.scrollView = self.mainTextScrollView;

	self.bottomBarHandler.keyPromptTextField = self.keyPromptTextField;
	self.bottomBarHandler.parsingModeTextField = self.parsingModeTextField;
	[self.bottomBarHandler mainThread_clearKeyPrompt];
	
	[self setupReceptionOfAppLevelNotifications];
}

- (void)setPositionAndSizeFromPrefs
{
	XT_TRACE_ENTRY;

	// Avoid new terp windows "wandering position":
	[self setShouldCascadeWindows:NO];
	
	NSRect winFrame = self.window.frame;
	
	NSString *mode = self.prefs.gameWindowStartMode.value;
	if ([XTPREFS_GAME_WINDOW_START_MODE_SAME_AS_LAST isEqualToString:mode]) {
		[XTUserDefaultsUtils readGameWindowPositionAndSize:&winFrame];
	} else if ([XTPREFS_GAME_WINDOW_START_MODE_NICE_IN_MIDDLE isEqualToString:mode]) {
		[XTUserDefaultsUtils getGameWindowPositionAndSizeNicelyInMiddle:&winFrame];
	}  else if ([XTPREFS_GAME_WINDOW_START_MODE_WHATEVER isEqualToString:mode]) {
		// Nothing, let the OS decide
	} else {
		XT_WARN_1(@"unknown gameWindowStartMode: %ld", self.prefs.gameWindowStartMode);
		// Nothing, let the OS decide
	}
	
	[self.window setFrame:winFrame display:YES];
	winFrame = self.window.frame;
	
	[XTUserDefaultsUtils saveGameWindowPositionAndSize:&winFrame];
}

- (void)setColorsFromPrefs
{
	for (XTBannerTextHandler *bh in self.bannersByHandle.allValues) {
		[bh setColorsFromPrefsColor];
	}
}

- (void)setColorsFromPrefAllowGameToSetColors
{
	for (XTBannerTextHandler *bh in self.bannersByHandle.allValues) {
		[bh setColorsFromPrefAllowGameToSetColors];
	}
}

- (void)setColorsFromBody
{
	for (XTBannerTextHandler *bh in self.bannersByHandle.allValues) {
		[bh setColorsFromBody];
	}
}

- (void)updateLinksUnderline
{
	for (XTBannerTextHandler *bh in self.bannersByHandle.allValues) {
		[bh updateLinksUnderline];
	}
}

- (void)startGame
{
	int exitCode = 0;
	if ([XTStringUtils url:self.gameFileUrl endsWith:@".gam"]) {
		exitCode = [self runTads2Game];
	} else if ([XTStringUtils url:self.gameFileUrl endsWith:@".t3"]) {
		exitCode = [self runTads3Game];
	} else {
		XT_DEF_SELNAME;
		XT_ERROR_1(@"got illegal game URL: %@", self.gameFileUrl);
	}
	self.tadsEventLoopThread.name = [NSString stringWithFormat:@"tadsVmThread-%lu", self.instanceIndex];
}

- (void)signalCommandEntered
{
	[self.os_gets_EventLoopBridge signal:0];
}

- (void)signalKeyPressed:(unichar)keyPressed
{
	[self.os_waitc_eventLoopBridge signal:keyPressed];
}

- (void)signalEvent
{
	[self.os_event_eventLoopBridge signal:0];
}

// The main thread has finished a file name dialog
- (void)signalFileNameDialogCompleted
{
	[self.os_fileNameDialog_EventLoopBridge signal:0];
}

- (void)mainThread_gameHasEnded
{
	XT_TRACE_ENTRY;
	
	//TODO? future: should in principle be able to do pagination here,
	//              without the VM running, but it's a little bit of work...
	//              (neither qtads nor mjr win terp does pagination in this situation)
	[self.mainTextHandler setNonstopMode:YES];
	
	[self.mainTextHandler hardFlushOutput]; // needed for e.g. when TADS VM exits immediately with an error message
	
	NSString *hardNewline = [self hardNewline];
	NSString *gameHasEndedMsg = [NSString stringWithFormat:@"%@%@[The game has ended]%@", hardNewline, hardNewline, hardNewline];

	[self.mainTextHandler resetForGameHasEndedMsg];
	[self.mainTextHandler appendOutput:gameHasEndedMsg];
	[self.mainTextHandler flushOutput];
	
	[self displayTradStatusLineScoreStringAfterCommandReplay];
	
	[self mainThread_updateGameTitle];

	XT_TRACE_0(@"exit");
}

- (void)displayTradStatusLineScoreStringAfterCommandReplay
{
	if ([self isReplayingCommandScript]) {
		// Make sure we display score string after command replay of plain-text T2 game:
		XTBannerTextHandler *bannerHandlerTSL = [self getBannerHandlerForTradStatusLine];
		if (bannerHandlerTSL != nil) {
			self.runGameParams.commandScriptAction = XT_CMD_SCRIPT_ACTION_NONE;
			[bannerHandlerTSL flushTradStatusLineScoreString];
			self.runGameParams.commandScriptAction = XT_CMD_SCRIPT_ACTION_REPLAY;
		}
	}
}

//-------------------------------------------------------------------------------
// TADS 2 game startup and driver loop

- (int)runTads2Game
{
	XT_TRACE_ENTRY;
	
	// run T2 event loop in its own thread:
	
	if (! [self shutDownTadsEventLoopThread]) {
		XT_WARN_0(@"cannot start new game - previous game VM thread isn't shut down yet");
		return 0;
	}
	
	self.gameIsStarting = YES;
	
	self.tadsEventLoopThread = [XTVmThread alloc];
	self.tadsEventLoopThread = [self.tadsEventLoopThread initWithTarget:self
													   selector:@selector(runTads2GameLoopThread:)
														 object:nil];
	//NSString *threadName = [NSString stringWithFormat:@"tads2EventLoopThread %@", self.mainTextHandler.gameTitle];
	//[self.tads2EventLoopThread setName:threadName];
	[self.tadsEventLoopThread start];
	
	self.gameIsT3 = NO;

	XT_TRACE_0(@"started T2 VM thread");
	
	return 0;
}

//-------------------------------------------------------------------------------
// TADS 3 game startup and driver loop

- (int)runTads3Game
{
	XT_TRACE_ENTRY;

	// run T3 event loop in its own thread:

	if (! [self shutDownTadsEventLoopThread]) {
		XT_WARN_0(@"cannot start new game - previous game VM thread isn't shut down yet");
		return 0;
	}
	
	self.gameIsStarting = YES;
	
	self.tadsEventLoopThread = [XTVmThread alloc];
	self.tadsEventLoopThread = [self.tadsEventLoopThread initWithTarget:self
													   selector:@selector(runTads3GameLoopThread:)
														 object:nil];
	[self.tadsEventLoopThread start];
	
	self.gameIsT3 = YES;
	
	return 0;
}

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

- (BOOL)shutDownTadsEventLoopThread
{
	XT_TRACE_ENTRY;
	//XT_WARN_2(@"entry _gameIsStarting=%d _shuttingDownTadsEventLoopThread=%d", _gameIsStarting, _shuttingDownTadsEventLoopThread);
	
	BOOL res = YES;

	if (self.tadsEventLoopThread != nil) {

		if ([self.tadsEventLoopThread isFinished]) {

			// Nothing

		} else {

			//XT_TRACE_0(@"executing...");
			
			_shuttingDownTadsEventLoopThread = YES;
				//TODO combine into one flag w/ thread cancel state?

			[self.tadsEventLoopThread cancel]; //TODO after signal 0 calls?
			
			[self.os_gets_EventLoopBridge signal:0];
			[self.os_waitc_eventLoopBridge signal:0];
			[self.os_event_eventLoopBridge signal:0];
			[self.os_fileNameDialog_EventLoopBridge signal:0];
			
			NSTimeInterval sleepTo = 0.2;
			[NSThread sleepForTimeInterval:sleepTo]; // so the signals can take effect
			
			// wait for tads event loop thread to exit:
			XT_TRACE_0(@"starting wait loop");
			NSInteger waitCount = 0;
			while (_shuttingDownTadsEventLoopThread &&
				   (! [self.tadsEventLoopThread isFinished]) &&
				   waitCount <= 30) {
				
				[self.os_gets_EventLoopBridge signal:0];
				[self.os_waitc_eventLoopBridge signal:0];
				[self.os_event_eventLoopBridge signal:0];
				[self.os_fileNameDialog_EventLoopBridge signal:0];
				
				[NSThread sleepForTimeInterval:((double)0.2)];
				waitCount += 1;
				XT_TRACE_0(@"in wait loop");
			}
			if (_shuttingDownTadsEventLoopThread) {
				XT_WARN_0(@"giving up waiting for TADS event loop thread");
				res = NO;
			} else {
				// Nothing
			}

			[self.os_gets_EventLoopBridge reset];
			[self.os_waitc_eventLoopBridge reset];
			[self.os_event_eventLoopBridge reset];
			[self.os_fileNameDialog_EventLoopBridge reset];
		}
	}

	XT_TRACE_0(@"exit");
	//XT_WARN_2(@"exit _gameIsStarting=%d _shuttingDownTadsEventLoopThread=%d", _gameIsStarting, _shuttingDownTadsEventLoopThread);
	
	return res;
}

- (void)cleanupAtVmThreadExit
{
	//XT_TRACE_ENTRY;

	[self.mainTextHandler removeHandler];

	XTTads2AppCtx *t2AppCtx = [XTTads2AppCtx context];
	[t2AppCtx resetState];
	
	[self teardownReceptionOfAppLevelNotifications];

	self.gameIsStarting = NO;
	self.gameIsRunning = NO;
	self.os_gets_EventLoopBridge = nil;
	self.os_waitc_eventLoopBridge = nil;
	self.os_event_eventLoopBridge = nil;
	self.os_fileNameDialog_EventLoopBridge = nil;
	
	[self removeBannerForTradStatusLine];
	[self bannerDeleteAll];
	[self deleteRootBanner];
	
	self.window.delegate = nil;
	
	// Needed for when game window controller is forcibly closed:
	[self.window close];
	self.window = nil;
	
	self.mainTextView.delegate = nil;
	//TODO rm - done on view's teardown method
	
	self.bottomBarHandler = nil;
	
	// In the rare case we're not deallocated, at least don't hang on to other objects:
	//TODO ? self.tadsEventLoopThread = nil;
	self.outermostContainerView = nil;
	self.gameOutputContainerView = nil;
	self.mainTextScrollView = nil;
	self.mainTextView = nil;
	self.keyPromptTextField = nil;
	self.parsingModeTextField = nil;
	self.tads2EncodingsByInternalId = nil;
	self.gameInfo = nil;
	self.tads2EncodingSetByGame = nil;
	[self.tads3Entry cleanup];
	_tads3Entry = nil;
	self.mainTextHandler = nil;
	self.bannersByHandle = nil;
	self.bannersByTagId = nil;
	self.bannerHandleForTradStatusLine = nil;
	self.inputEvent = nil;
	self.fileNameDialogUrl = nil;
	self.fontManager = nil;
	self.prefs = nil;
	self.fileNameHelper = nil;
	self.morePromptText = nil;
	self.pressAnyKeyPromptText = nil;
}

- (BOOL)isReplayingCommandScript
{
	BOOL res = (self.runGameParams.commandScriptAction == XT_CMD_SCRIPT_ACTION_REPLAY);
	return res;
}

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

- (void)setupReceptionOfAppLevelNotifications
{
	[[NSNotificationCenter defaultCenter] addObserver:self
											 selector:@selector(handleNotifyPrefsChanged:)
												 name:XTadsNotifyPrefsChanged
											   object:nil]; // nil means "for any sender"
}

- (void)teardownReceptionOfAppLevelNotifications
{
	[[NSNotificationCenter defaultCenter] removeObserver:self
													name:XTadsNotifyPrefsChanged
												  object:nil];
}

- (void)handleNotifyPrefsChanged:(NSNotification *)notification
{
	NSDictionary *userInfo = notification.userInfo;
	XTPrefsItem *prefsItem = userInfo[XTadsNotificationUserInfoKeyPrefsItem];
	if ([prefsItem isKindOfClass:[XTPrefsItemColor class]]) {
		[self setColorsFromPrefs]; // color selection
	} else if ([prefsItem isKindOfClass:[XTPrefsItemBoolAffectingColors class]]) {
		[self setColorsFromPrefAllowGameToSetColors];
	} else if ([prefsItem isKindOfClass:[XTPrefsItemBoolLinksUnderline class]]) {
		[self updateLinksUnderline];
	} else {
		int brkpt = 1;
	}
	[self mainThread_layoutAllBannerViews];
	[self.prefs persist]; // in case this was because of loading a new game, and we must persist its directory in prefs
}

//-------------------------------------------------------------------------------
// Text output

- (BOOL)printOutputText:(NSString *)string
{
	//XT_DEF_SELNAME;

	BOOL excessiveAmount = [self.mainTextHandler appendOutput:string];
	return excessiveAmount;
}

- (void)printOutputNewlineAfterCommand
{
	[self.mainTextHandler appendOutputNewlineAfterCommand:[self hardNewline]];
}

//-------------------------------------------------------------------------------
// Banners

- (void)createBannerForTradStatusLine
{
	//XT_DEF_SELNAME;

	if (self.bannerHandleForTradStatusLine == nil) {
		void *handle = [self bannerCreate:0
									where:OS_BANNER_FIRST
									other:0
								  wintype:OS_BANNER_TYPE_TEXT
									align:OS_BANNER_ALIGN_TOP
									 size:1
								sizeUnits:OS_BANNER_SIZE_ABS
									style:OS_BANNER_STYLE_BORDER];
		self.bannerHandleForTradStatusLine = [NSNumber numberWithUnsignedInteger:(NSUInteger)handle];
		XTBannerTextHandler *bhTSL = [self getBannerHandlerForTradStatusLine];
		bhTSL.mainTextHandler = self.mainTextHandler;
		bhTSL.isForTradStatusLine = YES;
		[bhTSL resetForTradStatusLine];
		bhTSL.htmlMode = self.htmlMode;
	}
}

- (void)removeBannerForTradStatusLine
{
	if (self.bannerHandleForTradStatusLine != nil) {
		void *banner_handle = (void *)self.bannerHandleForTradStatusLine.unsignedIntegerValue;
		XTBannerTextHandler *banner = [self getBanner:banner_handle];
		[self mainThread_bannerDelete:banner];
	}
}

- (void)createRootBanner
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"ENTER");
	
	XTMainTextHandler *banner = [XTMainTextHandler new];
	self.mainTextHandler = banner;
	banner.gameWindowController = self;
	
	NSUInteger handle = 0;
	NSNumber *handleObj = [NSNumber numberWithUnsignedInteger:handle];
	[self.bannersByHandle setObject:banner forKey:handleObj];
	
	[self traceBannerHierarchy];
	
	[banner createTextViewForMainOutputArea];
	
	banner.rootBannerContainerView = self.gameOutputContainerView;
	self.mainTextView = (XTMainTextView *)banner.textView;
	self.mainTextScrollView = banner.scrollView;
	
	[self mainThread_layoutAllBannerViews];
}

- (void)deleteRootBanner
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"");

	RETURN_IF_SHUTTING_DOWN_VM_THREAD;
	
	NSUInteger handle = 0;
	
	XTBannerTextHandler *banner = [self getBanner:(void *)handle];
	if (banner != nil) {
		[banner callOnMainThread:@selector(mainThread_removeHandler)];

		NSNumber *handleObj = [NSNumber numberWithUnsignedInteger:handle];
		[self.bannersByHandle removeObjectForKey:handleObj];
		
		self.mainTextHandler = nil;
		
		//[self.mainTextView teardown];  // already done by ???TextHandler
		[self.mainTextView removeFromSuperview];
		self.mainTextView = nil;
		
		[self.mainTextScrollView removeFromSuperview];
		self.mainTextScrollView = nil;
		
		[self.gameOutputContainerView removeFromSuperview];
		self.gameOutputContainerView = nil;
		
		[self.outermostContainerView removeFromSuperview];
		self.outermostContainerView = nil;
	}
}

- (XTBannerTextHandler *)getBannerHandlerForTradStatusLine
{
	XTBannerTextHandler *bh = nil;
	if (self.bannerHandleForTradStatusLine != nil) {
		bh = [self.bannersByHandle objectForKey:self.bannerHandleForTradStatusLine];
	}
	return bh;
}

- (void *)bannerCreate:(void *)parent
				 tagId:(NSString *)tagId
				 where:(NSInteger)where
				 other:(void *)other
			   wintype:(NSInteger)wintype //TODO unsigned
				 align:(NSInteger)align   //TODO unsigned
				  size:(NSInteger)size    //TODO unsigned
			 sizeUnits:(NSInteger)sizeUnits  //TODO unsigned
				 style:(NSUInteger)style
			  htmlMode:(BOOL)htmlMode
{
	XT_DEF_SELNAME;
	
	void *handle = nil;
	
	if ([self bannerHandleForTagId:tagId] == nil) {
		XTBannerCreateParams *params = [XTBannerCreateParams new];
		params.parent = parent;
		params.where = where;
		params.other = other;
		params.wintype = wintype;
		params.align = align;
		params.size = size;
		params.sizeUnits = sizeUnits;
		params.style = style;
		params.isForTagBanner = YES;
		params.htmlMode = htmlMode;
		
		[self mainThread_bannerCreate:params];
		handle = params.bannerHandle;
		if (handle != nil) {
			XTBannerTextHandler *banner = [self getBanner:handle];
			banner.tagId = tagId;
			[self.bannersByTagId setObject:banner forKey:tagId];
		}
	} else {
		XT_ERROR_1(@"banner with tagId %@ already exists", tagId);
	}
	
	return handle;
}

- (void)mainThread_bannerCreate:(XTBannerCreateParams *)params
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"ENTER");
	
	XTBannerTextHandler *parentBanner = [self getBanner:params.parent];
	
	XTBannerTextHandler *otherBanner = nil;
	if (params.other != nil) {
		otherBanner = [self getBanner:params.other];
	}
	
	XTBannerTextHandler *banner = [XTBannerTextHandler handlerWithParent:parentBanner
																   where:params.where
																   other:otherBanner
																 wintype:params.wintype
																   align:params.align
																	size:params.size
															   sizeUnits:params.sizeUnits
																   style:params.style];
	[banner setIsForTagBanner:params.isForTagBanner];
	[banner setIsForT3:self.gameIsT3];
	banner.isBeingCreated = YES;
	banner.htmlMode = params.htmlMode;
	
	NSUInteger handle = banner.bannerIndex;
	NSNumber *handleObj = [NSNumber numberWithUnsignedInteger:handle];
	[self.bannersByHandle setObject:banner forKey:handleObj];
	
	[self traceBannerHierarchy];
	
	XT_TRACE_1(@" %lu", handle);
	
	[banner createTextViewForBanner];
	banner.htmlMode = params.htmlMode; // must be done after createTextViewForBanner

	[self mainThread_layoutAllBannerViews];
	
	[banner mainThread_noteStartOfPagination];
	
	params.bannerHandle = (void *)handle;
}

- (void)mainThread_bannerDelete:(XTBannerTextHandler *)banner
{
	[banner mainThread_removeHandler];
	NSNumber *handleObj = [NSNumber numberWithUnsignedInteger:banner.bannerIndex];
	[self.bannersByHandle removeObjectForKey:handleObj];
	// ...but not children - it's up to the caller to do that
	if (banner.tagId != nil) {
		[self.bannersByTagId removeObjectForKey:banner.tagId];
	}
	if (banner.bannerIndex == [self.bannerHandleForTradStatusLine unsignedIntegerValue]) {
		self.bannerHandleForTradStatusLine = nil;
	}
	[self mainThread_layoutAllBannerViews];
}

- (void)mainThread_layoutAllBannerViews
{
	XT_DEF_SELNAME;
	//XT_WARN_0(@"");
	
	if (! [NSThread isMainThread]) {
		XT_WARN_0(@"should ONLY be called on main thread");
	}
	
	[self.mainTextHandler noteStartOfLayoutOfViews];
	[self.mainTextHandler configureViews];
	//[self.mainTextHandler noteStartOfPagination];
		// exp rm'd (contributes to broken pagination)
	[self.mainTextHandler noteEndOfLayoutOfViews];
}

- (void)exitingTagBanner
{
	//XT_TRACE_ENTRY;
	
	[self sizeActiveTagBannerToContentsIfNecessary];
	[self mainThread_layoutAllBannerViews];
	
	[self.mainTextHandler exitingTagBanner];
}

- (void)sizeActiveTagBannerToContentsIfNecessary
{
	void *activeTagBannerHandle = self.mainTextHandler.activeTagBannerHandle;
	if (activeTagBannerHandle != NULL) {
		NSNumber *objActiveTagBannerHandle = [NSNumber numberWithUnsignedInteger:(NSUInteger)activeTagBannerHandle];
		XTBannerTextHandler *activeTagBanner = [self.bannersByHandle objectForKey:objActiveTagBannerHandle];
		activeTagBanner.isBeingCreated = NO;
		if (activeTagBanner.tagBannerNeedsSizeToContent) {
			[self bannerSizeToContents:activeTagBannerHandle doLayout:NO];
			CGFloat	bannerSize = activeTagBanner.sizeOfContents;
			[activeTagBanner captureInitialSizeWhenViewSize:bannerSize];
		}
	}
}

- (void)resizeTagBannersToContentsIfNecessary
{
	NSArray *bannerKeys = self.bannersByHandle.allKeys;
	for (NSUInteger bki = 0; bki < bannerKeys.count; bki++) {
		XTBaseTextHandler *baseHandler = [self.bannersByHandle objectForKey:bannerKeys[bki]];
		if ([baseHandler isKindOfClass:[XTBannerTextHandler class]]) {
			XTBannerTextHandler *bannerHandler = (XTBannerTextHandler *)baseHandler;
			if ([bannerHandler isTagBannerThatShouldResizeToContents]) {
				NSNumber *bannerKeyObj = bannerKeys[bki];
				void *bannerHandle = (void *)[bannerKeyObj unsignedIntegerValue];
				[self bannerSizeToContents:bannerHandle doLayout:YES];
			}
		}
	}
}

- (void)mainThread_allBanners_resetForNextCommand
{
	[self.mainTextHandler moveCursorToEndOfOutputPosition];

	NSArray *bannerKeys = self.bannersByHandle.allKeys;
	for (NSUInteger bki = 0; bki < bannerKeys.count; bki++) {
		XTBannerTextHandler *bannerHandler = [self.bannersByHandle objectForKey:bannerKeys[bki]];
		[bannerHandler resetForNextCommand];
	}
}

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

- (BOOL)isWaitingForKeyPressed
{
	BOOL res = [self.os_waitc_eventLoopBridge isWaiting] || [self.os_event_eventLoopBridge isWaiting];
	return res;
}

- (void)mainThread_clearScreen
{
	//XT_WARN_ENTRY;
	
	[self.mainTextHandler clearText];
	
	if (! self.gameIsT3) {
		BOOL hadBanners = (self.bannersByHandle.count >= 1);
		if (hadBanners) {
			[self bannerDeleteAll];
			[self mainThread_layoutAllBannerViews];
		}
	}
}

- (void)mainThread_flushOutput
{
	[self.mainTextHandler flushOutput];

	for (XTBannerTextHandler *bh in self.bannersByHandle.allValues) {
		if (bh.bannerIndex >= 1) {
			NSMutableArray *params = [NSMutableArray arrayWithCapacity:2];
			params[0] = [NSNumber numberWithBool:YES]; // input param
			[bh mainThread_pumpOutputText:params];
		}
	}
}

- (void)mainThread_setGameTitle:(NSString *)title
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"\"%@\"", title);
	
	self.mainTextHandler.gameTitle = [NSMutableString stringWithString:title];
	[self mainThread_updateGameTitle];
}

- (void)mainThread_updateGameTitle
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"enter");

	NSString *gameTitle = self.mainTextHandler.gameTitle;
	if (gameTitle == nil || gameTitle.length == 0) {
		self.mainTextHandler.gameTitle = [NSMutableString stringWithString:self.gameFileUrl.lastPathComponent];
	}
	
	NSString *endedBit = (self.gameIsRunning ? @"" : @" [ended]");
	NSString *windowTitle = [NSString stringWithFormat:@"%@%@", self.mainTextHandler.gameTitle, endedBit];

	self.window.title = windowTitle;
	
	XTRecentGameFiles *recentGameFiles = [XTRecentGameFiles recentGameFiles];
	XTRecentGameFilesEntry *newestEntry = [recentGameFiles newestEntry];
	if (newestEntry != nil) {
		if (newestEntry.gameTitle == nil || ! [gameTitle isEqualToString:newestEntry.gameTitle]) {
			newestEntry.gameTitle = gameTitle;
			[recentGameFiles persist];
			XTAppDelegate *appDelegate = [[NSApplication sharedApplication] delegate];
			[appDelegate refreshMyOpenRecentMenu];
		}
	} else {
		XT_WARN_0(@"no entry in XTRecentGameFiles");
	}
}

- (NSString *)hardNewline
{
	NSString *res;
	if (self.htmlMode) {
		res = @"<br>";
	} else {
		res = @"\n";
	}
	return res;
}

- (void)mainThread_noteStartOfPagination
{
	[self.mainTextHandler mainThread_noteStartOfPagination];
	for (XTBannerTextHandler *bh in self.bannersByHandle.allValues) {
		[bh mainThread_noteStartOfPagination];
	}
}

//-------------------------------------------------------------------------------
// Position of cursor etc.

- (void)mainThread_moveCursorToEndOfOutputPosition
{
	[self.mainTextHandler moveCursorToEndOfOutputPosition];
}

- (BOOL)allowMoveCursorLeft
{
	BOOL res = ((! [self isWaitingForKeyPressed]) &&
						//TODO isWaitingForKeyPressed test needed?
				[self.mainTextHandler cursorIsInCommandButNotAtMinInsertionPosition]);
	return res;
}

//-------------------------------------------------------------------------------
#pragma mark NSWindowDelegate

- (void)windowWillStartLiveResize:(NSNotification *)notification
{
	//XT_DEF_SELNAME;
	//XT_TRACE_0(@"done");
}

- (void)windowDidEndLiveResize:(NSNotification *)notification
{
	//XT_DEF_SELNAME;
	//XT_TRACE_0(@"done");

	[self.mainTextHandler scrollToBottom];
	[self.mainTextHandler moveCursorToEndOfOutputPosition];
}

- (void)windowDidResize:(NSNotification *)notification
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");
	
	NSRect winFrame = self.window.frame;
	[XTUserDefaultsUtils saveGameWindowPositionAndSize:&winFrame];
	
	[self resizeTagBannersToContentsIfNecessary];
}

- (void)windowDidMove:(NSNotification *)notification
{
	NSRect winFrame = self.window.frame;
	[XTUserDefaultsUtils saveGameWindowPositionAndSize:&winFrame];
}

//-------------------------------------------------------------------------------
#pragma mark NSTextViewDelegate
	//TODO why isn't XTMainTextHandler the delegate?!

- (BOOL)textView:(NSTextView *)textView clickedOnLink:(id)link atIndex:(NSUInteger)charIndex
{
	BOOL handled;
	NSString *linkString = link;
	if ([XTStringUtils isInternetLink:linkString]) {
		// An Internet link - let the OS handle it.
		handled = NO;
	} else {
		// A "command link" - handle it ourselves.
		if (! [self handleClickedOnLinkAsTadsVmEvent:linkString]) {
			[XTNotifications notifyAboutTextLinkClicked:self linkText:link charIndex:charIndex];
		}
		handled = YES;
	}
	
	[XTNotifications notifySetFocusToMainOutputView:self];
	
	return handled;
}

//TODO mv:
- (BOOL)handleClickedOnLinkAsTadsVmEvent:(NSString *)linkString
{
	BOOL res;
	if (self.os_event_eventLoopBridge.isWaiting) {
		// The VM is waiting for an event, so give it one:
		self.inputEvent.type = OS_EVT_HREF;
		self.inputEvent.href = linkString;
		[self signalEvent];
		res = YES;
	} else if (self.os_waitc_eventLoopBridge.isWaiting) {
		// The VM is waiting for an key-press, so give it the first char in href attribute:
		unichar key = ' ';
		if (linkString != nil && linkString.length >= 1) {
			key = [linkString characterAtIndex:0];
		}
		[self signalKeyPressed:key];
		res = YES;
	} else {
		res = NO;
	}
	return res;
}

/* 
 Intercept certain non-editing operations.
 */
- (BOOL)textView:(NSTextView *)aTextView doCommandBySelector:(SEL)aSelector
{
	BOOL commandhandledHere = NO;

	if (! self.gameIsRunning) {

		commandhandledHere = YES;
		
	} else if (self.os_event_eventLoopBridge.isWaiting) {
		
		unichar keyPressed = [self handleCommandBySelectorWhenWaitingForCharacter:aSelector];
		self.inputEvent.type = OS_EVT_KEY;
		self.inputEvent.key0 = keyPressed;
		if (self.pendingKeyFlag) {
			self.inputEvent.key1 = self.pendingKey;
		}
		[self signalEvent];
		commandhandledHere = YES;

	} else if (self.os_waitc_eventLoopBridge.isWaiting) {

		unichar keyPressed = [self handleCommandBySelectorWhenWaitingForCharacter:aSelector];
		[self signalKeyPressed:keyPressed];
		commandhandledHere = YES;
		
	} else if (aSelector == @selector(insertNewline:)) {
	
		[self signalCommandEntered];
		commandhandledHere = YES;

	} else if (aSelector == @selector(moveUp:)) {

		if ([self.mainTextHandler cursorIsInCommand]) {
			[self.mainTextHandler goToPreviousCommand];
		} else {
			[self.mainTextHandler moveCursorToEndOfOutputPosition];
		}
		commandhandledHere = YES;
		
	} else if (aSelector == @selector(moveDown:)) {
		
		if ([self.mainTextHandler cursorIsInCommand]) {
			[self.mainTextHandler goToNextCommand];
		} else {
			[self.mainTextHandler moveCursorToEndOfOutputPosition];
		}
		commandhandledHere = YES;

	} else if (aSelector == @selector(moveLeft:)) {
		
		if (! [self.mainTextHandler cursorIsInCommand]) {
			[self.mainTextHandler moveCursorToEndOfOutputPosition];
			commandhandledHere = YES;
		} else {
			// no movement left of cmd prompt
			commandhandledHere = ! [self allowMoveCursorLeft];
		}
		
	} else if (aSelector == @selector(moveRight:)) {
		
		if (! [self.mainTextHandler cursorIsInCommand]) {
			[self.mainTextHandler moveCursorToEndOfOutputPosition];
			commandhandledHere = YES;
		}
		
	} else if (aSelector == @selector(cancelOperation:)) { // Esc key

		if (! [self.mainTextHandler cursorIsInCommand]) {
			[self.mainTextHandler moveCursorToEndOfOutputPosition];
			commandhandledHere = YES;
		}
	
	} else if (aSelector == @selector(moveToBeginningOfParagraph:) ||
			   aSelector == @selector(moveToLeftEndOfLine:)) { // ctrl-a
		
		if ([self.mainTextHandler cursorIsInCommand]) {
			[self.mainTextHandler moveCursorToStartOfCommand];
			commandhandledHere = YES;
		}

	} else if (aSelector == @selector(moveToEndOfParagraph:)) { // ctrl-e
		
		if ([self.mainTextHandler cursorIsInCommand]) {
			[self.mainTextHandler moveCursorToEndOfOutputPosition];
			commandhandledHere = YES;
		}

	} else if (aSelector == @selector(moveWordLeft:)) {  // alt-arrow-left
			
		if ([self.mainTextHandler cursorIsAtMinInsertionPosition]) {
			commandhandledHere = YES;
		}
		
	} else if (aSelector == @selector(insertTab:)) {
		
		// disable Tab key
		commandhandledHere = YES;
		
	} else {
		//TODO "delete forward", need to handle that?
		//TODO "scrollToEndOfDocument" ditto
		//TODO "scrollToBeginningOfDocument" ditto
	}
	
	if (commandhandledHere) {
		[self.mainTextHandler scrollToBottom];
	}

	return commandhandledHere;
}

//TODO is unichar the best return type here?
- (unichar)handleCommandBySelectorWhenWaitingForCharacter:(SEL)aSelector
{
	XT_DEF_SELNAME;
	XT_TRACE_2(@"pendingKeyFlag=%d pendingKey=%d", self.pendingKeyFlag, self.pendingKey);

	unichar keyPressed = ' ';
	
	if (self.hasPendingKey) {
		XT_WARN_0(@"self.hasPendingKey - shouldn't happen");
		keyPressed = self.pendingKey;
		self.pendingKeyFlag = NO;
	} else {
		if (aSelector == @selector(moveLeft:)) {
			keyPressed = 0;
			self.pendingKey = CMD_LEFT;
		} else if (aSelector == @selector(moveRight:)) {
			keyPressed = 0;
			self.pendingKey = CMD_RIGHT;
		} else if (aSelector == @selector(moveUp:)) {
			keyPressed = 0;
			self.pendingKey = CMD_UP;
		} else if (aSelector == @selector(moveDown:)) {
			keyPressed = 0;
			self.pendingKey = CMD_DOWN;
		} else if (aSelector == @selector(scrollToBeginningOfDocument:)) {
			keyPressed = 0;
			self.pendingKey = CMD_TOP;
		} else if (aSelector == @selector(scrollToEndOfDocument:)) {
			keyPressed = 0;
			self.pendingKey = CMD_BOT;
		} else if (aSelector == @selector(scrollPageUp:)) {
			keyPressed = 0;
			self.pendingKey = CMD_PGUP;
		} else if (aSelector == @selector(scrollPageDown:)) {
			keyPressed = 0;
			self.pendingKey = CMD_PGDN;
		} else if (aSelector == @selector(deleteForward:)) {
			keyPressed = 0;
			self.pendingKey = CMD_DEL;
		} else if (aSelector == @selector(moveToLeftEndOfLine:)) {
			keyPressed = 0;
			self.pendingKey = CMD_HOME;
		} else if (aSelector == @selector(moveToRightEndOfLine:)) {
			keyPressed = 0;
			self.pendingKey = CMD_END;
		} else if (aSelector == @selector(noop:)) { // disregard warning about noop:
			// needed - gets called for e.g. cmd-i when at a key prompt
			//TODO??? handle ctrl-* alt-* ... hardly worth it
			NSEvent *currentEvent = [[NSApplication sharedApplication] currentEvent];
			if (currentEvent.type == NSKeyDown) {
				NSString *charsIgnMods = [currentEvent charactersIgnoringModifiers];
				if (charsIgnMods.length >= 1) {
					unichar uch = [charsIgnMods characterAtIndex:0];
					NSInteger extKey = [self extendedKeyForFunctionKey:uch];
					if (extKey > 0) {
						keyPressed = 0;
						self.pendingKey = extKey;
					}
				}
			}
		} else if (aSelector == @selector(deleteBackward:)) {
			// Backspace
			keyPressed = 127;
		} else if (aSelector == @selector(insertNewline:)) {
			// Return
			keyPressed = 10;
		} else if (aSelector == @selector(cancelOperation:)) {
			// Esc(ape)
			keyPressed = 27;
		} else if (aSelector == @selector(complete:)) {
			// F5 (= auto-complete)
			keyPressed = 0;
			self.pendingKey = CMD_F5;
		} else {
			int brkpt = 1;
		}
		self.pendingKeyFlag = (keyPressed == 0);
	}

	XT_TRACE_3(@"-> %lu (pendingKeyFlag: %d, pendingKey: %d)", keyPressed, self.pendingKeyFlag, self.pendingKey);
	
	return keyPressed;
}

- (NSInteger)extendedKeyForFunctionKey:(unichar)key
{
	NSInteger res = 0; // meaning no extended key, i.e. key was not a function key
	switch (key) {
		case NSF1FunctionKey:
			res = CMD_F1;
			break;
		case NSF2FunctionKey:
			res = CMD_F2;
			break;
		case NSF3FunctionKey:
			res = CMD_F3;
			break;
		case NSF4FunctionKey:
			res = CMD_F4;
			break;
		case NSF5FunctionKey:
			// we don't get here - see @selector(complete:) case in handleCommandBySelectorWhenWaitingForCharacter
			res = CMD_F5;
			break;
		case NSF6FunctionKey:
			res = CMD_F6;
			break;
		case NSF7FunctionKey:
			res = CMD_F7;
			break;
		case NSF8FunctionKey:
			res = CMD_F8;
			break;
		case NSF9FunctionKey:
			res = CMD_F9;
			break;
		case NSF10FunctionKey:
			res = CMD_F10;
			break;
		default:
			res = 0;
			break;
	}
	return res;
}

/*
 Only allow editing after command prompt
 */
- (BOOL)textView:(NSTextView *)aTextView shouldChangeTextInRange:(NSRange)affectedCharRange replacementString:(NSString *)replacementString
{
	XT_DEF_SELNAME;

	BOOL shouldChangeText;

	if (! self.gameIsRunning) {

		shouldChangeText = NO;
		
	} else if (self.os_event_eventLoopBridge.isWaiting) {
		
		unichar keyPressed = ' ';
		if (replacementString != nil && replacementString.length >= 1) {
			keyPressed = [replacementString characterAtIndex:0];
		} else {
			XT_WARN_0(@"had no replacementString");
		}
		self.inputEvent.type = OS_EVT_KEY;
		self.inputEvent.key0 = keyPressed;
		[self signalEvent];
		shouldChangeText = NO;
		
	} else if (self.os_waitc_eventLoopBridge.isWaiting) {

		unichar keyPressed = ' ';
		if (replacementString != nil && replacementString.length >= 1) {
			keyPressed = [replacementString characterAtIndex:0];
		} else {
			XT_WARN_0(@"had no replacementString");
		}
		[self signalKeyPressed:keyPressed];
		shouldChangeText = NO;
		
	} else if (! self.os_gets_EventLoopBridge.isWaiting) {

		// Not expecting text input
		//TODO? consider "type-ahead buffering"
		XT_TRACE_1(@"skipping input \"%@\"", replacementString);
		shouldChangeText = NO;

	} else {
		
		BOOL allowTextInsertion = [self.mainTextHandler allowTextInsertion:affectedCharRange];
		
		if (replacementString.length >= 1) {
			if (! allowTextInsertion) {
				// cursor is somewhere in printed output text (where editing isn't allowed),
				// so move cursor to end of text and append the new text
				[self.mainTextHandler moveCursorToEndOfOutputPosition];
				[self.mainTextHandler appendInput:replacementString];
				[self.mainTextHandler moveCursorToEndOfOutputPosition];
				shouldChangeText = NO;
			} else {
				// cursor is somewhere in command being typed
				shouldChangeText = YES;
			}
		} else {
			shouldChangeText = allowTextInsertion;
		}
	}
	return shouldChangeText;
}

- (BOOL)hasPendingKey
{
	return self.pendingKeyFlag;
}

- (unichar)getPendingKey
{
	return self.pendingKey;
}

- (void)clearPendingKey
{
	self.pendingKeyFlag	= NO;
}

//TODO mv some to new .m file:

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

- (void) mainThread_getFileName:(NSArray *)args
{
	//TODO retest interaction with GUI events (cmd-r etc.)
	
	NSNumber *fileTypeAsNumber = args[0];
	XTadsFileNameDialogFileType fileType = fileTypeAsNumber.integerValue;
	NSURL *defaultDir = [self.fileNameHelper findDefaultDirectoryForFileType:fileType];

	NSString *dialogTitlePrefix = args[1];
	if (dialogTitlePrefix == nil || (id)dialogTitlePrefix == [NSNull null]) {
		dialogTitlePrefix = @"Select File to Save";
		//TODO not always Save
	}
	
	NSString *fileTypeDescription = args[2];
	if (fileTypeDescription == nil || (id)fileTypeDescription == [NSNull null]) {
		fileTypeDescription = @"Any file";
	}

	NSArray *allowedExtensions = args[3];
	NSString *displayedAllowedExt = nil;
	if (allowedExtensions != nil && (id)allowedExtensions != [NSNull null] && allowedExtensions.count >= 1) {
		displayedAllowedExt = allowedExtensions[0];
	} else {
		displayedAllowedExt = @"*";
		allowedExtensions = nil;
	}
	
	NSNumber *existingFileAsNumber = args[4];
	BOOL existingFile = existingFileAsNumber.boolValue;

	NSString *dialogTitle = [NSString stringWithFormat:@"%@  (%@ - *.%@)", dialogTitlePrefix, fileTypeDescription, displayedAllowedExt];
	
	self.fileNameDialogUrl = nil;

	NSWindow* window = [self window];
	
	if (existingFile) {
		NSOpenPanel* panel = [XTUIUtils makeFileOpenPanelWithTitle:dialogTitle allowedExtensions:allowedExtensions defaultDir:defaultDir];
		[XTNotifications notifyModalPanelOpened:self];
		[panel beginSheetModalForWindow:window completionHandler:^(NSInteger result){
			[XTNotifications notifyModalPanelClosed:self];
			if (result == NSFileHandlingPanelOKButton) {
				self.fileNameDialogUrl = [panel URL];
				[self.fileNameHelper noteUsedDirectory:self.fileNameDialogUrl forFileType:fileType];
			}
			[self signalFileNameDialogCompleted];
		}];
	} else {
		NSString *defaultBasename = [self.fileNameHelper findDefaultFileBasenameForGameFileUrl:self.gameFileUrl fileType:fileType];
		NSSavePanel* panel = [XTUIUtils makeFileSavePanelWithTitle:dialogTitle allowedExtensions:allowedExtensions defaultDir:defaultDir defaultBasename:defaultBasename];
		[XTNotifications notifyModalPanelOpened:self];
		[panel beginSheetModalForWindow:window completionHandler:^(NSInteger result){
			[XTNotifications notifyModalPanelClosed:self];
			if (result == NSFileHandlingPanelOKButton) {
				self.fileNameDialogUrl = [panel URL];
				[self.fileNameHelper noteUsedDirectory:self.fileNameDialogUrl forFileType:fileType];
			}
			[self signalFileNameDialogCompleted];
		}];
	}
}

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

// inkey.t test game calls this
- (void)mainThread_inputDialog:(NSArray *)args
{
	//XT_DEF_SELNAME;
	
	NSString *title = (NSString *)args[0];
	NSUInteger standardButtonSetId = ((NSNumber *)args[1]).unsignedIntegerValue;
	NSArray *customButtomSpecs = (NSArray *)args[2];
	NSUInteger defaultIndex = ((NSNumber *)args[3]).unsignedIntegerValue; // 1-based, left to right
	NSUInteger cancelIndex = ((NSNumber *)args[4]).unsignedIntegerValue; // 1-based, left to right
	XTadsInputDialogIconId iconId = ((NSNumber *)args[5]).unsignedIntegerValue;

	NSUInteger resIndex = [XTUIUtils runModalInputDialogForWindow:self.window
															title:title
											  standardButtonSetId:standardButtonSetId
												customButtomSpecs:customButtomSpecs
													 defaultIndex:defaultIndex
													  cancelIndex:cancelIndex
														   iconId:iconId];
	
	self.returnCodeFromInputDialogWithTitle = resIndex;
}

- (void)mainThread_showModalErrorDialog:(NSArray *)args
{
	NSWindow *window = args[0];
	NSString *messageText = args[1];
	[XTUIUtils showModalErrorDialogInWindow:window messageText:messageText];
}

@end
