//
//  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 "XTHtmlWhitespace.h"
#import "XTHtmlTagT2Hilite.h"
#import "XTHtmlTagT2TradStatusLine.h"
#import "XTLogger.h"
#import "XTStringUtils.h"
#import "XTBannerTextHandler.h"
#import "XTHtmlQuotedSpace.h"
#import "XTHtmlSpecialSpace.h"
#import "XTHtmlNonbreakingSpace.h"
#import "XTAllocDeallocCounter.h"
#import "XTViewLayoutUtils.h"


@interface XTMainTextHandler ()

@end


@implementation XTMainTextHandler

const NSUInteger initialCommandPromptPosition = NSUIntegerMax;
static const NSString *zeroWidthSpace = @"\u200B"; // non-printing

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:@""];
		
		//[self resetFlags];
		
		[self setupReceptionOfAppLevelNotifications];
    }
    return self;
}

- (void)setStatusLineMode:(NSUInteger)statusLineMode
{
	//XT_DEF_SELNAME;
	
	XTHtmlTagT2TradStatusLine *tag = [XTHtmlTagT2TradStatusLine new];
	NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
	NSString *mode = [NSString stringWithFormat:@"%lu", statusLineMode];
	[attributes setObject:mode forKey:@"mode"];
	[tag setAttributes:attributes];
	[self.formattingQueue addObject:tag];
}

- (void)setStatusLineModeNow:(NSUInteger)statusLineMode
{
	_statusLineMode = statusLineMode;
}

- (NSUInteger)statusLineMode {
	return _statusLineMode;
}

//TODO refactor wrt base & banner classes
- (void)removeHandler
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"");

	[self.outputFormatter teardown];
	self.outputFormatter = nil;
	
	[self.textView teardown];
	self.textView = nil;
	
	[self teardownReceptionOfAppLevelNotifications];

	self.scrollView = nil;
	self.gameWindowController = nil;
	self.gameTitle = nil;
	self.commandHistory = nil;
	self.activeTagBannerHandle = nil;
	
	[self.outputTextParserPlain teardown];
	[self.outputTextParserHtml teardown];
	
	[self.formattingQueue removeAllObjects];
	self.formattingQueue = nil;
	
	self.attributedStringThatBrokePaginationLimit = 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;
}

//TODO refactor wrt base & banner classes
- (void)resetToDefaults
{
	// called when game file loads and starts
	
	self.htmlMode = NO;
	self.hiliteMode = NO;
	[self setNonstopMode:NO];
	self.statusLineMode = STATUS_LINE_MODE_MAIN;
	
	[self clearText];
	[self.outputTextParserPlain resetForNextCommand];
	[self.outputTextParserHtml resetForNextCommand];
	[self.formattingQueue removeAllObjects];
	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
{
	[super resetForNextCommand];

	[self ensureInputFontIsInEffect];
}

- (void)resetForGameHasEndedMsg
{
	[[self getOutputTextParser] flush];
	[self.outputTextParserHtml resetForNextCommand];
	[self.outputTextParserPlain resetForNextCommand];
	[self.formattingQueue removeAllObjects];
	[self.outputFormatter resetFlags];
	[self mainThread_noteStartOfPagination];
	[self clearPaginationState];
}

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

- (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
{
	NSRange range = [self getCommandTextRange];
	NSString *command = [self.textView stringInRange:range];
	
	[self.commandHistory appendCommand:command];
	
	returnValue[0] = command;
}

- (void)clearText
{
	// also called when game clears screen
	
	// might be a <title> or something there that needs processing
	[self processFormattingQueue];
		//TODO exp - look out for unforeseen effects
	
	XT_DEF_SELNAME;
	
	[[self getOutputTextParser] flush];
	[self.outputTextParserPlain resetForNextCommand];
	[self.outputTextParserHtml resetForNextCommand];

	[self.outputFormatter resetFlags];
	
	[[[self.textView textStorage] mutableString] setString:@""];

	// Insert some temporary, invisible text to get font height set and paging calculations correct from the start:
	NSArray *formattedOutputElements = [self.outputFormatter formatElement:zeroWidthSpace];
	XTFormattedOutputElement *elt = [formattedOutputElements objectAtIndex:0];
	NSAttributedString *attrStr = elt.attributedString;
	[self appendAttributedStringToTextStorage:attrStr];
	[self.outputFormatter resetFlags]; // get rid of state due to the zws 
	
	[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];
	
	[self.formattingQueue removeAllObjects];
	
	XT_TRACE_0(@"done");
}

- (void)flushOutput
{
	XT_TRACE_ENTRY;
	
	NSArray *parseResultArray = [[self getOutputTextParser] flush];
	[self.formattingQueue addObjectsFromArray:parseResultArray];
	[self processFormattingQueue];
	[self flushFormattingQueue];
}

- (void)hardFlushOutput
{
	XT_TRACE_ENTRY;
	
	NSArray *parseResultArray = [[self getOutputTextParser] hardFlush];
	[self.formattingQueue addObjectsFromArray:parseResultArray];
	[self processFormattingQueue];
	[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];
}

//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:zeroWidthSpace];
		[self noteEndOfOutput];
	//}
}

- (BOOL)appendOutput:(NSString *)string
{
	XT_DEF_SELNAME;
	//XT_TRACE_0(@"-------------------------------------------------------------");
	XT_TRACE_1(@"\"%@\"", string);

	NSArray *parseResultArray = [[self getOutputTextParser] parse:string];
	[self.formattingQueue addObjectsFromArray:parseResultArray];

	BOOL excessiveAmountBuffered = (self.formattingQueue.count >= 1000);
	return excessiveAmountBuffered;
}

// 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)];
}

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

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

- (BOOL)cursorIsInCommandButNotAtMinInsertionPosition
{
	BOOL res = ([self insertionPoint] > [self minInsertionPoint]);
	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
{
	NSRange commandTextRange = [self getCommandTextRange];
	[self.textView removeText:commandTextRange];
	NSAttributedString *attrString = [self.outputFormatter formatInputText:newCommandText];
	[self appendAttributedStringToTextStorage:attrString];
}

- (void)setColorsFromPrefs
{
	//TODO when text/bg col set by game
	//TODO when/not xtads allows game to set text/bg col
	
	self.textView.backgroundColor = self.prefs.outputAreaBackgroundColor;
		//TODO? cursor color
	
	[self.textView setNeedsDisplay:YES];
	
	//TODO why is this needed?:
	[self scrollToBottom];
}

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

- (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
{
	// find new starting pos of cmd prompt
	NSUInteger tempPos = [self.textView endOfOutputPosition];
	self.commandPromptPosition = (tempPos > 0 ? tempPos - 1 : 0);
}

- (BOOL)processFormattingQueue
{
	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];
		[bannerHandler processFormattingQueueFromMainOutput:self.formattingQueue];
			// this will consume queue until </banner> or queue empty
	}
	
	if (self.statusLineMode == STATUS_LINE_MODE_STATUS && ! self.htmlMode) {
		XTBannerTextHandler *bannerHandler = [self.gameWindowController getBannerHandlerForTradStatusLine];
		[bannerHandler processFormattingQueueFromMainOutput:self.formattingQueue];
			// this will consume queue until <tads2tradstatusline mode=0> or queue empty
	}
	
	[self restoreStringThatBrokePaginationLimit];
	
	BOOL reachedPaginationLimit = NO;
	
	//XT_TRACE_1(@"entry formattingQueue.count=%lu", self.formattingQueue.count);
	
	if (self.formattingQueue.count == 0) {
		return reachedPaginationLimit;
	}
	
	while ([self.formattingQueue count] >= 1 && ! reachedPaginationLimit) {
		
		id parsedElement = [self.formattingQueue firstObject];
		[self.formattingQueue removeObjectAtIndex:0];
		
		NSArray *formattedOutputElements = [self.outputFormatter formatElement:parsedElement];
		
		NSAttributedString *lastAttrStringAppended = nil;
		
		for (XTFormattedOutputElement *outputElement in formattedOutputElements) {
			
			if ([outputElement isRegularOutputElement]) {
				
				lastAttrStringAppended = outputElement.attributedString;
				[self appendAttributedStringToTextStorage:lastAttrStringAppended];
				//TODO call ensure...
				
			} else if ([outputElement isTabElement]) {
				XTHtmlTagTab *tagTab = (XTHtmlTagTab *)outputElement.htmlTag;
				NSArray *tabFormattedOutputElements = [self.outputFormatter handleHtmlTagTabDelayed:tagTab];
				if (tabFormattedOutputElements.count >= 1) {
					XT_ERROR_1(@"tabFormattedOutputElements.count = %lu", tabFormattedOutputElements.count);
				}
			
			} else if ([outputElement isGameTitleElement]) {
				if ([outputElement.attributedString.string isEqualToString:@"{{clear}}"]) {
					//TODO make element type for this case
					self.gameTitle = [NSMutableString stringWithString:@""];
				} else {
					[self.gameTitle appendString:outputElement.attributedString.string];
				}
				
			} else if ([outputElement isBannerStartElement]) {
				[self handleBannerTagStart:outputElement];
			
			} else if ([outputElement isStatusLineModeStart]) {
				[self handleStatusLineModeStart];
				
			} else if ([outputElement isStatusLineModeEnd]) {
				if (! self.htmlMode) {
					//XT_ERROR_0(@"isStatusLineModeEnd && ! self.htmlMode");
					// nah, can happen at start of games
				}
				
			} else if ([outputElement isStatusLineModeSuppress]) {
				_statusLineMode = STATUS_LINE_MODE_SUPPRESS; // don't do .statusLineMode, it'll loop things
				
			} else {
				XT_ERROR_1(@"unknown XTFormattedOutputElement %d", outputElement.elementType);
			}
		}
		
		if (lastAttrStringAppended == nil) {
			// no text was added
			continue;
		}

		[self.textView ensureLayoutForTextContainer]; // or else frame rect isn't updated
		
		reachedPaginationLimit = [self checkIfHasReachedPaginationLimit:lastAttrStringAppended];
	}
	
	[self trimScrollbackBuffer];

	[self scrollToBottom];
	
	[self noteEndOfOutput];

	//XT_TRACE_1(@"exit formattingQueue.count=%lu", self.formattingQueue.count);
	
	return reachedPaginationLimit;
}

//TODO move
- (void)exitingStatusLineMode
{
	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)flushFormattingQueue
{
	NSArray *wsArray = [self.outputFormatter flushPendingWhitespace];
	for (XTFormattedOutputElement *wsFmtElt in wsArray) {
		[self appendAttributedStringToTextStorage:wsFmtElt.attributedString];
	}
}

- (void)handleBannerTagStart:(XTFormattedOutputElement *)outputElement
{
	XT_DEF_SELNAME;

	BOOL removeAllBanners = [outputElement.htmlTag hasAttribute:@"removeall"];
	if (removeAllBanners) {
		[self.gameWindowController bannerDeleteAll];
		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);
		}
		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];
		
		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;
		
		[self.gameWindowController bannerSetHtmlMode:bannerHandle on:YES];
		
	} 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);
	
	[bannerHandler noteStartedFromHtmlTag];
	[bannerHandler processFormattingQueueFromMainOutput:self.formattingQueue];
		// this will consume queue until </banner> or queue empty
}

- (void)handleStatusLineModeStart
{
	XT_DEF_SELNAME;

	if (self.htmlMode) {
		// let <banner> handling take care of it
		return;
	}

 	[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 mainThread_clear];
			[bhTSL mainThread_flush];
		} else {
			XT_ERROR_0(@"bhTSL == nil");
		}
	}

	[bhTSL processFormattingQueueFromMainOutput:self.formattingQueue];
		// 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.boolValue) {
		return;
	}
	
	NSUInteger scrollbackBufferSize = 1000 * prefs.scrollbackBufferSizeInKBs.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
