//
//  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 "XTCustomBackgroundColorView.h"
#import "XTStatusLineHandler.h"
#import "XTOutputTextHandler.h"
#import "XTBottomBarHandler.h"
#import "XTOutputFormatterProtocol.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 "os.h"
#import "appctx.h"
//#include <CFStringEncodingExt.h>


@interface XTGameWindowController ()

@property (weak) IBOutlet NSView *overallView;
@property (weak) IBOutlet XTCustomBackgroundColorView *statusLineView;
@property (weak) IBOutlet NSTextField *locationTextField;
@property (weak) IBOutlet NSTextField *scoreTextField;
@property (weak) IBOutlet XTCustomBackgroundColorView *dividerStatusLineOutputTextView;
@property (weak) IBOutlet NSScrollView *outputTextScrollView;
@property (unsafe_unretained) IBOutlet XTOutputTextView *outputTextView;
@property (weak) IBOutlet NSTextField *keyPromptTextField;
@property (weak) IBOutlet NSTextField *parsingModeTextField;

@property XTUIUtils *uiUtils;

@property NSDictionary *tads2EncodingsByInternalId;

@property NSURL *gameFileUrl;
@property NSString *nsFilename;
@property XTTadsGameInfo *gameInfo;
@property BOOL gameIsStarting;
	//TODO must cover stopping state too
@property BOOL gameIsT3;
@property NSNumber *tads2EncodingSetByGame;
@property BOOL hasWarnedAboutFailedT2Decoding;
@property BOOL hasWarnedAboutFailedT2Encoding;

@property XTTads3Entry *tads3Entry;

@property XTStatusLineHandler *statusLineHandler;
@property XTOutputTextHandler *outputTextHandler;
@property XTBottomBarHandler *bottomBarHandler;

@property NSThread *tadsEventLoopThread;
@property BOOL shuttingDownTadsEventLoopThread;
@property NSUInteger countTimesInTadsEventLoopThreadCancelledState;

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

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

@property NSUInteger returnCodeFromInputDialogWithTitle;
	//TODO reset when... ?

@property NSURL* fileNameDialogUrl;
	//TODO reset when... ?

@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 XTLogger* logger;

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

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

#include "XTGameWindowController_vmThreadFuncs.m"

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

- (void)dealloc
{
	XT_TRACE_ENTRY;
	
	_outputTextHandler = nil;
}

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

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

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

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

- (BOOL)reloadGameFile
{
	BOOL res = [self loadAndStartGameFile];
	return res;
}

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

	[self.statusLineHandler resetToDefaults];
	[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;
}

- (void)windowWillClose:(NSNotification *)notification
{
	XT_TRACE_ENTRY;
	
	[self.prefs stopObservingChangesToAll:self];
	//[self.prefs stopObservingChangesToFonts:self];
	
	[self quitGameIfRunning];
	
	// Async notification, so that the observer doesn't release us before this method has returned
	// (which can cause a bad access exc crash)
	NSNotification *myNotification = [NSNotification notificationWithName:XTadsNotifyGameWindowWillClose object:self];
	[[NSNotificationQueue defaultQueue] enqueueNotification:myNotification postingStyle:NSPostASAP];

	XT_TRACE_0(@"%after posting notification");
}

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

//TODO do we clear members properly after "quit"?
- (void)quitGameIfRunning
{
	XT_TRACE_ENTRY;
	
	if (! self.gameIsRunning) {
		XT_TRACE_0(@"game not running");
		return;
	}
	
	[self shutDownTadsEventLoopThread];
	
	XT_TRACE_0(@"after calling shutDownTadsEventLoopThread");
	
	self.gameIsStarting = NO;
	self.gameIsRunning = NO;
	self.os_gets_EventLoopBridge = nil;
	self.os_waitc_eventLoopBridge = nil;
	self.os_fileNameDialog_EventLoopBridge = nil;
	
	XT_TRACE_0(@"after nilling self.*");
}

- (void)closeWindow
{
	XT_TRACE_ENTRY;

	[self close];
}

//TODO collect XTGameRunnerProtocol stuff in sep. section
- (void)setStatusMode:(NSInteger)newStatusMode
{
	BOOL switchingToStatusLineMode = (newStatusMode == 1 && self.statusMode != 1);
	if (switchingToStatusLineMode) {
		[self.statusLineHandler enterStatusLineMode];
	} else {
		BOOL exitingStatusLineMode = (newStatusMode != 1 && self.statusMode == 1);
		if (exitingStatusLineMode) {
			[self.statusLineHandler exitStatusLineMode];
		}
	}
	_statusMode = newStatusMode;
}

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

- (void)myCustomInit
{
	_uiUtils = [XTUIUtils new];
	
	_os_gets_EventLoopBridge = [XTEventLoopBridge bridgeWithName:@"os_gets_EventLoopBridge"];
	_os_waitc_eventLoopBridge = [XTEventLoopBridge bridgeWithName:@"os_waitc_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;
	_isSleeping = NO;

	_statusLineHandler = [XTStatusLineHandler new];
	_outputTextHandler = [XTOutputTextHandler handler];
	_outputTextHandler.gameWindowController = self;
	_bottomBarHandler = [XTBottomBarHandler new];
	
	_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
{
    [super windowDidLoad];

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

	//TODO fonts for bottom bar fields?
	
	//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];
	
	self.outputTextHandler.outputTextView = self.outputTextView;
	self.outputTextHandler.outputTextScrollView = self.outputTextScrollView;

	self.statusLineHandler.locationTextField = [self locationTextField];
	self.statusLineHandler.scoreTextField = [self scoreTextField];
	// remove any placeholder texts set in Interface Builder
	[self.statusLineHandler clearStatusLine];
	
	self.bottomBarHandler.keyPromptTextField = self.keyPromptTextField;
	self.bottomBarHandler.parsingModeTextField = self.parsingModeTextField;
	[self.bottomBarHandler clearKeyPrompt];
	//TODO init p.m. field?
	//TODO other b.b. fields?
	
	[self.prefs startObservingChangesToAll:self];
		//TODO despite this being called on every new game start, we don't get the actual event when starting a new game
	//[self.prefs startObservingChangesToFonts: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
{
	NSColor *statusLineBackgroundColor = self.prefs.statusLineBackgroundColor;
	self.statusLineView.backgroundColor = statusLineBackgroundColor;
	self.locationTextField.backgroundColor = statusLineBackgroundColor;
	self.scoreTextField.backgroundColor = statusLineBackgroundColor;

	NSColor *statusLineTextColor = self.prefs.statusLineTextColor;
	self.locationTextField.textColor = statusLineTextColor;
	self.scoreTextField.textColor = statusLineTextColor;

	[self.statusLineView setNeedsDisplay:YES];
	
	self.dividerStatusLineOutputTextView.backgroundColor = self.prefs.outputAreaBackgroundColor;

	[self.dividerStatusLineOutputTextView setNeedsDisplay:YES]; // to fill in bg color initially
	
	NSColor *outputAreaBackgroundColor = self.prefs.outputAreaBackgroundColor;
	self.outputTextView.backgroundColor = outputAreaBackgroundColor;
	
	NSColor *outputAreaTextColor = self.prefs.outputAreaTextColor;
	self.outputTextView.textColor = outputAreaTextColor;

	// nothing to do for input color (but see formatter)
	
	//TODO cursor color?
	
	[self.outputTextView setNeedsDisplay:YES];
}

- (void)setFontsFromPrefs
{
	NSString *statusLineFontName = self.fontManager.xtadsStatusLineParameterizedFontName;
	NSFont *statusLineFont = [self.fontManager getFontWithParameterizedName:statusLineFontName];
	self.locationTextField.font = statusLineFont;
	self.scoreTextField.font = statusLineFont;
}

- (void)startGame
{
	int exitCode;
	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);
	}
}

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

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

// 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 flushOutput]; // needed for 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:
	
	//TODO also when window closes
	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 = [NSThread 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(@"exit");
	
	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 = [NSThread 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]) {

			self.tadsEventLoopThread = nil;

		} else {

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

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

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

//-------------------------------------------------------------------------------
// 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) {
		[self setColorsFromPrefs];
		[self setFontsFromPrefs];
		[self.prefs persist]; // in case this was because of loading a new game, and we must persist its directory in prefs
	} else {
		XT_TRACE_0(@"%@ OTHER");
	}
}

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

- (void)mainThread_printOutputText:(id)arg
{
	XT_DEF_SELNAME;

	NSMutableArray *argsArray = arg; // @[s, returnValueHolder]
	NSString *outputText = argsArray[0];
	argsArray[1] = [NSNumber numberWithBool:NO];
	
	if (self.statusMode == 0) {
		// regular output mode
		BOOL excessiveAmount = [self.outputTextHandler appendOutput:outputText];
		argsArray[1] = [NSNumber numberWithBool:excessiveAmount];
		
	} else if (self.statusMode == 1) {
		// status line mode
		outputText = [self prepareStringForStatusLine:outputText];
		[self.statusLineHandler append:outputText];

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

- (void)mainThread_pumpOutputText:(id)arg
{
	XT_DEF_SELNAME;
	
	NSMutableArray *argsArray = arg; // @[returnValueHolder]
	
	BOOL needMorePrompt = [self.outputTextHandler pumpOutput];
	argsArray[0] = [NSNumber numberWithBool:needMorePrompt];
}

/*
 *   '\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
{
	[self.outputTextHandler clearText];
}

- (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;

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

//TODO make @prop for eff?
- (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;

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


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

	[self.outputTextHandler scrollToEnd];
	[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]) {
		// Let the OS handle it.
		handled = NO;
	} else {
		// A "command link". Handle it ourselves, signalling a command entered if needed.
		BOOL commandEntered = [self.outputTextHandler handleCommandLinkClicked:linkString atIndex:charIndex];
		if (commandEntered) {
			[self textView:self.outputTextView doCommandBySelector:@selector(insertNewline:)];
		}
		handled = YES;
	}
	return handled;
}

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

	if (! self.gameIsRunning) {

		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) {
		// scroll to end of text
		[self.outputTextView scrollRangeToVisible:NSMakeRange(self.outputTextView.string.length, 0)];
		int z = 1;
	}

	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_waitc_eventLoopBridge.isWaiting) {
		
		//TODO mouse events? window events?
		unichar keyPressed = ' ';
		if (replacementString != nil && replacementString.length >= 1) {
			keyPressed = [replacementString characterAtIndex:0];
		} else {
			XT_WARN_0(@"had no replacementString");
		}
		if (keyPressed == 0) {
			int breakpt = 1;
		}
		[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
//TODO make sure modal window flag is set in app delegate! (what game calls this func?!)
// 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
