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

#import "XTBannerHandler.h"
#import "XTOutputTextHandler.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"


@interface XTBannerHandler ()

#define NO_CHILD_INDEX  (-1)

@property NSString *debugName;

@property XTPrefs *prefs;
@property XTFontUtils *fontUtils;

// These correspond to params to os_banner_create()
@property NSUInteger where; // "where"
@property (weak) XTBannerHandler *siblingForWhere; // "other"

@property (weak) XTBannerHandler *parentHandler;
@property NSMutableArray *childHandlers; // of XTBannerHandler

@property NSMutableArray *layoutViews;
@property (unsafe_unretained) XTBannerTextView *textView;

@property XTOutputTextParserPlain *outputTextParserPlain;
@property XTOutputTextParserHtml *outputTextParserHtml;
@property XTOutputFormatter *outputFormatter;

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

@property BOOL hasTornDownView;

@end


@implementation XTBannerHandler

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:[XTBannerHandler class]];
	[XTBannerHandler resetStaticState];
}

+ (void)resetStaticState
{
	nextBannerIndex = 0; // 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) {
		_outputTextHandler = nil;
		_prefs = [XTPrefs prefs];
		_fontUtils = [XTFontUtils fontUtils];
		_bannerIndex = nextBannerIndex;
		nextBannerIndex += 1;
		_where = 0;
		_siblingForWhere = nil;
		_type = 0;
		_alignment = 0;
		_size = 0;
		_sizeUnits = 0;
		_style = 0;
		_tagId = nil;
		_isBeingCreated = NO;
		_isSizedToContent = NO;
		_sizeOfContents = 0.0;
		_wasInitiallySizedToContents = NO;
		_initialSizeOfContents = 0.0;
		_debugName = [NSString stringWithFormat:@"b-%lu", _bannerIndex];
		_childHandlers = [NSMutableArray array];
		_outputTextParserPlain = [XTOutputTextParserPlain new];
		_outputTextParserHtml = [XTOutputTextParserHtml new];
		_outputFormatter = [XTOutputFormatter new];
		_outputFormatter.isForBanner = NO;
		_formattingQueue = [NSMutableArray arrayWithCapacity:200];
		_htmlMode = NO;
		_isForTradStatusLine = NO;
		_tagBannerNeedsSizeToContent = NO;
		_pendingNewline = NO;
		_tradStatusLineScoreString = nil;
		_tradStatusLineScoreStringStartIndex = nil;
		_gridTextModel = [XTBannerGridTextModel new];
		_layoutViews = [NSMutableArray arrayWithCapacity:5];
		_hasTornDownView = NO;

		[self setupReceptionOfAppLevelNotifications];
	}
	return self;
}

+ (instancetype)handlerForMainOutputArea
{
	XTBannerHandler *banner = [XTBannerHandler new];
	banner.outputFormatter.isForBanner = NO;
	return banner;
}

+ (instancetype)handlerWithParent:(XTBannerHandler*)parent
							where:(NSInteger)where
							other:(XTBannerHandler *)other
						  wintype:(NSInteger)wintype
							align:(NSInteger)align
							 size:(NSInteger)size
						sizeUnits:(NSInteger)sizeUnits
							style:(NSUInteger)style
{
	XTBannerHandler *banner = [XTBannerHandler 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;
	banner.outputFormatter.isForBanner = YES;

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

- (void)noteStartedFromHtmlTag
{
	self.outputFormatter.tagBannerMode = YES;
}

- (void)traceWithIndentLevel:(NSUInteger)indentLevel
{
	if (! XT_TRACE_ON) {
		return;
	}
	XT_DEF_SELNAME;
	
	XTBannerHandler *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 (XTBannerHandler *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 totalHorizontalInsetForView: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 totalVerticalInsetForView:YES];
		height -= totalInset;
		if (height > 0.0) {
			res = ceil(height);
		}
	}

	return res;
}

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

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

- (void)addChildHandler:(XTBannerHandler *)childHandler
{
	/*
	 *   'where' is OS_BANNER_FIRST to make the new window the first child of its
	 *   parent; OS_BANNER_LAST to make it the last child of its parent;
	 *   OS_BANNER_BEFORE to insert it immediately before the existing banner
	 *   identified by handle in 'other'; or OS_BANNER_AFTER to insert
	 *   immediately after 'other'.  When BEFORE or AFTER is used, 'other' must  -- !!
	 *   be another child of the same parent; if it is not, the routine should
	 *   act as though 'where' were given as OS_BANNER_LAST.
	 *
	 *   'other' is a banner handle for an existing banner window.  This is used
	 *   to specify the relative position among children of the new banner's
	 *   parent, if 'where' is either OS_BANNER_BEFORE or OS_BANNER_AFTER.  If
	 *   'where' is OS_BANNER_FIRST or OS_BANNER_LAST, 'other' is ignored.
	 */

	XT_TRACE_ENTRY;
	
	NSUInteger childHandlersCount = self.childHandlers.count;
	NSInteger indexForChild = childHandlersCount; // i.e. last
	
	switch (childHandler.where) {
		case OS_BANNER_FIRST:
			indexForChild = 0; // i.e. first
			break;
		case OS_BANNER_LAST:
			// keep default last
			break;
		case OS_BANNER_BEFORE: {
			NSInteger tempIndex = [self indexOfChild:childHandler.siblingForWhere];
			if (tempIndex != NO_CHILD_INDEX) {
				indexForChild = tempIndex;
			}
			break;
		}
		case OS_BANNER_AFTER: {
			NSInteger tempIndex = [self indexOfChild:childHandler.siblingForWhere];
			if (tempIndex != NO_CHILD_INDEX) {
				indexForChild = tempIndex + 1;
				// Make sure childHandler is *last* among children with same where and siblingForWhere:
				for (; indexForChild < childHandlersCount; indexForChild += 1) {
					XTBannerHandler *siblingHandler = [self.childHandlers objectAtIndex:indexForChild];
					if ((siblingHandler.where != childHandler.where) ||
						(siblingHandler.siblingForWhere != childHandler.siblingForWhere)) {
						break;
					}
				}
			}
			break;
		}
		default:
			XT_ERROR_1(@"unexpected value for member where: %lu", childHandler.where);
			break;
	}

	[self.childHandlers insertObject:childHandler atIndex:indexForChild];
}

- (NSInteger)indexOfChild:(XTBannerHandler*)childHandler
{
	NSInteger res = NO_CHILD_INDEX;
	if (childHandler != nil) {
		NSInteger index = 0;
		for (XTBannerHandler *child in self.childHandlers) {
			if (child == childHandler) {
				res = index;
				break;
			}
			index += 1;
		}
	}
	return res;
}

- (void)setHtmlMode:(BOOL)htmlMode
{
	XT_TRACE_ENTRY;
	
	_htmlMode = htmlMode;
	self.outputFormatter.htmlMode = htmlMode;
}

- (void)setHiliteMode:(BOOL)hiliteMode
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"%d", hiliteMode);
	
	// represent hilite mode on/off by a special tag object
	XTHtmlTagT2Hilite *t2HiliteTag = [XTHtmlTagT2Hilite new];
	t2HiliteTag.closing = (! hiliteMode);
	[self.formattingQueue addObject:t2HiliteTag];
}

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

- (BOOL)isForMainOutputArea
{
	return (self.bannerIndex == 0);
}

- (void)mainThread_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)mainThread_createTextViewForMainOutputArea
{
	XT_TRACE_ENTRY;
	
	NSScrollView *newTextScrollView = [self createNewScrollViewWithTextViewForMainOutputArea];
	self.scrollView = newTextScrollView;
	self.outputTextView = self.scrollView.documentView;
}

- (void)mainThread_configureViews
{
	XT_TRACE_ENTRY;

	if (self.bannerIndex != 0) {
		XT_ERROR_1(@"self.bannerIndex had unexpected value %lu", self.bannerIndex);
		return;
	}
	
	CGSize oldMainOutputAreaSize = self.scrollView.frame.size;
	
	[self tearDownLayoutViews];
	
	NSView *overallView = [self internalRebuildViewHierarchy];
	[self.rootBannerContainerView addSubview:overallView];

	// Make overallView fill all of its superview:
	[self addEdgeConstraint:NSLayoutAttributeLeft superview:self.rootBannerContainerView subview:overallView];
	[self addEdgeConstraint:NSLayoutAttributeRight superview:self.rootBannerContainerView subview:overallView];
	[self addEdgeConstraint:NSLayoutAttributeTop superview:self.rootBannerContainerView subview:overallView];
	[self 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];
}

//TODO mv
- (void)recalcDynamicTabStops:(BOOL)changedMainOutputAreaSize
{
	//XT_DEF_SELNAME;
	
	if (self.isForMainOutputArea) {
		if (changedMainOutputAreaSize) {
			// expensive if lots of text in main output area, so only do if necessary
			[self.outputTextView.outputFormatter recalcAllTabStops];
		}
	} else {
		[self.outputFormatter recalcAllTabStops];
	}

	for (XTBannerHandler *child in self.childHandlers) {
		[child recalcDynamicTabStops:NO];
	}
}

- (NSView *)internalRebuildViewHierarchy
{
	XT_TRACE_ENTRY;
	
	// Configure children's (top level) views:
	
	NSMutableArray *childViews = [NSMutableArray array];
	//[childViews addObject:self.scrollView];
	
	for (XTBannerHandler *child in self.childHandlers) {
		NSView *childView = [child internalRebuildViewHierarchy];
		[childViews addObject:childView];
	}
	
	// Compose own (top level) view:

	NSView *ownTopLevelView = self.scrollView;
	
	for (NSInteger i = self.childHandlers.count - 1; i >= 0; i--) {
		
		XTBannerHandler *childHandler = [self.childHandlers objectAtIndex:i];
		NSView *childView = [childViews objectAtIndex:i];
		
		NSRect tempFrame = NSMakeRect(0.0, 0.0, 0.0, 0.0);
		XTBannerContainerView *tempOwnTopLevelView = [[XTBannerContainerView alloc] initWithFrame:tempFrame];
		[self.layoutViews addObject:tempOwnTopLevelView];
		
		[tempOwnTopLevelView addSubview:ownTopLevelView];
		[tempOwnTopLevelView addSubview:childView];
		
		CGFloat childViewSize = [childHandler calcViewSizeForConstraint];
		BOOL childViewIsAbsSized = (childHandler.sizeUnits == OS_BANNER_SIZE_ABS || childHandler.sizeUnits == OS_BANNER_SIZE_PIXELS);
		[childHandler captureInitialSizeWhenViewSize:childViewSize];

		[self newLayoutInParentView:tempOwnTopLevelView
						 childView1:ownTopLevelView
						 childView2:childView
				childView2Alignment:childHandler.alignment
					 childView2Size:childViewSize
			   childView2IsAbsSized:childViewIsAbsSized];

		ownTopLevelView = tempOwnTopLevelView;
	}
	
	if (! self.isForMainOutputArea) {
		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];
			
			[self newLayoutInParentView:tempOwnTopLevelView
							 childView1:ownTopLevelView
							 childView2:borderView
					childView2Alignment:borderAlignment
						 childView2Size:[self borderSize]
				   childView2IsAbsSized:YES];

			ownTopLevelView = tempOwnTopLevelView;
		}
	}

	return ownTopLevelView;
}

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

- (void)newLayoutInParentView:(NSView *)parentView
				   childView1:(NSView *)childView1
				   childView2:(NSView *)childView2
		  childView2Alignment:(NSUInteger)childView2Alignment
			   childView2Size:(CGFloat)childView2Size
		 childView2IsAbsSized:(BOOL)childView2IsAbsSized
{
	XT_TRACE_ENTRY;
	
	NSLayoutAttribute childView1Edge1;
	NSLayoutAttribute childView1Edge2;
	NSLayoutAttribute childView1Edge3;
	NSLayoutAttribute childView1Edge4; // facing childView2
	NSLayoutAttribute childView2Edge1;
	NSLayoutAttribute childView2Edge2;
	NSLayoutAttribute childView2Edge3;
	NSLayoutAttribute childView2Edge4; // facing childView1
	NSLayoutAttribute childView2SizeOrientation; // is size height or width?

	switch (childView2Alignment) {
		case OS_BANNER_ALIGN_TOP:
			childView1Edge1 = NSLayoutAttributeBottom;
			childView1Edge2 = NSLayoutAttributeLeft;
			childView1Edge3 = NSLayoutAttributeRight;
			childView1Edge4 = NSLayoutAttributeTop;
			childView2Edge1 = NSLayoutAttributeTop;
			childView2Edge2 = NSLayoutAttributeLeft;
			childView2Edge3 = NSLayoutAttributeRight;
			childView2Edge4 = NSLayoutAttributeBottom;
			childView2SizeOrientation = NSLayoutAttributeHeight;
			break;
		case OS_BANNER_ALIGN_BOTTOM:
			childView1Edge1 = NSLayoutAttributeTop;
			childView1Edge2 = NSLayoutAttributeLeft;
			childView1Edge3 = NSLayoutAttributeRight;
			childView1Edge4 = NSLayoutAttributeBottom;
			childView2Edge1 = NSLayoutAttributeBottom;
			childView2Edge2 = NSLayoutAttributeLeft;
			childView2Edge3 = NSLayoutAttributeRight;
			childView2Edge4 = NSLayoutAttributeTop;
			childView2SizeOrientation = NSLayoutAttributeHeight;
			break;
		case OS_BANNER_ALIGN_LEFT:
			childView1Edge1 = NSLayoutAttributeTop;
			childView1Edge2 = NSLayoutAttributeBottom;
			childView1Edge3 = NSLayoutAttributeRight;
			childView1Edge4 = NSLayoutAttributeLeft;
			childView2Edge1 = NSLayoutAttributeTop;
			childView2Edge2 = NSLayoutAttributeBottom;
			childView2Edge3 = NSLayoutAttributeLeft;
			childView2Edge4 = NSLayoutAttributeRight;
			childView2SizeOrientation = NSLayoutAttributeWidth;
			break;
		case OS_BANNER_ALIGN_RIGHT:
			childView1Edge1 = NSLayoutAttributeTop;
			childView1Edge2 = NSLayoutAttributeBottom;
			childView1Edge3 = NSLayoutAttributeLeft;
			childView1Edge4 = NSLayoutAttributeRight;
			childView2Edge1 = NSLayoutAttributeTop;
			childView2Edge2 = NSLayoutAttributeBottom;
			childView2Edge3 = NSLayoutAttributeRight;
			childView2Edge4 = NSLayoutAttributeLeft;
			childView2SizeOrientation = NSLayoutAttributeWidth;
			break;
		default:
			XT_ERROR_1(@"unknown alignment %lu", childView2Alignment);
			return;
	}
	
	[self addEdgeConstraint:childView1Edge1 superview:parentView subview:childView1];
	[self addEdgeConstraint:childView1Edge2 superview:parentView subview:childView1];
	[self addEdgeConstraint:childView1Edge3 superview:parentView subview:childView1];
	
	[self addEdgeConstraint:childView2Edge1 superview:parentView subview:childView2];
	[self addEdgeConstraint:childView2Edge2 superview:parentView subview:childView2];
	[self addEdgeConstraint:childView2Edge3 superview:parentView subview:childView2];
	
	[parentView addConstraint:[NSLayoutConstraint constraintWithItem:childView1
														   attribute:childView1Edge4
														   relatedBy:NSLayoutRelationEqual
															  toItem:childView2
														   attribute:childView2Edge4
														  multiplier:1
															constant:0]];

	NSView *otherView = nil;
	CGFloat multiplier = 1.0;
	if (! childView2IsAbsSized) {
		otherView = parentView;
		multiplier = childView2Size / 100.0;
		childView2Size = 0.0;
	}
	
	[parentView addConstraint:[NSLayoutConstraint constraintWithItem:childView2
													   attribute:childView2SizeOrientation
													   relatedBy:NSLayoutRelationEqual
														  toItem:otherView
													   attribute:childView2SizeOrientation
													  multiplier:multiplier
														constant:childView2Size]];
}

- (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 totalVerticalInsetForView:respectInsetsIfNoText];
	} else {
		inset = [self totalHorizontalInsetForView:respectInsetsIfNoText];
	}
	
	if (self.style & OS_BANNER_STYLE_BORDER) {
		inset += [self borderSize];
	}
	
	return inset;
}

- (CGFloat)totalHorizontalInsetForView:(BOOL)respectInsetsIfNoText
{
	//TODO use respectInsetsIfNoText
		//TODO but write test game a la cruise's banner 
	
	CGFloat inset = self.textView.textContainerInset.width;

	CGFloat lfPadding = self.textView.textContainer.lineFragmentPadding;
	inset += lfPadding;

	inset = (2 * inset);

	if (self.style & OS_BANNER_STYLE_VSCROLL) {
		NSScroller *scroller = self.scrollView.verticalScroller;
		if (scroller != nil) {
			CGFloat size = [self widthOfScroller:scroller];
			inset += size;
		}
	}
	
	return inset;
}
	
- (CGFloat)totalVerticalInsetForView:(BOOL)respectInsetsIfNoText
{
	CGFloat inset = 0;
	BOOL respectInsets;
	if (respectInsetsIfNoText) {
		respectInsets = YES;
	} else {
		NSUInteger textLen = self.textView.string.length;
		respectInsets = (textLen >= 1);
	}
	
	if (respectInsets) {
		inset = self.textView.textContainerInset.height;
		inset = (2 * inset);
	} else {
		int brkpt = 1;
	}
	
	if (self.style & OS_BANNER_STYLE_HSCROLL) {
		NSScroller *scroller = self.scrollView.horizontalScroller;
		if (scroller != nil) {
			CGFloat size = [self widthOfScroller:scroller];
			inset += size;
		}
	}
	
	return inset;
}

- (CGFloat)widthOfScroller:(NSScroller *)scroller
{
	CGFloat res = 0.0;
	
	if (scroller != nil) {
		NSControlSize ctrlSize = [scroller controlSize];
		NSScrollerStyle scrollerStyle = [scroller scrollerStyle];
		CGFloat size = [NSScroller scrollerWidthForControlSize:ctrlSize scrollerStyle:scrollerStyle];
		res = size;
	}
	
	return res;
}

//TODO inline use of:
- (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 = [self fontHeight:bannerFont];
			break;
		case OS_BANNER_ALIGN_LEFT:
		case OS_BANNER_ALIGN_RIGHT:
			res = [self fontWidth:bannerFont];
			break;
		default:
			XT_ERROR_1(@"unknown alignment %lu", self.alignment);
			res = [self.fontUtils defaultTextLineHeight:bannerFont];
			break;
	}
	
	return res;
}

- (CGFloat)fontHeight:(NSFont *)font
{
	CGFloat res = [self.fontUtils defaultTextLineHeight:font];
	return res;
}

- (CGFloat)fontWidth:(NSFont *)font
{
	NSString *string = @"0000000000";
	CGFloat widthOfString = [self.fontUtils defaultWidthOfString:string inFont:font];
	CGFloat res = widthOfString / ((CGFloat)string.length);
	return res;
}

- (void)tearDownLayoutViews
{
	//XT_DEF_SELNAME;

	for (XTBannerHandler *child in self.childHandlers) {
		[child tearDownLayoutViews];
	}

	[self.scrollView removeFromSuperview];
	
	for (NSView *view in self.layoutViews) {
		[view removeFromSuperview];
	}
	[self.layoutViews removeAllObjects];
}

- (void)mainThread_removeHandler
{
	//XT_TRACE_ENTRY;

	if ([self isForMainOutputArea]) {
		[self.outputFormatter teardown];
		return;
	}

	for (XTBannerHandler *child in self.childHandlers) {
		[child teardownView];
	}
	[self.childHandlers removeAllObjects];
	
	[self teardownView];
	
	if (self.parentHandler != nil) {
		[self.parentHandler.childHandlers removeObject:self];
		self.parentHandler = nil;
	}
}

- (void)teardownView
{
	//XT_TRACE_ENTRY;
	
	if (! self.hasTornDownView) {
		//XT_TRACE_0(@"! self.hasTornDownView");
		[self.outputFormatter teardown];
		[self.textView teardown];
		self.textView = nil;
		[self teardownReceptionOfAppLevelNotifications];
		[self.scrollView removeFromSuperview];
		self.scrollView = nil;
		for (NSView *view in self.layoutViews) {
			[view removeFromSuperview];
		}
		[self.layoutViews removeAllObjects];
		self.hasTornDownView = YES;
	}
}

- (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)mainThread_flushTradStatusLineScoreString
{
	//XT_DEF_SELNAME;
	//XT_TRACE_0(@"");
	
	if (self.isForTradStatusLine) {

		NSMutableString *textStorage = [[self.textView textStorage] mutableString];
		
		//TODO mv to sep method:
		if (self.tradStatusLineScoreStringStartIndex != nil) {
			NSUInteger startIndex = self.tradStatusLineScoreStringStartIndex.unsignedIntegerValue;
			NSUInteger lengthToDelete = textStorage.length - startIndex;
			NSRange rangeWhitespaceAfterLastNewline = NSMakeRange(startIndex, lengthToDelete);
			if (rangeWhitespaceAfterLastNewline.location != NSNotFound) {
				[textStorage deleteCharactersInRange:rangeWhitespaceAfterLastNewline];
				[self.outputFormatter removeTabStopAtRhsOfWindow];
			}
		}
		
		if (self.tradStatusLineScoreString != nil) {
			self.tradStatusLineScoreStringStartIndex = [NSNumber numberWithUnsignedInteger:textStorage.length];
			
			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];
			
			if (emulateHtmlBanner) {
				[self setItalicsMode:NO];
			}
		} else {
			self.tradStatusLineScoreStringStartIndex = nil;
		}
	}
}

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

	if (! [self isGridMode]) {
		[self processFormattingQueue];
	} else {
		[self processFormattingQueueInGridMode];
	}
	
	//TODO mv up?
	[self mainThread_flushTradStatusLineScoreString];
	
	if (! [self isGridMode]) {
		if (self.style & OS_BANNER_STYLE_AUTO_VSCROLL) {
			[self scrollToBottom];
		}
		// (horiz. scroll not relevant unless grid mode)
	} else {
		if (self.style & OS_BANNER_STYLE_AUTO_HSCROLL) {
			[self scrollToGridInsertionPosition];
		} else if (self.style & OS_BANNER_STYLE_AUTO_VSCROLL) {
			[self scrollToGridInsertionLine];
		}
	}

	XT_TRACE_0(@"exit");
}

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

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

- (void)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.tradStatusLineScoreStringStartIndex = nil;
}

//TODO mv
- (void)executeClear
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"enter");
	
	if (! [self isForMainOutputArea]) {
		[[[self.textView textStorage] mutableString] setString:@""];
	}
	
	[self.gridTextModel clear];
	
	[XTNotifications notifySetFocusToMainOutputView:self];
}

- (void)mainThread_setSize:(NSArray *)args
{
	XT_DEF_SELNAME;

	NSNumber *isAdvisoryWrapped = args[2];
	if (isAdvisoryWrapped.boolValue) {
		XT_TRACE_0(@"isAdvisory==YES, skipping");
		return;
	}
	
	NSNumber *sizeWrapped = args[0];
	NSNumber *sizeUnitsWrapped = args[1];
	
	self.size = sizeWrapped.unsignedIntegerValue;
	self.sizeUnits = sizeUnitsWrapped.unsignedIntegerValue;

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

//TODO run on non vm thread?
- (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 (XTBannerHandler *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;
	}
}

- (NSUInteger)heightOfContents:(CGFloat *)heightOfText
{
	XT_TRACE_ENTRY;
	
	NSUInteger res;

	if ([self isGridMode]) {
		if ([self.textView countCharsInText] >= 1) {
			res = self.gridTextModel.maxRowIndex + 1;
		} else {
			res = 0;
		}
	} else {
		res = [self.textView countRenderedTextLines];
	}

	if (heightOfText != nil) {
		if (res >= 1) {
			CGFloat hOAL = [self.fontUtils heightOfText:self.textView];
			*heightOfText = hOAL;
		} else {
			*heightOfText = 0.0;
		}
	}
	
	return res;
}

- (NSUInteger)widthOfContents:(CGFloat *)widthOfLongestIndivisibleWord
{
	XT_TRACE_ENTRY;
	
	NSUInteger res;
	
	if ([self isGridMode]) {
		if ([self.textView countCharsInText] >= 1) {
			res = self.gridTextModel.maxColumnIndex + 1;
			if (widthOfLongestIndivisibleWord != nil) {
				// for grid banners, lines are indivisible
				*widthOfLongestIndivisibleWord = [self.fontUtils 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 = [self.fontUtils 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
	//TODO handle coloured horiz divider lines
	
	BOOL isMainAreaBanner = [self isForMainOutputArea];
	
	if (isMainAreaBanner) {
		// "Main" banner, i.e. main output view
		self.textView.backgroundColor = self.prefs.outputAreaBackgroundColor;
		// nothing to do for input color (but see formatter)
		//TODO? cursor color
		
	} else {
		// Regular banner
		self.textView.backgroundColor = self.prefs.statusLineBackgroundColor;
	}
	
	[self.textView setNeedsDisplay:YES];
	
	//TODO why is this needed?:
	if (isMainAreaBanner) {
		[self scrollToBottom];
	} else {
		[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 != 0) {
		XT_WARN_1(@"self.formattingQueue.count was %lu - emptying", self.formattingQueue.count);
		[self.formattingQueue removeAllObjects];
	}

	self.formattingQueue = formattingQueue;
	
	[self processFormattingQueue];
	
	self.formattingQueue = [NSMutableArray array];
}

//----------- Internal: --------------

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

- (void)addEdgeConstraint:(NSLayoutAttribute)edge
				superview:(NSView *)superview
				  subview:(NSView *)subview
{
	[superview addConstraint:[NSLayoutConstraint constraintWithItem:subview
														  attribute:edge
														  relatedBy:NSLayoutRelationEqual
															 toItem:superview
														  attribute:edge
														 multiplier:1
														   constant:0]];
}

- (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?!
		//TODO dep on style flag?

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

//TODO refactor wrt createNewScrollViewWithTextView
- (NSScrollView*)createNewScrollViewWithTextViewForMainOutputArea
{
	NSRect tempFrame = NSMakeRect(0.0, 0.0, 0.0, 0.0);
	XTScrollView *scrollView = [[XTScrollView alloc] initWithFrame:tempFrame];
	//scrollView.allowUserScrolling = NO;
		// allow mouse scroll wheel in main output area
	
	NSSize contentSize = [scrollView contentSize];
	[scrollView setBorderType:NSNoBorder];
	
	BOOL hasVerScrollBar = YES;
	BOOL hasHorScrollBar = NO;
	[scrollView setHasVerticalScroller:hasVerScrollBar];
	[scrollView setHasHorizontalScroller:hasHorScrollBar];
	
	[scrollView setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
	[scrollView setTranslatesAutoresizingMaskIntoConstraints:NO];
	
	NSRect tvFrame = NSMakeRect(0.0, 0.0, 0.0, 0.0);
	NSTextView *textView = [[XTOutputTextView 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];
	[textContainer setWidthTracksTextView:YES];
	
	[scrollView setDocumentView:textView];

	textView.delegate = self;
	
	return scrollView;
}

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

	[[self getOutputTextParser] flush];
	[self.outputTextParserHtml resetForNextCommand];
	[self.outputTextParserPlain resetForNextCommand];
	[self.formattingQueue removeAllObjects];
	[self.outputFormatter 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];
	}
}

- (void)processFormattingQueue
{
	XT_DEF_SELNAME;
	
	BOOL abortProcessing = NO;
	
	while ([self.formattingQueue count] >= 1 && ! abortProcessing) {
		
		id parsedElement = [self.formattingQueue firstObject];
		[self.formattingQueue removeObjectAtIndex:0];
		
		NSArray *formattedOutputElements = [self.outputFormatter formatElement:parsedElement];
		
		for (XTFormattedOutputElement *outputElement in formattedOutputElements) {
			
			if ([outputElement isRegularOutputElement]) {
				
				NSMutableAttributedString *lastAttrStringAppended = outputElement.attributedString;
				[self appendAttributedStringToTextStorage:lastAttrStringAppended];
				[self.textView ensureLayoutForTextContainer]; // or else x pos calc doesn't work
					//TODO mv call into appendAttributedStringToTextStorage?
			
			} else if ([outputElement isTabElement]) {

				XTHtmlTagTab *tagTab = (XTHtmlTagTab *)outputElement.htmlTag;
				[self flushPendingNewline];
					//TODO always right? really only need to for attrs "id" and "to"?
				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.outputTextHandler.gameTitle = [NSMutableString stringWithString:@""];
				} else {
					[self.outputTextHandler.gameTitle appendString:outputElement.attributedString.string];
				}
				
			} else if ([outputElement isBannerEndElement]) {

				[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 {
				XT_ERROR_1(@"unknown XTFormattedOutputElement %d", outputElement.elementType);
			}
		}

		[self.textView ensureLayoutForTextContainer]; // or else x pos calc doesn't work
	}
	
	// Seems to be cause of blighted isle arrow-key / scrolling error:
	//[self scrollToEnd];
	
	//XT_TRACE_1(@"exit formattingQueue.count=%lu", self.formattingQueue.count);
}

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

- (id<XTOutputTextParserProtocol>)getOutputTextParser
{
	id<XTOutputTextParserProtocol> res = (self.htmlMode ? self.outputTextParserHtml : self.outputTextParserPlain);
	return res;
}

- (void)appendAttributedStringToTextStorage:(NSMutableAttributedString *)attrString
{
	XT_DEF_SELNAME;
	
	if (attrString == nil || attrString.length == 0) {
		return;
	}
	
	//XT_TRACE_1(@"\"%@\"", attrString.string);
	
	[self flushPendingNewline];

	if (self.outputFormatter.isForBanner) {
		if ([XTStringUtils string:attrString.string endsWith:@"\n"]) {
			self.pendingNewline = YES;
			NSRange deleteRange = NSMakeRange(attrString.length - 1, 1);
			[attrString deleteCharactersInRange:deleteRange];
		}
	}

	if (attrString.length >= 1) {
		
		NSTextStorage *ts = [self.textView textStorage];
		
		NSUInteger insertionIndexBefore = ts.length;
		[ts appendAttributedString:attrString];
		
		// Apply temporary attributes:
		NSDictionary *attrDict = [attrString attributesAtIndex:0 effectiveRange:nil];
		if (attrDict != nil) {
			NSDictionary *tempAttrDict = attrDict[XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT];
			if (tempAttrDict != nil && tempAttrDict.count >= 1) {
				NSUInteger insertionIndexAfter = ts.length;
				NSRange range = NSMakeRange(insertionIndexBefore, insertionIndexAfter - insertionIndexBefore);
				[self.textView.layoutManager addTemporaryAttributes:tempAttrDict forCharacterRange:range];
			}
		}
	}
	
	// Make sure selected range is at end of text, so that calc'ing x pos for tabs stops will work:
	NSUInteger index = [self.textView textStorage].length;
	[self.textView setSelectedRange:NSMakeRange(index, 0)];
		//TODO use method on tV class!
}

- (void)flushPendingNewline
{
	XT_DEF_SELNAME;

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

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

- (void)scrollToBottom
{
	NSUInteger len = self.textView.string.length;
	[self.textView scrollRangeToVisible:NSMakeRange(len, 1)];
}

// 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;
	
	[self.textView unselectText];
}

@end
