//
//  XTGameWindowController_vmThreadFuncs.m
//  XTads
//
//  Copyright (c) 2015 Rune Berg. All rights reserved.
//
//  This file contains the portion of XTGameWindowController that is
//  called exclusively by the TADS game VM thread.
//
//  IMPORTANT NOTE: This is not a standalone .m file, and so should
//  *not* be member of any build targets. Instead, it's #included by
//  XTGameWindowController.m proper.
//

#define RETURN_IF_SHUTTING_DOWN_VM_THREAD \
	if (_shuttingDownTadsEventLoopThread) { \
		return; \
	}

#define RETURN_VALUE_IF_SHUTTING_DOWN_VM_THREAD(retValue) \
	if (_shuttingDownTadsEventLoopThread) { \
		return retValue; \
	}

//---------------------------------------------------------------------
//  TADS 2/3 VM entry points
//---------------------------------------------------------------------

- (void)runTads2GameLoopThread:(id)arg
{
	XT_TRACE_ENTRY;
	
	XTTads2AppCtx *t2AppCtx = [XTTads2AppCtx context];
	appctxdef *appctxPtr = [t2AppCtx getAppCtxPtr];
	
	// trdmain() requires argc/argv style arguments
	
	const char* argv[30] = {"xtads"};
	int argc = 1;
	if (! self.prefs.printTadsBannerOnGameStart.value.boolValue) {
		argv[argc++] = "-nobanner";
	}
	
	// -o file option: this will run the game as normal, with input coming from the keyboard and output going to the screen, but will also write every command you type to the file you specified
	if (self.runGameParams.commandScriptAction == XT_CMD_SCRIPT_ACTION_RECORD) {
		argv[argc++] = "-o";
		argv[argc++] = self.runGameParams.commandScriptFileUrl.fileSystemRepresentation;
	// -i file option to tell the run-time system to read commands
	} else if (self.runGameParams.commandScriptAction == XT_CMD_SCRIPT_ACTION_REPLAY) {
		argv[argc++] = "-i";
		argv[argc++] = self.runGameParams.commandScriptFileUrl.fileSystemRepresentation;
	}

	// -l logfile option; this will redirect all output from your game to the log file
	if (self.runGameParams.captureTranscript) {
		argv[argc++] = "-l";
		argv[argc++] = self.runGameParams.transcriptFileUrl.fileSystemRepresentation;
	}

	const char* filename = [self.gameFileUrl fileSystemRepresentation];
	argv[argc++] = filename;
	
	self.gameIsStarting = NO;
	self.gameIsRunning = YES;
	self.mainTextView.showCursor = NO;
	
	[self childThread_updateGameTitle];
	
	// Run the Tads 2 VM event loop
	char savExt[] = "sav";
	int vmRet = trdmain(argc, argv, appctxPtr, savExt);
	
	self.gameIsRunning = NO;
	self.mainTextView.showCursor = NO;
	
	if (! _shuttingDownTadsEventLoopThread) {
		// shutting down due to user "quit"-ting game
		[self childThread_gameHasEnded];
	}
	_shuttingDownTadsEventLoopThread = NO;
	_hasExitedVmThread = YES;
	
	XT_TRACE_1(@"exit: %d", vmRet);
}

- (void)runTads3GameLoopThread:(id)arg
{
	XT_TRACE_ENTRY;
	
	self.gameIsStarting = NO;
	self.gameIsRunning = YES;
	self.mainTextView.showCursor = NO;
	
	[self childThread_updateGameTitle];
	
	// Run the Tads 3 VM event loop
	self.tads3Entry = [XTTads3Entry new];
	[self.tads3Entry runTads3Game:self.runGameParams
				   showTerpBanner:self.prefs.printTadsBannerOnGameStart.value.boolValue];
	int vmRet = 0; //TODO ? from runTads3Game
	
	self.gameIsStarting = NO;
	self.gameIsRunning = NO;
	self.mainTextView.showCursor = NO;
	
	if (! _shuttingDownTadsEventLoopThread) {
		// shutting down due to user "quit"-ting game
		[self childThread_gameHasEnded];
	}
	_shuttingDownTadsEventLoopThread = NO;
	_hasExitedVmThread = YES;
	
	XT_TRACE_1(@"exit: %d", vmRet);
}


//---------------------------------------------------------------------
//  Functions called directly from our osifc impl.
//---------------------------------------------------------------------

#pragma mark XTGameRunnerProtocol

- (BOOL)htmlMode
{
	return self.mainTextHandler.htmlMode;
}

- (void)setHtmlMode:(BOOL)htmlMode
{
	NSArray *args = @[[NSNumber numberWithBool:htmlMode]];
	[self.mainTextHandler callOnMainThread:@selector(mainThread_setHtmlMode:)
				withObject:args];

	[self getBannerHandlerForTradStatusLine].htmlMode = htmlMode;

	NSNumber *arg = [NSNumber numberWithBool:htmlMode];
	RETURN_IF_SHUTTING_DOWN_VM_THREAD;
	[self callOnMainThread:self.bottomBarHandler
				  selector:@selector(mainThread_setParsingModeHtml:)
				withObject:arg];
}

- (void)setHiliteMode:(BOOL)hiliteMode
{
	if (self.statusLineMode == STATUS_LINE_MODE_MAIN) {
		[self.mainTextHandler setHiliteMode:hiliteMode];
	} else {
		[[self getBannerHandlerForTradStatusLine] setHiliteMode:hiliteMode];
	}
}

- (void)setNonstopMode:(BOOL)nonstopMode
{
	[self.mainTextHandler setNonstopMode:nonstopMode];
}

- (void)setStatusLineMode:(NSInteger)mode
{
	XT_DEF_SELNAME;
	
	if (self.gameIsT3) {
		XT_ERROR_0(@"unexpected call for T3 game");
	}
	
	RETURN_IF_SHUTTING_DOWN_VM_THREAD;
	
	self.mainTextHandler.statusLineMode = mode;
}

- (NSInteger)statusLineMode
{
	return self.mainTextHandler.statusLineMode;
}

- (void)showScore:(NSString *)scoreString
{
	if (self.htmlMode) {
		return;
	}
	
	if (_shuttingDownTadsEventLoopThread) {
		//XT_TRACE_0(@"bail out - shutting down VM thread");
		return;
	}

	[self createBannerForTradStatusLine]; // ... if none exists already
	
	void *handle = (void *)[self.bannerHandleForTradStatusLine unsignedIntegerValue];
	[self bannerDisplayTradStatusLineScoreString:handle text:scoreString];
}

- (BOOL)showTerpCopyrightAtGameStart
{
	BOOL res = self.prefs.printTadsBannerOnGameStart.value.boolValue;
	return res;
}

- (void)setTads2InternalCharSet:(NSString*)charSet
{
	NSNumber *encNum = [self.tads2EncodingsByInternalId objectForKey:charSet];
	if (encNum != nil) {
		self.tads2EncodingSetByGame = encNum;
	} else {
		NSString *dialogMsg = [NSString stringWithFormat:@"This TADS 2 game uses an unknown text encoding, %@. This will be ignored, in favour of that selected in the Preferences.", charSet];
		[self childThread_showModalErrorDialogInWindow:self.window messageText:dialogMsg];
	}
}

- (NSString *)makeString:(const char *)str
{
	NSString *s = [self makeString:str len:strlen(str)];
	return s;
}

- (NSString *)makeString:(const char *)str len:(size_t)len
{
	NSStringEncoding encoding;
	if (self.gameIsT3) {
		encoding = NSUTF8StringEncoding;
	} else {
		// T2
		encoding = [self getTads2Encoding];
	}
	NSString *s = [[NSString alloc] initWithBytes:str length:len encoding:encoding];
	if (s == nil) {
		//TODO no dlg if "non interactive" mode -- e.g. cmd replay?
		if (! self.hasWarnedAboutFailedT2Decoding) {
			NSString *encName = [NSString localizedNameOfStringEncoding:encoding];
			NSString *dialogMsg = [NSString stringWithFormat:@"Cannot decode a string from %@. Please use Preferences to select a suitable text encoding.", encName];
			[self childThread_showModalErrorDialogInWindow:self.window messageText:dialogMsg];
			self.hasWarnedAboutFailedT2Decoding = YES;
		}
		// But always emit some text:
		NSString *encSafeName = [XTStringUtils safeNameForEncoding:encoding];
		s = [NSString stringWithFormat:@"Cannot-decode-string-from-%@ ", encSafeName];
	}
	return s;
}

- (const char*)makeCStringInteractive:(NSString *)string
{
	NSStringEncoding encoding;
	if (self.gameIsT3) {
		encoding = NSUTF8StringEncoding;
	} else {
		// T2
		encoding = [self getTads2Encoding];
	}
	const char *cs = [string cStringUsingEncoding:encoding];
	if (cs == nil) {
		//TODO no dlg if "non interactive" mode -- e.g. cmd replay?
		if (! self.hasWarnedAboutFailedT2Encoding) {
			NSString *encName = [NSString localizedNameOfStringEncoding:encoding];
			NSString *dialogMsg = [NSString stringWithFormat:@"Cannot encode a string to %@. Please use Preferences to select a suitable text encoding.", encName];
			[self childThread_showModalErrorDialogInWindow:self.window messageText:dialogMsg];
			self.hasWarnedAboutFailedT2Encoding = YES;
		}
		// Emit an error msg that'll get echoed as-is if the input string was a command:
		NSString *encSafeName = [XTStringUtils safeNameForEncoding:encoding];
		NSString *errMsg = [NSString stringWithFormat:@"Cannot-encode-string-to-%@ ", encSafeName];
		cs = [errMsg cStringUsingEncoding:NSASCIIStringEncoding]; // In Good Old ASCII We Trust - works everywhere ;-)
	}
	return cs;
}

- (const char*)makeCStringQuiet:(NSString *)string
{
	NSStringEncoding encoding;
	if (self.gameIsT3) {
		encoding = NSUTF8StringEncoding;
	} else {
		// T2
		encoding = [self getTads2Encoding];
	}
	const char *cs = [string cStringUsingEncoding:encoding];
	return cs;
}

- (void)printOutput:(NSString *)s
{
	XT_DEF_SELNAME;

	if (_shuttingDownTadsEventLoopThread) {
		//XT_TRACE_0(@"bail out - shutting down VM thread");
		return;
	}
	
	BOOL excessiveAmountBuffered = [self printOutputText:s];
	if (excessiveAmountBuffered) {
		XT_TRACE_0(@"excessiveAmountBuffered - pump output");
		[self pumpOutputText];
	} else {
		// wait until next input event
	}
}

- (void)waitForEvent:(XTGameInputEvent *)inputEvent;
{
	XT_TRACE_ENTRY;
	
	// (No bottom bar prompting here)
	
	[self flushOutputBeforeWaitingForUserInput];
	
	[self.inputEvent reset];

	if ([self tadsEventLoopThreadIsCancelled]) {
		XT_TRACE_0(@"(thread is cancelled on entry)");
		inputEvent.type = OS_EVT_EOF;
		return;
	}
	
	[self childThread_updateGameTitle];
	
	[self.os_event_eventLoopBridge waitForSignal];
	
	if ([self tadsEventLoopThreadIsCancelled]) {
		XT_TRACE_0(@"(thread is cancelled after waitForSignal)");
		inputEvent.type = OS_EVT_EOF;
		return;
	} else {
		XT_TRACE_0(@"continue after waitForSignal");
	}
	
	[inputEvent setFrom:self.inputEvent];
	
	[self childThread_moveCursorToEndOfOutputPosition];
	
	[self clearPendingKey];
	
	[self childThread_noteStartOfPagination];
}

- (NSUInteger)waitForAnyKeyPressed
{
	XT_TRACE_ENTRY;
	
	[self flushOutputBeforeWaitingForUserInput];
	
	NSUInteger keyPressed;
	if ([self hasPendingKey]) {
		keyPressed = [self getPendingKey];
		XT_TRACE_1(@"got pending key %lu", keyPressed);
		[self clearPendingKey];
	} else {
		keyPressed = [self showPromptAndWaitForKeyPressed:self.pressAnyKeyPromptText];
		XT_TRACE_1(@"got direct key %lu", keyPressed);
	}
	
	[self childThread_noteStartOfPagination];
	
	return keyPressed;
}

- (void)showMorePromptAndWaitForKeyPressed
{
	XT_TRACE_ENTRY;
	
	if ((! self.gameIsT3) && (self.runGameParams.commandScriptAction == XT_CMD_SCRIPT_ACTION_REPLAY)) {
		// When replaying a T2 command script, there's no sense in waiting for key prompts that yield no return value.
		// (T3 handles this already.)
		return;
	}
	
	[self flushOutputBeforeWaitingForUserInput];
	
	[self showPromptAndWaitForKeyPressed:self.morePromptText];
	
	[self childThread_noteStartOfPagination];
	
	[self clearPendingKey];
}

- (NSString *)waitForCommand
{
	XT_TRACE_ENTRY;

	if ([self tadsEventLoopThreadIsCancelled]) {
		XT_TRACE_0(@"-> nil (thread is cancelled");
		return nil;
	}
	
	//XT_WARN_1(@"calls on main thread: %lu", [XTCallOnMainThreadCounter getCount]);
	
	[self flushOutputBeforeWaitingForUserInput];

	[self childThread_updateGameTitle];
	
	[self childThread_allBanners_resetForNextCommand];

	self.mainTextView.showCursor = YES;
	
	[self.os_gets_EventLoopBridge waitForSignal];
	
	self.mainTextView.showCursor = NO;
	
	if ([self tadsEventLoopThreadIsCancelled]) {
		XT_TRACE_0(@"-> nil (thread is cancelled");
		return nil;
	}
	
	NSMutableArray *returnValue = [NSMutableArray arrayWithCapacity:1];
	[self callOnMainThread:self.mainTextHandler
				  selector:@selector(mainThread_getCommand:)
				withObject:returnValue];
	NSString *command = returnValue[0];

	[self printOutputNewlineAfterCommand];

	[self childThread_moveCursorToEndOfOutputPosition];
	
	XT_TRACE_1(@"-> %@", command);
	return command;
}

- (void)clearScreen
{
	RETURN_IF_SHUTTING_DOWN_VM_THREAD;
	[self callOnMainThread:@selector(mainThread_clearScreen)];
}

- (void)setGameTitle:(NSString *)title
{
	RETURN_IF_SHUTTING_DOWN_VM_THREAD;
	[self callOnMainThread:@selector(mainThread_setGameTitle:)];
}

- (void)flushOutput
{
	//XT_DEF_SELNAME;
	
	if (self.gameIsT3 || ! self.htmlMode) {
		// A few T2 HTML games actually need os_flush() to flush immediately.
		// Otherwise, wait for VM requesting user input (or ending).
		//XT_WARN_2(@"return - self.gameIsT3=%ld self.htmlMode=%ld", self.gameIsT3, self.htmlMode);
		return;
	}
	RETURN_IF_SHUTTING_DOWN_VM_THREAD;
	//XT_WARN_2(@"continue... self.gameIsT3=%ld self.htmlMode=%ld", self.gameIsT3, self.htmlMode);

	// for test games that never call a regular input function
	[self pumpOutputText];

	RETURN_IF_SHUTTING_DOWN_VM_THREAD;
	[self callOnMainThread:@selector(mainThread_flushOutput)];
}

- (void)flushOutputBeforeWaitingForUserInput
{
	RETURN_IF_SHUTTING_DOWN_VM_THREAD;

	[self pumpOutputTextForceFlush];
	
	RETURN_IF_SHUTTING_DOWN_VM_THREAD;

	[self callOnMainThread:@selector(mainThread_flushOutput)];
}

- (NSUInteger)inputDialogWithTitle:title
				standardButtonSetId:(NSUInteger)standardButtonSetId
				  customButtomSpecs:(NSArray *)customButtomSpecs
					   defaultIndex:(NSUInteger)defaultIndex
						cancelIndex:(NSUInteger)cancelIndex
							 iconId:(XTadsInputDialogIconId)iconId
{
	XT_TRACE_ENTRY;

	NSNumber *argStandardButtonSetId = [NSNumber numberWithUnsignedInteger:standardButtonSetId];
	NSNumber *argDefaultIndex = [NSNumber numberWithUnsignedInteger:defaultIndex];
	NSNumber *argCancelIndex = [NSNumber numberWithUnsignedInteger:cancelIndex];
	NSNumber *argIconId = [NSNumber numberWithInteger:iconId];
	
	NSArray *args = @[title, argStandardButtonSetId, customButtomSpecs, argDefaultIndex, argCancelIndex, argIconId];
	
	RETURN_VALUE_IF_SHUTTING_DOWN_VM_THREAD(0);
	if ([self tadsEventLoopThreadIsCancelled]) {
		XT_TRACE_0(@"-> nil (thread is cancelled");
		return 0;
	}
	[self callOnMainThread:@selector(mainThread_inputDialog:)
				withObject:args];
	
	return self.returnCodeFromInputDialogWithTitle;
}

- (NSURL *)getFileNameForFor:(XTadsFileNameDialogFileType)fileType
				 dialogTitle:(NSString *)dialogTitle
		 fileTypeDescription:(NSString *)fileTypeDescription
		   allowedExtensions:(NSArray *)allowedExtensions
				existingFile:(BOOL)existingFile
{
	XT_TRACE_ENTRY;

	self.fileNameDialogUrl = nil;
	
	if (fileTypeDescription == nil) {
		fileTypeDescription = (NSString *)[NSNull null];
	}
	if (allowedExtensions == nil) {
		allowedExtensions = (NSArray *)[NSNull null];
	}
	
	NSNumber *fileTypeAsNumber = [NSNumber numberWithInteger:fileType];
	NSNumber *existingFileAsNumber = [NSNumber numberWithBool:existingFile];
	
	RETURN_VALUE_IF_SHUTTING_DOWN_VM_THREAD(nil);
	if ([self tadsEventLoopThreadIsCancelled]) {
		XT_TRACE_0(@"-> nil (thread is cancelled");
		return nil;
	}
	[self callOnMainThread:@selector(mainThread_getFileName:)
				withObject:@[fileTypeAsNumber, dialogTitle, fileTypeDescription, allowedExtensions, existingFileAsNumber]];
	
	[self.os_fileNameDialog_EventLoopBridge waitForSignal];
	
	return self.fileNameDialogUrl;
}

- (void)sleepFor:(double)seconds
{
	self.isSleeping = YES;
	[NSThread sleepForTimeInterval:seconds];
	self.isSleeping = NO;
}


//---------------------------------------------------------------------
//  Internal
//---------------------------------------------------------------------

- (void)pumpOutputText
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"");
	
	for (XTBaseTextHandler *th in self.bannersByHandle.allValues) {
		if ([th isKindOfClass:[XTBannerTextHandler class]]) {
			[self pumpOutputTextForHandler:th];
		}
	}
	
	[self pumpOutputTextForHandler:self.mainTextHandler];
}

- (void)pumpOutputTextForceFlush
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"");
	
	for (XTBaseTextHandler *th in self.bannersByHandle.allValues) {
		if ([th isKindOfClass:[XTBannerTextHandler class]]) {
			[self pumpOutputTextForHandlerForceFlush:th];
		}
	}
	
	[self pumpOutputTextForHandlerForceFlush:self.mainTextHandler];
}

- (NSUInteger)pumpOutputTextForHandler:(XTBaseTextHandler *)handler
{
	return [self pumpOutputTextForHandler:handler flushIfBanner:NO];
}

- (NSUInteger)pumpOutputTextForHandlerForceFlush:(XTBaseTextHandler *)handler
{
	XT_DEF_SELNAME;

	RETURN_VALUE_IF_SHUTTING_DOWN_VM_THREAD(0);
	
	NSUInteger countMorePrompts = 0;
	
	BOOL needMorePrompt = [handler awaitingMorePromptForPagination];
	if (! needMorePrompt) {
		needMorePrompt = [self childThread_banner_pumpOutputTextForceFlush:handler];
	} else {
		int brkpt = 1;
	}
	while (needMorePrompt) {
		XT_TRACE_0(@"waiting for pagination more key...");
		[self showMorePromptAndWaitForKeyPressedNonVM];
		XT_TRACE_0(@"after waiting for pagination more key");
		if ([self tadsEventLoopThreadIsCancelled]) {
			//XT_WARN_0(@"thread cancelled");
			XT_TRACE_0(@"-> (thread is cancelled)");
			break;
		}
		needMorePrompt = [self childThread_banner_pumpOutputTextForceFlush:handler];
		countMorePrompts += 1;
		if (countMorePrompts >= 10 && (countMorePrompts % 10) == 0) {
			XT_WARN_1(@"countMorePrompts==%lu", countMorePrompts);
		}
	}
	
	return countMorePrompts;
}

- (NSUInteger)pumpOutputTextForHandler:(XTBaseTextHandler *)handler flushIfBanner:(BOOL)flushIfBanner
{
	XT_DEF_SELNAME;
	//XT_WARN_1(@"flushIfBanner=%d", flushIfBanner);

	RETURN_VALUE_IF_SHUTTING_DOWN_VM_THREAD(0);
	
	NSUInteger countMorePrompts = 0;
	
	BOOL needMorePrompt = [handler awaitingMorePromptForPagination];
	if (! needMorePrompt) {
		needMorePrompt = [self childThread_banner_pumpOutputText:handler flushIfBanner:flushIfBanner];
	} else {
		int brkpt = 1;
	}
	while (needMorePrompt) {
		//XT_WARN_0(@"waiting for pagination more key...");
		[self showMorePromptAndWaitForKeyPressedNonVM];
		//XT_WARN_0(@"after waiting for pagination more key");
		if ([self tadsEventLoopThreadIsCancelled]) {
			//XT_WARN_0(@"thread cancelled");
			XT_TRACE_0(@"-> (thread is cancelled)");
			break;
		}
		needMorePrompt = [self childThread_banner_pumpOutputText:handler flushIfBanner:flushIfBanner];
		countMorePrompts += 1;
		if (countMorePrompts >= 10 && (countMorePrompts % 10) == 0) {
			XT_WARN_1(@"countMorePrompts==%lu", countMorePrompts);
		}
	}
	
	return countMorePrompts;
}

- (NSUInteger)showPromptAndWaitForKeyPressed:(NSString *)prompt
{
	XT_TRACE_ENTRY;
	
	NSUInteger key;
	
	if ([self tadsEventLoopThreadIsCancelled]) {
		
		key = NSUIntegerMax;
		
	} else {
		
		[self childThread_updateGameTitle];
		
		[self callOnMainThread:self.bottomBarHandler
					  selector:@selector(mainThread_showKeyPrompt:)
					withObject:prompt];
		
		key = [self waitForKeyPressed]; // in UI / main thread
		
		if ([self tadsEventLoopThreadIsCancelled]) {
			key = NSUIntegerMax;
		} else {
			[self callOnMainThread:self.bottomBarHandler
						  selector:@selector(mainThread_clearKeyPrompt)];
			
			[self childThread_moveCursorToEndOfOutputPosition];
		}
	}
	
	XT_TRACE_1("-> %lu", key);
	
	return key;
}

- (void)showMorePromptAndWaitForKeyPressedNonVM
{
	XT_TRACE_ENTRY;

	[self showPromptAndWaitForKeyPressedNonVM:self.morePromptText];
	
	[self childThread_noteStartOfPagination];
}

- (NSUInteger)showPromptAndWaitForKeyPressedNonVM:(NSString *)prompt
{
	XT_DEF_SELNAME;
	
	NSUInteger key;
	
	key = [self showPromptAndWaitForKeyPressed:prompt];
	
	if ([self hasPendingKey]) {
		key = [self getPendingKey];
		[self clearPendingKey];
	}
	
	XT_TRACE_1("-> %lu", key);
	
	return key;
}

- (NSUInteger)waitForKeyPressed
{
	XT_TRACE_ENTRY;
	
	if ([self tadsEventLoopThreadIsCancelled]) {
		XT_TRACE_0(@"-> NSUIntegerMax (thread is cancelled on entry)");
		return NSUIntegerMax;
	}
	
	[self childThread_updateGameTitle];
	
	NSUInteger keyPressed = [self.os_waitc_eventLoopBridge waitForSignal];
	
	if ([self tadsEventLoopThreadIsCancelled]) {
		XT_TRACE_0(@"-> NSUIntegerMax (thread is cancelled after waitForSignal)");
		return NSUIntegerMax;
	} else {
		XT_TRACE_0(@"continue after waitForSignal");
	}
	
	[self childThread_moveCursorToEndOfOutputPosition];
	
	XT_TRACE_1(@"-> %lu", keyPressed);
	
	return keyPressed;
}

- (BOOL)tadsEventLoopThreadIsCancelled
{
	XT_TRACE_ENTRY;
	
	BOOL res = NO;
	
	if ([[NSThread currentThread] isCancelled]) {
		res = YES;
		_countTimesInTadsEventLoopThreadCancelledState += 1;
		if (_countTimesInTadsEventLoopThreadCancelledState > 300) {
			// Typically because VM won't exit despite being sent CMD_EOF
			XT_WARN_0(@"Forcing VM thread exit due to thread spending excessive time in cancelled state");
			
			// This leaks when released from main/UI thread (don't know why),
			// so make sure we do it here in VM thread:
			[_tads3Entry cleanup];
			_tads3Entry = nil;
			
			_shuttingDownTadsEventLoopThread = NO;
			self.hasExitedVmThread = YES;
			
			[NSThread exit];
		}
	} else {
		_countTimesInTadsEventLoopThreadCancelledState = 0;
	}
	XT_TRACE_1(@"-> %d", res);
	return res;
}

- (NSStringEncoding)getTads2Encoding
{
	NSStringEncoding res = self.prefs.tads2Encoding.value.unsignedIntegerValue;
	if (self.tads2EncodingSetByGame != nil) {
		if (! self.prefs.tads2EncodingOverride.value.boolValue) {
			res = self.tads2EncodingSetByGame.unsignedIntegerValue;
		}
	}
	return res;
}

//-------------------------------------------------------------------------------
//  "Bridge functions" for stuff that must be delegated to main/UI thread
//-------------------------------------------------------------------------------

- (BOOL)childThread_banner_pumpOutputText:(XTBaseTextHandler *)handler flushIfBanner:(BOOL)flushIfBanner
{
	RETURN_VALUE_IF_SHUTTING_DOWN_VM_THREAD(NO);
	NSMutableArray *params = [NSMutableArray arrayWithCapacity:2];
	params[0] = [NSNumber numberWithBool:flushIfBanner]; // input param
	[handler callOnMainThread:@selector(mainThread_pumpOutputText:)
				   withObject:params];
	NSNumber *retVal = [params objectAtIndex:1]; // output param
	return retVal.boolValue;
}

- (BOOL)childThread_banner_pumpOutputTextForceFlush:(XTBaseTextHandler *)handler
{
	RETURN_VALUE_IF_SHUTTING_DOWN_VM_THREAD(NO);
	NSMutableArray *params = [NSMutableArray arrayWithCapacity:1];
	//TODO !!! rm: params[0] = [NSNumber numberWithBool:flushIfBanner]; // input param
	[handler callOnMainThread:@selector(mainThread_pumpOutputTextForceFlush:)
				   withObject:params];
	NSNumber *retVal = [params objectAtIndex:0]; // output param
	return retVal.boolValue;
}

- (void)childThread_allBanners_resetForNextCommand
{
	RETURN_IF_SHUTTING_DOWN_VM_THREAD;
	[self callOnMainThread:@selector(mainThread_allBanners_resetForNextCommand)];
}

- (void)childThread_moveCursorToEndOfOutputPosition
{
	RETURN_IF_SHUTTING_DOWN_VM_THREAD;
	[self callOnMainThread:@selector(mainThread_moveCursorToEndOfOutputPosition)];
}

- (void)childThread_gameHasEnded
{
	RETURN_IF_SHUTTING_DOWN_VM_THREAD;
	[self callOnMainThread:@selector(mainThread_gameHasEnded)];
}

- (void)childThread_updateGameTitle
{
	RETURN_IF_SHUTTING_DOWN_VM_THREAD;
	[self callOnMainThread:@selector(mainThread_updateGameTitle)];
}

- (void)childThread_showModalErrorDialogInWindow:(NSWindow *)window
									 messageText:(NSString *)msgText
{
	RETURN_IF_SHUTTING_DOWN_VM_THREAD;
	NSArray *args = @[window, msgText];
	[self callOnMainThread:@selector(mainThread_showModalErrorDialog:)
				withObject:args];
}

- (void)childThread_noteStartOfPagination
{
	RETURN_IF_SHUTTING_DOWN_VM_THREAD;
	[self callOnMainThread:@selector(mainThread_noteStartOfPagination)];
}

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

- (void)callOnMainThread:(SEL)selector
{
	XT_COUNT_CALL_ON_VM_THREAD;
	[self performSelectorOnMainThread:selector
						   withObject:nil
						waitUntilDone:YES];
}

- (void)callOnMainThread:(SEL)selector
			  withObject:(id)obj
{
	XT_COUNT_CALL_ON_VM_THREAD;
	[self performSelectorOnMainThread:selector
						   withObject:obj
						waitUntilDone:YES];
}

- (void)callOnMainThread:(NSObject *)target
				selector:(SEL)selector
{
	XT_COUNT_CALL_ON_VM_THREAD;
	[target performSelectorOnMainThread:selector
							 withObject:nil
						  waitUntilDone:YES];
}

- (void)callOnMainThread:(NSObject *)target
				selector:(SEL)selector
			  withObject:(id)obj
{
	XT_COUNT_CALL_ON_VM_THREAD;
	[target performSelectorOnMainThread:selector
							 withObject:obj
						  waitUntilDone:YES];
}

