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

#import "XTGameWindowController.h"
#import "XTTads3Entry.h"
#import "XTLogger.h"
#import "XTStringUtils.h"
#import "XTEventLoopBridge.h"
#import "XTBannerHandler.h"
#import "XTOutputTextHandler.h"
#import "XTBottomBarHandler.h"
#import "XTOutputFormatterProtocol.h"
#import "XTBannerContainerView.h"
#import "XTOutputTextView.h"
#import "XTTadsGameInfo.h"
#import "XTPrefs.h"
#import "XTFontManager.h"
#import "XTFontUtils.h"
#import "XTDirectoryHelper.h"
#import "XTUIUtils.h"
#import "XTNotifications.h"
#import "XTTads2AppCtx.h"
#import "XTBannerHandler.h"
#import "os.h"
#import "appctx.h"
#import "XTAllocDeallocCounter.h"
//#include <CFStringEncodingExt.h>


@interface XTGameWindowController ()

@property NSUInteger instanceIndex;

@property (weak) IBOutlet NSView *outermostContainerView;
@property (weak) IBOutlet XTBannerContainerView *gameOutputContainerView;
@property (weak) NSScrollView *outputTextScrollView;
@property (unsafe_unretained) XTOutputTextView *outputTextView;
@property (weak) IBOutlet NSTextField *keyPromptTextField;
@property (weak) IBOutlet NSTextField *parsingModeTextField;

@property XTUIUtils *uiUtils;

@property NSDictionary *tads2EncodingsByInternalId;

@property NSString *nsFilename;
@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 XTOutputTextHandler *outputTextHandler;
@property XTBottomBarHandler *bottomBarHandler;

@property NSMutableDictionary *bannersByHandle;
@property NSMutableDictionary *bannersByTagId;
@property NSNumber *bannerHandleForTradStatusLine;

@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 NSString *tradStatusLineScoreString;

@property NSUInteger returnCodeFromInputDialogWithTitle;

@property NSURL* fileNameDialogUrl;

@property XTFontManager *fontManager;
@property XTPrefs *prefs;
@property XTDirectoryHelper *directoryHelper;

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

@end


@implementation XTGameWindowController {

	NSRange sharedRangeFor_childThread_deleteCharactersInRange;
	//TODO prop?
}

static NSUInteger instanceCount = 0;

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

static XTLogger* logger;

// for XTGameRunnerProtocol:
@synthesize statusMode = _statusMode;
@synthesize isSleeping = _isSleeping;

@synthesize gameIsT3 = _gameIsT3;

#define KEY_GAMEWINDOW_FRAME @"XTadsGameWindowFrame"
#define VALUE_FMT_GAMEWINDOW_FRAME "x=%d y=%d w=%d h=%d"


#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;
}

- (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:(NSURL *)gameFileUrl
{
	_gameFileUrl = gameFileUrl;
	const char* filename = [self.gameFileUrl fileSystemRepresentation];
	self.nsFilename = [NSString stringWithUTF8String:filename];
	
	BOOL res = [self loadAndStartGameFile];
	return res;
}

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

	[self.outputTextHandler 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.nsFilename];
	if (self.gameInfo != nil) {
		if (self.gameInfo.gameTitle != nil && self.gameInfo.gameTitle.length >= 1) {
			NSMutableString *gameTitle = [NSMutableString stringWithString:self.gameInfo.gameTitle];
			self.outputTextHandler.gameTitle = gameTitle;
		}
	}
	
	BOOL res = YES; //TODO real value from startGame
	[self startGame];
	
	return res;
}

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

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

- (BOOL)gameIsT3
{
	return _gameIsT3;
}

- (void)setGameIsT3:(BOOL)gameIsT3
{
	_gameIsT3 = gameIsT3;
	[self.outputTextHandler 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.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 = [self.uiUtils 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 = [self.uiUtils 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;
}

//TODO mv into caller
- (void)resourceCleanupAtVmExit
{
	//XT_TRACE_ENTRY;

	XTTads2AppCtx *t2AppCtx = [XTTads2AppCtx context];
	[t2AppCtx resetState];
	
	[self.prefs stopObservingChangesToAll:self];
	
	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.outputTextView.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.outputTextScrollView = nil;
	self.outputTextView = nil;
	self.keyPromptTextField = nil;
	self.parsingModeTextField = nil;
	self.uiUtils = nil;
	self.tads2EncodingsByInternalId = nil;
	self.nsFilename = nil;
	self.gameInfo = nil;
	self.tads2EncodingSetByGame = nil;
	[self.tads3Entry cleanup];
	_tads3Entry = nil;
	self.outputTextHandler = nil;
	self.bannersByHandle = nil;
	self.bannersByTagId = nil;
	self.bannerHandleForTradStatusLine = nil;
	self.inputEvent = nil;
	self.tradStatusLineScoreString = nil;
	self.fileNameDialogUrl = nil;
	self.fontManager = nil;
	self.prefs = nil;
	self.directoryHelper = nil;
	self.morePromptText = nil;
	self.pressAnyKeyPromptText = nil;
}

- (void)closeWindow
{
	//XT_TRACE_ENTRY;

	self.windowWasClosedProgrammatically = YES;
	[self close];
}

//TODO runs in vm thread - move
//TODO collect XTGameRunnerProtocol stuff in sep. section
- (void)setStatusMode:(NSInteger)newStatusMode
{
	XT_DEF_SELNAME;
	
	if (self.gameIsT3) {
		XT_ERROR_0(@"unexpected call for T3 game");
	}

	if (_shuttingDownTadsEventLoopThread) {
		//XT_TRACE_0(@"bail out - shutting down VM thread");
		return;
	}
	
	if (! self.htmlMode) {
		
		[self createBannerForTradStatusLine]; // ...if none exists already

		BOOL switchingToStatusLineMode = (newStatusMode == 1 && self.statusMode != 1);
		if (switchingToStatusLineMode) {
			if (self.bannerHandleForTradStatusLine != nil) {
				void *handle = (void *)[self.bannerHandleForTradStatusLine unsignedIntegerValue];
				[self bannerClear:handle];
			} else {
				XT_ERROR_0(@"self.bannerHandleForTradStatusLine == nil");
			}
		} else {
			/*TODO exp rm
			BOOL exitingStatusLineMode = (newStatusMode != 1 && self.statusMode == 1);
			if (exitingStatusLineMode) {
				if (self.bannerHandleForTradStatusLine != nil) {
					void *handle = (void *)[self.bannerHandleForTradStatusLine unsignedIntegerValue];
					[self bannerFlushTradStatusLineScoreString:handle];
				} else {
					XT_ERROR_0(@"self.bannerHandleForTradStatusLine == nil");
				}
			}
			 */
		}
	}
	
	self.outputTextHandler.statusLineMode = newStatusMode;
	_statusMode = newStatusMode;
	XT_TRACE_1(@"statusMode=%ld", _statusMode);
}

//TODO move
- (void)exitingTagBanner
{
	[self sizeActiveTagBannerToContentsIfNecessary];
	[self layoutAllBannerViews];
	
	self.outputTextHandler.activeTagBannerHandle = NULL;
}

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

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

- (void)myCustomInit
{
	instanceCount += 1;
	_instanceIndex = instanceCount;
	
	_uiUtils = [XTUIUtils new];
	
	_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;

	//_statusLineHandler = [XTStatusLineHandler new];
	_outputTextHandler = [XTOutputTextHandler handler];
	_outputTextHandler.gameWindowController = self;
	_bottomBarHandler = [XTBottomBarHandler new];
	
	_bannersByHandle = [NSMutableDictionary dictionary];
	_bannersByTagId = [NSMutableDictionary dictionary];
	[XTBannerHandler resetStaticState];
	
	_fontManager = [XTFontManager fontManager];
	_prefs = [XTPrefs prefs];
		//TODO bug! get a default-value-only instance here
	_directoryHelper = [XTDirectoryHelper 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 setFontsFromPrefs];

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

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

	//TODO mv
	self.outputTextHandler.outputTextView = self.outputTextView;
	self.outputTextHandler.outputTextScrollView = self.outputTextScrollView;

	self.bottomBarHandler.keyPromptTextField = self.keyPromptTextField;
	self.bottomBarHandler.parsingModeTextField = self.parsingModeTextField;
	[self.bottomBarHandler clearKeyPrompt];
	
	[self.prefs startObservingChangesToAll:self];
}

- (void)setPositionAndSizeFromPrefs
{
	XT_TRACE_ENTRY;

	// Avoid new terp windows "wandering position":
	[self setShouldCascadeWindows:NO];
	
	NSRect winFrame = self.window.frame;
	
	switch (self.prefs.gameWindowStartMode) {
		case XTPREFS_GAME_WINDOW_SAME_AS_LAST: {
			[self readGameWindowPositionAndSize:&winFrame];
			break;
		}
		case XTPREFS_GAME_WINDOW_NICE_IN_MIDDLE: {
			[self getGameWindowPositionAndSizeNicelyInMiddle:&winFrame];
			break;
		}
		case XTPREFS_GAME_WINDOW_WHATEVER:
			// Nothing, let the OS decide
			break;
		default:
			XT_WARN_1(@"unknown gameWindowStartMode: %ld", self.prefs.gameWindowStartMode);
			// Nothing, let the OS decide
			break;
	}
	
	[self.window setFrame:winFrame display:YES];
	winFrame = self.window.frame;
	
	[self saveGameWindowPositionAndSize:&winFrame];
}

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

- (void)setFontsFromPrefs
{
}

- (void)startGame
{
	int exitCode = 0;
	if ([self gameFileUrlEndsWith:@".gam"]) {
		exitCode = [self runTads2Game];
	} else if ([self gameFileUrlEndsWith:@".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;

	[self.outputTextHandler 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.outputTextHandler resetForGameHasEndedMsg];
	[self.outputTextHandler appendOutput:gameHasEndedMsg];
	
	self.outputTextHandler.paginationActive = NO;
	[self.outputTextHandler flushOutput];
	self.outputTextHandler.paginationActive = YES;
	
	[self mainThread_updateGameTitle];

	XT_TRACE_0(@"exit");
}

//TODO mv to utils?
- (BOOL)gameFileUrlEndsWith:(NSString *)dotFileExtension
{
	BOOL res = NO;
	if (self.gameFileUrl != nil) {
		NSString *gameFileUrlString = [self.gameFileUrl absoluteString];
		res = [XTStringUtils string:gameFileUrlString endsWithCaseInsensitive:dotFileExtension];
	}
	return res;
}

//-------------------------------------------------------------------------------
// 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.outputTextHandler.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];
		//TODO get rif of tadsEventLoopThread member
	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.outputTextHandler removeHandler];
	[self resourceCleanupAtVmExit];
}

//-------------------------------------------------------------------------------
// Prefs changes affecting us

- (void)observeValueForKeyPath:(NSString *)keyPath
					  ofObject:(id)object
						change:(NSDictionary *)change
					   context:(void *)context
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"keyPath=\"%@\"", keyPath);
	
	if (object == self.prefs) {
		XT_TRACE_0(@"syncing...");
		[self setColorsFromPrefs];
		[self setFontsFromPrefs];
		if ([self prefsChangeRequiresBannerLayout:keyPath]) {
			[self layoutAllBannerViews];
			//TODO? ideally we should re-render banners' contents here...
		}
		[self.prefs persist]; // in case this was because of loading a new game, and we must persist its directory in prefs
		XT_TRACE_0(@"...syncing done");
	} else {
		XT_TRACE_0(@"OTHER");
	}
}

- (BOOL)prefsChangeRequiresBannerLayout:(NSString *)keyPath
{
	NSString *keyPathLc = [keyPath lowercaseString];
	BOOL res = [keyPathLc containsString:@"font"];
	//TODO also banner inset when impl'd as a user pref.
	return res;
}

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

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

	BOOL excessiveAmount = NO;
	
	if (self.statusMode == 0) {
		// regular output mode
		excessiveAmount = [self.outputTextHandler appendOutput:string];
		
	} else if (self.statusMode == 1) {
		// status line mode
		if (self.htmlMode) {
			[self.outputTextHandler appendOutput:string]; // will contain "<banner>..."
		} else {
			[self createBannerForTradStatusLine]; // ... if none exists already
			string = [self prepareStringForStatusLine:string];
			[self appendTextToTradStatusLine:string];
		}

	} else {
		XT_WARN_1(@"unknown statusMode %d", self.statusMode);
	}
	
	return excessiveAmount;
}

//TODO mv
- (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];
		XTBannerHandler *bhTSL = [self getBannerHandlerForTradStatusLine];
		bhTSL.isForTradStatusLine = YES;
		//XT_WARN_3(@"created XTOutputFormatter %@ for banner handler for banner %lu, isForTradStatusLine=%lu",
		//		  bhTSL.outputFormatter, bhTSL.bannerIndex, bhTSL.isForTradStatusLine);
		[bhTSL resetForTradStatusLine];
		bhTSL.htmlMode = self.htmlMode;
		/*...TODO try in flush instead?
		if (self.tradStatusLineScoreString != nil) {
			[self bannerDisplayTradStatusLineScoreString:handle text:self.tradStatusLineScoreString];
		}*/
	}
}

//TODO mv
- (void)removeBannerForTradStatusLine
{
	if (self.bannerHandleForTradStatusLine != nil) {
		[self bannerDelete:(void *)self.bannerHandleForTradStatusLine.unsignedIntegerValue];
	}
}

//TODO mv
- (void)deleteRootBanner
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"");
	
	if (_shuttingDownTadsEventLoopThread) {
		return;
	}
	
	NSUInteger handle = 0;
	
	XTBannerHandler *banner = [self getBanner:(void *)handle];
	if (banner != nil) {
		[banner performSelectorOnMainThread:@selector(mainThread_removeHandler)
								 withObject:nil
							  waitUntilDone:YES];
		NSNumber *handleObj = [NSNumber numberWithUnsignedInteger:handle];
		[self.bannersByHandle removeObjectForKey:handleObj];
		
		[self performSelectorOnMainThread:@selector(mainThread_removeRootBanner)
								 withObject:nil
							  waitUntilDone:YES];
	}
}

//TODO mv
- (void)mainThread_removeRootBanner
{
	self.outputTextHandler = nil;
	
	//[self.outputTextView teardown];  // already done by OutputTextHandler
	[self.outputTextView removeFromSuperview];
	self.outputTextView = nil;
	
	[self.outputTextScrollView removeFromSuperview];
	self.outputTextScrollView = nil;
	
	[self.gameOutputContainerView removeFromSuperview];
	self.gameOutputContainerView = nil;
	
	[self.outermostContainerView removeFromSuperview];
	self.outermostContainerView = nil;
}

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

//TODO mv
- (void)appendTextToTradStatusLine:(NSString *)text
{
	XTBannerHandler *bhTSL = [self getBannerHandlerForTradStatusLine];
	[bhTSL display:text];
}
	
- (void)mainThread_pumpOutputText:(id)arg
{
	//XT_DEF_SELNAME;
	
	NSMutableArray *argsArray = arg; // @[returnValueHolder]
	
	for (XTBannerHandler *bh in self.bannersByHandle.allValues) {
		if (! [bh isForMainOutputArea]) {
			[bh mainThread_flush];
		}
	}
	
	BOOL needMorePrompt = [self.outputTextHandler pumpOutput];
	argsArray[0] = [NSNumber numberWithBool:needMorePrompt];

	[XTNotifications notifySetFocusToMainOutputView:self];
}

/*
 *   '\n' - newline: end the current line and move the cursor to the start of
 *   the next line.  If the status line is supported, and the current status
 *   mode is 1 (i.e., displaying in the status line), then two special rules
 *   apply to newline handling: newlines preceding any other text should be
 *   ignored, and a newline following any other text should set the status
 *   mode to 2, so that all subsequent output is suppressed until the status
 *   mode is changed with an explicit call by the client program to
 *   os_status().
 *
 *   '\r' - carriage return: end the current line and move the cursor back to
 *   the beginning of the current line.  Subsequent output is expected to
 *   overwrite the text previously on this same line.  The implementation
 *   may, if desired, IMMEDIATELY clear the previous text when the '\r' is
 *   written, rather than waiting for subsequent text to be displayed.
 */
- (NSString *)prepareStringForStatusLine:(NSString *)str
{
	if (str.length == 0) {
		return str;
	}
	
	const char *cstr = [str UTF8String];
	const char *cstrCurrent = cstr;
	while (*cstrCurrent == '\n') {
		cstrCurrent += 1;
	}
	const char *cstrStart = cstrCurrent;
	while (*cstrCurrent != '\n' && *cstrCurrent != '\0') {
		cstrCurrent += 1;
	}
	if (*cstrCurrent == '\n') {
		self.statusMode = 2;
	}
	const char *cstrEnd = cstrCurrent;
	NSInteger len = cstrEnd - cstrStart;
	NSString *res = [[NSString alloc] initWithBytes:cstrStart length:len encoding:NSUTF8StringEncoding];
	return res;
}

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

- (void)mainThread_deleteCharactersInRange
{
	NSTextStorage *textStorage = [self.outputTextView textStorage];
	NSUInteger len = [textStorage length];
	NSRange range = sharedRangeFor_childThread_deleteCharactersInRange;
	[textStorage deleteCharactersInRange:range];
}

//TODO rename
- (void)mainThread_outputTextHandler_resetForNextCommand
{
	[self.outputTextHandler resetForNextCommand];
}

- (void)mainThread_clearScreen
{
	//XT_DEF_SELNAME;
	//XT_TRACE_0(@"");

	[self.outputTextHandler clearText];
	
	if (self.gameIsT3) {
		//TODO exp: out commented because of Blighted Isle was missing banner text at game start
		//for (XTBannerHandler *bh in self.bannersByHandle.allValues) {
		//	[bh mainThread_clear];
		//}
	} else {
		BOOL hadBanners = (self.bannersByHandle.count >= 1);
		if (hadBanners) {
			[self bannerDeleteAll];
			[self layoutAllBannerViews];
				//TODO write new method for main thread
		}
	}
}

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

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

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

	NSString *gameTitle = self.outputTextHandler.gameTitle;
	//XT_TRACE_0(@"1");
	if (gameTitle == nil || gameTitle.length == 0) {
		//XT_TRACE_0(@"2");
		self.outputTextHandler.gameTitle = [NSMutableString stringWithString:self.gameFileUrl.lastPathComponent];
		//XT_TRACE_0(@"3");
	}
	
	NSString *endedBit = (self.gameIsRunning ? @"" : @" [ended]");
	//XT_TRACE_0(@"4");
	NSString *windowTitle = [NSString stringWithFormat:@"%@%@", self.outputTextHandler.gameTitle, endedBit];
	//XT_TRACE_0(@"5");
	
	self.window.title = windowTitle;
	
	//XT_TRACE_1(@"\"%@\"", windowTitle);
}

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

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

//TODO mv to handler

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

- (void)mainThread_moveCursorToStartOfCommand
{
	NSUInteger index = self.outputTextHandler.minInsertionPoint;
	[self.outputTextView setSelectedRange:NSMakeRange(index, 0)];
}

- (BOOL)allowMoveCursorLeft
{
	BOOL res = ((! [self isWaitingForKeyPressed]) &&
						//TODO isWaitingForKeyPressed test needed?
				[self.outputTextHandler insertionPoint] > [self.outputTextHandler minInsertionPoint]);
	return res;
}

- (BOOL)cursorIsInCommand
{
	BOOL res = ([self.outputTextHandler insertionPoint] >= [self.outputTextHandler minInsertionPoint]);
	return res;
}

- (BOOL)cursorIsAtMinInsertionPosition
{
	BOOL res = ([self.outputTextHandler insertionPoint] == [self.outputTextHandler minInsertionPoint]);
	return res;
}

- (BOOL)handleSelectAll // cmd-a
{
	//TODO delegate to outputTextHandler
	//TODO when waiting for event / single key press...
	BOOL handled = NO;
	if ([self cursorIsInCommand]) {
		NSUInteger minInsertionPoint = self.outputTextHandler.minInsertionPoint;
		NSUInteger endOfOutputPosition = [self.outputTextHandler endOfOutputPosition];
		NSRange selectedTextRange = NSMakeRange(minInsertionPoint, endOfOutputPosition - minInsertionPoint);
		[self.outputTextView setSelectedRange:selectedTextRange];
		handled = YES;
	}
	return handled;
}

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

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

	NSRect winFrame = self.window.frame;
	[self saveGameWindowPositionAndSize:&winFrame];
}


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

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

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

//-------------------------------------------------------------------------------
#pragma mark NSTextViewDelegate
	//TODO why isn't XTOutputTextHandler 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 cursorIsInCommand]) {
			[self.outputTextHandler goToPreviousCommand];
		} else {
			[self mainThread_moveCursorToEndOfOutputPosition];
		}
		commandhandledHere = YES;
		
	} else if (aSelector == @selector(moveDown:)) {
		
		if ([self cursorIsInCommand]) {
			[self.outputTextHandler goToNextCommand];
		} else {
			[self mainThread_moveCursorToEndOfOutputPosition];
		}
		commandhandledHere = YES;

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

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

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

	} else if (aSelector == @selector(moveWordLeft:)) {  // alt-arrow-left
			
		if ([self 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
		int z = 1; // for brkpt
	}
	
	if (commandhandledHere) {
		[self.outputTextHandler 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:)) {
			//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;
					}
				}
			}
			int brkpt = 1;
		} 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.outputTextHandler 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 mainThread_moveCursorToEndOfOutputPosition];
				[self.outputTextHandler appendInput:replacementString];
				[self mainThread_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;

	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 = [NSOpenPanel openPanel];
		[panel setTitle:dialogTitle];
		[panel setPrompt:@"Open"];
		[panel setMessage:dialogTitle];
		[panel setShowsTagField:NO];
		BOOL allowOtherFileTypes = (allowedExtensions == nil);
		[panel setAllowsOtherFileTypes:allowOtherFileTypes];
		//[panel setExtensionHidden:NO];
		//[panel setCanSelectHiddenExtension:YES];
		[panel setAllowedFileTypes:allowedExtensions];

		NSURL *defaultDir = [self findDefaultDirectoryForFileType:fileType];
		if (defaultDir != nil) {
			[panel setDirectoryURL:defaultDir];
		}
		
		[XTNotifications notifyModalPanelOpened:self];
		
		[panel beginSheetModalForWindow:window completionHandler:^(NSInteger result){
			
			[XTNotifications notifyModalPanelClosed:self];
			
			if (result == NSFileHandlingPanelOKButton) {
				self.fileNameDialogUrl = [panel URL];
				[self noteUsedDirectory:self.fileNameDialogUrl forFileType:fileType];
			}
			
			[self signalFileNameDialogCompleted];
		}];
		
	} else {
		
		NSSavePanel* panel = [NSSavePanel savePanel];
		[panel setTitle:dialogTitle];
		[panel setPrompt:@"Save"];
		[panel setMessage:dialogTitle];
		[panel setShowsTagField:NO];
		BOOL allowOtherFileTypes = (allowedExtensions == nil);
		[panel setAllowsOtherFileTypes:allowOtherFileTypes];
		[panel setExtensionHidden:NO];
		[panel setCanSelectHiddenExtension:YES];
		[panel setAllowedFileTypes:allowedExtensions];
		
		NSURL *defaultDir = [self findDefaultDirectoryForFileType:fileType];
		if (defaultDir != nil) {
			[panel setDirectoryURL:defaultDir];
		}

		NSString *defaultFileBasename = [self findDefaultFileBasenameForFileType:fileType];
		if (defaultFileBasename != nil) {
			[panel setNameFieldStringValue:defaultFileBasename];
		}
		
		[XTNotifications notifyModalPanelOpened:self];
		
		[panel beginSheetModalForWindow:window completionHandler:^(NSInteger result){
			
			[XTNotifications notifyModalPanelClosed:self];
			
			if (result == NSFileHandlingPanelOKButton) {
				self.fileNameDialogUrl = [panel URL];
				[self noteUsedDirectory:self.fileNameDialogUrl forFileType:fileType];
			}
			
			[self signalFileNameDialogCompleted];
		}];
	}
}

- (NSURL*)findDefaultDirectoryForFileType:(XTadsFileNameDialogFileType)fileType
{
	XT_DEF_SELNAME;
	
	NSURL *res = nil;
	
	switch (fileType) {
		case XTadsFileNameDialogFileTypeSavedGame:
			res = [self.directoryHelper findDefaultSavesDirectory];
			break;
		case XTadsFileNameDialogFileTypeTranscript:
			res = [self.directoryHelper findDefaultTranscriptsDirectory];
			break;
		case XTadsFileNameDialogFileTypeCommandScript:
			res = [self.directoryHelper findDefaultCommandScriptsDirectory];
			break;
		case XTadsFileNameDialogFileTypeGeneral:
			// no default dir for this
			break;
		default:
			XT_WARN_1(@"unknown fileType %d", fileType);
			break;
	}

	return res;
}

- (NSString*)findDefaultFileBasenameForFileType:(XTadsFileNameDialogFileType)fileType
{
	XT_DEF_SELNAME;
	
	NSString *res = nil;
	
	switch (fileType) {
		case XTadsFileNameDialogFileTypeSavedGame:
			res = [self makeDefaultFileBasename:self.prefs.savesFileNameMode];
			break;
		case XTadsFileNameDialogFileTypeTranscript:
			res = [self makeDefaultFileBasename:self.prefs.transcriptsFileNameMode];
			break;
		case XTadsFileNameDialogFileTypeCommandScript:
			res = [self makeDefaultFileBasename:self.prefs.commandScriptsFileNameMode];
			break;
		case XTadsFileNameDialogFileTypeGeneral:
			// no default basename for this
			break;
		default:
			XT_WARN_1(@"unknown fileType %d", fileType);
			break;
	}
	
	return res;
}

- (NSString *)makeDefaultFileBasename:(XTPrefsFileNameMode)defaultFileNameMode
{
	XT_DEF_SELNAME;
	
	NSString *res = nil;
	
	switch (defaultFileNameMode) {
		case XTPREFS_FILENAME_MODE_GAMENAME_DATETIME: {
			NSURL *gameFileUrlMinusExtension = [self.gameFileUrl URLByDeletingPathExtension];
			NSString *gameFileBasename = [gameFileUrlMinusExtension lastPathComponent];
			NSDateFormatter *dateFormatter = [NSDateFormatter new];
			[dateFormatter setDateFormat:@"yyyyMMdd_HHmmss"];
			NSDate *now = [NSDate date];
			NSString *nowString = [dateFormatter stringFromDate:now];
			res = [NSString stringWithFormat:@"%@-%@", gameFileBasename, nowString];
			break;
		}
		case XTPREFS_FILENAME_MODE_UNTITLED:
			res = @"untitled";
			break;
		default:
			XT_WARN_1(@"unknown defaultFileNameMode %d", defaultFileNameMode);
			break;
	}
	
	return res;
}

- (void)noteUsedDirectory:(NSURL *)dirUrl forFileType:(XTadsFileNameDialogFileType)fileType
{
	XT_DEF_SELNAME;
	
	switch (fileType) {
		case XTadsFileNameDialogFileTypeSavedGame:
			[self.directoryHelper noteUsedSavesDirectory:dirUrl];
			break;
		case XTadsFileNameDialogFileTypeTranscript:
			[self.directoryHelper noteUsedTranscriptsDirectory:dirUrl];
			break;
		case XTadsFileNameDialogFileTypeCommandScript:
			[self.directoryHelper noteUsedCommandScriptsDirectory:dirUrl];
			break;
		case XTadsFileNameDialogFileTypeGeneral:
			// nothing to note for this
			break;
		default:
			XT_WARN_1(@"unknown fileType %d", fileType);
			break;
	}
}

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

//TODO mv up
// inkey.t test game calls this
- (void) mainThread_inputDialog:(NSArray *)args
{
	XT_DEF_SELNAME;
	
	self.returnCodeFromInputDialogWithTitle = 0; // in case anything goes wrong...

	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 numberOfButtons = 0;

	NSAlert *alert = [[NSAlert alloc] init];
	[alert setMessageText:title];
	
	// Create the buttons
	
	switch (standardButtonSetId) {
		case OS_INDLG_OK:
			[self addInputDialogButton:@"OK" toAlert:alert];
			numberOfButtons = 1;
			break;
		case OS_INDLG_OKCANCEL:
			[self addInputDialogButton:@"Cancel" toAlert:alert];
			[self addInputDialogButton:@"OK" toAlert:alert];
			numberOfButtons = 2;
			break;
		case OS_INDLG_YESNO:
			[self addInputDialogButton:@"No" toAlert:alert];
			[self addInputDialogButton:@"Yes" toAlert:alert];
			numberOfButtons = 2;
			break;
		case OS_INDLG_YESNOCANCEL:
			[self addInputDialogButton:@"Cancel" toAlert:alert];
			[self addInputDialogButton:@"No" toAlert:alert];
			[self addInputDialogButton:@"Yes" toAlert:alert];
			numberOfButtons = 3;
			break;
		default: {
			// Custom buttons
			NSEnumerator *en = [customButtomSpecs reverseObjectEnumerator];
			for (NSString *buttonSpec = [en nextObject]; buttonSpec != nil; buttonSpec = [en nextObject]) {
				[self addInputDialogCustomButton:buttonSpec toAlert:alert];
				numberOfButtons += 1;
			}
			break;
		}
	}

	NSArray *buttons = alert.buttons;
	if (buttons == nil || buttons.count == 0) {
		XT_WARN_0(@"no buttons");
		return;
	}
	
	// Neuter any built-in handling of Return and Esc
	
	for (NSButton *btn in buttons) {
		NSString *keyEquiv = [btn keyEquivalent];
		BOOL isDefaultButton = [keyEquiv isEqualToString:@"\r"];  // Return
		BOOL isCancelButton = [keyEquiv isEqualToString:@"\033"]; // octal for Esc
		if (isDefaultButton || isCancelButton) {
			[btn setKeyEquivalent:@""];
		}
	}
	
	// Set key equivs for default (Return) and cancel (Esc) if so requested,
	// but only if they don't conflict with &...-spec'd key equivs.
	// (Stock NSAlert doesn't let us add explicit NSButton objects,
	// so we can't add a specialized NSButton that would support
	// multiple key equivs.)
	
	if (defaultIndex >= 1 && defaultIndex <= buttons.count) {
		NSUInteger defaultIndexRightToLeft = buttons.count - defaultIndex;
		NSButton *defaultButton = buttons[defaultIndexRightToLeft];
		if (! [self buttonHasKeyEquiv:defaultButton]) {
			[defaultButton setKeyEquivalent:@"\r"]; // Return
		}
	}
	
	if (cancelIndex >= 1 && cancelIndex <= buttons.count) {
		NSUInteger cancelIndexRightToLeft = buttons.count - cancelIndex;
		NSButton *cancelButton = buttons[cancelIndexRightToLeft];
		if (! [self buttonHasKeyEquiv:cancelButton]) {
			[cancelButton setKeyEquivalent:@"\033"]; // octal for Esc
		}
	}
	
	// Alert style (lhs icon)
	
	NSAlertStyle alertStyle = NSInformationalAlertStyle;
	if (iconId == XTadsInputDialogIconIdError) {
		alertStyle = NSCriticalAlertStyle;
	}
	[alert setAlertStyle:alertStyle];
		//TODO? ideally call setIcon too
	
	// Run the popup modally, and figure out which button was pressed

	NSInteger buttonIndex = [self.uiUtils runModalSheet:alert forWindow:self.window];
	
	NSUInteger resIndexRightToLeft = (buttonIndex - NSAlertFirstButtonReturn);
	NSUInteger resIndex = numberOfButtons - resIndexRightToLeft;
	
	self.returnCodeFromInputDialogWithTitle = resIndex;
}

- (BOOL)buttonHasKeyEquiv:(NSButton *)button {
	
	NSString *keyEquiv = button.keyEquivalent;
	BOOL res = ([keyEquiv length] >= 1);
	return res;
}

- (void)addInputDialogButton:(NSString *)title toAlert:(NSAlert *)alert {
	
	NSButton *button = [alert addButtonWithTitle:title];
}

//TODO mv to util?
- (void)addInputDialogCustomButton:(NSString *)s toAlert:(NSAlert *)alert
{
	NSRange rangeShortcutMarker = [s rangeOfString:@"&"];
	NSString *prefix = nil;
	NSString *shortcutKey = nil;
	NSString *suffix = nil;

	if (rangeShortcutMarker.location == NSNotFound) {
		prefix = s;
	} else {
		prefix = [s substringToIndex:rangeShortcutMarker.location];
		if (rangeShortcutMarker.location + 1 < s.length) {
			NSRange rangeShortcutKey = NSMakeRange(rangeShortcutMarker.location + 1, 1);
			shortcutKey = [s substringWithRange:rangeShortcutKey];
			if (rangeShortcutKey.location + 1 < s.length) {
				suffix = [s substringFromIndex:rangeShortcutKey.location + 1];
			}
		}
	}

	NSButton *button = [alert addButtonWithTitle:@"temp"];
	NSMutableString *title = [NSMutableString string];

	if (prefix != nil && prefix.length >= 1) {
		[title appendString:prefix];
	}
	if (shortcutKey != nil) {
		[title appendString:shortcutKey];
		NSString *shortcutKeyLowerCase = [shortcutKey lowercaseString];
		[button setKeyEquivalent:shortcutKeyLowerCase];
	}
	if (suffix != nil && suffix.length >= 1) {
		[title appendString:suffix];
	}

	[button setTitle:title];
}


- (void)mainThread_showModalErrorDialogWithMessageText:(NSString *)msgText
{
	//TODO show as sheet connected to window, not as a free-standing dlg window
	[self.uiUtils showModalErrorDialogWithMessageText:msgText];
}

//-------------------------------------------------------------------------------
// Support methods for setting/saving/reading game window's position and size

- (void)saveGameWindowPositionAndSize:(NSRect *)winFrame
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"called");
	
	int x = winFrame->origin.x;
	int y = winFrame->origin.y;
	int w = winFrame->size.width;
	int h = winFrame->size.height;
	
	char frameCString[200];
	sprintf(frameCString, VALUE_FMT_GAMEWINDOW_FRAME, x, y, w, h);
	NSString *frameString = [NSString stringWithUTF8String:frameCString];
	
	NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
	[userDefaults setObject:frameString forKey:KEY_GAMEWINDOW_FRAME];
}

- (void)readGameWindowPositionAndSize:(NSRect *)winFrame
{
	XT_DEF_SELNAME;
	
	NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
	NSString *frameString = [userDefaults stringForKey:KEY_GAMEWINDOW_FRAME];
	if (frameString != nil) {
		int x, y, w, h;
		int argsParsed = sscanf([frameString UTF8String], VALUE_FMT_GAMEWINDOW_FRAME, &x, &y, &w, &h);
		if (argsParsed == 4) {
			winFrame->origin.x = x;
			winFrame->origin.y = y;
			winFrame->size.width = w;
			winFrame->size.height = h;
		} else {
			XT_WARN_1(@"failed to parse frameString \"%@\"", frameString);
			// Nothing, let the OS decide
		}
	} else {
		XT_WARN_0(@"no frameString in NSUserDefaults");
		// Nothing, let the OS decide
	}
	
}

- (void)getGameWindowPositionAndSizeNicelyInMiddle:(NSRect *)winFrame
{
	NSRect screenFrame = [[NSScreen mainScreen] visibleFrame];
	CGFloat newHeightPct = 80.0;
	winFrame->size.width = 700;
	winFrame->size.height = screenFrame.size.height * (newHeightPct / 100.0);
	winFrame->origin.x = (screenFrame.size.width / 2.0) - (winFrame->size.width / 2.0);
	winFrame->origin.y = (screenFrame.size.height - winFrame->size.height) / 2.0 + 20;
}

@end
