//
//  XTBaseTextHandler.m
//  XTads
//
//  Created by Rune Berg on 11/08/2017.
//  Copyright © 2017 Rune Berg. All rights reserved.
//

#import "XTBaseTextHandler.h"
#import "XTOutputFormatter.h"
#import "XTHtmlTagT2Hilite.h"
#import "XTHtmlTagWhitespace.h"
#import "XTHtmlTagTable.h"
#import "XTHtmlTagTr.h"
#import "XTHtmlTagTd.h"
#import "XTHtmlTagText.h"
#import "XTHtmlTagCenter.h"
#import "XTHtmlTagQ.h"
#import "XTHtmlTagSecondOutermost.h"
#import "XTLogger.h"
#import "XTBaseTextHandler_private.h"
#import "XTBannerTextHandler.h"
#import "XTCallOnMainThreadCounter.h"
#import "XTNotifications.h"
#import "XTViewLayoutUtils.h"
#import "XTStringUtils.h"
#import "XTMutableAttributedStringHelper.h"
#import "XTTabStopUtils.h"


@interface XTBaseTextHandler ()

@property BOOL hasTornDown;
@property NSInteger oldTextLength;
@property CGFloat lastGoodTextViewVisibleHeight;

@property NSMutableArray<XTFormattedOutputElement *>* tempFormattedElementQueueForTable;
	//TODO !!! use XTFormattedElementQueue ?

@end


@implementation XTBaseTextHandler

static XTLogger* logger;

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

@synthesize htmlMode = _htmlMode;

@synthesize isInTable = _isInTable;

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

- (id)init
{
	//XT_DEF_SELNAME;
	
	self = [super init];
	if (self != nil) {
		_bannerIndex = 0;
		_debugName = [NSString stringWithFormat:@"b-%lu", self.bannerIndex];
		_gameWindowController = nil;
		_siblingForWhere = nil;
		_formattedElementQueue = [XTFormattedElementQueue new];
		_outputTextParserPlain = [XTOutputTextParserPlain new];
		_outputTextParserHtml = [XTOutputTextParserHtml new];
		_outputFormatter = [XTOutputFormatter new];
		_outputFormatter.isForBanner = NO;
		_htmlMode = NO;
		_childHandlers = [NSMutableArray array];
		_layoutViews = [NSMutableArray arrayWithCapacity:5];
		_prefs = [XTPrefs prefs];
		_textStorageBatcher = [XTTextStorageBatcher new];
		_hasTornDown = NO;
		_oldTextLength = 0;
		_maxTextViewHeightBeforePagination = 0.0;
		_visibleHeightBeforeLayoutOfViews = 0.0;
		_totalHeightBeforeLayoutOfViews = 0.0;
		_nonstopModeState = NO;
		_visibleRectAtStartOfWindowResize = NSZeroRect;
		_lastGoodTextViewVisibleHeight = 0.0;
		_processFormattedElementQueueClearedTextStorage = NO;
		_isInTable = NO;
		_tempFormattedElementQueueForTable = nil;
	}
	return self;
}

- (void)setBannerIndex:(NSUInteger) bannerIndex
{
	_bannerIndex = bannerIndex;
}

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

- (void)mainThread_setHtmlMode:(NSArray *)args
{
	[self processTagTree];

	NSNumber *htmlMode = args[0];
	[self setHtmlMode:htmlMode.boolValue];
}

- (void)setHiliteMode:(BOOL)hiliteMode
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"%d", hiliteMode);
	
	// represent hilite mode on/off by a special tag object
	XTHtmlTagT2Hilite *t2TagHilite = [XTHtmlTagT2Hilite new];
	t2TagHilite.closing = (! hiliteMode);
	//TODO !!! flush like in oshtml_display_html_tags(const textchar_t *txt)
	id<XTOutputTextParserProtocol> parser = [self getOutputTextParser];
	[parser appendTagToCurrentContainer:t2TagHilite];
}

- (void)setAbortProcessingTags
{
	self.abortProcessingTags = YES;
}

- (void)receiveFormattedElements:(NSArray *)formattedElements
{
	if (formattedElements.count >= 1) {
		[self.formattedElementQueue addObjectsFromArray:formattedElements];
	}
}

- (void)resetForNextCommand
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"");
	
	[[self getOutputTextParser] flush];
	[self.outputTextParserHtml resetForNextCommand];
	[self.outputTextParserPlain resetForNextCommand];
	[self.outputFormatter resetForNextCommand];

	self.needMorePrompt = NO;
	self.processFormattedElementQueueClearedTextStorage = NO;

	//TODO !!! clear batched state?
	
	[self mainThread_noteStartOfPagination];
}

- (void)clearTextStorage
{
	NSMutableAttributedString *textStorage = [self.textView textStorage];
	[XTStringUtils removeXTadsParagraphStyles:textStorage];
	[[textStorage mutableString] setString:@""];
}

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

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

	if (attrString == nil || attrString.length == 0) {
		return nil;
	}
	
	NSTextStorage *textStorage = [self.textView textStorage];

	[textStorage appendAttributedString:attrString];
	//XT_WARN_1(@"\"%@\"", attrString.string);
	
	[self applyCustomTemporaryAttributesToEndOfTextStorage:attrString];

	// Make sure selected range is at end of text, so that calc'ing x pos for tab stops will work:
	[self moveCursorToEndOfOutputPosition];
	
	return attrString;
}

- (void)applyCustomTemporaryAttributesToEndOfTextStorage:(NSAttributedString *)attrString
{
	NSUInteger idxAttrString = 0;
	NSUInteger lengthAttrString = attrString.length;

	NSTextStorage *textStorage = [self.textView textStorage];
	NSUInteger idxTextStorage = textStorage.length - attrString.length; //TODO !!! mv into loop?
	
	while (idxAttrString < lengthAttrString) {
		NSRange rangeAttrString;
		NSDictionary *attrDict = [attrString attributesAtIndex:idxAttrString effectiveRange:&rangeAttrString];
		if (attrDict != nil) {
			NSDictionary *tempAttrDict = attrDict[XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT];
			if (tempAttrDict != nil && tempAttrDict.count >= 1) {
				NSRange rangeTextStorage = NSMakeRange(idxTextStorage, rangeAttrString.length);
				[self.textView.layoutManager addTemporaryAttributes:tempAttrDict forCharacterRange:rangeTextStorage];
			}
		}
		idxAttrString += rangeAttrString.length;
		idxTextStorage += rangeAttrString.length;
	}
}

- (void)addChildHandler:(XTBannerTextHandler *)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) {
					XTBannerTextHandler *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:(XTBannerTextHandler*)childHandler
{
	NSInteger res = NO_CHILD_INDEX;
	if (childHandler != nil) {
		NSInteger index = 0;
		for (XTBannerTextHandler *child in self.childHandlers) {
			if (child == childHandler) {
				res = index;
				break;
			}
			index += 1;
		}
	}
	return res;
}

- (void)mainThread_pumpOutputText:(NSMutableArray *)params
{
	//XT_WARN_ENTRY;
	
	self.needMorePrompt = [self processTagTree];
	
	NSNumber *retVal = [NSNumber numberWithBool:self.needMorePrompt];
	[params setObject:retVal atIndexedSubscript:1];
	
	//XT_WARN_1(@"lengthOfTempFormattedElementQueueForTable=%lu", [self lengthOfTempFormattedElementQueueForTable]);
}

- (void)mainThread_pumpOutputTextForceFlush:(NSMutableArray *)params
{
	//XT_TRACE_ENTRY;
	
	self.needMorePrompt = [self processTagTree];
	
	[self handleTableEnd];
	if (! self.needMorePrompt) {
		[self flushFormattingQueue];
			//TODO? adapt: could in principle get us over the pagin. limit
	}

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

//TODO !!! mv
- (void)flushBatchedtext
{
	NSArray<NSMutableAttributedString *> *attrStringArray = [self.textStorageBatcher flush];
	NSMutableArray<NSMutableAttributedString *> *attrStringMutArray = [NSMutableArray arrayWithArray:attrStringArray];
	[self expandTabsAndAppendToTextStorage:attrStringMutArray];
}

- (void)ensureWeHaveCurrentTag
{
	if (self.currentTag == nil) {
		self.currentTag = [[self getOutputTextParser] getOuterContainer];
	}
}

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

	NSUInteger lengthOfTextBefore = self.textView.textStorage.length;
	
	self.abortProcessingTags = NO;
	BOOL reachedPaginationLimit = NO;
	self.processFormattedElementQueueClearedTextStorage = NO;
	
	while (self.currentTag != nil && [self.currentTag isReadyToFormat] && ! self.abortProcessingTags && ! reachedPaginationLimit) {
		XTHtmlTag *oldCurrentTag = self.currentTag; // useful for debugging
		
		XTFormattedElementQueue *fmtEltQueue = self.formattedElementQueue;  // keep - useful for debugging

		//XT_WARN_2(@"iter: <%@> (uid=%lu)", self.currentTag.name, self.currentTag.uniqueId);
		
		if (self.currentTag.hasFormatted) {
			int brkpt = 1;
		}
		
		BOOL shouldFormat = NO;
		
		if ([self.outputFormatter willProcessTag:self.currentTag]) {
			
			//XT_WARN_2(@"will process: <%@> (uid=%lu)", self.currentTag.name, self.currentTag.uniqueId);

			[self.currentTag checkNotHasFormatted];
			shouldFormat = ! self.currentTag.hasFormatted;
			
			//XT_WARN_3(@"shouldFormat: <%@> (uid=%lu) --> %d", self.currentTag.name, self.currentTag.uniqueId, shouldFormat);
			
			if (shouldFormat) {
				[self.currentTag preFormatForBlockLevel:self.outputFormatter textHandler:self];
			}

			NSUInteger countFormattedElementQueueBefore = self.formattedElementQueue.count;
			
			if (shouldFormat) {
				[self.currentTag format:self.outputFormatter textHandler:self];
				[self.currentTag noteHasFormatted];
			}
			
			NSUInteger countFormattedElementQueueAfter = self.formattedElementQueue.count;
			if (countFormattedElementQueueAfter > countFormattedElementQueueBefore) {
				//TODO !!! messed up that this is done here...
				[self.outputFormatter clearAfterBlockLevelSpacing];
			}

			if (shouldFormat && self.currentTag.isStandalone) {
				BOOL tagIsContainer = [self.currentTag isKindOfClass:[XTHtmlTagContainer class]];
				if (! tagIsContainer) { //TODO !!! exp for testgames/xtads_nested_html/a_tag_tryout/dl_dt_dd_tags_1.t
					[self.currentTag postFormatForBlockLevel:self.outputFormatter textHandler:self];
				}
			}
		} else {
			int brkpt = 1;
			//XT_WARN_2(@"will NOT process: <%@> (uid=%lu)", self.currentTag.name, self.currentTag.uniqueId);
		}
		self.currentTag = [self.currentTag getNextTagToFormat:self.outputFormatter textHandler:self shouldFormat:shouldFormat];
		XTHtmlTag *newCurrentTag = self.currentTag; // keep - useful for debugging

		reachedPaginationLimit = [self processFormattedElementQueue];
		
		[self possiblyRemoveFromContainer:oldCurrentTag];

	} // while (self.currentTag != nil && ...
	
	if (! reachedPaginationLimit) {
		reachedPaginationLimit = [self processBatched];
		//XT_WARN_0(@"after processBatched");
	}
	
	[self possiblyRecalcAllTabStops:lengthOfTextBefore];

	return reachedPaginationLimit;
}

- (void)possiblyRemoveFromContainer:(XTHtmlTag *)tag
{
	if (! [tag isKindOfClass:[XTHtmlTagContainer class]]) {
		if ([tag getContainer] != nil) {
			[tag removeFromContainer];
		}
	} else {
		XTHtmlTagContainer *tagContainer = (XTHtmlTagContainer *)tag;
		if (tagContainer.closed) {
			if (! [tagContainer isKindOfClass:[XTHtmlTagSecondOutermost class]]) {
				if (! [tagContainer hasContents]) {
					if ([tagContainer getContainer] != nil) {
						[tagContainer removeFromContainer];
					}
				}
			}
		}
	}
}

- (void)possiblyRecalcAllTabStops:(NSUInteger)lengthOfTextBefore
{
	//XT_DEF_SELNAME;

	BOOL needRecalcAllTabStops = self.outputFormatter.needsRecalcAllTabStops;
	NSRange rangeOfTextAdded = NSMakeRange(NSNotFound, 0);
	if (needRecalcAllTabStops) {
		NSUInteger lengthOfTextAfter = self.textView.textStorage.length;
		if (self.processFormattedElementQueueClearedTextStorage) {
			rangeOfTextAdded = NSMakeRange(0, lengthOfTextAfter);
		} else {
			NSUInteger lengthAdded = lengthOfTextAfter - lengthOfTextBefore;
			if (lengthAdded >= 1) {
				rangeOfTextAdded = NSMakeRange(lengthOfTextBefore, lengthAdded);
			}
		}
		if (rangeOfTextAdded.location != NSNotFound) {
			//rangeOfTextAdded = NSMakeRange(lengthOfTextBefore, lengthAdded);
			[self.outputFormatter prepareForRecalcAllOfTabStopsInRange:rangeOfTextAdded];
			//XT_WARN_0(@"after prepareForRecalcAllOfTabStops");
		} else {
			//XT_WARN_0(@"after SKIPPING prepareForRecalcAllOfTabStops");
		}
	}

	self.textView.tableCellChangedContentRect = NO;
	//XT_WARN_0(@"before ensureTableCellsAreCorrectlySized");
	[self ensureTableCellsAreCorrectlySized:nil];
	//XT_WARN_0(@"after ensureTableCellsAreCorrectlySized");

	if ((rangeOfTextAdded.location != NSNotFound) || self.textView.tableCellChangedContentRect) {
		//XT_WARN_0(@"before recalcAllTabStops");
		if (rangeOfTextAdded.location != NSNotFound) {
			[self.outputFormatter recalcAllTabStops];
		}
		if (self.textView.tableCellChangedContentRect) {
			if (rangeOfTextAdded.location != NSNotFound) {
				[self.outputFormatter prepareForRecalcAllOfTabStopsInRange:rangeOfTextAdded];
			} else {
				[self.outputFormatter prepareForRecalcAllOfTabStops];
			}
			[self.outputFormatter recalcAllTabStops];
				// 2nd call needed for right-aligned tabs that require stable/final table cell widths
		}
		//XT_WARN_0(@"after recalcAllTabStops");
	} else {
		int brkpt = 1;
	}
	self.textView.tableCellChangedContentRect = NO;
}

- (BOOL)processFormattedElementQueueRegularElementString:(NSAttributedString *)attrString
{
	BOOL reachedPaginationLimit = NO;

	BOOL overBatchLimit = [self.textStorageBatcher append:attrString];
	if (overBatchLimit) {
		// *batch* limit (not pagination limit) - output what we can until hitting pagination limit
		reachedPaginationLimit = [self processBatchedOutputUntilReachedPaginationLimit];
		if (! reachedPaginationLimit) {
			[self.textStorageBatcher reset];
		}
	}
	
	return reachedPaginationLimit;
}

- (BOOL)processBatched
{
	BOOL reachedPaginationLimit = [self processBatchedOutputUntilReachedPaginationLimit];
	if (! reachedPaginationLimit) {
		[self.textStorageBatcher reset];
	}
	
	return reachedPaginationLimit;
}

- (BOOL)hasElementsInFormattedElementQueue
{
	BOOL res = ! [self.formattedElementQueue isEmpty];
	return res;
}

- (void)ensureTableCellsAreCorrectlySized:(XTHtmlTag *)tagFormatted
{
	//XT_DEF_SELNAME;

	//TODO !!! exp:
	//[self.gameWindowController mainThread_layoutAllBannerViews];
	
	[self.textView forceNewLayoutForTextContainer]; // or else table cells might not be resized in display

	//TODO !!! exp:
	//[self.gameWindowController mainThread_layoutAllBannerViews];
}

- (BOOL)checkIfHasReachedPaginationLimit
{
	//XT_DEF_SELNAME;
	
	BOOL res = NO;
	if ([self paginationIsActive]) {
		[self.textView ensureLayoutForTextContainer];
		res = [self recalcPagination];
	}
	return res;
}
- (BOOL)processBatchedOutputUntilReachedPaginationLimit
{
	XT_DEF_SELNAME;
	
	//TODO !!! rewrite comment:
	// try to re-insert part of string that broke the pagination limit,
	// by finding index of longest batched prefix that doesn't break the pagination limit:
	
	//XT_WARN_0(@"replay start...");
	
	NSUInteger count = [self.textStorageBatcher getBatchedSubStringCount];
	if (count == 0) {
		return NO;
	}
	
	NSInteger indexLowestTried = 0;
	NSInteger indexHighestTried = count - 1;
	NSInteger indexHighestNotBreakingPaginationLimit = -1;
	NSInteger batchedSubStringIndex = indexHighestTried;
	NSUInteger iterCount = 0;
	NSMutableArray<NSMutableAttributedString *> *attrStringMutArrayLastAppended = nil;
	BOOL res = NO;
	BOOL mustRemoveStringThatBrokePaginationLimit = NO;
	
	while (indexLowestTried <= indexHighestTried) {
		
		NSArray<NSMutableAttributedString *> *attrStringArray = [self.textStorageBatcher getBatchedSubStringsToIndex:batchedSubStringIndex];
		NSMutableArray<NSMutableAttributedString *> *attrStringMutArray = [NSMutableArray arrayWithArray:attrStringArray];
		[self expandTabsAndAppendToTextStorage:attrStringMutArray];
		attrStringMutArrayLastAppended = attrStringMutArray;

		BOOL reachedPaginationLimitOnReInsert = [self checkIfHasReachedPaginationLimit];
		
		if (! reachedPaginationLimitOnReInsert) {
			if (batchedSubStringIndex > indexHighestNotBreakingPaginationLimit) {
				// best hit so far:
				indexHighestNotBreakingPaginationLimit = batchedSubStringIndex;
				//XT_WARN_1(@"replay batched, !reached, ...IndexHighestNotBreakingPaginationLimit=", batchedSubStringIndexHighestNotBreakingPaginationLimit);
			}
			// attrString might be too short - look at longer prefixes:
			if (batchedSubStringIndex + 1 > indexLowestTried) {
				indexLowestTried = batchedSubStringIndex + 1;
			} else {
				indexLowestTried += 1;
			}
			//XT_WARN_2(@"replay, !reached, Index=%lu - look at longer prefixes, batchedSubStringIndexLowestTried=%ld",
			//		  batchedSubStringIndex, batchedSubStringIndexLowestTried);
		} else {
			res = YES;
			// attrString is too long - look at shorter prefixes:
			if (batchedSubStringIndex - 1 < indexHighestTried) {
				indexHighestTried = batchedSubStringIndex - 1;
			} else {
				indexHighestTried -= 1;
			}
			//XT_WARN_2(@"replay, reached, Index=%lu - look at shorter prefixes, batchedSubStringIndexHighestTried=%ld",
			//		  batchedSubStringIndex, batchedSubStringIndexHighestTried);
		}
		
		if (indexLowestTried <= indexHighestTried) {
			// will make another iteration, so rm text we appended to text storage:
			[self removeFromTextStorage:attrStringMutArray];
			attrStringMutArrayLastAppended = nil;
		} else {
			// will not make another iteration, but note if we need to remove string that broke pagination limit
			if (reachedPaginationLimitOnReInsert) {
				mustRemoveStringThatBrokePaginationLimit = YES;
			}
		}

		if (indexLowestTried == indexHighestTried) {
			batchedSubStringIndex = indexLowestTried;
		} else if (indexHighestTried > indexLowestTried) {
			//TODO need to look closer at when diff is 1?
			batchedSubStringIndex = indexLowestTried + ((indexHighestTried - indexLowestTried) / 2);
		} else {
			//XT_ERROR_2(@"batchedSubStringIndexHighestTried=%ld < batchedSubStringIndexLowestTried=%ld",
			//		   indexHighestTried, indexLowestTried); // can happen now that we start with batchedSubStringIndex = count - 1
			batchedSubStringIndex = indexLowestTried + ((indexHighestTried - indexLowestTried) / 2);
		}
		//XT_WARN_3(@"replay batched, ...IndexLowestTried=%ld ...IndexHighestTried=%ld ...Index=%lu",
		//		  batchedSubStringIndexLowestTried, batchedSubStringIndexHighestTried, batchedSubStringIndex);
		//XT_WARN_1(@"replay ...Index=%lu",  batchedSubStringIndex);

		iterCount += 1;
	} // while (indexLowestTried <= indexHighestTried)
	
	//if (iterCount >= 2) {
	//	XT_WARN_2(@"iterCount=%lu for batched count %lu", iterCount, count);
	//}
	
	if (indexHighestNotBreakingPaginationLimit >= 0) {
		//XT_WARN_1(@"replay DONE, re-append \"%@\"", lastAttrStringAppended.string);
		if (mustRemoveStringThatBrokePaginationLimit) {
			NSInteger indextBreakingPaginationLimit = indexHighestNotBreakingPaginationLimit + 1;
			NSAttributedString *attrStringThatBrokePaginationLimit = [self.textStorageBatcher getBatchedSubStringAtIndex:indextBreakingPaginationLimit];
			NSArray<NSAttributedString *> *attrStringArrayToRemove = [NSArray arrayWithObject:attrStringThatBrokePaginationLimit];
			[self removeFromTextStorage:attrStringArrayToRemove];
		}
		[self.textStorageBatcher pruneToIndex:indexHighestNotBreakingPaginationLimit];
	} else {
		// The very first batched entry broke pagination limit
		if (attrStringMutArrayLastAppended == nil) {
			XT_ERROR_0(@"attrStringMutArrayLastSppended == nil"); // shouldn't happen
		}
		[self removeFromTextStorage:attrStringMutArrayLastAppended];
	}
	
	//CGFloat textViewHeightAfterReplay = [self.textView findTotalHeight];
	//NSUInteger lengthOfRemainingBatchedText = [self.textStorageBatcher lengthOfRemainingBatchedText];
	//XT_WARN_2(@"textViewHeightAfterReplay=%lf lengthOfRemainingBatchedText=%ld", textViewHeightAfterReplay, lengthOfRemainingBatchedText);
	
	return res;
}

- (void)expandTabsAndAppendToTextStorage:(NSMutableArray<NSMutableAttributedString *> *)attrStringArray
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"entry");
	
	NSMutableAttributedString *bufferedAttrStringToAppend = [NSMutableAttributedString new];
	
	for (NSUInteger idx = 0; idx < attrStringArray.count; idx++) {
		NSMutableAttributedString *attrString = attrStringArray[idx];
		
		NSArray<XTHtmlTagTab *> *tabTags = [XTMutableAttributedStringHelper getTabTagsForString:attrString];
		if (tabTags.count >= 1) {
			
			// Ensure we have started the latest paragraph in text storage (needed by outputFormatter applyTagTab:):
			[self flushPendingNewline];
				//TODO !!! adapt: probably wrong/unecessary now
			
			[self appendAttributedStringToTextStorage:bufferedAttrStringToAppend];
			bufferedAttrStringToAppend = [NSMutableAttributedString new];

			// Ensure we have started the latest paragraph in text storage (needed by outputFormatter applyTagTab:):
			// (appendAttributedStringToTextStorage above might have set pendingNewline)
			[self flushPendingNewline];

			BOOL didExpandAttrString = NO;
			NSInteger idxBackStart = idx;
			for (XTHtmlTagTab *tagTab in tabTags) { //TODO !!! in what case can there be >1 entries?
				[self.textView ensureLayoutForTextContainer];
				NSMutableAttributedString *expandedAttrString = [self.outputFormatter applyTagTab:tagTab forMutableAttributedString:attrString];
				if (expandedAttrString != attrString) {
					didExpandAttrString = YES;
					attrString = expandedAttrString;
				}
			}
			
			// Important that this is done after applyTagTab:, so tab itself does not affect x-pos calc:
			[self appendAttributedStringToTextStorage:attrString];

			if (! didExpandAttrString) {
				// Apply last paragraph's tab stops (which were updated by applyTagTab:) to attrStringArray back to paragraph break:
				NSMutableParagraphStyle *pgStyleStartOfParagraph = [XTStringUtils getParagraphStyleAtStartOfLastParagraphOf:self.textView.textStorage];
				NSArray *tabStops = pgStyleStartOfParagraph.tabStops;
				NSMutableParagraphStyle *pgStyleAttrString = [XTStringUtils mutableParagraphStyleFor:attrString];
				pgStyleAttrString.tabStops = tabStops;
				for (NSInteger idxBack = idxBackStart; idxBack >= 0; idxBack--) {
					NSMutableAttributedString *attrStringBack = attrStringArray[idxBack];
					if (attrStringBack.length >= 1) {
						unichar lastCh = [XTStringUtils lastChar:attrStringBack.string];
						if ([XTStringUtils isEffectiveNewline:lastCh]) {
							// Hit end of previous paragraph (newline is considered part of *previous* paragraph)
							break;
						}
					}
					NSRange range = NSMakeRange(0, attrStringBack.length);
					[attrStringBack addAttribute:NSParagraphStyleAttributeName value:pgStyleAttrString range:range];
				}
			}
			
			[XTMutableAttributedStringHelper removeTabTagsForString:attrString];
			
		} else {
			[bufferedAttrStringToAppend appendAttributedString:attrString];
			/*TODO exp rm (why was this needed?)
			if ([attrString.string isEqualToString:ZERO_WIDTH_SPACE]) {
				[self appendAttributedStringToTextStorage:bufferedAttrStringToAppend];
				bufferedAttrStringToAppend = [NSMutableAttributedString new];
			}*/
		}
	}
	if (bufferedAttrStringToAppend.length >= 1) {
		[self appendAttributedStringToTextStorage:bufferedAttrStringToAppend];
	}
}

- (void)removeFromTextStorage:(NSArray<NSAttributedString *> *)attrStringArray
{
	//XT_DEF_SELNAME;

	[self flushPendingNewline];

	NSUInteger numCharsToRemove = 0;
	for (NSAttributedString *attrString in attrStringArray) {
		//XT_WARN_1(@"\"%@\"", attrString.string);
		numCharsToRemove += attrString.length;
	}
	
	[self.textView removeTextFromEnd:numCharsToRemove];
}

- (BOOL)recalcPagination
{
	//XT_DEF_SELNAME;
	
	[self moveCursorToEndOfOutputPosition];
	
	CGFloat newTextViewHeight = [self.textView findTotalHeight];
	CGFloat maxTextViewHeightBeforePagination = self.maxTextViewHeightBeforePagination;
	CGFloat exceededBy = newTextViewHeight - maxTextViewHeightBeforePagination;
	
	//if (exceededBy < 0.0) {
	//	int brkpt = 1;
	//}
	
	//if (self.bannerIndex == 0) {
	//	if (exceededBy > 0.0) {
	//		XT_WARN_3(@"exceededBy=%lf (newTextViewHeight=%lf self.maxTextViewHeightBeforePagination=%lf)", exceededBy, newTextViewHeight, self.maxTextViewHeightBeforePagination);
	//	}
	//}
	
	BOOL res = (exceededBy > 0.0);

	// hack for 1893_menu_3a.t repeating more-prompts:
	NSInteger textLength = self.textView.attributedString.length;
	if (res && ((textLength == self.oldTextLength) || (textLength <= 10))) {
		res = NO;
	}
	self.oldTextLength = textLength;

	//if (res) {
	//	int brkpt = 1;
	//	XT_TRACE_1(@"--> YES, exceeded by %f", exceededBy);
	//}
	
	return res;
}

- (BOOL)paginationIsActive
{
	return (! self.nonstopModeState);
}

- (BOOL)awaitingMorePromptForPagination
{
	return self.needMorePrompt;
}

- (void)clearWhitespaceBeforeOrAfterBlockLevelTagOutputElement
{
	if ([self.textStorageBatcher hasContents]) {
		[self.textStorageBatcher removeTrailingWhitespace];
		NSUInteger lengthOfBatched = [self.textStorageBatcher lengthOfRemainingBatchedText];
		if (lengthOfBatched == 0) {
			[XTStringUtils removeTrailingWhitespaceInLastParagraph:self.textView.textStorage];
		}
	} else {
		[XTStringUtils removeTrailingWhitespaceInLastParagraph:self.textView.textStorage];
	}
}

- (void)handleBody:(XTFormattedOutputElement *)outputElement
{
	[self flushBatchedtext];
	
	XTHtmlTagBody *tag = (XTHtmlTagBody *)outputElement.htmlTag;
	[self.outputFormatter executeTagBody:tag];
}

- (void)handleTableStart
{
	//XT_DEF_SELNAME;
	//XT_WARN_1(@"isInTable = %ld", _isInTable);
	
	_isInTable = YES;
	self.tempFormattedElementQueueForTable = [NSMutableArray array];
}

- (void)handleTableEnd
{
	//XT_DEF_SELNAME;
	//XT_WARN_1(@"isInTable = %ld", _isInTable);

	_isInTable = NO;
	NSArray *array = [self.outputFormatter cancelOngoingTable];
	for (XTFormattedOutputElement *fmtElt in array) {
		[self addToTempFormattedElementQueueForTable:fmtElt];
	}
	
	if (self.tempFormattedElementQueueForTable.count == 0) {
		return;
	}
	
	//TODO !!! exp rm: [self optimizeTempFormattedElementQueueForTable];
	[self mergeTempFormattedElementQueueForTable];
	self.tempFormattedElementQueueForTable = nil;
}

- (void)addToTempFormattedElementQueueForTable:(XTFormattedOutputElement *)fmtOutputElement
{
	if (self.tempFormattedElementQueueForTable == nil) {
		self.tempFormattedElementQueueForTable = [NSMutableArray array];
	}	
	[self.tempFormattedElementQueueForTable addObject:fmtOutputElement];
}

- (NSUInteger)lengthOfTempFormattedElementQueueForTable
{
	NSUInteger res = self.tempFormattedElementQueueForTable.count;
	return res;
}

/*TODO !!! rm if not needed
- (void)optimizeTempFormattedElementQueueForTable
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");
	
	NSMutableArray<XTFormattedOutputElement *>* newTempFormattedElementQueueForTable = [NSMutableArray arrayWithCapacity:self.tempFormattedElementQueueForTable.count];
	NSMutableAttributedString *ongoingTableCellAttrString = nil;
	XTTextTableBlock *ongoingTextTableBlock = nil;
	NSUInteger mergeCount = 0;
	
	for (XTFormattedOutputElement *fmtOutputElement in self.tempFormattedElementQueueForTable) {
		BOOL didMerge = NO;
		if ([fmtOutputElement isRegularOutputElement]) {
			if (ongoingTableCellAttrString == nil) {
				ongoingTableCellAttrString = [NSMutableAttributedString new];
				mergeCount = 0;
			}
			NSAttributedString *attrString = fmtOutputElement.attributedString;
			NSParagraphStyle *pgStyle = [attrString attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:nil];
			XTTextTableBlock *textTableBlock = nil;
			if (pgStyle.textBlocks.count >= 1) {
				textTableBlock = pgStyle.textBlocks[0];
			}
				//TODO !!! adapt: for nested tables
			if (textTableBlock != nil &&
					(ongoingTextTableBlock == nil || [textTableBlock isEqual:ongoingTextTableBlock]) &&
					mergeCount <= 30) {
				[ongoingTableCellAttrString appendAttributedString:attrString];
				didMerge = YES;
				mergeCount += 1;
			//} else {
			//	XT_DEF_SELNAME;
			//	XT_WARN_1(@"mergeCount = %lu", mergeCount);
			}
			ongoingTextTableBlock = textTableBlock;
			
		} else if ([fmtOutputElement isClearWhitespaceBeforeOrAfterBlockLevelTagOutputElement]) {
			if (ongoingTableCellAttrString != nil) {
				[XTStringUtils removeTrailingWhitespaceInLastParagraph:ongoingTableCellAttrString];
			}
			didMerge = YES;
			
		} else {
			ongoingTextTableBlock = nil;
		}
		
		if (! didMerge) {
			if (ongoingTableCellAttrString != nil) {
				XTFormattedOutputElement *tableCellFmtOutputElement = [XTFormattedOutputElement regularOutputElement:ongoingTableCellAttrString];
				[newTempFormattedElementQueueForTable addObject:tableCellFmtOutputElement];
			}
			if ([fmtOutputElement isRegularOutputElement]) {
				ongoingTableCellAttrString = [[NSMutableAttributedString alloc] initWithAttributedString:fmtOutputElement.attributedString];
			} else {
				[newTempFormattedElementQueueForTable addObject:fmtOutputElement];
				ongoingTableCellAttrString = nil;
			}
			mergeCount = 0;
		}
	}
	if (ongoingTableCellAttrString != nil) {
		XTFormattedOutputElement *tableCellFmtOutputElement = [XTFormattedOutputElement regularOutputElement:ongoingTableCellAttrString];
		[newTempFormattedElementQueueForTable addObject:tableCellFmtOutputElement];
	}

	self.tempFormattedElementQueueForTable = newTempFormattedElementQueueForTable;
}
*/

- (void)mergeTempFormattedElementQueueForTable
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");

	[self.formattedElementQueue prependObjectsFromArray:self.tempFormattedElementQueueForTable];

	//XT_WARN_0(@"exit");
}

//TODO !!! mv
- (CGFloat)findVisibleHeight
{
	//XT_DEF_SELNAME;
	CGFloat textViewVisibleHeight = [self.textView findVisibleHeight];
	if (textViewVisibleHeight > 0.0) {
		//if (self.bannerIndex == 0) {
		//	XT_WARN_2(@"lastGoodTextViewVisibleHeight %lf", self.lastGoodTextViewVisibleHeight, textViewVisibleHeight);
		//}
		self.lastGoodTextViewVisibleHeight = textViewVisibleHeight;
	}
	return self.lastGoodTextViewVisibleHeight;
}

- (void)mainThread_noteStartOfPagination
{
	//XT_DEF_SELNAME;
	//XT_TRACE_0(@"ENTER");

	//if (self.bannerIndex == 0) {
	//	int brkpt = 1;
	//}

	[self moveCursorToEndOfOutputPosition];
	
	CGFloat textViewVisibleHeight = [self findVisibleHeight];
	
	CGFloat textViewHeight = [self.textView findTotalHeight];  // height of text regardless of visible portion
	//if (self.bannerIndex == 0) {
		//XT_WARN_1(@"ENTER textViewHeight=%lf", textViewHeight);
	//}
	
	NSFont *currentFontForOutput = [self.outputFormatter getCurrentFontForOutput];
	NSInteger currentFontHeight = currentFontForOutput.pointSize;
	NSInteger verticalInset = (NSInteger)self.textView.topBottomInset;
	
	CGFloat toAdd;
	if (textViewHeight > textViewVisibleHeight) {
		// If we've filled at least a screenfull (or we're in some unstable, initial state)
		//XT_WARN_2(@"textViewHeight=%lf > textViewVisibleHeight=%lf - we've filled at least a screenfull", textViewHeight, textViewVisibleHeight);
		
		NSInteger intToAdd = textViewVisibleHeight;
		if (intToAdd > verticalInset) {
			intToAdd -= verticalInset;
		}
		if (intToAdd > currentFontHeight) {
			intToAdd -= currentFontHeight; // ensure a bit of overlap
		}
		if (intToAdd > currentFontHeight) {
			intToAdd -= (intToAdd % currentFontHeight); // compensate for partially visible lines
		}
		if (intToAdd > textViewVisibleHeight) {
			int brkpt = 1;
		}
		toAdd = intToAdd;
		//if (self.bannerIndex == 0) {
		//	XT_WARN_3(@"textViewHeight=%lf > textViewVisibleHeight=%lf : intToAdd=%lu", textViewHeight, textViewVisibleHeight, intToAdd);
		//}
		
	} else {
		// Before we've filled the first screenfull
		//XT_WARN_2(@"textViewHeight=%lf <= textViewVisibleHeight=%lf - Before we've filled at least a screenfull", textViewHeight, textViewVisibleHeight);

		CGFloat yCoordBottomOfText = [self.textView findYCoordOfInsertionPoint]; // reverse y-axis: 0 is top
		NSInteger intYCoordBottomOfText = yCoordBottomOfText;
		NSInteger intToAdd;
		if (intYCoordBottomOfText >= currentFontHeight) {
			intToAdd = intYCoordBottomOfText;
			//XT_TRACE_1(@"intToAdd = %lu (uintYCoordBottomOfText)", uintYCoordBottomOfText);
			if (intToAdd > currentFontHeight) {
				intToAdd -= currentFontHeight;
			}
			//XT_TRACE_1(@"intToAdd -= %lu (currentFontHeight)", currentFontHeight);
			NSInteger partiallyVisibleLineHeight = (intToAdd % currentFontHeight);
			if (intToAdd > partiallyVisibleLineHeight) {
				intToAdd -= partiallyVisibleLineHeight;  // compensate for partially visible lines
			}
			//XT_TRACE_1(@"uintToAdd -= %lu (partiallyVisibleLineHeight)", partiallyVisibleLineHeight);
			if (intToAdd > verticalInset) {
				//XT_TRACE_1(@"uintToAdd -= %lu (verticalInset)", verticalInset);
				intToAdd -= verticalInset;
			}
			//if (self.bannerIndex == 0) {
			//	XT_WARN_5(@"textViewHeight=%lf <= textViewVisibleHeight=%lf && intYCoordBottomOfText=%ld >= currentFontHeight=%ld : intToAdd=%ld",
			//			  textViewHeight, textViewVisibleHeight, intYCoordBottomOfText, currentFontHeight, intToAdd);
			//}
			//if (intToAdd > textViewVisibleHeight) {
			//	int brkpt = 1;
			//}
		} else {
			// just in case
			intToAdd = intYCoordBottomOfText;
			NSInteger partiallyVisibleLineHeight = (intToAdd % currentFontHeight);
			if (intToAdd > partiallyVisibleLineHeight) {
				intToAdd -= partiallyVisibleLineHeight;
			}
			//if (self.bannerIndex == 0) {
			//	XT_WARN_5(@"textViewHeight=%lf <= textViewVisibleHeight=%lf && intYCoordBottomOfText=%ld < currentFontHeight=%ld : intToAdd=%lu",
			//			  textViewHeight, textViewVisibleHeight, intYCoordBottomOfText, currentFontHeight, intToAdd);
			//}
			//if (intToAdd > textViewVisibleHeight) {
			//	int brkpt = 1;
			//}
		}
		toAdd = intToAdd;
	}
	//XT_TRACE_1(@"toAdd=%f", toAdd);
	//if (toAdd < 0.0) {
	//	XT_ERROR_1(@"toAdd=%f", toAdd);
	//}
	//CGFloat oldMTVHBP = self.maxTextViewHeightBeforePagination;
	self.maxTextViewHeightBeforePagination = textViewHeight + toAdd;
	//XT_WARN_3(@"maxTextViewHeightBeforePagination= textViewHeight=%lf + toAdd=%lf = %lf", textViewHeight, toAdd, self.maxTextViewHeightBeforePagination);
	
	self.visibleHeightBeforeLayoutOfViews = 0.0;
	self.totalHeightBeforeLayoutOfViews = 0.0;
	
	//if (self.bannerIndex == 0) {
	//	XT_WARN_3(@"maxTextViewHeightBeforePagination %lf -> %lf (toAdd=%lf)", oldMTVHBP, self.maxTextViewHeightBeforePagination, toAdd);
	//}
}

- (void)flushFormattingQueue
{
	//XT_DEF_SELNAME;
	
	[self flushBatchedtext];
	
	NSArray *wsArray = [self.outputFormatter flushPendingWhitespace];
	for (XTFormattedOutputElement *wsFmtElt in wsArray) {
		//XT_WARN_1(@"appending pending ws \"%@\"", wsFmtElt.attributedString.string);
		[self appendAttributedStringToTextStorage:wsFmtElt.attributedString];
	}
	[self autoScrollToBottom];
}

- (void)moveCursorToEndOfOutputPosition
{
	[self.textView setInsertionPointAtEndOfText];
}

- (void)mainThread_removeHandler
{
	//XT_TRACE_ENTRY;
	
	for (XTBannerTextHandler *child in self.childHandlers) {
		[child teardown];
	}
	[self.childHandlers removeAllObjects];
	
	[self teardown];
	
	if (self.parentHandler != nil) {
		[self.parentHandler.childHandlers removeObject:self];
		self.parentHandler = nil;
	}
}

- (void)teardown
{
	//XT_TRACE_ENTRY;
	
	if (! self.hasTornDown) {
		//XT_TRACE_0(@"! self.hasTornDownView");
		
		[self.outputTextParserPlain teardown];
		self.outputTextParserPlain = nil;
		[self.outputTextParserHtml teardown];
		self.outputTextParserHtml = nil;

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

		[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.hasTornDown = YES;
	}
}

- (void)tearDownLayoutViews
{
	//XT_DEF_SELNAME;
	
	for (XTBannerTextHandler *child in self.childHandlers) {
		[child tearDownLayoutViews];
	}
	
	[self.scrollView removeFromSuperview];
	
	for (NSView *view in self.layoutViews) {
		[view removeFromSuperview];
	}
	[self.layoutViews removeAllObjects];
}

- (NSView *)internalRebuildViewHierarchy
{
	XT_TRACE_ENTRY;
	
	NSView *ownTopLevelView = self.scrollView;
	if (ownTopLevelView == nil) {
		// can happen when a game window is closing
		return nil;
	}
	
	// Configure children's (top level) views:
	
	NSMutableArray *childViews = [NSMutableArray array];
	//[childViews addObject:self.scrollView];
	
	for (XTBannerTextHandler *child in self.childHandlers) {
		NSView *childView = [child internalRebuildViewHierarchy];
		[childViews addObject:childView];
	}
	
	// Compose own (top level) view:
	
	for (NSInteger i = self.childHandlers.count - 1; i >= 0; i--) {
		
		XTBannerTextHandler *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];
		//if (childViewSize == 0.0) {
		//	XT_WARN_2(@"child %lu childViewSize=%lf", i, childViewSize);
		//}
		BOOL childViewIsAbsSized = (childHandler.sizeUnits == OS_BANNER_SIZE_ABS || childHandler.sizeUnits == OS_BANNER_SIZE_PIXELS);
		[childHandler captureInitialSizeWhenViewSize:childViewSize];
		
		[XTViewLayoutUtils newLayoutInParentView:tempOwnTopLevelView
									  childView1:ownTopLevelView
									  childView2:childView
							 childView2Alignment:childHandler.alignment
								  childView2Size:childViewSize
							childView2IsAbsSized:childViewIsAbsSized];
		
		ownTopLevelView = tempOwnTopLevelView;
	}
	
	return ownTopLevelView;
}

- (void)noteStartOfLayoutOfViews
{
	//XT_DEF_SELNAME;
	
	//self.visibleHeightBeforeLayoutOfViews = [self.textView findVisibleHeight];
	self.visibleHeightBeforeLayoutOfViews = [self findVisibleHeight];
	self.totalHeightBeforeLayoutOfViews = [self.textView findTotalHeight];
	
	//if (self.bannerIndex == 0) {
	//	XT_WARN_1(@"maxTextViewHeightBeforePagination=%f", self.maxTextViewHeightBeforePagination);
	//	XT_WARN_1(@"visibleHeightBeforeLayoutOfViews=%f", self.visibleHeightBeforeLayoutOfViews);
	//	XT_WARN_1(@"totalHeightBeforeLayoutOfViews=%f", self.totalHeightBeforeLayoutOfViews);
	//}
}

- (void)noteEndOfLayoutOfViews
{
	//XT_DEF_SELNAME;
	//XT_TRACE_0(@"ENTER");
	
	if (self.visibleHeightBeforeLayoutOfViews > 0.0) {
		//CGFloat visibleHeightAfterLayoutOfViews = [self.textView findVisibleHeight];
		CGFloat visibleHeightAfterLayoutOfViews = [self findVisibleHeight];
		if (visibleHeightAfterLayoutOfViews > 0.0) {
			
			CGFloat totalHeightAfterLayoutOfViews = [self.textView findTotalHeight];
			
			CGFloat diffTotalHeight = totalHeightAfterLayoutOfViews - self.totalHeightBeforeLayoutOfViews;
			if ((self.maxTextViewHeightBeforePagination + diffTotalHeight) > 0.0) {
				//CGFloat oldMTVHBP = self.maxTextViewHeightBeforePagination;
				self.maxTextViewHeightBeforePagination += diffTotalHeight;
				//if (self.bannerIndex == 0) {
				//	XT_WARN_3(@"maxTextViewHeightBeforePagination=%f (was %f) diffTotalHeight=%lf", self.maxTextViewHeightBeforePagination, oldMTVHBP, diffTotalHeight);
				//}
			}
			
			if (totalHeightAfterLayoutOfViews > visibleHeightAfterLayoutOfViews) {
				CGFloat diffVisibleHeight = visibleHeightAfterLayoutOfViews - self.visibleHeightBeforeLayoutOfViews;
				if (diffVisibleHeight < 0.0) {
					// visible height has shrunk as result of layout
					//CGFloat oldMTVHBP = self.maxTextViewHeightBeforePagination;
					self.maxTextViewHeightBeforePagination += diffVisibleHeight;
					//if (self.bannerIndex == 0) {
					//	XT_WARN_3(@"maxTextViewHeightBeforePagination=%f (was %f) diffVisibleHeight=%lf", self.maxTextViewHeightBeforePagination, oldMTVHBP, diffVisibleHeight);
					//}
				}
			}
		}
		//XT_TRACE_1(@"totalHeight=%f", [self.textView findTotalHeight]);
	} else {
		//if (self.bannerIndex == 0) {
			//TODO !!! do anything for this?
			//XT_WARN_1(@"self.visibleHeightBeforeLayoutOfViews=%lf <= 0.0", self.visibleHeightBeforeLayoutOfViews);
		//}
	}
	self.visibleHeightBeforeLayoutOfViews = 0.0;

	//if (self.bannerIndex == 0) {
	//	XT_WARN_1(@"maxTextViewHeightBeforePagination=%f", self.maxTextViewHeightBeforePagination);
	//	XT_WARN_1(@"visibleHeightBeforeLayoutOfViews=%f", self.visibleHeightBeforeLayoutOfViews);
	//	XT_WARN_1(@"totalHeightBeforeLayoutOfViews=%f", self.totalHeightBeforeLayoutOfViews);
	//}
	
	[self autoScrollToBottom];
}

- (void)clearPaginationState
{
	//if (self.bannerIndex == 0) {
	//	XT_WARN_ENTRY;
	//}
	
	self.maxTextViewHeightBeforePagination = 0.0;
	self.visibleHeightBeforeLayoutOfViews = 0.0;
	self.totalHeightBeforeLayoutOfViews = 0.0;
	//TODO !!! clear batched text?
}

- (void)autoScrollToBottom
{
	if ([self shouldAutoScrollToBottom]) {
		[self scrollToBottom];
	}
}

- (void)scrollToBottom
{
	[self.textView scrollToBottom];
}

- (void)recalcDynamicTabStops:(BOOL)recalcOwnTabStops
{
	if (recalcOwnTabStops) {
		[self.outputFormatter prepareForRecalcAllOfTabStops];
		[self.outputFormatter recalcAllTabStops];
	}
	
	for (XTBannerTextHandler *child in self.childHandlers) {
		[child recalcDynamicTabStops:NO];
	}
}

- (void)updateLinksUnderline
{
	[self.colorationHelper updateLinksUnderline];
}

- (void)callOnMainThread:(SEL)selector
{
	XT_COUNT_CALL_ON_VM_THREAD;
	[self performSelectorOnMainThread:selector
						   withObject:nil
						waitUntilDone:YES];
}

- (void)callOnMainThread:(SEL)selector withObject:(id)obj
{
	XT_COUNT_CALL_ON_VM_THREAD;
	[self performSelectorOnMainThread:selector
						   withObject:obj
						waitUntilDone:YES];
}

//--------- to avoid xcode warnings -:( :

- (void)setColorsFromPrefsColor
{
	XT_DEF_SELNAME;
	XT_ERROR_0(@"should never be called on base class")
}

- (void)setColorsFromPrefAllowGameToSetColors
{
	XT_DEF_SELNAME;
	XT_ERROR_0(@"should never be called on base class")
}

- (BOOL)processTagTree
{
	XT_DEF_SELNAME;
	XT_ERROR_0(@"should never be called on base class")
	
	return NO;
}

- (void)traceWithIndentLevel:(NSUInteger)indentLevel
{
	XT_DEF_SELNAME;
	XT_ERROR_0(@"should never be called on base class")
}

- (BOOL)shouldAutoScrollToBottom
{
	XT_DEF_SELNAME;
	XT_ERROR_0(@"should never be called on base class")
	
	return NO;
}

- (void)teardownReceptionOfAppLevelNotifications
{
	XT_DEF_SELNAME;
	XT_ERROR_0(@"should never be called on base class")
}

@end
