
//
//  XTBannerHandler.m
//  XTads
//
//  Created by Rune Berg on 14/12/15.
//  Copyright © 2015 Rune Berg. All rights reserved.
//

#import "XTBaseTextHandler_private.h"
#import "XTBannerTextHandler.h"
#import "XTMainTextHandler.h"
#import "XTBannerContainerView.h"
#import "XTBannerBorderView.h"
#import "XTBannerTopDividerView.h"
#import "XTBannerTextView.h"
#import "XTBannerGridTextModel.h"
#import "XTScrollView.h"
#import "XTOutputFormatter.h"
#import "XTFormattedOutputElement.h"
#import "XTOutputTextParserPlain.h"
#import "XTOutputTextParserHtml.h"
#import "XTHtmlTagBanner.h"
#import "XTHtmlTagBannerClear.h"
#import "XTStringUtils.h"
#import "XTNotifications.h"
#import "XTPrefs.h"
#import "XTFontUtils.h"
#import "XTHtmlTagT2Italics.h"
#import "XTHtmlTagTab.h"
#import "XTHtmlTagWhitespace.h"
#import "XTHtmlTagText.h"
#import "XTHtmlTagT2TradStatusLine.h"
#import "XTLogger.h"
#import "XTAllocDeallocCounter.h"
#import "XTTimer.h"
#import "XTCallOnMainThreadCounter.h"
#import "XTViewLayoutUtils.h"
#import "XTFormattingSpecificationForHtmlTag.h"


@interface XTBannerTextHandler ()

@property id<XTOutputTextParserProtocol> parserFromMainTextHandler;

@property (readonly) NSUInteger usableWidthInPoints;
@property (readonly) NSUInteger usableHeightInPoints;
@property (readonly) NSUInteger usableHeightInRows;
@property (readonly) NSUInteger usableWidthInColumns;

@property NSAttributedString *pendingNewline;
	//TODO reset on goto?
@property NSString *tradStatusLineScoreString;
@property NSUInteger lengthOfLastTradStatusLineScoreString;
@property XTBannerGridTextModel *gridTextModel;

@end


@implementation XTBannerTextHandler

static XTLogger* logger;

#undef XT_DEF_SELNAME
#define XT_DEF_SELNAME NSString *selName = [NSString stringWithFormat:@"%@:%@", self.debugName, NSStringFromSelector(_cmd)];

@synthesize type = _type;

static NSUInteger nextBannerIndex;

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

+ (void)resetStaticState
{
	nextBannerIndex = 1; // assuming 0 will always and only be for the "root" banner, i.e. main output area
}

OVERRIDE_ALLOC_FOR_COUNTER
OVERRIDE_DEALLOC_FOR_COUNTER

- (id)init
{
	//XT_DEF_SELNAME;

	self = [super init];
	if (self != nil) {
		_mainTextHandler = nil;
		_parserFromMainTextHandler = nil;
		self.bannerIndex = nextBannerIndex;
		nextBannerIndex += 1;
		self.debugName = [NSString stringWithFormat:@"b-%lu", self.bannerIndex];
		_where = 0;
		_type = 0;
		_alignment = 0;
		_size = 0;
		_sizeUnits = 0;
		_style = 0;
		_tagId = nil;
		_isBeingCreated = NO;
		_isSizedToContent = NO;
		_sizeOfContents = 0.0;
		_wasInitiallySizedToContents = NO;
		_initialSizeOfContents = 0.0;
		self.outputFormatter.isForBanner = YES;
		_isForTradStatusLine = NO;
		_tagBannerNeedsSizeToContent = NO;
		_pendingNewline = nil;
		_tradStatusLineScoreString = nil;
		_lengthOfLastTradStatusLineScoreString = 0;
		_gridTextModel = [XTBannerGridTextModel withTextFormatter:self.outputFormatter];

		[self setupReceptionOfAppLevelNotifications];
	}
	return self;
}

- (void)setType:(NSUInteger)type
{
	_type = type;
	self.outputFormatter.isForGridBanner = [self isGridMode];
}

- (NSUInteger)type
{
	return _type;
}

+ (instancetype)handlerWithParent:(XTBannerTextHandler*)parent
							where:(NSInteger)where
							other:(XTBannerTextHandler *)other
						  wintype:(NSInteger)wintype
							align:(NSInteger)align
							 size:(NSInteger)size
						sizeUnits:(NSInteger)sizeUnits
							style:(NSUInteger)style
{
	XTBannerTextHandler *banner = [XTBannerTextHandler new];
	
	banner.gameWindowController = parent.gameWindowController;
	banner.parentHandler = parent;
	banner.where = where;
	banner.siblingForWhere = other;
	banner.type = wintype;
	banner.alignment = align;
	banner.size = size;
	banner.sizeUnits = sizeUnits;
	banner.style = style;
	if (banner.type == OS_BANNER_TYPE_TEXT) {
		if (style & OS_BANNER_STYLE_MOREMODE) {
			banner.style |= OS_BANNER_STYLE_AUTO_VSCROLL;
		}
	}

	if (banner.parentHandler != nil) {
		[banner.parentHandler addChildHandler:banner];
	}
	
	return banner;
}

- (BOOL)startedFromHtmlTag
{
	BOOL res = self.outputFormatter.isForTagBanner;
	return res;
}

- (void)traceWithIndentLevel:(NSUInteger)indentLevel
{
	if (! XT_TRACE_ON) {
		return;
	}
	XT_DEF_SELNAME;
	
	NSString *indent = [XTStringUtils stringOf:indentLevel string:@"   "];
	
	NSString *whereStr;
	switch (self.where) {
		case OS_BANNER_FIRST:
			whereStr = @"first";
			break;
		case OS_BANNER_LAST:
			whereStr = @"last";
			break;
		case OS_BANNER_BEFORE:
			whereStr = @"before";
			break;
		case OS_BANNER_AFTER:
			whereStr = @"after";
			break;
		default:
			whereStr = [NSString stringWithFormat:@"??? %lu", self.where];
			break;
	}
	
	NSUInteger siblingForWhereIndex = 999;
	if (self.siblingForWhere != nil) {
		siblingForWhereIndex = self.siblingForWhere.bannerIndex;
	}
	
	NSString *alignStr;
	switch (self.alignment) {
		case OS_BANNER_ALIGN_TOP:
			alignStr = @"top";
			break;
		case OS_BANNER_ALIGN_BOTTOM:
			alignStr = @"bottom";
			break;
		case OS_BANNER_ALIGN_LEFT:
			alignStr = @"left";
			break;
		case OS_BANNER_ALIGN_RIGHT:
			alignStr = @"right";
			break;
		default:
			alignStr = [NSString stringWithFormat:@"??? %lu", self.alignment];
			return;
	}
	
	NSString *sizeUnitsStr;
	switch (self.sizeUnits) {
		case OS_BANNER_SIZE_ABS:
			sizeUnitsStr = @"nu";
			break;
		case OS_BANNER_SIZE_PCT:
			sizeUnitsStr = @"%";
			break;
		case OS_BANNER_SIZE_PIXELS:
			sizeUnitsStr = @"px";
			break;
		default:
			sizeUnitsStr = [NSString stringWithFormat:@"??? %lu", self.sizeUnits];
			break;
	}
	
	NSString *typeStr;
	switch (self.type) {
		case OS_BANNER_TYPE_TEXT:
			typeStr = @"text";
			break;
		case OS_BANNER_TYPE_TEXTGRID:
			typeStr = @"textgrid";
			break;
		default:
			typeStr = [NSString stringWithFormat:@"??? %lu", self.type];
			break;
	}
	
	NSString *bs = [NSString stringWithFormat:@"%@parent=%lu where=%@(%lu) other=%lu align=%@(%lu) size=%lu%@ type=%@(%lu)",
					indent, self.parentHandler.bannerIndex, whereStr, self.where, siblingForWhereIndex, alignStr, self.alignment, self.size, sizeUnitsStr, typeStr, self.type];
	
	XT_TRACE_1(@"%@", bs);
	
	for (XTBannerTextHandler *child in self.childHandlers) {
		[child traceWithIndentLevel:indentLevel + 1];
	}
}

- (NSUInteger)usableWidthInPoints
{
	NSUInteger res = 0;

	CGFloat width = self.scrollView.frame.size.width;
	if (width > 0.0) {
		CGFloat totalInset = [self.textView totalHorizontalInset:YES];
		width -= totalInset;
		if (width > 0.0) {
			res = ceil(width);
		}
	}
	
	return res;
}

- (NSUInteger)usableHeightInPoints
{
	NSUInteger res = 0;
	
	CGFloat height = self.scrollView.frame.size.height;
	if (height > 0.0) {
		CGFloat totalInset = [self.textView totalVerticalInset:YES];
		height -= totalInset;
		if (height > 0.0) {
			res = ceil(height);
		}
	}

	return res;
}

- (NSUInteger)usableWidthInColumns
{
	NSFont *font = [self defaultBannerFont];
	CGFloat width = [self usableWidthInPoints];
	CGFloat fontWidth = [XTFontUtils fontWidth:font];
	width /= fontWidth;
	width = floor(width + 0.07);
	NSUInteger widthInt = width;
	
	return widthInt;
}

- (NSUInteger)usableHeightInRows
{
	NSFont *font = [self defaultBannerFont];
	CGFloat height = [self usableHeightInPoints];
	CGFloat fontSize = [XTFontUtils fontHeight:font];
	height /= fontSize;
	height = floor(height);
	NSUInteger heightInt = height;
	
	return heightInt;
}

- (void)setItalicsMode:(BOOL)italicsMode
{
	// represent italics mode on/off by a special tag object
	XTHtmlTagT2Italics *t2ItalicsTag = [XTHtmlTagT2Italics new];
	t2ItalicsTag.closing = (! italicsMode);
	[self appendToParseTree:t2ItalicsTag];
}

- (void)setHiliteModeDirectly:(BOOL)hiliteMode
{
	self.outputFormatter.formattingSpecForHtmlTag.formattingSpec.t2Hilite = hiliteMode;
}

- (void)setItalicsModeDirectly:(BOOL)italicsMode
{
	self.outputFormatter.formattingSpecForHtmlTag.formattingSpec.t2Italics = italicsMode;
}

//TODO !!! mv
- (id<XTOutputTextParserProtocol>)getOutputTextParser
{
	id<XTOutputTextParserProtocol> res;
	if (self.parserFromMainTextHandler != nil) {
		res = self.parserFromMainTextHandler;
	} else {
		res = [super getOutputTextParser];
	}
	return res;
}

- (void)appendToParseTree:(XTHtmlTag *)tag
{
	[[self getOutputTextParser] appendTagToCurrentContainer:tag];
}
	
- (void)setIsForT3:(BOOL)isForT3
{
	self.outputFormatter.isForT3 = isForT3;
	self.colorationHelper.isForT3 = isForT3;
}

- (void)setIsForTagBanner:(BOOL)isForTagBanner
{
	self.outputFormatter.isForTagBanner = isForTagBanner;
}

- (void)createTextViewForBanner
{
	XT_TRACE_ENTRY;

	NSScrollView *newTextScrollView = [self createNewScrollViewWithTextViewForBanner];
	self.scrollView = newTextScrollView;
	self.textView = self.scrollView.documentView;
	self.outputFormatter.textView = self.textView;
	self.textView.outputFormatter = self.outputFormatter;
	self.colorationHelper = [XTColorationHelper forTextView:self.textView
														isForT3:self.outputFormatter.isForT3
													isForBanner:YES
												isForGridBanner:self.outputFormatter.isForGridBanner];
	self.colorationHelper.htmlMode = self.htmlMode;
	self.outputFormatter.colorationHelper = self.colorationHelper;

	[self setColorsFromPrefsColor];
}

- (void)captureInitialSizeWhenViewSize:(CGFloat)viewSize
{
	if (! self.isBeingCreated) {
		if (self.initialSize == nil) {
			self.initialSize = [NSNumber numberWithUnsignedInteger:self.size];
			self.initialSizeUnits = self.sizeUnits;
		}
	}
}

- (CGFloat)borderSize
{
	return 1.0; //TODO? make user pref
}

- (NSUInteger)borderAlignment
{
	XT_DEF_SELNAME;
	
	NSUInteger res;

	switch (self.alignment) {
		case OS_BANNER_ALIGN_TOP:
			res = OS_BANNER_ALIGN_BOTTOM;
			break;
		case OS_BANNER_ALIGN_BOTTOM:
			res = OS_BANNER_ALIGN_TOP;
			break;
		case OS_BANNER_ALIGN_LEFT:
			res = OS_BANNER_ALIGN_RIGHT;
			break;
		case OS_BANNER_ALIGN_RIGHT:
			res = OS_BANNER_ALIGN_LEFT;
			break;
		default:
			XT_ERROR_1(@"unknown alignment %lu", self.alignment);
			res = OS_BANNER_ALIGN_LEFT;
			break;
	}
	
	return res;
}

- (NSView *)internalRebuildViewHierarchy
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");

	NSView *ownTopLevelView = [super internalRebuildViewHierarchy];
	
	if ((self.style & OS_BANNER_STYLE_BORDER) && (self.size > 0.0)) {
		NSRect tempFrame = NSMakeRect(0.0, 0.0, 0.0, 0.0);
		XTBannerContainerView *tempOwnTopLevelView = [[XTBannerContainerView alloc] initWithFrame:tempFrame];
		[self.layoutViews addObject:tempOwnTopLevelView];
		XTBannerBorderView *borderView = [[XTBannerBorderView alloc] initWithFrame:tempFrame];
		[self.layoutViews addObject:borderView];
		borderView.backgroundColor = [NSColor scrollBarColor]; //TODO? make colour user pref?
		[tempOwnTopLevelView addSubview:ownTopLevelView];
		[tempOwnTopLevelView addSubview:borderView];
		
		NSUInteger borderAlignment = [self borderAlignment];
		
		[XTViewLayoutUtils newLayoutInParentView:tempOwnTopLevelView
									  childView1:ownTopLevelView
									  childView2:borderView
							 childView2Alignment:borderAlignment
								  childView2Size:[self borderSize]
							childView2IsAbsSized:YES];
		
		ownTopLevelView = tempOwnTopLevelView;
	}

	return ownTopLevelView;
}

- (CGFloat)calcViewSizeForConstraint
{
	CGFloat viewSize;
	
	if (self.isSizedToContent) {
		
		CGFloat inset = 0.0;
		if (self.sizeOfContents > 0.0) {
			inset = [self totalInsetForViewSize:YES];
		}
		
		viewSize = self.sizeOfContents + inset;
		viewSize = ceil(viewSize);
		
	} else if (self.sizeUnits == OS_BANNER_SIZE_ABS) {
		
		CGFloat naturalUnitSize = [self naturalFontUnitSize];
		CGFloat naturalSize = (CGFloat)self.size; // in num. of rows/columns of '0' characters in the default font for the window
		CGFloat inset = 0.0;
		if (naturalSize >= 1) {
			inset = [self totalInsetForViewSize:YES];
		}
		viewSize = naturalSize * naturalUnitSize + inset;
		viewSize = ceil(viewSize);
		
	} else if (self.sizeUnits == OS_BANNER_SIZE_PIXELS) {
		// for T2 <banner>
		
		viewSize = (CGFloat)self.size;
		CGFloat inset = 0.0;
		if (viewSize >= 1.0) {
			inset = [self totalInsetForViewSize:NO];
		}
		viewSize += inset;
		viewSize = ceil(viewSize);
		
	} else {
		// Some percentage of parent's size
		viewSize = (CGFloat)self.size;
	}
	
	return viewSize;
}

- (CGFloat)totalInsetForViewSize:(BOOL)respectInsetsIfNoText
{
	// https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/TextUILayer/Tasks/SetTextMargins.html#//apple_ref/doc/uid/20001802-CJBJHGAG
	
	NSLayoutAttribute orientationAttr = [self orientationAttribute];
	
	CGFloat inset;
	if (orientationAttr == NSLayoutAttributeHeight) {
		inset = [self.textView totalVerticalInset:respectInsetsIfNoText];
	} else {
		inset = [self.textView totalHorizontalInset:respectInsetsIfNoText];
	}
	
	if (self.style & OS_BANNER_STYLE_BORDER) {
		inset += [self borderSize];
	}
	
	return inset;
}

- (NSLayoutAttribute)orientationAttribute
{
	XT_DEF_SELNAME;
	
	NSLayoutAttribute orientationAttr;
	
	switch (self.alignment) {
		case OS_BANNER_ALIGN_TOP:
		case OS_BANNER_ALIGN_BOTTOM:
			orientationAttr = NSLayoutAttributeHeight;
			break;
		case OS_BANNER_ALIGN_LEFT:
		case OS_BANNER_ALIGN_RIGHT:
			orientationAttr = NSLayoutAttributeWidth;
			break;
		default:
			XT_ERROR_1(@"unknown alignment %lu", self.alignment);
			orientationAttr = NSLayoutAttributeHeight;
			break;
	}
	
	return orientationAttr;
}

- (BOOL)isHorizontalBanner
{
	BOOL res = ([self orientationAttribute] == NSLayoutAttributeHeight);
	return res;
}

- (NSFont *)bannerFont
{
	NSFont *bannerFont;
	if (self.type == OS_BANNER_TYPE_TEXTGRID) {
		bannerFont = [self.outputFormatter getCurrentFontForGridBanner];
	} else {
		bannerFont = [self.outputFormatter getCurrentFontForOutput];
	}
	return bannerFont;
}

- (NSFont *)defaultBannerFont
{
	return [self bannerFont];
}

- (CGFloat)naturalFontUnitSize
{
	XT_DEF_SELNAME;
	
	CGFloat res;

	NSFont *bannerFont = [self bannerFont];
		//TODO *default* banner font(?)
	
	switch (self.alignment) {
		case OS_BANNER_ALIGN_TOP:
		case OS_BANNER_ALIGN_BOTTOM:
			res = [XTFontUtils fontHeight:bannerFont];
			break;
		case OS_BANNER_ALIGN_LEFT:
		case OS_BANNER_ALIGN_RIGHT:
			res = [XTFontUtils fontWidth:bannerFont];
			break;
		default:
			XT_ERROR_1(@"unknown alignment %lu", self.alignment);
			res = [XTFontUtils defaultTextLineHeight:bannerFont];
			break;
	}
	
	return res;
}

- (void)display:(NSString *)string
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"\"%@\"", string);

	[[self getOutputTextParser] parse:(NSString *)string];
	
	// Wait for flush to actually display
}

- (void)displayTradStatusLineScoreString:(NSString *)string
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"\"%@\"", string);

	self.tradStatusLineScoreString = string;
	
	// Wait for flush to actually display
}

- (void)flushTradStatusLineScoreString
{
	//XT_DEF_SELNAME;
	
	// This is a costly method, so skip when replaying command script
	if ([self.gameWindowController isReplayingCommandScript]) {
		return;
	}

	if (self.isForTradStatusLine) {
		
		[self processBatched];

		//TODO !!! adapt: make sep. method:
		if (self.lengthOfLastTradStatusLineScoreString >= 1) {
			NSMutableAttributedString *textStorage = self.textView.textStorage;
			NSUInteger textStorageLength = textStorage.length;
			NSUInteger loc = textStorageLength - self.lengthOfLastTradStatusLineScoreString;
			NSRange rangeToRemove = NSMakeRange(loc, self.lengthOfLastTradStatusLineScoreString);
			[textStorage deleteCharactersInRange:rangeToRemove];
		}

		//TODO !!! adapt: make sep. method:
		if (self.tradStatusLineScoreString != nil) {
			
			//XTTimer *timer = [XTTimer fromNow];
			
			// In case we're replaying a T2 command script,
			// use a temporary formatting queue to avoid recursing into starting banner
			
			XTFormattedElementQueue *savedFormattingQueue = self.formattedElementQueue;
			self.formattedElementQueue = [XTFormattedElementQueue new];
			
			BOOL emulateHtmlBanner = self.prefs.emulateHtmlBannerForTradStatusLine.value.boolValue;
			if (emulateHtmlBanner) {
				[self setHiliteModeDirectly:NO];
				[self setItalicsModeDirectly:YES];
			}

			XTHtmlTagTab *tabTag = [XTHtmlTagTab rightAligned];
			NSArray *array1 = [self.outputFormatter handleHtmlTagTabWhenNotOppressed:tabTag];
			[self.formattedElementQueue addObjectsFromArray:array1];
			
			NSArray<NSAttributedString *>* scoreAttrStringArray = [self.outputFormatter formatOutputText:self.tradStatusLineScoreString];
			NSAttributedString *scoreAttrString = [XTStringUtils concatenateAttributedStringArray:scoreAttrStringArray];
			NSMutableAttributedString *scoreMutAttrString = [[NSMutableAttributedString alloc] initWithAttributedString:scoreAttrString];
			XTFormattedOutputElement *scoreStringElement = [XTFormattedOutputElement regularOutputElement:scoreMutAttrString];
			NSArray *array2 = [NSArray arrayWithObject:scoreStringElement];
			[self.formattedElementQueue addObjectsFromArray:array2];
			
			NSUInteger oldLengthOfTextStorage = self.textView.textStorage.length;
			
			[self processFormattedElementQueue];
			[self processBatched];
			
			self.formattedElementQueue = savedFormattingQueue;
			
			if (emulateHtmlBanner) {
				[self setItalicsModeDirectly:NO];
			}

			NSUInteger newLengthOfTextStorage = self.textView.textStorage.length;
			self.lengthOfLastTradStatusLineScoreString = newLengthOfTextStorage - oldLengthOfTextStorage;

			//double timeUsed = [timer timeElapsed];
			//XT_WARN_1(@"timeUsed=%lf", timeUsed);
		}
	}
}

- (void)mainThread_pumpOutputText:(NSMutableArray *)params
{
	//XT_WARN_ENTRY;
	
	NSNumber *param0 = params[0];
	BOOL flushIfBanner = [param0 boolValue];
	
	if (flushIfBanner) {
		[[self getOutputTextParser] flush];
	}
	
	self.needMorePrompt = [self processTagTree];

	if (flushIfBanner) {
		if (! self.needMorePrompt) {
			[self flushFormattingQueue];
				//TODO? adapt: could in principle get us over the pagin. limit
		}
		
		if (! [self isGridMode]) {
			if ([self shouldAutoScrollToBottom]) {
				[self scrollToBottom];
			}
			// (horiz. scroll not relevant unless grid mode)
		} else {
			if (self.style & OS_BANNER_STYLE_AUTO_HSCROLL) {
				[self scrollToGridInsertionPosition];
			} else if ([self shouldAutoScrollToBottom]) {
				[self scrollToGridInsertionLine];
			}
		}
		
		[XTNotifications notifySetFocusToMainOutputView:self];
	}

	NSNumber *retVal = [NSNumber numberWithBool:self.needMorePrompt];
	[params setObject:retVal atIndexedSubscript:1];
	
	//XT_WARN_1(@"lengthOfTempFormattedElementQueueForTable=%lu", [self lengthOfTempFormattedElementQueueForTable]);
}

- (void)mainThread_pumpOutputTextForceFlush:(NSMutableArray *)params
{
	//XT_WARN_ENTRY;
	
	[[self getOutputTextParser] flush];
	
	self.needMorePrompt = [self processTagTree];

	[self handleTableEnd]; //TODO !!! very exp
	if (! self.needMorePrompt) {
		[self flushFormattingQueue];
			//TODO? adapt: could in principle get us over the pagin. limit
	}
	
	if (! [self isGridMode]) {
		if ([self shouldAutoScrollToBottom]) {
			[self scrollToBottom];
		}
		// (horiz. scroll not relevant unless grid mode)
	} else {
		if (self.style & OS_BANNER_STYLE_AUTO_HSCROLL) {
			[self scrollToGridInsertionPosition];
		} else if ([self shouldAutoScrollToBottom]) {
			[self scrollToGridInsertionLine];
		}
	}
	
	[XTNotifications notifySetFocusToMainOutputView:self];

	NSNumber *retVal = [NSNumber numberWithBool:self.needMorePrompt];
	[params setObject:retVal atIndexedSubscript:0];
}

- (void)scrollToGridInsertionPosition
{
	NSString *bannerText = [[self.textView textStorage] string];
	NSUInteger row = self.gridTextModel.rowIndex;
	NSUInteger column = self.gridTextModel.columnIndex;
	
	NSUInteger insPtIndex = [XTStringUtils indexInString:bannerText ofCharAtRow:row column:column];
	
	[self.textView scrollRangeToVisible:NSMakeRange(insPtIndex, 0)];
}

- (void)scrollToGridInsertionLine
{
	NSString *bannerText = [[self.textView textStorage] string];
	NSUInteger row = self.gridTextModel.rowIndex;
	NSUInteger column = 0;
	
	NSUInteger insPtIndex = [XTStringUtils indexInString:bannerText ofCharAtRow:row column:column];
	
	[self.textView scrollRangeToVisible:NSMakeRange(insPtIndex, 0)];
}

- (BOOL)shouldAutoScrollToBottom
{
	BOOL res = ((self.style & OS_BANNER_STYLE_AUTO_VSCROLL) == OS_BANNER_STYLE_AUTO_VSCROLL);
	return res;
}

- (void)autoScrollToBottom
{
	if (! [self isGridMode]) {
		[super autoScrollToBottom];
	}
}

- (void)mainThread_clear
{
	[self resetState];

	// Handle the actual UI update on next flush:
	XTHtmlTagBannerClear *tag = [XTHtmlTagBannerClear new];
	[self appendToParseTree:tag];
}

- (void)synchClear
{
	[self resetState];
	[self executeClear];
}

- (void)resetState
{
	[[self getOutputTextParser] flush];
		//TODO hardFlush?
	[self.outputTextParserPlain resetForNextCommand];
	[self.outputTextParserHtml resetForNextCommand];
	[self.outputFormatter resetFlags];
	[self resetForTradStatusLine];
	self.pendingNewline = nil;
	[self clearPaginationState];
	[self mainThread_noteStartOfPagination];
}

- (void)executeClear
{
	//XT_DEF_SELNAME;
	
	[self clearTextStorage];
	
	[self.gridTextModel clear];

	[self.outputFormatter resetFlags];
	[self.colorationHelper resetBodyColors];
	
	self.pendingNewline = nil;
	[self clearPaginationState];
	[self mainThread_noteStartOfPagination];
	
	[XTNotifications notifySetFocusToMainOutputView:self];
}

- (void)setSize:(NSUInteger)size sizeUnits:(NSUInteger)sizeUnits isAdvisory:(BOOL)isAdvisory
{
	//XT_DEF_SELNAME;
	
	self.size = size;
	self.sizeUnits = sizeUnits;

	self.isSizedToContent = NO;
	self.sizeOfContents = 0.0;
	
	//XT_TRACE_2(@"size=%lu sizeUnits=%lu", self.size, self.sizeUnits);
}

- (BOOL)isTagBannerThatShouldResizeToContents
{
	BOOL res = [self startedFromHtmlTag] && self.tagBannerNeedsSizeToContent;
	return res;
}

- (void)mainThread_sizeToContents
{
	XT_TRACE_ENTRY;
	
	self.isSizedToContent = NO;
	self.sizeOfContents = 0.0;
	
	NSUInteger newSize;
	CGFloat newSizeOfContents = 0.0;
	
	switch (self.alignment) {
		case OS_BANNER_ALIGN_TOP:
		case OS_BANNER_ALIGN_BOTTOM:
			newSize = [self heightOfContents:&newSizeOfContents];
			//XT_WARN_2(@"top/bottom newSize=%lu newSizeOfContents=%lf", newSize, newSizeOfContents);
			break;
		case OS_BANNER_ALIGN_LEFT:
		case OS_BANNER_ALIGN_RIGHT:
			newSize = [self widthOfContents:&newSizeOfContents];
			//XT_WARN_2(@"left/right newSize=%lu newSizeOfContents=%lf", newSize, newSizeOfContents);
			break;
		default:
			XT_ERROR_1(@"unknown alignment %lu", self.alignment);
			return;
	}

	self.isSizedToContent = YES;
	
	//TODO handle other orientation too - await email from mjr 2016-06-30...
	BOOL isHorizontalBanner = [self isHorizontalBanner];
	NSUInteger largestStrutSize = 0;
	CGFloat largestStrutSizeOfContent = 0.0;
	BOOL foundChildWithStrut = NO;
	for (XTBannerTextHandler *child in self.childHandlers) {
		NSUInteger childSize = 0;
		CGFloat strutSizeOfContent = 0.0;
		if (isHorizontalBanner) {
			if (child.style & OS_BANNER_STYLE_VSTRUT) {
				foundChildWithStrut = YES;
				childSize = [child heightOfContents:&strutSizeOfContent];
			}
		} else {
			if (child.style & OS_BANNER_STYLE_HSTRUT) {
				foundChildWithStrut = YES;
				childSize = [child widthOfContents:&strutSizeOfContent];
			}
		}
		if (childSize > largestStrutSize || strutSizeOfContent > largestStrutSizeOfContent) {
			largestStrutSize = childSize;
			largestStrutSizeOfContent = strutSizeOfContent;
		}
	}

	if (foundChildWithStrut) {
		if (largestStrutSize > newSize || largestStrutSizeOfContent > newSizeOfContents) {
			newSize = largestStrutSize;
			newSizeOfContents = largestStrutSizeOfContent;
		}
	}
	
	self.sizeUnits = OS_BANNER_SIZE_ABS;
	self.size = newSize;
	self.sizeOfContents = newSizeOfContents;
	//XT_WARN_1(@"self.sizeOfContents = %lf", self.sizeOfContents);
	
	if (self.initialSize == nil) {
		self.initialSize = [NSNumber numberWithDouble:newSizeOfContents];
		self.initialSizeUnits = OS_BANNER_SIZE_PIXELS;
		self.wasInitiallySizedToContents = YES;
		self.initialSizeOfContents = newSizeOfContents;
	}
}

- (void)mainThread_getUsableSizes:(NSMutableArray *)results
{
	results[0] = [NSNumber numberWithInt:(int)[self usableHeightInRows]];
	results[1] = [NSNumber numberWithInt:(int)[self usableWidthInColumns]];
	results[2] = [NSNumber numberWithInt:(int)[self usableHeightInPoints]];
	results[3] = [NSNumber numberWithInt:(int)[self usableWidthInPoints]];
}

- (NSUInteger)heightOfContents:(CGFloat *)heightOfText
{
	XT_TRACE_ENTRY;
	
	NSUInteger res;
	XTBannerTextView *bannerTextView = (XTBannerTextView *)self.textView;

	if ([self isGridMode]) {
		if ([bannerTextView countCharsInText] >= 1) {
			res = self.gridTextModel.maxRowIndex + 1;
		} else {
			res = 0;
		}
	} else {
		res = [bannerTextView countRenderedTextLines];
	}
	
	if (heightOfText != nil) {
		if (res >= 1) {
			CGFloat hOAL = [XTFontUtils heightOfText:self.textView];
			*heightOfText = hOAL;
		} else {
			*heightOfText = 0.0;
		}
	}
	
	return res;
}

- (NSUInteger)widthOfContents:(CGFloat *)widthOfLongestIndivisibleWord
{
	XT_TRACE_ENTRY;
	
	NSUInteger res;
	
	if ([self isGridMode]) {
		XTBannerTextView *bannerTextView = (XTBannerTextView *)self.textView;
		if ([bannerTextView countCharsInText] >= 1) {
			res = self.gridTextModel.maxColumnIndex + 1;
			if (widthOfLongestIndivisibleWord != nil) {
				// for grid banners, lines are indivisible
				*widthOfLongestIndivisibleWord = [XTFontUtils widthOfLongestLineInTextStorage:[self.textView textStorage]];
			}
		} else {
			res = 0;
		}
	} else {
		//TODO rework when supporting images
		// "For a left-aligned or right-aligned banner, this sets the banner's width so
		// that the banner is just wide enough to hold the banner's single widest indivisible element (such as a single word or a picture)"
		NSUInteger numCharsInLongest;
		CGFloat wOLIW = [XTFontUtils widthOfLongestIndivisibleWordInTextStorage:[self.textView textStorage]
															  numCharsInLongest:&numCharsInLongest];
		if (widthOfLongestIndivisibleWord != nil) {
			*widthOfLongestIndivisibleWord = wOLIW;
		}
		res = numCharsInLongest;
	}
	return res;
}

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

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

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

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

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

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

- (void)gotoRow:(NSUInteger)row column:(NSUInteger)column
{
	XT_DEF_SELNAME;
	XT_TRACE_2(@"row=%lu column=%lu", row, column);

	if (! [self isGridMode]) {
		XT_ERROR_0(@"banner is not grid mode");
		return;
	}
	self.gridTextModel.rowIndex = row;
	self.gridTextModel.columnIndex = column;
}

- (void)mainThread_setForegroundColorForGrid:(NSNumber *)foregroundColorObj
{
	if (! [self isGridMode]) {
		XT_DEF_SELNAME;
		XT_ERROR_0(@"banner is not grid mode"); //TODO !!! adapt: mv to caller
		return;
	}

	// flush any outstanding text before we set new color:
	[self flushTextForGridMode];

	os_color_t foregroundColor = foregroundColorObj.unsignedIntegerValue;

	XTHtmlColor *htmlColor = [XTHtmlColor forOsifcColor:foregroundColor];
	[self.colorationHelper setGridModeForegroundColor:htmlColor];
}

- (void)mainThread_setBackgroundColorForGrid:(NSNumber *)backgroundColorObj
{
	if (! [self isGridMode]) {
		XT_DEF_SELNAME;
		XT_ERROR_0(@"banner is not grid mode"); //TODO !!! adapt: mv to caller
		return;
	}
	
	// flush any outstanding text before we set new color:
	[self flushTextForGridMode];

	os_color_t backgroundColor = backgroundColorObj.unsignedIntegerValue;

	XTHtmlColor *htmlColor = [XTHtmlColor forOsifcColor:backgroundColor];
	[self.colorationHelper setGridModeBackgroundColor:htmlColor];
}

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

- (void)mainThread_setScreenColorForGrid:(NSNumber *)screenColorObj
{
	if (! [self isGridMode]) {
		XT_DEF_SELNAME;
		XT_ERROR_0(@"banner is not grid mode"); //TODO !!! adapt: mv to caller
		return;
	}
	
	os_color_t screenColor = screenColorObj.unsignedIntegerValue;

	XTHtmlColor *htmlColor = [XTHtmlColor forOsifcColor:screenColor];
	[self.colorationHelper setGridModeScreenColor:htmlColor];
}

- (void)mainThread_noteStartOfPagination
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"ENTER");
	
	if ([self isGridMode]) {
		[self moveCursorToEndOfOutputPosition];
		// ... but nothing else - grid banners don't use pagination
	} else {
		[super mainThread_noteStartOfPagination];
	}
}

- (void)moveCursorToEndOfOutputPosition
{
	if (! [NSThread isMainThread]) {
		// Turns out this needs to be done on main thread, too...
		if ([self.gameWindowController isShuttingDownTadsEventLoopThread]) {
			return;
		}
		[self callOnMainThread:@selector(moveCursorToEndOfOutputPosition)];
	} else {
		[super moveCursorToEndOfOutputPosition];
	}
}

- (BOOL)isGridMode
{
	return ((self.type & OS_BANNER_TYPE_TEXTGRID) == OS_BANNER_TYPE_TEXTGRID);
}

- (NSScrollView*)createNewScrollViewWithTextViewForBanner
{
	// https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/TextUILayer/Tasks/TextInScrollView.html
	// http://stackoverflow.com/questions/3174140/how-to-disable-word-wrap-of-nstextview
	
	NSRect tempFrame = NSMakeRect(0.0, 0.0, 0.0, 0.0);
	XTScrollView *scrollView = [[XTScrollView alloc] initWithFrame:tempFrame];
	//scrollView.allowUserScrolling = NO;
		// why did I disallow this for banners?!

	NSSize contentSize = [scrollView contentSize];
	[scrollView setBorderType:NSNoBorder];

	BOOL hasVerScrollBar = (self.style & OS_BANNER_STYLE_VSCROLL);
	BOOL hasHorScrollBar = (self.style & OS_BANNER_STYLE_HSCROLL);
	[scrollView setHasVerticalScroller:hasVerScrollBar];
	[scrollView setHasHorizontalScroller:hasHorScrollBar];

	[scrollView setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
	[scrollView setTranslatesAutoresizingMaskIntoConstraints:NO];
	
	NSRect tvFrame = NSMakeRect(0.0, 0.0, 0.0 /*contentSize.width*/, contentSize.height);
	XTBannerTextView *textView = [[XTBannerTextView alloc] initWithFrame:tvFrame];
	[textView setMinSize:NSMakeSize(0.0, contentSize.height)];
	[textView setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)];
	[textView setVerticallyResizable:YES];
	[textView setHorizontallyResizable:NO];
	[textView setAutoresizingMask:NSViewWidthSizable];
	NSTextContainer *textContainer = [textView textContainer];

	// Prevent line wrapping for grid banners:
	BOOL isGridBanner = (self.type == OS_BANNER_TYPE_TEXTGRID);
	if (isGridBanner) {
		[textContainer setContainerSize:NSMakeSize(FLT_MAX, FLT_MAX)];
		[textView setHorizontallyResizable:YES];
	}
	[textContainer setWidthTracksTextView:(!isGridBanner)];
	
	[scrollView setDocumentView:textView];
	
	textView.delegate = self;
	
	return scrollView;
}

- (void)resetForNextCommand
{
	[super resetForNextCommand];
}

- (void)resetForTradStatusLine
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"");
	
	if (self.isForTradStatusLine) {
		if (self.prefs.emulateHtmlBannerForTradStatusLine.value.boolValue) {
			[self setHiliteModeDirectly:YES];
		} else {
			[self setHiliteModeDirectly:NO];
		}
		[self setItalicsModeDirectly:NO];
		self.lengthOfLastTradStatusLineScoreString = 0;
	}
}

// Used for handling <banner>...</banner>
- (XTHtmlTag *)processTagTreeFromMainOutput:(XTHtmlTag *)currentTag
									 parser:(id<XTOutputTextParserProtocol>)parser
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"");
	
	if (self.currentTag != nil) {
		XT_WARN_0(@"self.currentTag was not nil");
		
	}
	self.currentTag = currentTag;
	if (self.currentTag == nil) {
		int brkpt = 1;
	}
	self.parserFromMainTextHandler = parser;
	
	[self processTagTree];
	
	XTHtmlTag *res = self.currentTag;
	self.currentTag = nil; // not ours to process
	
	return res;
}

- (BOOL)processTagTree
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");

	BOOL res;
	if (! [self isGridMode]) {
		NSUInteger lengthOfTextBefore = self.textView.textStorage.length;
		
		res = [self processTagTreeInRegularMode];
		
		NSUInteger lengthOfTextAfter = self.textView.textStorage.length;
		if (lengthOfTextAfter > lengthOfTextBefore) {
			//TODO !!! adapt: also check if tabs recalced?
			//TODO !!! adapt: test if only one zwsp in text?
			[self.gameWindowController mainThread_layoutAllBannerViews];
		}
	} else {
		[self processTagTreeInGridMode];
		res = NO;
	}
	return res;
}

- (BOOL)processTagTreeInRegularMode
{
	//XT_DEF_SELNAME;

	[self ensureWeHaveCurrentTag];
	BOOL reachedPaginationLimit = reachedPaginationLimit = [self processTags];
	
	if ([self shouldAutoScrollToBottom]) {
		[self handleTrailingNewlineIfReachedPaginationLimit:reachedPaginationLimit];
			//TODO this call/code should probably moved to checkIfHasReachedPaginationLimit...
	}
	
	return reachedPaginationLimit;
}

- (void)processTagTreeInGridMode
{
	//XT_DEF_SELNAME;

	[self ensureWeHaveCurrentTag];
	[self processTags];
	
	if (! self.gridTextModel.hasChanged) {
		return;
	}

	// Replace entire view text with that in self.gridTextModel:

	NSAttributedString *newAttrString = [self.gridTextModel getFullAttributedString];

	NSMutableAttributedString *ts = [self.textView textStorage];
	[ts setAttributedString:newAttrString];
	
	// Because we're replacing entire text, and the user may changed prefs for param'd colors and/or allowing games to set colors:
	[self.colorationHelper applyPrefsTextAndInputColors];
	[self.colorationHelper applyTextAndInputColorsForAllowGameToSetColors];
}

- (BOOL)processFormattedElementQueue
{
	BOOL res;
	if (! [self isGridMode]) {
		res = [self processFormattedElementQueueInRegularMode];
	} else {
		[self processFormattedElementQueueInGridMode];
		res = NO;
	}
	return res;
}

- (BOOL)processFormattedElementQueueInRegularMode
{
	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);

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

			reachedPaginationLimit = [self processFormattedElementQueueRegularElementString:outputElement.attributedString];

		} else if ([outputElement isGameTitleElement]) {
			
			[self.mainTextHandler processFormattedElementQueueGameTitleElement:outputElement];
			
		} else if ([outputElement isBannerEndElement]) {
			
			//XT_WARN_1(@"handling BannerEndElement %lu", self.bannerIndex);
			//XT_WARN_1(@"handling BannerEndElement id=\"%@\"", self.tagId);
			
			//TODO mv to sep method when working
			[[self getOutputTextParser] hardFlush];
			
			[self.outputFormatter flushPendingWhitespace]; // ignore return value
			
			[self flushBatchedtext];
			
			// Back to main output area's handler:
			[self.gameWindowController exitingTagBanner];

			self.abortProcessingTags = YES;
			self.parserFromMainTextHandler = nil;
			break;
			
		} else if ([outputElement isBannerClearElement]) {
			
			[self executeClear];
			self.processFormattedElementQueueClearedTextStorage = YES;

		} else if ([outputElement isStatusLineModeEnd]) {

			self.abortProcessingTags = YES; //TODO !!! exp. for iotc command script playback bug

			//XT_WARN_0(@"handling StatusLineModeEnd");
			// Back to main output area's handler:
			[self.mainTextHandler exitingStatusLineMode:outputElement];

			self.abortProcessingTags = YES; //TODO !!! exp. for iotc command script playback bug
			self.parserFromMainTextHandler = nil;
			break;

		} 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)processFormattedElementQueueInGridMode
{
	while (! [self.formattedElementQueue isEmpty]) {
		
		XTFormattedOutputElement *outputElement = [self.formattedElementQueue removeFirst];
		
		if ([outputElement isRegularOutputElement]) {

			[self.gridTextModel addAttributedString:outputElement.attributedString];

		} else if ([outputElement isBannerClearElement]) {

			[self executeClear];
			
		} else {
			XT_DEF_SELNAME;
			XT_ERROR_1(@"unexpected XTFormattedOutputElement %d in grid mode", outputElement.elementType);
		}
	}
}

- (NSAttributedString *)handleForTradStatusLine:(NSAttributedString *)attrString
{
	NSAttributedString *res = attrString;
	if (self.isForTradStatusLine) {
		if (self.mainTextHandler.statusLineMode == STATUS_LINE_MODE_STATUS) {
			res = [self prepareStringForTradStatusLine:attrString];
		}
	}
	return res;
}

/*
 *   '\n' - newline: end the current line and move the cursor to the start of
 *   the next line.  If the status line is supported, and the current status
 *   mode is 1 (i.e., displaying in the status line), then two special rules
 *   apply to newline handling: newlines preceding any other text should be
 *   ignored, and a newline following any other text should set the status
 *   mode to 2, so that all subsequent output is suppressed until the status
 *   mode is changed with an explicit call by the client program to
 *   os_status().
 *
 *   '\r' - carriage return: end the current line and move the cursor back to
 *   the beginning of the current line.  Subsequent output is expected to
 *   overwrite the text previously on this same line.  The implementation
 *   may, if desired, IMMEDIATELY clear the previous text when the '\r' is
 *   written, rather than waiting for subsequent text to be displayed.
 */
- (NSAttributedString *)prepareStringForTradStatusLine:(NSAttributedString *)attrStr
{
	NSString *str = attrStr.string;
	
	if (str.length == 0) {
		return attrStr;
	}
	
	const char *cstr = [str UTF8String];
	const char *cstrCurrent = cstr;
	while (*cstrCurrent == '\n') {
		cstrCurrent += 1;
	}
	const char *cstrStart = cstrCurrent;
	while (*cstrCurrent != '\n' && *cstrCurrent != '\0') {
		cstrCurrent += 1;
	}
	if (*cstrCurrent == '\n') {
		[self.mainTextHandler setStatusLineModeNow:STATUS_LINE_MODE_SUPPRESS];
	}
	const char *cstrEnd = cstrCurrent;
	NSInteger len = cstrEnd - cstrStart;
	NSString *resStr = [[NSString alloc] initWithBytes:cstrStart length:len encoding:NSUTF8StringEncoding];
	
	NSDictionary<NSAttributedStringKey, id> *attrs = [attrStr attributesAtIndex:0 effectiveRange:nil];
	NSAttributedString *res = [[NSAttributedString alloc] initWithString:resStr attributes:attrs];
	return res;
}

// kludgy fix for t3 42b bug that I'll probably regret :-(
- (void)handleTrailingNewlineIfReachedPaginationLimit:(BOOL)reachedPaginationLimit
{
	if (reachedPaginationLimit) {
		if (self.pendingNewline == nil) {
			NSMutableAttributedString *textStorage = [self.textView textStorage];
			NSUInteger lengthOfTrailingEffectiveNewline = [XTStringUtils lengthOfTrailingEffectiveNewlineNotInTableCell:textStorage];
			if (lengthOfTrailingEffectiveNewline >= 1) {
				NSRange range = NSMakeRange(textStorage.length - lengthOfTrailingEffectiveNewline, lengthOfTrailingEffectiveNewline);
				self.pendingNewline = [textStorage attributedSubstringFromRange:range];
				[textStorage deleteCharactersInRange:range];
			}
		}
	}
}

- (NSAttributedString *)appendAttributedStringToTextStorage:(NSAttributedString *)attrString
{
	//XT_DEF_SELNAME;

	NSAttributedString *attrStrAppended = nil;
	
	if (attrString == nil || attrString.length == 0) {
		return attrStrAppended;
	}
	
	[self flushPendingNewline];
	NSUInteger lengthOfTrailingEffectiveNewline = [XTStringUtils lengthOfTrailingEffectiveNewlineNotInTableCell:attrString];
	if (lengthOfTrailingEffectiveNewline >= 1) {
		NSMutableAttributedString *mutAttrString = [attrString mutableCopy];
		NSRange range = NSMakeRange(attrString.length - lengthOfTrailingEffectiveNewline, lengthOfTrailingEffectiveNewline);
		self.pendingNewline = [mutAttrString attributedSubstringFromRange:range];
		[mutAttrString deleteCharactersInRange:range];
			//TODO? adapt: does this account for chars added to textstorage by flushPendingNewline?! N/A...
		attrString = mutAttrString;
	}
	
	if (attrString.length >= 1) {
		attrStrAppended = [super appendAttributedStringToTextStorage:attrString];
	}
	
	return attrStrAppended;
}

- (void)flushPendingNewline
{
	if (self.pendingNewline != nil) {
		XT_DEF_SELNAME;

		NSTextStorage *textStorage = [self.textView textStorage];
		XT_TRACE_1(@"flushing \"%@\"", self.pendingNewline);
		[textStorage appendAttributedString:self.pendingNewline];
		self.pendingNewline = nil;
		[self.textView ensureLayoutForTextContainer]; // or else x pos calc doesn't work
	}
}

- (BOOL)paginationIsActive
{
	BOOL res;
	if (self.nonstopModeState) {
		res = NO;
	} else {
		res = (self.style & OS_BANNER_STYLE_MOREMODE);
	}
	return res;
}

//TODO call when game window is resized
- (void)scrollToTop
{
	//TODO only when size > 0 ?
	[self.textView scrollRangeToVisible:NSMakeRange(0, 0)];
}

// NSTextViewDelegate

- (BOOL)textView:(NSTextView *)textView clickedOnLink:(id)link atIndex:(NSUInteger)charIndex
{
	BOOL handled;
	NSString *linkString = link;
	if ([XTStringUtils isInternetLink:linkString]) {
		// An Internet link - let the OS handle it.
		handled = NO;
	} else {
		// A "command link" - handle it ourselves.
		if (! [self.gameWindowController handleClickedOnLinkAsTadsVmEvent:linkString]) {
			[XTNotifications notifyAboutTextLinkClicked:self linkText:link charIndex:charIndex];
		}
		handled = YES;
	}
	
	[XTNotifications notifySetFocusToMainOutputView:self];
	
	return handled;
}

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

- (void)setupReceptionOfAppLevelNotifications
{
	//XT_TRACE_ENTRY;
	
	[[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:XTadsNotifySetFocusToMainOutput
												  object:nil];
}

- (void)handleSetFocusToMainOutput:(NSNotification *)notification
{
	XT_TRACE_ENTRY;
	
	XTBannerTextView *bannerTextView = (XTBannerTextView *)self.textView;
	[bannerTextView unselectText];
}

@end
