//
//  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 "XTHtmlTagT2Italics.h"
#import "XTLogger.h"
#import "XTBaseTextHandler_private.h"
#import "XTBannerTextHandler.h"
#import "XTCallOnMainThreadCounter.h"
#import "XTNotifications.h"
#import "XTViewLayoutUtils.h"


@interface XTBaseTextHandler ()

@property BOOL hasTornDownView;

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

+ (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;
		_formattingQueue = [NSMutableArray arrayWithCapacity:200];
		_outputTextParserPlain = [XTOutputTextParserPlain new];
		_outputTextParserHtml = [XTOutputTextParserHtml new];
		_outputFormatter = [XTOutputFormatter new];
		_outputFormatter.isForBanner = NO;
		_htmlMode = NO;
		_childHandlers = [NSMutableArray array];
		_layoutViews = [NSMutableArray arrayWithCapacity:5];
		_prefs = [XTPrefs prefs];
		_hasTornDownView = NO;
		_attributedStringThatBrokePaginationLimit = nil;
		_maxTextViewHeightBeforePagination = 0.0;
		_visibleHeightBeforeLayoutOfViews = 0.0;
		_totalHeightBeforeLayoutOfViews = 0.0;
		_nonstopModeState = NO;
		_visibleRectAtStartOfWindowResize = NSZeroRect;
	}
	return self;
}

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

- (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)resetForNextCommand
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"");
	
	[[self getOutputTextParser] flush];
	[self.outputTextParserHtml resetForNextCommand];
	[self.outputTextParserPlain resetForNextCommand];
	if (self.formattingQueue.count >= 1) {
		int brkpt = 1;
	}
	[self.formattingQueue removeAllObjects];
	[self.outputFormatter resetForNextCommand];

	self.attributedStringThatBrokePaginationLimit = nil;
	[self mainThread_noteStartOfPagination];
}

- (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 *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 tab stops will work:
	[self moveCursorToEndOfOutputPosition];
	
	return attrString;
}

- (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 *)retValHolder
{
	//XT_TRACE_ENTRY;

	BOOL needMorePrompt = [self processFormattingQueue];
	NSNumber *retVal = [NSNumber numberWithBool:needMorePrompt];
	[retValHolder setObject:retVal atIndexedSubscript:0];
}

- (BOOL)checkIfHasReachedPaginationLimit:(NSAttributedString *)lastAttrStringAppended
{
	XT_DEF_SELNAME;
	
	BOOL res = NO;
	if ([self paginationIsActive]) {
		if (lastAttrStringAppended.length >= 1) {
			res = [self recalcPagination];
			if (res) {
				// remove the text that brought us over the limit, but rememember it so we can print it next time
				XT_TRACE_1(@"reachedPaginationLimit for \"%@\"", lastAttrStringAppended.string);
				[self.textView removeAttributedStringFromEnd:lastAttrStringAppended];
				self.attributedStringThatBrokePaginationLimit = lastAttrStringAppended;
			}
		}
	}
	return res;
}

- (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;
	}
	
	XT_TRACE_3(@"exceededBy=%f (newTextViewHeight=%f self.maxTextViewHeightBeforePagination=%f)", exceededBy, newTextViewHeight, self.maxTextViewHeightBeforePagination);
	
	BOOL res = (exceededBy > 0.0);
	if (res) {
		XT_TRACE_1(@"--> YES, exceeded by %f", exceededBy);
	}
	
	return res;
}

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

- (BOOL)awaitingMorePromptForPagination
{
	BOOL res = (self.attributedStringThatBrokePaginationLimit != nil);
	return res;
}

- (void)restoreStringThatBrokePaginationLimit
{
	if (self.attributedStringThatBrokePaginationLimit != nil) {
		// print the text that brought us over the limit last time
		[self appendAttributedStringToTextStorage:self.attributedStringThatBrokePaginationLimit];
		self.attributedStringThatBrokePaginationLimit = nil;
	}
}

- (void)mainThread_noteStartOfPagination
{
	XT_DEF_SELNAME;
	XT_TRACE_0(@"ENTER");
	
	[self moveCursorToEndOfOutputPosition];
	
	CGFloat textViewVisibleHeight = [self.textView findVisibleHeight];
	XT_TRACE_1(@"textViewVisibleHeight=%f", textViewVisibleHeight);
	
	CGFloat textViewHeight = [self.textView findTotalHeight];  // height of text regardless of visible portion
	XT_TRACE_1(@"textViewHeight=%f", textViewHeight);
	
	NSFont *currentFontForOutput = [self.outputFormatter getCurrentFontForOutput];
	NSUInteger currentFontHeight = currentFontForOutput.pointSize;
	NSUInteger verticalInset = (NSUInteger)self.textView.topBottomInset;
	
	CGFloat toAdd;
	if (textViewHeight > textViewVisibleHeight) {
		// If we've filled at least a screenfull
		NSUInteger uintToAdd = textViewVisibleHeight;
		uintToAdd -= verticalInset;
		uintToAdd -= currentFontHeight; // ensure a bit of overlap
		uintToAdd -= (uintToAdd % currentFontHeight); // compensate for partially visible lines
		toAdd = uintToAdd;
	} else {
		// Before we've filled the first screenfull
		CGFloat yCoordBottomOfText = [self.textView findYCoordOfInsertionPoint]; // reverse y-axis: 0 is top
		NSUInteger uintYCoordBottomOfText = yCoordBottomOfText;
		NSUInteger uintToAdd;
		if (uintYCoordBottomOfText >= currentFontHeight) {
			uintToAdd = uintYCoordBottomOfText;
			XT_TRACE_1(@"uintToAdd = %lu (uintYCoordBottomOfText)", uintYCoordBottomOfText);
			uintToAdd -= currentFontHeight;
			XT_TRACE_1(@"uintToAdd -= %lu (currentFontHeight)", currentFontHeight);
			NSUInteger partiallyVisibleLineHeight = (uintToAdd % currentFontHeight);
			uintToAdd -= partiallyVisibleLineHeight;  // compensate for partially visible lines
			XT_TRACE_1(@"uintToAdd -= %lu (partiallyVisibleLineHeight)", partiallyVisibleLineHeight);
			if (uintToAdd > verticalInset) {
				XT_TRACE_1(@"uintToAdd -= %lu (verticalInset)", verticalInset);
				uintToAdd -= verticalInset;
			}
		} else {
			// just in case
			uintToAdd = uintYCoordBottomOfText;
			uintToAdd -= (uintToAdd % currentFontHeight);
		}
		toAdd = uintToAdd;
	}
	XT_TRACE_1(@"toAdd=%f", toAdd);
	if (toAdd < 0) {
		XT_ERROR_1(@"toAdd=%f", toAdd);
	}
	CGFloat oldMTVHBP = self.maxTextViewHeightBeforePagination;
	self.maxTextViewHeightBeforePagination = textViewHeight + toAdd;
	
	self.visibleHeightBeforeLayoutOfViews = 0.0;
	self.totalHeightBeforeLayoutOfViews = 0.0;
	
	XT_TRACE_2(@"maxTextViewHeightBeforePagination %f -> %f", oldMTVHBP, self.maxTextViewHeightBeforePagination);
}

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

- (void)mainThread_removeHandler
{
	//XT_TRACE_ENTRY;
	
	for (XTBannerTextHandler *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)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];
		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.totalHeightBeforeLayoutOfViews = [self.textView findTotalHeight];
	
	//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];
		if (visibleHeightAfterLayoutOfViews > 0.0) {
			
			CGFloat totalHeightAfterLayoutOfViews = [self.textView findTotalHeight];
			CGFloat diffTotalHeight = totalHeightAfterLayoutOfViews - self.totalHeightBeforeLayoutOfViews;
			if (diffTotalHeight != 0.0) {
				int brkpt = 1;
			}
			CGFloat oldMTVHBP = self.maxTextViewHeightBeforePagination;
			self.maxTextViewHeightBeforePagination += diffTotalHeight;
			XT_TRACE_2(@"maxTextViewHeightBeforePagination=%f (was %f)", self.maxTextViewHeightBeforePagination, oldMTVHBP);
			
			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;
					XT_TRACE_2(@"maxTextViewHeightBeforePagination=%f (was %f)", self.maxTextViewHeightBeforePagination, oldMTVHBP);
				}
			}
		}
		//XT_TRACE_1(@"totalHeight=%f", [self.textView findTotalHeight]);
	}
	self.visibleHeightBeforeLayoutOfViews = 0.0;

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

- (void)clearPaginationState
{
	self.maxTextViewHeightBeforePagination = 0.0;
	self.visibleHeightBeforeLayoutOfViews = 0.0;
	self.totalHeightBeforeLayoutOfViews = 0.0;
	self.attributedStringThatBrokePaginationLimit = nil;
}

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

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

- (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)setColorsFromPrefs
{
	XT_DEF_SELNAME;
	XT_ERROR_0(@"should never be called on base class")
}

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