//
//  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.
//

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

- (void)runTads2GameLoopThread:(id)arg
{
	XT_TRACE_ENTRY;
	
	XTTads2AppCtx *t2AppCtx = [XTTads2AppCtx new];
	appctxdef *appctxPtr = [t2AppCtx getAppCtxPtr];
	
	// trdmain() requires argc/argv style arguments
	
	const char* filename = [self.gameFileUrl fileSystemRepresentation];
	const char* argv[2] = {"xtads", filename}; //"/Users/rune/if/tads2/islecult.gam"};
	
	self.gameIsStarting = NO;
	self.gameIsRunning = YES;
	self.outputTextView.showCursor = NO;
	
	[self childThread_updateGameTitle];
	
	// Run the Tads 2 VM event loop
	char savExt[] = "sav";
	int vmRet = trdmain(2, argv, appctxPtr, savExt);
	
	self.gameIsRunning = NO;
	self.outputTextView.showCursor = NO;
	
	if (! _shuttingDownTadsEventLoopThread) {
		// shutting down due to user "quit"-ting game
		[self childThread_gameHasEnded];
	}
	_shuttingDownTadsEventLoopThread = NO;
	
	XT_TRACE_1(@"exit: %d", vmRet);
}

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


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

#pragma mark XTGameRunnerProtocol

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

- (void)setHtmlMode:(BOOL)htmlMode
{
	[self.outputTextHandler setHtmlMode:htmlMode];
	[self.statusLineHandler setHtmlMode:htmlMode];
	[self.bottomBarHandler setParsingModeHtml:htmlMode];
}

- (void)setHiliteMode:(BOOL)hiliteMode
{
	[self.outputTextHandler setHiliteMode:hiliteMode];
}

- (BOOL)nonstopMode
{
	return self.outputTextHandler.nonstopMode;
}

- (void)setNonstopMode:(BOOL)nonstopMode
{
	self.outputTextHandler.nonstopMode = nonstopMode;
}

- (void)showScore:(NSString *)scoreString
{
	[self.statusLineHandler showScore:scoreString];
}

- (BOOL)showTerpCopyrightAtGameStart
{
	BOOL res = self.prefs.printTadsBannerOnGameStart.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_showModalErrorDialogWithMessageText: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_showModalErrorDialogWithMessageText:dialogMsg];
			self.hasWarnedAboutFailedT2Decoding = YES;
		}
		// But always emit some text:
		NSString *encSafeName = [self 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_showModalErrorDialogWithMessageText:dialogMsg];
			self.hasWarnedAboutFailedT2Encoding = YES;
		}
		// Emit an error msg that'll get echoed as-is if the input string was a command:
		NSString *encSafeName = [self 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) {
		return;
	}
	
	BOOL excessiveAmountBuffered = [self childThread_printOutputText:s];
	if (excessiveAmountBuffered) {
		XT_TRACE_0(@"excessiveAmountBuffered - pump output");
		[self pumpOutputText];
	} else {
		// wait until next input event
	}
}

- (NSUInteger)waitForKeyEvent:(NSInteger*)keyData
{
	XT_TRACE_ENTRY;
	
	// (No bottom bar prompting here)
	
	[self pumpOutputText];
	
	BOOL hadPendingKey = [self hasPendingKey];
	NSUInteger keyPressed;
	
	if (hadPendingKey) {
		keyPressed = [self getPendingKey];
		XT_TRACE_1(@"got pending key %lu", keyPressed);
		[self clearPendingKey];
		keyData[0] = 0;
		keyData[1] = (NSInteger)keyPressed;
	} else {
		keyPressed = [self waitForKeyPressed];
		if ([self hasPendingKey]) {
			keyPressed = [self getPendingKey];
			XT_TRACE_1(@"got pending key %lu", keyPressed);
			[self clearPendingKey];
			keyData[0] = 0;
			keyData[1] = (NSInteger)keyPressed;
		} else {
			XT_TRACE_1(@"got direct key %lu", keyPressed);
			keyData[0] = (NSInteger)keyPressed;
		}
	}
	
	[self.outputTextHandler noteStartOfPagination];
	
	return keyPressed;
}

- (NSUInteger)waitForAnyKeyPressed
{
	XT_DEF_SELNAME;
	
	[self pumpOutputText];
	
	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.outputTextHandler noteStartOfPagination];
	
	return keyPressed;
}

- (void)showMorePromptAndWaitForKeyPressed
{
	XT_TRACE_ENTRY;
	
	[self pumpOutputText];
	
	[self showPromptAndWaitForKeyPressed:self.morePromptText];
	[self.outputTextHandler noteStartOfPagination];
	[self clearPendingKey];
}

- (NSString *)waitForCommand
{
	if ([self tadsEventLoopThreadIsCancelled]) {
		return nil;
	}
	
	[self childThread_updateGameTitle];
	
	[self pumpOutputText];
	
	[self childThread_outputTextHandler_resetForNextCommand];
	[self childThread_moveCursorToEndOfOutputPosition];
	
	self.outputTextView.showCursor = YES;
	
	[self.os_gets_EventLoopBridge waitForSignal];
	
	self.outputTextView.showCursor = NO;
	
	if ([self tadsEventLoopThreadIsCancelled]) {
		return nil;
	}
	
	NSString *command = [self.outputTextHandler getCommand];
	
	NSString *newlineAfterCommand = [self hardNewline];
	[self childThread_printOutputText:newlineAfterCommand];
	
	[self childThread_moveCursorToEndOfOutputPosition];
	
	return command;
}

- (void)clearScreen
{
	if (_shuttingDownTadsEventLoopThread) {
		return;
	}
	[self performSelectorOnMainThread:@selector(mainThread_clearScreen)
						   withObject:nil
						waitUntilDone:YES];
}

- (void)setGameTitle:(NSString *)title
{
	if (_shuttingDownTadsEventLoopThread) {
		return;
	}
	[self performSelectorOnMainThread:@selector(mainThread_setGameTitle:)
						   withObject:title
						waitUntilDone:YES];
}

- (void)flushOutput
{
	if (_shuttingDownTadsEventLoopThread) {
		return;
	}

	// for test games that never call a regular input function
	[self pumpOutputText];
	if (_shuttingDownTadsEventLoopThread) {
		return;
	}
	
	[self performSelectorOnMainThread:@selector(mainThread_flushOutput)
						   withObject:nil
						waitUntilDone:YES];
}

- (NSUInteger)inputDialogWithTitle:title
				standardButtonSetId:(NSUInteger)standardButtonSetId
				  customButtomSpecs:(NSArray *)customButtomSpecs
					   defaultIndex:(NSUInteger)defaultIndex
						cancelIndex:(NSUInteger)cancelIndex
							 iconId:(XTadsInputDialogIconId)iconId
{
	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];
	
	if (_shuttingDownTadsEventLoopThread) {
		return 0;
	}
	if ([self tadsEventLoopThreadIsCancelled]) {
		return 0;
	}
	[self performSelectorOnMainThread:@selector(mainThread_inputDialog:)
						   withObject:args
						waitUntilDone:YES];
	
	return self.returnCodeFromInputDialogWithTitle;
}

- (NSURL *)getFileNameForFor:(XTadsFileNameDialogFileType)fileType
				 dialogTitle:(NSString *)dialogTitle
		 fileTypeDescription:(NSString *)fileTypeDescription
		   allowedExtensions:(NSArray *)allowedExtensions
				existingFile:(BOOL)existingFile
{
	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];
	
	if (_shuttingDownTadsEventLoopThread) {
		return nil;
	}
	if ([self tadsEventLoopThreadIsCancelled]) {
		return nil;
	}
	[self performSelectorOnMainThread:@selector(mainThread_getFileName:)
						   withObject:@[fileTypeAsNumber, dialogTitle, fileTypeDescription, allowedExtensions, existingFileAsNumber]
						waitUntilDone:YES];
	
	[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(@"");
	
	NSUInteger countMorePrompts = 0;
	BOOL needMorePrompt = [self childThread_pumpOutputText];
	while (needMorePrompt) {
		//XT_WARN_0(@"waiting for pagination more key");
		[self showMorePromptAndWaitForKeyPressedNonVM];
		if ([self tadsEventLoopThreadIsCancelled]) {
			//XT_WARN_0(@"thread cancelled");
			return;
		}
		//XT_WARN_0(@"got pagination more key");
		needMorePrompt = [self childThread_pumpOutputText];
		countMorePrompts += 1;
		if (countMorePrompts >= 10 && (countMorePrompts % 10) == 0) {
			XT_WARN_1(@"countMorePrompts==%lu", countMorePrompts);
		}
	}
}

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

- (void)showMorePromptAndWaitForKeyPressedNonVM
{
	[self showPromptAndWaitForKeyPressedNonVM:self.morePromptText];
	[self.outputTextHandler 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_DEF_SELNAME;
	
	if ([self tadsEventLoopThreadIsCancelled]) {
		return NSUIntegerMax;
	}
	
	[self childThread_updateGameTitle];
	
	NSUInteger keyPressed = [self.os_waitc_eventLoopBridge waitForSignal];
	
	if ([self tadsEventLoopThreadIsCancelled]) {
		return NSUIntegerMax;
	}
	
	[self childThread_moveCursorToEndOfOutputPosition];
	
	XT_TRACE_1(@"-> %lu", keyPressed);
	
	return keyPressed;
}

- (BOOL)tadsEventLoopThreadIsCancelled
{
	XT_DEF_SELNAME;
	
	BOOL res = NO;
	
	if ([[NSThread currentThread] isCancelled]) {
		res = YES;
		_countTimesInTadsEventLoopThreadCancelledState += 1;
		if (_countTimesInTadsEventLoopThreadCancelledState > 200) {
			XT_WARN_0(@"Exiting VM thread due to excessive time in cancelled state");
			_shuttingDownTadsEventLoopThread = NO;
			[NSThread exit];
		}
	} else {
		_countTimesInTadsEventLoopThreadCancelledState = 0;
	}
	return res;
}

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

- (NSString *)safeNameForEncoding:(NSStringEncoding) encoding {
	
	NSString *encName = [NSString localizedNameOfStringEncoding:encoding];
	encName = [encName stringByReplacingOccurrencesOfString:@" " withString:@"-"];
	encName = [encName stringByReplacingOccurrencesOfString:@"(" withString:@"-"];
	encName = [encName stringByReplacingOccurrencesOfString:@")" withString:@"-"];
	return encName;
}

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

- (BOOL)childThread_printOutputText:(NSString *)s
{
	XT_TRACE_ENTRY;
	
	// self.responseTextView is not thread-safe, so...
	
	NSMutableArray *args = [NSMutableArray arrayWithArray:@[s, [NSNull null]]];
	
	if (_shuttingDownTadsEventLoopThread) {
		return NO;
	}
	[self performSelectorOnMainThread:@selector(mainThread_printOutputText:)
						   withObject:args
						waitUntilDone:YES];
	
	BOOL res = NO;
	if (args.count == 2) {
		id rawReturnValueHolder = args[1];
		if ([rawReturnValueHolder isKindOfClass:[NSNumber class]]) {
			NSNumber *returnValueHolder = rawReturnValueHolder;
			res = returnValueHolder.boolValue;
		} else {
			XT_ERROR_0(@"on return from main thread, args[0] wasn't a NSNumber");
		}
	} else {
		XT_ERROR_0(@"on return from main thread, args didn't have 2 entries");
	}
	if (res) {
		XT_TRACE_1(@"--> %d", res);
	}
	return res;
}

- (BOOL) childThread_pumpOutputText
{
	XT_TRACE_ENTRY;
	
	// self.responseTextView is not thread-safe, so...
	
	NSMutableArray *args = [NSMutableArray arrayWithArray:@[[NSNull null]]];
	
	if (_shuttingDownTadsEventLoopThread) {
		return NO;
	}
	[self performSelectorOnMainThread:@selector(mainThread_pumpOutputText:)
						   withObject:args
						waitUntilDone:YES];
	
	BOOL res = NO;
	if (args.count == 1) {
		id rawReturnValueHolder = args[0];
		if ([rawReturnValueHolder isKindOfClass:[NSNumber class]]) {
			NSNumber *returnValueHolder = rawReturnValueHolder;
			res = returnValueHolder.boolValue;
		} else {
			XT_ERROR_0(@"on return from main thread, args[0] wasn't a NSNumber");
		}
	} else {
		XT_ERROR_0(@"on return from main thread, args didn't have 1 entry");
	}
	XT_TRACE_1(@"--> %d", res);
	return res;
}

- (void)childThread_deleteCharactersInRange:(NSRange)aRange
{
	// self.responseTextView is not thread-safe, so...
	sharedRangeFor_childThread_deleteCharactersInRange = aRange;
	if (_shuttingDownTadsEventLoopThread) {
		return;
	}
	[self performSelectorOnMainThread:@selector(mainThread_deleteCharactersInRange)
						   withObject:nil // uses sharedRangeFor_childThread_deleteCharactersInRange
						waitUntilDone:YES];
}

- (void)childThread_outputTextHandler_resetForNextCommand
{
	if (_shuttingDownTadsEventLoopThread) {
		return;
	}
	[self performSelectorOnMainThread:@selector(mainThread_outputTextHandler_resetForNextCommand)
						   withObject:nil
						waitUntilDone:YES];
}

- (void) childThread_moveCursorToEndOfOutputPosition
{
	// self.responseTextView is not thread-safe, so...
	if (_shuttingDownTadsEventLoopThread) {
		return;
	}
	[self performSelectorOnMainThread:@selector(mainThread_moveCursorToEndOfOutputPosition)
						   withObject:nil
						waitUntilDone:YES];
}


- (void) childThread_gameHasEnded
{
	XT_TRACE_ENTRY;
	
	if (_shuttingDownTadsEventLoopThread) {
		return;
	}
	[self performSelectorOnMainThread:@selector(mainThread_gameHasEnded)
						   withObject:nil
						waitUntilDone:YES];
	
	XT_TRACE_0(@"exit");
}

- (void)childThread_updateGameTitle
{
	if (_shuttingDownTadsEventLoopThread) {
		return;
	}
	[self performSelectorOnMainThread:@selector(mainThread_updateGameTitle)
						   withObject:nil
						waitUntilDone:YES];
}

- (void)childThread_showModalErrorDialogWithMessageText:(NSString *)msgText
{
	if (_shuttingDownTadsEventLoopThread) {
		return;
	}
	[self performSelectorOnMainThread:@selector(mainThread_showModalErrorDialogWithMessageText:)
						   withObject:msgText
						waitUntilDone:YES];
}




