
//
//  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 "XTStringUtils.h"
#import "XTNotifications.h"
#import "XTPrefs.h"
#import "XTFontUtils.h"
#import "XTHtmlTagT2Hilite.h"
#import "XTHtmlTagT2Italics.h"
#import "XTHtmlTagTab.h"
#import "XTHtmlWhitespace.h"
#import "XTLogger.h"
#import "XTAllocDeallocCounter.h"
#import "XTTimer.h"
#import "XTCallOnMainThreadCounter.h"
#import "XTViewLayoutUtils.h"


@interface XTBannerTextHandler ()

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

@property BOOL pendingNewline;
	//TODO reset on goto?
@property NSString *tradStatusLineScoreString;
@property XTBannerGridTextModel *gridTextModel;

@property Class tagT2HiliteClass;
@property Class tagT2ItalicsClass;

@end


@implementation XTBannerTextHandler

static XTLogger* logger;

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


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;
		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;
		_isForTradStatusLine = NO;
		_tagBannerNeedsSizeToContent = NO;
		_pendingNewline = NO;
		_tradStatusLineScoreString = nil;
		_gridTextModel = [XTBannerGridTextModel new];
		_tagT2HiliteClass = [XTHtmlTagT2Hilite class];
		_tagT2ItalicsClass = [XTHtmlTagT2Italics class];

		[self setupReceptionOfAppLevelNotifications];
	}
	return self;
}

+ (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;
		}
	}
	banner.outputFormatter.isForBanner = YES;

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

- (void)noteStartedFromHtmlTag
{
	self.outputFormatter.tagBannerDepth = 1;
}

- (void)traceWithIndentLevel:(NSUInteger)indentLevel
{
	if (! XT_TRACE_ON) {
		return;
	}
	XT_DEF_SELNAME;
	
	XTBannerTextHandler *parent = self.parentHandler;
	
	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, parent.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.formattingQueue addObject:t2ItalicsTag];
}
	
- (void)setIsForT3:(BOOL)isForT3
{
	self.outputFormatter.isForT3 = isForT3;
}

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

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

	NSArray *parseResultArray = [[self getOutputTextParser] parse:(NSString *)string];
	[self.formattingQueue addObjectsFromArray:parseResultArray];
	
	// 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
{
	if (self.isForTradStatusLine) {
		if (self.tradStatusLineScoreString != nil) {

			// In case we're replaying a T2 command script,
			// use a temporary formatting queue to avoid recursing into starting banner
			
			NSMutableArray *savedFormattingQueue = self.formattingQueue;
			self.formattingQueue = [NSMutableArray array];
			
			BOOL emulateHtmlBanner = self.prefs.emulateHtmlBannerForTradStatusLine.boolValue;
			if (emulateHtmlBanner) {
				[self setHiliteMode:NO];
				[self setItalicsMode:YES];
			}
			
			XTHtmlTagTab *tabTag = [XTHtmlTagTab rightAligned];
			[self.formattingQueue addObject:tabTag];

			[self.formattingQueue addObject:self.tradStatusLineScoreString];

			[self processFormattingQueue];
			
			self.formattingQueue = savedFormattingQueue;
			
			if (emulateHtmlBanner) {
				[self setItalicsMode:NO];
			}
		}
	}
}

- (void)mainThread_flush
{
	//XT_DEF_SELNAME;
	//XT_TRACE_0(@"enter");
	
	NSArray *parseResultArray = [[self getOutputTextParser] flush];
	[self.formattingQueue addObjectsFromArray:parseResultArray];

	[self processFormattingQueue];
	
	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];
	
	//XT_TRACE_0(@"exit");
}

- (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)mainThread_clear
{
	[self resetState];

	// Handle the actual UI update on next flush:
	XTSpecialAction *specialAction = [XTSpecialAction new];
	[self.formattingQueue addObject:specialAction];
}

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

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

- (void)executeClear
{
	[[[self.textView textStorage] mutableString] setString:@""];
	
	[self.gridTextModel clear];

	self.pendingNewline = NO;
	[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);
}

- (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];
			break;
		case OS_BANNER_ALIGN_LEFT:
		case OS_BANNER_ALIGN_RIGHT:
			newSize = [self widthOfContents:&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;
	
	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)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.statusLineBackgroundColor;
	
	[self.textView setNeedsDisplay:YES];
	
	//TODO why is this needed?!
	[self scrollToTop];
}

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

// Used for handling <banner>...</banner>
- (void)processFormattingQueueFromMainOutput:(NSMutableArray *)formattingQueue
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"");

	if (self.formattingQueue.count >= 1) {
		XT_WARN_1(@"self.formattingQueue.count was %lu - emptying", self.formattingQueue.count);
		[self.formattingQueue removeAllObjects];
	}

	self.formattingQueue = formattingQueue;
	//XT_WARN_1(@"self.formattingQueue has %lu entries", self.formattingQueue.count);
	
	[self processFormattingQueue];
	
	self.formattingQueue = [NSMutableArray array];
}

- (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);
	NSTextView *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];
	
	[self resetForTradStatusLine];
}

- (void)resetForTradStatusLine
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"");
	
	if (self.isForTradStatusLine) {
		if (self.prefs.emulateHtmlBannerForTradStatusLine.boolValue) {
			[self setHiliteMode:YES];
		} else {
			[self setHiliteMode:NO];
		}
		[self setItalicsMode:NO];
	}
}

- (BOOL)processFormattingQueue
{
	//XT_DEF_SELNAME;
	//NSUInteger mtCount = [XTCallOnMainThreadCounter getCount];
	//XT_TRACE_1(@"%lu", mtCount);
	
	BOOL res;
	if (! [self isGridMode]) {
		res = [self processFormattingQueueRegularMode];
	} else {
		[self processFormattingQueueInGridMode];
		res = NO;
	}
	return res;
}

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

	[self restoreStringThatBrokePaginationLimit];
	
	BOOL reachedPaginationLimit = NO;

	if (self.formattingQueue.count == 0) {
		return reachedPaginationLimit;
	}
	
	NSUInteger countFQEltsProcessed = 0;
	
	while ([self.formattingQueue count] >= 1 && ! reachedPaginationLimit && ! abortProcessing) {
		
		id parsedElement = [self.formattingQueue firstObject];
		[self.formattingQueue removeObjectAtIndex:0];
		
		countFQEltsProcessed += 1;
		
		if (self.mainTextHandler.statusLineMode == STATUS_LINE_MODE_SUPPRESS) {
			continue;
		}
		
		parsedElement = [self handleParsedElementForTradStatusLine:parsedElement];
		
		NSArray *formattedOutputElements = [self.outputFormatter formatElement:parsedElement];
		
		for (XTFormattedOutputElement *outputElement in formattedOutputElements) {
			
			if (self.attributedStringThatBrokePaginationLimit != nil) {
				// this can, in principle, happen, so ensure we print all text
				// even if it might make pagination not 100% correct
				XT_WARN_0(@"self.attributedStringThatBrokePaginationLimit != nil");
				[self appendAttributedStringToTextStorage:self.attributedStringThatBrokePaginationLimit];
				self.attributedStringThatBrokePaginationLimit = nil;
			}
			
			if ([outputElement isRegularOutputElement]) {
				
				NSAttributedString *lastAttrStringAppended = [self appendAttributedStringToTextStorage:outputElement.attributedString];
				if (lastAttrStringAppended != nil) {
					[self.textView ensureLayoutForTextContainer]; // or else x pos calc doesn't work
					reachedPaginationLimit = [self checkIfHasReachedPaginationLimit:lastAttrStringAppended];
					lastAttrStringAppended = nil;
				}
				
			} else if ([outputElement isTabElement]) {

				XTHtmlTagTab *tagTab = (XTHtmlTagTab *)outputElement.htmlTag;
				NSAttributedString *lastAttrStringAppended = [self flushPendingNewline];
				if (lastAttrStringAppended != nil) {
					reachedPaginationLimit = [self checkIfHasReachedPaginationLimit:lastAttrStringAppended];
					lastAttrStringAppended = nil;
				}

				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.mainTextHandler.gameTitle = [NSMutableString stringWithString:@""];
				} else {
					[self.mainTextHandler.gameTitle appendString:outputElement.attributedString.string];
				}
				
			} else if ([outputElement isBannerEndElement]) {

				//TODO mv to sep method when working
				NSArray *parseResultArray = [[self getOutputTextParser] hardFlush];
				[self.formattingQueue addObjectsFromArray:parseResultArray];
				
				[self.gameWindowController exitingTagBanner];
				// Back to main output area's handler:
				abortProcessing = YES;
				break;
				
			} else if ([outputElement isSpecialActionElement]) {
				
				//TODO test what kind of sp. action
				[self executeClear];

			} else if ([outputElement isStatusLineModeEnd]) {

				[self.mainTextHandler exitingStatusLineMode];
				// Back to main output area's handler:
				abortProcessing = YES;
				break;

			} else {
				XT_ERROR_1(@"unknown XTFormattedOutputElement %d", outputElement.elementType);
			}
		}
	}
	
	if ([self shouldAutoScrollToBottom]) {
		[self handleTrailingNewlineIfReachedPaginationLimit:reachedPaginationLimit];
			//TODO this call/code should probably moved to checkIfHasReachedPaginationLimit...
		[self scrollToBottom];
	}
	
	//XT_TRACE_1(@"exit formattingQueue.count=%lu", self.formattingQueue.count);
	//XT_WARN_1(@"countFQEltsProcessed=%lu", countFQEltsProcessed);
	
	return reachedPaginationLimit;
}

- (id)handleParsedElementForTradStatusLine:(id)parsedElement
{
	if (self.isForTradStatusLine) {
		if ([parsedElement isKindOfClass:[NSString class]]) {
			if (self.mainTextHandler.statusLineMode == STATUS_LINE_MODE_STATUS) {
				parsedElement = [self prepareStringForTradStatusLine:parsedElement];
			}
		}
	}
	return parsedElement;
}

/*
 *   '\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.
 */
- (NSString *)prepareStringForTradStatusLine:(NSString *)str
{
	if (str.length == 0) {
		return str;
	}
	
	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 *res = [[NSString alloc] initWithBytes:cstrStart length:len encoding:NSUTF8StringEncoding];
	return res;
}

// kludgy fix for t3 42b bug that I'll probably regret :-(
- (void)handleTrailingNewlineIfReachedPaginationLimit:(BOOL)reachedPaginationLimit
{
	if (reachedPaginationLimit) {
		if (! self.pendingNewline) {
			NSMutableAttributedString *textStorage = [self.textView textStorage];
			if ([XTStringUtils string:textStorage.string endsWith:@"\n"]) {
				self.pendingNewline = YES;
				NSRange deleteRange = NSMakeRange(textStorage.length - 1, 1);
				[textStorage deleteCharactersInRange:deleteRange];
			}
		}
	}
}

- (void)processFormattingQueueInGridMode
{
	XT_DEF_SELNAME;

	while ([self.formattingQueue count] >= 1) {
		id parsedElement = [self.formattingQueue firstObject];
		[self.formattingQueue removeObjectAtIndex:0];
		
		NSArray *formattedOutputElements = [self.outputFormatter formatElement:parsedElement];
		
		for (XTFormattedOutputElement *outputElement in formattedOutputElements) {
			
			if ([outputElement isRegularOutputElement]) {
				[self.gridTextModel setAttributedString:outputElement.attributedString];

            } else if ([outputElement isSpecialActionElement]) {
                
                //TODO test what kind of sp. action
                [self executeClear];

            } else {
				XT_ERROR_1(@"unexpected XTFormattedOutputElement %d in grid mode", outputElement.elementType);
			}
		}
	}
	
	// Replace entire view text with that in self.gridTextModel:

	NSMutableAttributedString *newText = [NSMutableAttributedString new];
	BOOL printSep = NO;
	
	for (NSString *row in self.gridTextModel.rows) {		
		if (printSep) {
			NSAttributedString *attrStr = [self.outputFormatter formatStringForGridBanner:@"\n"];
			[newText appendAttributedString:attrStr];
		}
		NSAttributedString *attrStr = [self.outputFormatter formatStringForGridBanner:row];
		[newText appendAttributedString:attrStr];
		printSep = YES;
	}
	
	NSMutableAttributedString *ts = [self.textView textStorage];
	[ts setAttributedString:newText];
}

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

	NSAttributedString *attrStrAppended = nil;
	
	if (attrString == nil || attrString.length == 0) {
		return attrStrAppended;
	}
	
	attrStrAppended = [self flushPendingNewline];
	if ([XTStringUtils string:attrString.string endsWith:@"\n"]) {
		self.pendingNewline = YES;
		NSRange deleteRange = NSMakeRange(attrString.length - 1, 1);
		[attrString deleteCharactersInRange:deleteRange];
	}
	
	if (attrString.length >= 1) {
		attrStrAppended = [super appendAttributedStringToTextStorage:attrString];
	}
	
	return attrStrAppended;
}

- (NSAttributedString *)flushPendingNewline
{
	XT_DEF_SELNAME;

	NSAttributedString *attrStrAppended = nil;
	if (self.pendingNewline) {
		XT_TRACE_0(@"flushing");
		NSTextStorage *textStorage = [self.textView textStorage];
		attrStrAppended = [self.outputFormatter formatOutputText:@"\n"];
		[textStorage appendAttributedString:attrStrAppended];
		[self.textView ensureLayoutForTextContainer]; // or else x pos calc doesn't work
	}
	self.pendingNewline = NO;
	return attrStrAppended;
}

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