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

#import "XTBaseTextHandler_private.h"
#import "XTMainTextHandler.h"
#import "XTMainTextView.h"
#import "XTScrollView.h"
#import "XTNotifications.h"
#import "XTPrefs.h"
#import "XTOutputFormatter.h"
#import "XTFormattedOutputElement.h"
#import "XTOutputTextParserPlain.h"
#import "XTOutputTextParserHtml.h"
#import "XTHtmlTag.h"
#import "XTHtmlTagBr.h"
#import "XTHtmlTagAboutBox.h"
#import "XTHtmlTagTitle.h"
#import "XTHtmlTagBanner.h"
#import "XTHtmlTagWhitespace.h"
#import "XTHtmlTagT2TradStatusLine.h"
#import "XTLogger.h"
#import "XTStringUtils.h"
#import "XTBannerTextHandler.h"
#import "XTHtmlTagQuotedSpace.h"
#import "XTHtmlTagSpecialSpace.h"
#import "XTHtmlTagNonbreakingSpace.h"
#import "XTAllocDeallocCounter.h"
#import "XTViewLayoutUtils.h"
#import "XTTimer.h"
#import "XTMutableAttributedStringHelper.h"
#import "XTTimedCommandState.h"


@interface XTMainTextHandler ()

@property XTTimedCommandState *timedCommandState;

@property NSTimeInterval totalTimeInAppendAttributedStringToTextStorage;
@property BOOL afterNewlineAfterCommand;

@end


@implementation XTMainTextHandler

const NSUInteger initialCommandPromptPosition = NSUIntegerMax;

static XTLogger* logger;

@synthesize statusLineMode = _statusLineMode;

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

OVERRIDE_ALLOC_FOR_COUNTER
OVERRIDE_DEALLOC_FOR_COUNTER

- (id)init
{
	//XT_DEF_SELNAME;

	self = [super init];
    if (self) {
		_commandHistory = [XTCommandHistory new];
		
		_activeTagBannerHandle = nil;
		// no text entry before first input prompt:
		_commandPromptPosition = initialCommandPromptPosition;
		_statusLineMode = STATUS_LINE_MODE_MAIN;
		_gameTitle = [NSMutableString stringWithString:@""];
		_timedCommandState = [XTTimedCommandState new];
		_totalTimeInAppendAttributedStringToTextStorage = 0.0;
		_afterNewlineAfterCommand = NO;
		
		[self setupReceptionOfAppLevelNotifications];
    }
    return self;
}

- (void)setStatusLineMode:(NSUInteger)statusLineMode
{
	//XT_DEF_SELNAME;
	//XT_WARN_1(@"%lu", statusLineMode);
	
	XTHtmlTagT2TradStatusLine *tag = [XTHtmlTagT2TradStatusLine withMode:statusLineMode];
	//TODO !!! ??? flush like in oshtml_display_html_tags(const textchar_t *txt)
	if (statusLineMode == STATUS_LINE_MODE_STATUS) {
		int brkpt = 1;
	}
	[[self getOutputTextParser] appendTagToCurrentContainer:tag];
}

- (void)setStatusLineModeNow:(NSUInteger)statusLineMode
{
	//XT_DEF_SELNAME;
	//XT_WARN_1(@"%lu", statusLineMode);
	
	_statusLineMode = statusLineMode;
}

- (NSUInteger)statusLineMode {
	return _statusLineMode;
}

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

	[self teardown];
	
	self.gameWindowController = nil;
	self.gameTitle = nil;
	self.commandHistory = nil;
	self.activeTagBannerHandle = nil;
}

+ (instancetype)handler
{
	XTMainTextHandler *handler = [[XTMainTextHandler alloc] init];
	return handler;
}

- (void)traceWithIndentLevel:(NSUInteger)indentLevel
{
	XT_TRACE_ENTRY;
	
	for (XTBannerTextHandler *child in self.childHandlers) {
		[child traceWithIndentLevel:indentLevel + 1];
	}
}

- (void)setIsForT3:(BOOL)isForT3
{
	self.outputFormatter.isForT3 = isForT3;
	self.colorationHelper.isForT3 = isForT3;
}

// called when game file loads and starts
- (void)resetToDefaults
{
	//XT_DEF_SELNAME;
	
	self.htmlMode = NO;
	self.hiliteMode = NO;
	[self setNonstopMode:NO];
	self.statusLineMode = STATUS_LINE_MODE_MAIN;
	self.timedCommandState = [XTTimedCommandState new];
	self.afterNewlineAfterCommand = NO;
	//XT_WARN_0(@"afterNewlineAfterCommand set to NO");

	[self clearText];
	[self.outputTextParserPlain resetForNextCommand];
	[self.outputTextParserHtml resetForNextCommand];
	self.activeTagBannerHandle = nil;
	
	self.gameTitle = [NSMutableString stringWithString:@""];
}

- (void)setNonstopMode:(BOOL)nonstopMode
{
	self.nonstopModeState = nonstopMode;
	for (XTBannerTextHandler *child in self.childHandlers) {
		child.nonstopModeState = nonstopMode;
	}
}

- (void)resetForNextCommand
{
	XT_DEF_SELNAME;
	
	if ([self.timedCommandState isTimedOut]) {
		XT_TRACE_0(@"aborting because timedCommandState.state == timedOut");
		return;
	}
	//XT_WARN_0(@"executing");
	
	[super resetForNextCommand];
	[self.textStorageBatcher reset];

	[self ensureInputFontIsInEffect];

	self.afterNewlineAfterCommand = NO;
	//XT_WARN_0(@"afterNewlineAfterCommand set to NO");
}

- (void)resetForGameHasEndedMsg
{
	//XT_DEF_SELNAME;
	
	self.timedCommandState = [XTTimedCommandState new];
	[[self getOutputTextParser] flush];
	[self.outputTextParserHtml resetForGameHasEndedMsg];
	[self.outputTextParserPlain resetForNextCommand];
	[self.outputFormatter resetFlags];
	[self mainThread_noteStartOfPagination];
	[self clearPaginationState];
	[self.textStorageBatcher reset];

	self.afterNewlineAfterCommand = NO;
	//XT_WARN_0(@"afterNewlineAfterCommand set to NO");
}

- (void)exitingTagBanner
{
	self.activeTagBannerHandle = NULL;
	[self.outputFormatter resetTagBannerDepth];
	[self.outputFormatter clearShouldWriteWhiteSpace];
}

- (void)createTextViewForMainOutputArea
{
	XT_TRACE_ENTRY;

	NSScrollView *newTextScrollView = [XTUIUtils createScrollViewWithTextViewForMainOutputArea:self.gameWindowController];
	self.scrollView = newTextScrollView;
	self.textView = self.scrollView.documentView;
	self.outputFormatter.textView = self.textView;
	self.textView.outputFormatter = self.outputFormatter;
	self.textView.backgroundColor = [self.colorationHelper getOutputBackgroundColorForTextView];
	self.colorationHelper = [XTColorationHelper forTextView:self.textView
													isForT3:self.outputFormatter.isForT3
													isForBanner:NO
												isForGridBanner:NO];
	self.outputFormatter.colorationHelper = self.colorationHelper;
}

- (void)noteStartOfLayoutOfViews
{
	[super noteStartOfLayoutOfViews];

	for (XTBannerTextHandler *child in self.childHandlers) {
		[child noteStartOfLayoutOfViews];
	}
}

- (void)noteEndOfLayoutOfViews
{
	[super noteEndOfLayoutOfViews];

	for (XTBannerTextHandler *child in self.childHandlers) {
		[child noteEndOfLayoutOfViews];
	}
}

- (void)mainThread_getCommand:(NSMutableArray *)returnValue
{
	NSString *command = [self getCommandString];
	
	[self.commandHistory appendCommand:command];
	
	returnValue[0] = command;
}

- (void)mainThread_cancelTimedOutCommand:(NSArray *)args
{
	BOOL reset = ((NSNumber *)args[0]).boolValue;
	
	[self endCommandInProgressAtTimeout];
	
	[self.timedCommandState setCancelled];
	
	if (reset) {
		self.timedCommandState.command = nil;
	}
	
	/*TODO !!! tm
	if (reset) {
		self.timedCommandState = [XTTimedCommandState new];
	} else {
		self.timedCommandState.timedOut = NO;
	}*/
}

- (NSString *)getCommandString
{
	NSRange range = [self getCommandTextRange];
	NSString *res = [self.textView stringInRange:range];
	return res;
}

- (void)clearText
{
	//XT_DEF_SELNAME;
	//XT_WARN_ENTRY;

	// also called when game clears screen
	
	// might be a <title> or something there that needs processing
	[self processTagTree];
	
	[self.textStorageBatcher reset];
	
	[[self getOutputTextParser] flush];
	[self.outputTextParserPlain resetForNextCommand];
	[self.outputTextParserHtml resetForNextCommand];

	[self processTagTree];

	[self.outputFormatter resetFlags];
	
	[self clearTextStorage];

	self.afterNewlineAfterCommand = NO;
	//XT_WARN_0(@"afterNewlineAfterCommand set to NO");

	//TODO !!! rm? shouldn't be needed anymore
	// Insert some temporary, invisible text to get font height set and pagination calculations correct from the start:
	NSArray<NSAttributedString *> *mutAttrStringArray = [self.outputFormatter formatOutputText:ZERO_WIDTH_SPACE];
	for (NSAttributedString *attrString in mutAttrStringArray) {
		[self appendAttributedStringToTextStorage:attrString];
	}
	[self.outputFormatter resetFlags]; // get rid of state due to the zwsp
	
	[self.colorationHelper resetBodyColors];
	
	[self.textView scrollPageUp:self]; // needed to ensure new text isn't initially "scrolled past"
	[self moveCursorToEndOfOutputPosition];

	self.commandPromptPosition = initialCommandPromptPosition;

	[self clearPaginationState];
	
	[self mainThread_noteStartOfPagination];
	
	// Remove the invisible text we added earlier, so that we haven't "used up"
	// the text alignment of the first paragraph:
	[self.textView removeLastChar];
	
	//XT_TRACE_0(@"done");
}

- (void)flushOutput
{
	XT_TRACE_ENTRY;
	
	[[self getOutputTextParser] flush];
	[self processTagTree];
	[self flushFormattingQueue];
}

- (void)hardFlushOutput
{
	XT_TRACE_ENTRY;
	
	[[self getOutputTextParser] hardFlush];
	[self processTagTree];
	[self flushFormattingQueue];
}

- (void)appendInput:(NSString *)string
{
	// Note: this is called for paste event
	
	if (! [self canAppendNonTypedInput]) {
		return;
	}
	
	NSAttributedString *attrString = [self.outputFormatter formatInputText:string];
	[self appendAttributedStringToTextStorage:attrString];
	
	[self scrollToBottom];
}

//TODO mv down
// Allow appending pasted text, text from clicked command link, etc. ?
- (BOOL)canAppendNonTypedInput
{
	BOOL res = YES;
	if (! self.gameWindowController.gameIsRunning) {
		res = NO;
	}
	if ([self.gameWindowController isWaitingForKeyPressed]) {
		res = NO;
	}
	return res;
}

- (void)handleCommandLinkClicked:(NSString *)linkText atIndex:(NSUInteger)charIndex
{
	if (! [self canAppendNonTypedInput]) {
		return;
	}
	
	NSRange proposedRange = NSMakeRange(charIndex, 1);
	NSRange actualRange;
	NSAttributedString *as = [self.textView attributedSubstringForProposedRange:proposedRange
																	actualRange:&actualRange];
	id appendAttr = [as attribute:XT_OUTPUT_FORMATTER_ATTR_CMDLINK_APPEND atIndex:0 effectiveRange:nil];
	BOOL append = (appendAttr != nil);
	id noenterAttr = [as attribute:XT_OUTPUT_FORMATTER_ATTR_CMDLINK_NOENTER atIndex:0 effectiveRange:nil];
	BOOL noenter = (noenterAttr != nil);
	
	if (! append) {
		[self replaceCommandText:linkText];
	} else {
		NSAttributedString *attrLinkString = [self.outputFormatter formatInputText:linkText];
		[self appendAttributedStringToTextStorage:attrLinkString];
	}
	
	[self moveCursorToEndOfOutputPosition];
	
	if (! noenter) {
		[self.textView.delegate textView:self.textView doCommandBySelector:@selector(insertNewline:)];
	}
}

- (void)ensureInputFontIsInEffect
{
	//XTPrefs *prefs = [XTPrefs prefs];
	//if (prefs.inputFontUsedEvenIfNotRequestedByGame.boolValue) {
		[self appendInput:ZERO_WIDTH_SPACE];
		[self noteEndOfOutput];
	//}
}

- (BOOL)appendOutput:(NSString *)string
{
	XT_DEF_SELNAME;
	//XT_TRACE_0(@"-------------------------------------------------------------");
	XT_TRACE_1(@"\"%@\"", string);
	
	if ([self.timedCommandState isTimedOut]) {
		XT_TRACE_0(@"return because timedCommandState.state == timedOut");
		return NO;
	}

	if (! self.htmlMode) {
		if ([string isEqualToString:@"\n"]) {
			if (! self.afterNewlineAfterCommand) {
				//XT_WARN_2(@"\\n - afterNewlineAfterCommand=%lu parsing \"%@\"", self.afterNewlineAfterCommand, string);
				[[self getOutputTextParser] parse:string];
			} else {
				//XT_WARN_2(@"\\n - afterNewlineAfterCommand=%lu SKIPPING \"%@\"", self.afterNewlineAfterCommand, string);
			}
		} else {
			[[self getOutputTextParser] parse:string];
		}
		self.afterNewlineAfterCommand = NO;
		//XT_WARN_0(@"afterNewlineAfterCommand set to NO");
	} else {
		[[self getOutputTextParser] parse:string];
	}

	//TODO !!! rework
	//BOOL excessiveAmountBuffered = (self.formattingQueue.count >= 1000);
	//return excessiveAmountBuffered;
	return NO;
}

- (void)appendOutputNewlineAfterCommand:(NSString *)string
{
	XT_DEF_SELNAME;

	if (! self.htmlMode) {
		if ([string isEqualToString:@"\n"]) {
			//XT_WARN_2(@"\\n - afterNewlineAfterCommand=%lu parsing \"%@\"", self.afterNewlineAfterCommand, string);
			if (self.afterNewlineAfterCommand) {
				XT_DEF_SELNAME;
				XT_ERROR_0(@"afterNewlineAfterCommand should not be true here");
				int brkpt = 1;
			}
			//TODO !!! exp rm:
			self.afterNewlineAfterCommand = YES;
			//XT_WARN_0(@"afterNewlineAfterCommand set to YES");
		} else {
			int brkpt = 1;
		}
	} else {
		int brkpt = 1;
	}
	[[self getOutputTextParser] parse:string];
}

// the index where new input text is appended
- (NSInteger)insertionPoint
{
	NSRange r = [self.textView selectedRange];
	return r.location;
}

- (NSInteger)minInsertionPoint
{
	NSInteger res = self.commandPromptPosition + 1;
	return res;
}

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

- (void)moveCursorToEndOfOutputPosition
{
	XT_DEF_SELNAME;
	
	NSUInteger backOffset = self.timedCommandState.cursorOffsetFromEndOfCommand.location;
	if (backOffset >= 1) {
		NSUInteger textLength = [self.textView endOfOutputPosition];
		NSUInteger minInsertionPoint = [self minInsertionPoint];
		NSInteger newInsertionPoint = textLength - backOffset;
		if (newInsertionPoint < minInsertionPoint) {
			newInsertionPoint = minInsertionPoint;
		}
		NSUInteger selectedTextLength = self.timedCommandState.cursorOffsetFromEndOfCommand.length;
		if (selectedTextLength >= 1) {
			NSRange range = NSMakeRange(newInsertionPoint, selectedTextLength);
			[self.textView setSelectedRange:range affinity: self.timedCommandState.selectedTextAffinity stillSelecting:YES];
			//XT_WARN_3(@"selectedTextLength=%lu -> setSelectedRange loc=%lu len=%lu", selectedTextLength, range.location, range.length);
		} else {
			[self.textView setInsertionPoint:newInsertionPoint];
			//XT_WARN_2(@"selectedTextLength=%lu -> setInsertionPoint=%lu", selectedTextLength, newInsertionPoint);
		}
	} else {
		[super moveCursorToEndOfOutputPosition];
	}
}

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

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

- (BOOL)cursorIsInCommandButNotAtMinInsertionPosition
{
	//XT_DEF_SELNAME;
	
	NSInteger insPt = [self insertionPoint];
	NSInteger minInsPt = [self minInsertionPoint];
	BOOL res = (insPt > minInsPt);
	
	//XT_WARN_3(@"-> %d (insPt=%d minInsPt=%d)", res, insPt, minInsPt);
	return res;
}

- (BOOL)allowTextInsertion:(NSRange)affectedCharRange
{
	NSInteger minInsPt = [self minInsertionPoint];
	BOOL res = (affectedCharRange.location >= minInsPt);
	return res;
}

- (void)goToPreviousCommand
{
	NSString *previousCommand = [self.commandHistory getPreviousCommand];
	if (previousCommand != nil) {
		[self replaceCommandText:previousCommand];
	}
}

- (void)goToNextCommand
{
	NSString *newCommandText = [self.commandHistory getNextCommand];
	if (newCommandText == nil) {
		if ([self.commandHistory hasBeenAccessed]) {
			// we're back out of the historic commands
			newCommandText = @"";
			//TODO better: replace with command that was *being typed*
			//		- requires capturing that conmand on every keystroke
			[self.commandHistory resetHasBeenAccessed];
		}
	}
	if (newCommandText != nil) {
		[self replaceCommandText:newCommandText];
	}
}

- (void)replaceCommandText:(NSString *)newCommandText
{
	//XT_WARN_ENTRY;
	
	NSRange commandTextRange = [self getCommandTextRange];
	[self.textView removeText:commandTextRange];
	NSAttributedString *attrString = [self.outputFormatter formatInputText:newCommandText];
	[self appendAttributedStringToTextStorage:attrString];
}

- (void)endCommandInProgressAtTimeout
{
	//XT_DEF_SELNAME;
	
	if (! [self.timedCommandState isTimedOut]) {
		return;
	}

	NSAttributedString *attrStringNewline = [self.outputFormatter formatInputText:@"\n"];
	[self appendAttributedStringToTextStorage:attrStringNewline];
}

//TODO !!! refactor logic?
- (void)mainThread_restoreCommandInProgressAtTimeout
{
	//XT_DEF_SELNAME;
	
	if (! [self.timedCommandState isCancelled]) {
		self.timedCommandState = [XTTimedCommandState new];
		return;
	}

	if (self.timedCommandState.command.length == 0) {
		self.timedCommandState = [XTTimedCommandState new];
		return;
	}
	
	NSString *usedCommand = self.timedCommandState.command;
	unichar firstChar = [self.timedCommandState.command characterAtIndex:0];
	if (firstChar != ZERO_WIDTH_SPACE_CHAR) {
		NSMutableString *mutCommand = [NSMutableString stringWithString:self.timedCommandState.command];
		[mutCommand insertString:ZERO_WIDTH_SPACE atIndex:0];
		usedCommand = [NSString stringWithString:mutCommand];
		//TODO !!! exp:
		self.commandPromptPosition += 1;
	}
	
	NSAttributedString *attrString = [self.outputFormatter formatInputText:usedCommand];
	[self appendAttributedStringToTextStorage:attrString];
	
	self.timedCommandState = [XTTimedCommandState new];
}

- (void)mainThread_captureCommandInProgressAtTimeout
{
	NSString *command = [self getCommandString];
	[self setCommandInProgressAtTimeout:command];
	[self.timedCommandState setTimedOut];
}

- (void)setCommandInProgressAtTimeout:(NSString *)command
{
	//XT_DEF_SELNAME;
	
	if (command.length >= 1) {
		self.timedCommandState.command = command;
	} else {
		self.timedCommandState.command = nil;
	}
	NSUInteger minPos = [self commandPromptPosition];
	self.timedCommandState.cursorOffsetFromEndOfCommand = [self.textView cursorOffsetFromEndOfTextMinPosition:minPos];	
	self.timedCommandState.selectedTextAffinity = [self.textView selectedTextAffinity];
}

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

- (void)setColorsFromPrefsColor
{
	self.textView.backgroundColor = [self.colorationHelper getOutputBackgroundColorForTextView];
	
	[self.colorationHelper applyPrefsTextAndInputColors];
	[self.colorationHelper updateLinkColors];
	[self.colorationHelper updateTableColors];

	[self updateCursorColor];

	[self.textView setNeedsDisplay:YES];
	
	//TODO why is this needed?:
	[self scrollToBottom];
}

- (void)setColorsFromPrefAllowGameToSetColors
{
	self.textView.backgroundColor = [self.colorationHelper getOutputBackgroundColorForTextView];
	
	[self.colorationHelper applyTextAndInputColorsForAllowGameToSetColors];
	[self.colorationHelper updateLinkColors];
	[self.colorationHelper updateTableColors];

	[self updateCursorColor];

	[self.textView setNeedsDisplay:YES];
	
	//TODO why is this needed?:
	[self scrollToBottom];
}

- (void)setColorsFromBody
{
	self.textView.backgroundColor = [self.colorationHelper getOutputBackgroundColorForTextView];
	
	[self.colorationHelper applyBodyTextAndInputColorsForceApply:YES];
	[self.colorationHelper applyBodyLinkColor];

	[self updateCursorColor];

	[self.textView setNeedsDisplay:YES];
	
	//TODO why is this needed?:
	[self scrollToBottom];
}

//=========  Internal functions  =======================================

- (void)updateCursorColor
{
	[self.outputFormatter updateCursorColor];
}

- (BOOL)shouldAutoScrollToBottom
{
	// main output area should always autoscroll to bottom of text when asked to
	return YES;
}

- (NSRange)getCommandTextRange
{
	NSUInteger minInsertionPoint = self.minInsertionPoint;
	NSUInteger endOfOutputPosition = [self.textView endOfOutputPosition];
	NSRange commandTextRange = NSMakeRange(minInsertionPoint, endOfOutputPosition - minInsertionPoint);
	return commandTextRange;
}

- (void)noteEndOfOutput
{
	//XT_DEF_SELNAME;

	// find new starting pos of cmd prompt
	NSUInteger oldPos = self.commandPromptPosition;
	NSUInteger tempPos = [self.textView endOfOutputPosition];
	NSUInteger newPos = (tempPos > 0 ? tempPos - 1 : 0);
	if (newPos > oldPos) {
		int brkpt = 1;
	}
	self.commandPromptPosition = newPos;
	//XT_WARN_2(@"commandPromptPosition %lu -> %lu", oldPos, newPos);
}

- (BOOL)processTagTree
{
	//XT_DEF_SELNAME;
	//XT_TRACE_1(@"%lu entries", [self.formattingQueue count]);

	if (self.activeTagBannerHandle != nil) {
		//XT_WARN_1(@"activeTagBannerHandle != nil - continue with %lu elts in fmt queue", self.formattingQueue.count);
		XTBannerTextHandler *bannerHandler = [self.gameWindowController bannerHandlerForHandle:self.activeTagBannerHandle];
		self.currentTag = [bannerHandler processTagTreeFromMainOutput:self.currentTag parser:[self getOutputTextParser]];
			// this will consume queue until </banner> or queue empty
	}
	
	if (self.statusLineMode == STATUS_LINE_MODE_STATUS && ! self.htmlMode) {
		XTBannerTextHandler *bannerHandler = [self.gameWindowController getBannerHandlerForTradStatusLine];
		self.currentTag = [bannerHandler processTagTreeFromMainOutput:self.currentTag parser:[self getOutputTextParser]];
			// this will consume queue until <tads2tradstatusline mode=0> or queue empty
	}

	BOOL reachedPaginationLimit = NO;

	NSUInteger oldTextLength = self.textView.textStorage.length;
	
	[self ensureWeHaveCurrentTag];
	reachedPaginationLimit = [self processTags];

	NSUInteger newTextLength = self.textView.textStorage.length;
	
	[self trimScrollbackBuffer];
	[self scrollToBottom];

	NSUInteger textLengthAdded = newTextLength - oldTextLength;
	if (textLengthAdded >= 1) {
		[self noteEndOfOutput];
	}

	return reachedPaginationLimit;
}

- (BOOL)processFormattedElementQueue
{
	XT_DEF_SELNAME;

	BOOL reachedPaginationLimit = NO;
	
	while (! [self.formattedElementQueue isEmpty] && ! reachedPaginationLimit) {
		
		XTFormattedOutputElement *outputElement = [self.formattedElementQueue removeFirst];

		if ([self isInTable]) {
			if ([outputElement isTableEnd]) {
				[self handleTableEnd];
			} else {
				[self addToTempFormattedElementQueueForTable:outputElement];
			}

		} else if ([outputElement isRegularOutputElement]) {
			//XT_WARN_1(@"handling RegularOutputElement \"%@\"", outputElement.attributedString.string);
			reachedPaginationLimit = [self processFormattedElementQueueRegularElementString:outputElement.attributedString];

		} else if ([outputElement isTabElement]) {
			reachedPaginationLimit = [self processFormattedElementQueueRegularElementString:outputElement.attributedString];

		} else if ([outputElement isGameTitleElement]) {
			[self processFormattedElementQueueGameTitleElement:outputElement];
			
		} else if ([outputElement isBannerStartElement]) {
			//XT_WARN_1(@"handling BannerStartElement id=\"%@\"", [((XTHtmlTagBanner *)outputElement.htmlTag) safeBannerId]);
			[self handleBannerTagStart:outputElement];
			
		} else if ([outputElement isStatusLineModeStart]) {
			//XT_WARN_0(@"handling StatusLineModeStart");
			[self handleStatusLineModeStart:outputElement];
			
		} else if ([outputElement isStatusLineModeEnd]) {
			//XT_WARN_0(@"handling StatusLineModeEnd");
			if (! self.htmlMode) {
				//XT_ERROR_0(@"isStatusLineModeEnd && ! self.htmlMode");
				// nah, can happen at start of games
			}
			
		} else if ([outputElement isStatusLineModeSuppress]) {
			//XT_WARN_0(@"handling StatusLineModeSuppress]");
			_statusLineMode = STATUS_LINE_MODE_SUPPRESS; // don't do .statusLineMode, it'll loop things
			
		} else if ([outputElement isClearWhitespaceBeforeOrAfterBlockLevelTagOutputElement]) {
			[self clearWhitespaceBeforeOrAfterBlockLevelTagOutputElement];
			
		} else if ([outputElement isBody]) {
			[self handleBody:outputElement];
		
		} else if ([outputElement isTableStart]) {
			[self handleTableStart];

		} else if ([outputElement isTableEnd]) {
			// Ignore. Can happen game asks for key prompt in middle of a table.

		} else {
			XT_ERROR_1(@"unexpected XTFormattedOutputElement %@", [outputElement elementTypeAsString]);
		}
	}
	
	return reachedPaginationLimit;
}

- (void)processFormattedElementQueueGameTitleElement:(XTFormattedOutputElement *)outputElement
{
	if ([outputElement.attributedString.string isEqualToString:@"{{clear}}"]) {
		//TODO make element type for this case
		self.gameTitle = [NSMutableString stringWithString:@""];
	} else {
		[self.gameTitle appendString:outputElement.attributedString.string];
	}
}

- (void)flushPendingNewline
{
	// only relevant for banners
}

//TODO move
- (void)exitingStatusLineMode:(XTFormattedOutputElement *)outputElement
{
	//XT_DEF_SELNAME;
	
	XTBannerTextHandler *bhTSL = [self.gameWindowController getBannerHandlerForTradStatusLine];
	if (bhTSL != nil) {
		[bhTSL flushTradStatusLineScoreString];
	}
	
	_statusLineMode = STATUS_LINE_MODE_MAIN; // don't do .statusLineMode, it'll loop things
}

- (void)handleBannerTagStart:(XTFormattedOutputElement *)outputElement
{
	XT_DEF_SELNAME;
	
	//TODO !!! exp for erudite missing space bug:
	//[self.outputFormatter flushPendingWhitespace]; // ignore result value
	[self flushFormattingQueue];

	BOOL removeAllBanners = [outputElement.htmlTag hasAttribute:@"removeall"];
	if (removeAllBanners) {
		[self.gameWindowController bannerDeleteAll];
		[self.outputFormatter clearShouldWriteWhiteSpace];
		return;
	}
	
	NSString *tagId = [outputElement.htmlTag attributeAsString:@"id"];
	if (tagId == nil || tagId.length == 0) {
		tagId = @"xtads-id-less-banner";
		XT_TRACE_0(@"<banner> has no id attribute - using a default id");
	}
	
	void *bannerHandle = [self.gameWindowController bannerHandleForTagId:tagId];
	
	BOOL removeOneBanner = [outputElement.htmlTag hasAttribute:@"remove"];
	if (removeOneBanner) {
		if (bannerHandle != NULL) {
			[self.gameWindowController bannerDelete:bannerHandle];
		} else {
			XT_WARN_1(@"Cannot remove non-existent banner with tagId %@", tagId);
		}
		[self.outputFormatter clearShouldWriteWhiteSpace];
		return;
	}

	NSString *alignStr = [outputElement.htmlTag attributeAsString:@"align"];
	NSInteger align = [self bannerAlignmentFrom:alignStr];

	NSInteger sizeUnits = OS_BANNER_SIZE_ABS;
	NSInteger size = 0;
	BOOL sizeToContents = YES;
	BOOL sizeAsPrevious = NO;
	NSString *sizeAttrName = @"height";
	if ((align == OS_BANNER_ALIGN_LEFT) || (align == OS_BANNER_ALIGN_RIGHT)) {
		sizeAttrName = @"width";
	}
	NSString *sizeStr = [outputElement.htmlTag attributeAsString:sizeAttrName];
	[self extractTagBannerSizeFrom:sizeStr
						  attrName:sizeAttrName
					sizeToContents:&sizeToContents
					sizeAsPrevious:&sizeAsPrevious
							  size:&size
						 sizeUnits:&sizeUnits];
	
	NSInteger style = 0;
	if ([outputElement.htmlTag hasAttribute:@"border"]) {
		style |= OS_BANNER_STYLE_BORDER;
	}
	
	XTBannerTextHandler *bannerHandler;
	
	if (bannerHandle == NULL) {
	
		void *parent = 0; // parent is always"root banner", i.e. main output area
		NSInteger where = OS_BANNER_LAST;
		void *other = 0;
		NSInteger wintype = OS_BANNER_TYPE_TEXT;

		bannerHandle = [self.gameWindowController bannerCreate:parent
														 tagId:tagId
														 where:where
														 other:other
													   wintype:wintype
														 align:align
														  size:size
													 sizeUnits:sizeUnits
														 style:style
													  htmlMode:YES];
		
		bannerHandler = [self.gameWindowController bannerHandlerForHandle:bannerHandle];
		
		bannerHandler.wasInitiallySizedToPrevious = sizeAsPrevious;
		
		bannerHandler.tagBannerNeedsSizeToContent = (sizeToContents || sizeAsPrevious);
			// keep this value, so that tag banner gets resized to current contents if necessary
			//TODO very clumsy to do it this way...
		
		bannerHandler.mainTextHandler = self;
		
	} else {
		
		bannerHandler = [self.gameWindowController bannerHandlerForHandle:bannerHandle];
		
		//XT_WARN_0(@"call bannerHandler synchClear");
		[bannerHandler synchClear];
			
		//TODO? don't resize if size not changed
		[self.gameWindowController tagBannerReconfigure:bannerHandle
												  align:align
										 sizeToContents:sizeToContents
										 sizeAsPrevious:sizeAsPrevious
												   size:size
											  sizeUnits:sizeUnits
												  style:style];
	}

	bannerHandler.hadUnspecifiedSizeLastTime = sizeToContents;
	bannerHandler.hadPreviousSizeLastTime = sizeAsPrevious;
		// Needed for a weird-ass special case :-(
	
	self.activeTagBannerHandle = bannerHandle;

	//XT_WARN_1(@"self.formattingQueue has %lu entries", self.formattingQueue.count);
	
	self.currentTag = [bannerHandler processTagTreeFromMainOutput:self.currentTag parser:[self getOutputTextParser]];
		// this will consume queue until </banner> or queue empty
}

- (void)handleStatusLineModeStart:(XTFormattedOutputElement *)outputElement
{
	XT_DEF_SELNAME;
	//XT_WARN_0(@"");

	if (self.htmlMode) {
		// let <banner> handling take care of it
		//XT_WARN_0(@"let <banner> handling take care of it");
		return;
	}
	//XT_WARN_0(@"handling it here");
	
 	[self.gameWindowController createBannerForTradStatusLine]; // ... if none exists already
	XTBannerTextHandler *bhTSL = [self.gameWindowController getBannerHandlerForTradStatusLine];
 	//TODO mv into this class?

	BOOL switchingToStatusLineMode = (self.statusLineMode != STATUS_LINE_MODE_STATUS);
	_statusLineMode = STATUS_LINE_MODE_STATUS; // don't do .statusLineMode, it'll loop things
	if (switchingToStatusLineMode) {
		if (bhTSL != nil) {

			[bhTSL executeClear];
			[bhTSL resetForTradStatusLine];
			
			NSMutableArray *params = [NSMutableArray arrayWithCapacity:2];
			params[0] = [NSNumber numberWithBool:YES]; // input param
			[bhTSL mainThread_pumpOutputText:params];
		} else {
			XT_ERROR_0(@"bhTSL == nil");
		}
	} else {
		int brkpt = 1;
	}

	self.currentTag = [bhTSL processTagTreeFromMainOutput:self.currentTag parser:[self getOutputTextParser]];
		// this will consume queue until <tads2tradstatusline mode=0> or queue empty
}

- (void)extractTagBannerSizeFrom:(NSString *)string
						attrName:(NSString *)attrName
				  sizeToContents:(BOOL *)sizeToContents
				  sizeAsPrevious:(BOOL *)sizeAsPrevious
							size:(NSInteger *)size
					   sizeUnits:(NSInteger *)sizeUnits
{
	XT_DEF_SELNAME;

	*sizeToContents = YES;
	*sizeAsPrevious = NO;
	
	if ([string length] >= 1) {
		string = [string lowercaseString];
		
		if ([string isEqualToString:@"previous"]) {
			*sizeToContents = NO;
			*sizeAsPrevious = YES;
			
		} else if ([string hasSuffix:@"%"]) {
			NSUInteger idxPctSign = string.length - 1;
			NSString *numPrefix = [string substringToIndex:idxPctSign];
			NSScanner *scanner = [NSScanner scannerWithString:numPrefix];
			NSInteger tempSize;
			BOOL found = [scanner scanInteger:&tempSize];
			if (found && [scanner isAtEnd] && tempSize >= 0 && tempSize <= 100) {
				*size = tempSize;
				*sizeUnits = OS_BANNER_SIZE_PCT;
				*sizeToContents = NO;
				*sizeAsPrevious = NO;
			} else {
				XT_WARN_2(@"illegal %* attribute \"%@\" - defaulting to content size", attrName, string);
				// keep default "size to content"
			}
		} else {
			NSScanner *scanner = [NSScanner scannerWithString:string];
			NSInteger tempSize;
			BOOL found = [scanner scanInteger:&tempSize];
			if (found && [scanner isAtEnd] && tempSize >= 0) {
				*size = tempSize;
				*sizeUnits = OS_BANNER_SIZE_PIXELS;
				*sizeToContents = NO;
				*sizeAsPrevious = NO;
			} else {
				XT_WARN_2(@"illegal %* attribute \"%@\" - defaulting to content size", attrName, string);
				// keep default "size to content"
			}
		}
	}
}

- (NSInteger)bannerAlignmentFrom:(NSString *)alignStr
{
	XT_DEF_SELNAME;

	NSInteger res = OS_BANNER_ALIGN_TOP;
	
	if (alignStr != nil) {
		NSString *alignStrLc = [alignStr lowercaseString];
		if ([alignStrLc isEqualToString:@"top"]) {
			res = OS_BANNER_ALIGN_TOP;
		} else if ([alignStrLc isEqualToString:@"bottom"]) {
			res = OS_BANNER_ALIGN_BOTTOM;
		} else if ([alignStrLc isEqualToString:@"left"]) {
			res = OS_BANNER_ALIGN_LEFT;
		} else if ([alignStrLc isEqualToString:@"right"]) {
			res = OS_BANNER_ALIGN_RIGHT;
		} else {
			XT_WARN_1(@"unknown alignment %@ - using default TOP", alignStr);
			res = OS_BANNER_ALIGN_TOP;
		}
	}
	
	return res;
}

- (void)trimScrollbackBuffer
{
	XT_DEF_SELNAME;
	
	XTPrefs *prefs = [XTPrefs prefs];
	
	if (! prefs.limitScrollbackBufferSize.value.boolValue) {
		return;
	}
	
	NSUInteger scrollbackBufferSize = 1000 * prefs.scrollbackBufferSizeInKBs.value.unsignedIntegerValue;
	
	NSTextStorage *ts = [self.textView textStorage];
	NSUInteger tsSize = ts.length;
	
	XT_TRACE_1(@"tsSize=%lu", tsSize);
	
	if (tsSize > scrollbackBufferSize) {
		NSUInteger excess = tsSize - scrollbackBufferSize;
		NSUInteger deleteBlockSize = 20000; // so we only delete if in excess by a goodish amount
		if (excess > deleteBlockSize) {
			CGFloat trimmedTextViewHeight = [self.textView removeTextFromStart:excess];
			self.maxTextViewHeightBeforePagination -= trimmedTextViewHeight;
			XT_TRACE_1(@"maxTextViewHeightBeforePagination", self.maxTextViewHeightBeforePagination);
		}
	}
}

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

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

	[[NSNotificationCenter defaultCenter] addObserver:self
											 selector:@selector(handleSetFocusToMainOutput:)
												 name:XTadsNotifySetFocusToMainOutput
											   object:nil]; // nil means "for any sender"
}

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

- (void)handleNotifyTextLinkClicked:(NSNotification *)notification
{
	NSString *linkText = notification.userInfo[XTadsNotificationUserInfoKeyLink];
	NSNumber *tempCharIndex = notification.userInfo[XTadsNotificationUserInfoKeyLinkCharIndex];
	NSUInteger charIndex = tempCharIndex.unsignedIntegerValue;
	
	[self handleCommandLinkClicked:linkText atIndex:charIndex];
}

- (void)handleSetFocusToMainOutput:(NSNotification *)notification
{
	XT_TRACE_ENTRY;
	
	// Transfer focus back to main output view
	[[self.textView window] makeFirstResponder:self.textView];
	[self moveCursorToEndOfOutputPosition];
}

- (void)configureViews
{
	XT_TRACE_ENTRY;
	
	CGSize oldMainOutputAreaSize = self.scrollView.frame.size;
	
	[self tearDownLayoutViews];
	
	NSView *overallView = [self internalRebuildViewHierarchy];
	if (overallView == nil) {
		// can happen when closing game window
		return;
	}
	
	[self.rootBannerContainerView addSubview:overallView];
	
	// Make overallView fill all of its superview:
	[XTViewLayoutUtils addEdgeConstraint:NSLayoutAttributeLeft superview:self.rootBannerContainerView subview:overallView];
	[XTViewLayoutUtils addEdgeConstraint:NSLayoutAttributeRight superview:self.rootBannerContainerView subview:overallView];
	[XTViewLayoutUtils addEdgeConstraint:NSLayoutAttributeTop superview:self.rootBannerContainerView subview:overallView];
	[XTViewLayoutUtils addEdgeConstraint:NSLayoutAttributeBottom superview:self.rootBannerContainerView subview:overallView];
	
	// Make sure view frames are up to date, for pagination calcs
	// See https://www.objc.io/issues/3-views/advanced-auto-layout-toolbox/
	[self.rootBannerContainerView layoutSubtreeIfNeeded];
	
	CGSize newMainOutputAreaSize = self.scrollView.frame.size;
	BOOL changedMainOutputAreaSize = ((newMainOutputAreaSize.width != oldMainOutputAreaSize.width) ||
									  (newMainOutputAreaSize.height != oldMainOutputAreaSize.height));
	
	[self recalcDynamicTabStops:changedMainOutputAreaSize];
	
	[XTNotifications notifySetFocusToMainOutputView:self];
}

@end
