//
//  XTOutputFormatter.m
//  XTads
//
//  Created by Rune Berg on 09/07/14.
//  Copyright (c) 2014 Rune Berg. All rights reserved.
//

#import "XTOutputFormatter.h"
#import "XTHtmlLinebreakHandler2.h"
#import "XTFormattedOutputElement.h"
#import "XTHtmlTagA.h"
#import "XTHtmlTagHr.h"
#import "XTHtmlTagBlockQuote.h"
#import "XTHtmlTagB.h"
#import "XTHtmlTagI.h"
#import "XTHtmlTagU.h"
#import "XTHtmlTagQ.h"
#import "XTHtmlTagAboutBox.h"
#import "XTHtmlTagBanner.h"
#import "XTHtmlTagCenter.h"
#import "XTHtmlTagH1.h"
#import "XTHtmlTagH2.h"
#import "XTHtmlTagH3.h"
#import "XTHtmlTagH4.h"
#import "XTHtmlTagOl.h"
#import "XTHtmlTagUl.h"
#import "XTHtmlTagLi.h"
#import "XTHtmlTagNoop.h"
#import "XTHtmlTagQuestionMarkT2.h"
#import "XTHtmlTagQuestionMarkT3.h"
#import "XTHtmlTagTt.h"
#import "XTHtmlTagTab.h"
#import "XTHtmlTagDiv.h"
#import "XTHtmlTagBr.h"
#import "XTHtmlTagP.h"
#import "XTHtmlTagTitle.h"
#import "XTHtmlTagCite.h"
#import "XTHtmlTagFont.h"
#import "XTHtmlTagPre.h"
#import "XTHtmlTagImg.h"
#import "XTHtmlTagTable.h"
#import "XTHtmlTagTr.h"
#import "XTHtmlTagTh.h"
#import "XTHtmlTagTd.h"
#import "XTHtmlTagT2Hilite.h"
#import "XTHtmlWhitespace.h"
#import "XTHtmlQuotedSpace.h"
#import "XTHtmlNonbreakingSpace.h"
#import "XTLogger.h"
#import "XTFontManager.h"
#import "XTPrefs.h"
#import "XTStringUtils.h"


@interface XTOutputFormatter ()

typedef NS_ENUM(NSInteger, XTTextAlignMode) {
	XT_TEXT_ALIGN_LEFT = 1,
	XT_TEXT_ALIGN_CENTER = 2,
	XT_TEXT_ALIGN_RIGHT = 3,
	XT_TEXT_ALIGN_UNSPECIFIED = 99
};

@property XTHtmlLinebreakHandler2 *linebreakHandler2;
@property BOOL afterBlockLevelSpacing;

@property BOOL hiliteMode;
@property BOOL boldFaceMode;
@property BOOL italicsMode;
@property BOOL underlineMode;
@property BOOL ttMode;
@property BOOL preMode;
@property XTTextAlignMode textAlignMode;
//@property XTTextAlignMode tabAlignMode;
@property BOOL aboutBoxMode;
@property BOOL bannerMode;
@property BOOL orderedListMode;
@property BOOL unorderedListMode;
@property NSUInteger blockquoteLevel;
@property NSUInteger orderedListIndex;
@property BOOL h1Mode;
@property BOOL h2Mode;
@property BOOL h3Mode;
@property BOOL h4Mode;
@property BOOL receivingGameTitle;
@property BOOL shouldWriteWhitespace;
@property BOOL shouldWriteQuotedSpace;
@property BOOL lastElementFormattedEndedInWhitespace;

@property XTHtmlTagA *activeTagA;

@property NSNumber *htmlFontSize; // 1..7, 3 being "default"
@property NSArray *htmlFontFaceList;

@property NSArray *defaultTabStops;
@property NSArray *listItemPrefixTabStops;

@property NSArray *emptyArray;

@property XTFontManager *fontManager;

@property XTPrefs *prefs;

@end


@implementation XTOutputFormatter

static XTLogger* logger;

static NSString * const tabString = @" \t"; // include a space to ensure _some_ visual spacing
static NSString * const tableCellSeparator = @"   ";

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

- (id)init
{
    self = [super init];
    if (self) {
		_linebreakHandler2 = [XTHtmlLinebreakHandler2 new];
		_fontManager = [XTFontManager fontManager];
		_prefs = [XTPrefs prefs];
		_emptyArray = [NSArray array];
		_isForBanner = NO;
		
		[self resetFlags];
    }
    return self;
}

- (void)resetFlags
{
	[self resetNonHtmlModesFlags];
	[self resetHtmlModesFlags];
	[self.linebreakHandler2 resetForNextCommand];
}

- (void)resetForNextCommand
{
	[self resetNonHtmlModesFlags];
	[self.linebreakHandler2 resetForNextCommand];
}

- (void)resetNonHtmlModesFlags
{
	_receivingGameTitle = NO;
	_afterBlockLevelSpacing = NO;
	_shouldWriteWhitespace = NO;
	_shouldWriteQuotedSpace = YES;
	_aboutBoxMode = NO;
	_bannerMode = NO;
	_activeTagA = nil;
	_lastElementFormattedEndedInWhitespace = NO;
}

- (void)resetHtmlModesFlags
{
	_boldFaceMode = NO;
	_italicsMode = NO;
	_underlineMode = NO;
	_ttMode = NO;
	_preMode = NO;
	_h1Mode = NO;
	_h2Mode = NO;
	_h3Mode = NO;
	_h4Mode = NO;
	_textAlignMode = XT_TEXT_ALIGN_LEFT;
	//_tabAlignMode = XT_TEXT_ALIGN_UNSPECIFIED;
	_htmlFontSize = nil;
	_htmlFontFaceList = nil;
	_orderedListMode = NO;
	_unorderedListMode = NO;
	_orderedListIndex = 0;
	_blockquoteLevel = 0;
	_activeTagA = nil;
}

- (NSArray *)formatElement:(id)elt
{
	XT_DEF_SELNAME;
	
	NSArray *eltRes = nil;
	
	if ([elt isKindOfClass:[NSString class]]) {
		eltRes = [self handleRegularText:(NSString *)elt];
		
	} else if ([elt isKindOfClass:[XTHtmlTag class]]) {
		eltRes = [self handleHtmlTag:(XTHtmlTag *)elt];
		
	} else if ([elt isKindOfClass:[XTHtmlWhitespace class]]) {
		eltRes = [self handleWhitespace:(XTHtmlWhitespace *)elt];
		
	} else if ([elt isKindOfClass:[XTHtmlQuotedSpace class]]) {
		eltRes = [self handleQuotedSpace:(XTHtmlQuotedSpace *)elt];
		
	} else if ([elt isKindOfClass:[XTHtmlNonbreakingSpace class]]) {
		eltRes = [self handleNonbreakingSpace:(XTHtmlNonbreakingSpace *)elt];
		
	} else {
		NSString *className;
		if (elt == nil) {
			className = @"nil";
		} else {
			className = NSStringFromClass([elt class]);
		}
		XT_ERROR_1(@"got element of unknown class \"%@\"", className);
	}
	
	self.lastElementFormattedEndedInWhitespace = NO;
	if (eltRes.count >= 1) {
		id lastEltId = [eltRes lastObject];
		if ([lastEltId isKindOfClass:[XTFormattedOutputElement class]]) {
			XTFormattedOutputElement *lastElt = (XTFormattedOutputElement *)lastEltId;
			if ([lastElt isRegularOutputElement]) {
				NSString *s = [lastElt.attributedString string];
				if ([s hasSuffix:@" "]) {
					self.lastElementFormattedEndedInWhitespace = YES;
				}
			}
		}
	}
	
	return eltRes;
}

- (NSAttributedString *)formatInputText:(NSString *)string
{
	return [self makeAttributedStringForInput:string];
}

//-------------------------------------------------------------------------------------------

- (NSArray *)handleHtmlTag:(XTHtmlTag *)tag
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"\"%@\"", [tag debugString]);
	
	NSArray *res = nil;
	
	if (tag != nil) {
		
		BOOL executeTag =
			[tag isKindOfClass:[XTHtmlTagAboutBox class]] ||
			[tag isKindOfClass:[XTHtmlTagBanner class]] ||
			(! self.aboutBoxMode && !self.bannerMode);
		
		//TODO display of empty eg <h1>
		//	"Text1<h1></h1>Text2<h2></h2>Text3<h3></h3>Text4<h4></h4>Text5";
		//		qtads / chrome print block sep. such as empty line for each <hN></hN>
		//  "Text1<h1></h1><h2></h2><h3></h3><h4></h4>Text5"
		//		qtads / chrome print ONE block sep. such as empty line for entire <h1></h1><h2></h2><h3></h3><h4></h4>
		// "Text1<h1></h1><h2> </h2><h3></h3><h4></h4>Text5" -- NOTE space in h2
		//		qtads prints extra nl, chrome does not
		
		BOOL wasOutsideListModes = (! self.orderedListMode && ! self.unorderedListMode);
		
		if (executeTag) {
			NSMutableArray *resTemp = [NSMutableArray array];
			
			//TODO must clear self.afterBlockLevelSpacing
			// - for non block level tag
			
			if (tag.isBlockLevel) {
				
				//self.tabAlignMode = XT_TEXT_ALIGN_UNSPECIFIED;
				
				if (tag.isStandalone || ! tag.closing) {
					//TODO hack...find general sol'n:
					if ([tag isKindOfClass:[XTHtmlTagLi class]] && wasOutsideListModes) {
						// nothing
					} else {
						[resTemp addObject:[XTFormattedOutputElement removeTabsToStartOfLineElement]];
					}
					// ... hack
					//TODO exp...
					//TODO try and combine with lbh2
					if (tag.needsBlockLevelSpacing)	{
						if (! self.afterBlockLevelSpacing) {
							NSMutableAttributedString *blockLevelSpacing = [self makeAttributedStringForOutput:@"\n"];
							[resTemp addObject:[XTFormattedOutputElement regularOutputElement:blockLevelSpacing]];
							self.afterBlockLevelSpacing = YES;
						}
					}
					//TODO ...exp
					NSString *resPrefix = [self.linebreakHandler2 handleBlockLevelNewline:tag];
						//TODO check behaviour for <p>
					if (resPrefix != nil) {
						NSMutableAttributedString *resPrefixAttrString = [self makeAttributedStringForOutput:resPrefix];
						[resTemp addObject:[XTFormattedOutputElement regularOutputElement:resPrefixAttrString]];
					}
					self.shouldWriteWhitespace = (self.linebreakHandler2.state != XT_LINEBREAKHANDLER2_AT_START_OF_LINE);
				}
			}
			
			NSArray *resMain = [tag dispatchToFormatter:self];
			[resTemp addObjectsFromArray:resMain];
			//TODO exp...
			BOOL clearAfterBlockLevelSpacing = NO;
			for (XTFormattedOutputElement *elt in resMain) {
				if (! [elt isRemoveTabsToStartOfLineElement]) {
					clearAfterBlockLevelSpacing = YES;
				}
			}
			if (clearAfterBlockLevelSpacing) { 
				self.afterBlockLevelSpacing = NO;
			}
			//TODO ...exp
			
			if (tag.isBlockLevel) {
				if (tag.isStandalone || tag.closing) {
					//[resTemp addObject:[XTFormattedOutputElement removeTabsToStartOfLineElement]];
						//TODO exp rm'd
					BOOL dontExecuteTag = ([tag isKindOfClass:[XTHtmlTagUl class]] || [tag isKindOfClass:[XTHtmlTagOl class]]) &&
											wasOutsideListModes;
						//TODO hack...find general sol'n:
					if (! dontExecuteTag) {
						NSString *resSuffix = [self.linebreakHandler2 handleBlockLevelNewline:tag];
						if (resSuffix != nil) {
							NSMutableAttributedString *resSuffixAttrString = [self makeAttributedStringForOutput:resSuffix];
							[resTemp addObject:[XTFormattedOutputElement regularOutputElement:resSuffixAttrString]];
						}
					}
					self.shouldWriteWhitespace = (self.linebreakHandler2.state != XT_LINEBREAKHANDLER2_AT_START_OF_LINE);
					if (! dontExecuteTag) {
						//TODO exp...
						//TODO try and combine with lbh2
						if (tag.needsBlockLevelSpacing)	{
							if (! self.afterBlockLevelSpacing) {
								NSMutableAttributedString *blockLevelSpacing = [self makeAttributedStringForOutput:@"\n"];
								[resTemp addObject:[XTFormattedOutputElement regularOutputElement:blockLevelSpacing]];
								self.afterBlockLevelSpacing = YES;
							}
						}
						//TODO ...exp
					}
				}
			}
			
			res = [NSArray arrayWithArray:resTemp];
		}
		self.shouldWriteQuotedSpace = YES;
	}
	
	return res;
}

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

	//if (self.isForBanner) {
	//	int brkpt = 1;
	//}
	
	self.afterBlockLevelSpacing = NO;
	
	NSArray *res = nil;
	
	if (! self.htmlMode) {
		res = [self makeArrayWithRegularOutputElement:string];
		//TODO self.shouldWriteWhitespace = YES;
		
	} else {
		if (self.receivingGameTitle) {
			res = [self makeArrayWithGameTitleElement:string];
		} else if (self.aboutBoxMode || self.bannerMode) {
			// nothing
		} else {
			NSArray *stringsSepdByNewline = [string componentsSeparatedByString:@"\n"];
				//TODO mv to parser
			for (NSString *s in stringsSepdByNewline) {
				if (s.length >= 1) {
					[self.linebreakHandler2 handleText:s];
					res = [self makeArrayWithRegularOutputElement:s];
					self.shouldWriteWhitespace = YES;
					self.shouldWriteQuotedSpace = YES;
				}
			}
		}
	}
	
	return res;
}

- (NSArray *)handleWhitespace:(XTHtmlWhitespace *)whitespace
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"shouldWriteWhitespace=%d", self.shouldWriteWhitespace);
	
	//TODO handle adjacency to quoted apace
	
	//TODO check html mode?
	
	//if (self.isForBanner) {
	//	int brkpt = 1;
	//}
	
	NSArray *res = nil;
	
	if (self.receivingGameTitle) {
		res = [self makeArrayWithGameTitleElement:@" "];
	} else {
		//TODO check about box mode / banner mode
		if (self.preMode) {
			res = [self makeArrayWithRegularOutputElement:whitespace.text];
		} else {
			if (self.shouldWriteWhitespace) {
				NSString *ws = @" ";
				[self.linebreakHandler2 handleText:ws];
				res = [self makeArrayWithRegularOutputElement:ws];
				self.shouldWriteWhitespace = NO;
				self.shouldWriteQuotedSpace = NO;
					//TODO use FSM for this stuff!!
			}
		}
	}
	
	return res;
}

- (NSArray *)handleQuotedSpace:(XTHtmlQuotedSpace *)quotedSpace
{
	XT_TRACE_ENTRY;

	//TODO unit test
	//TODO handle adjacency to space
	// sqs  -> " "
	// sqqs -> "  "
	// qq   -> "  "
	//TODO also test wrt. text, tabs and tags
	
	//TODO check html mode?
	
	NSArray *res = nil;
	
	if (self.receivingGameTitle) {
		res = [self makeArrayWithGameTitleElement:@" "];
	} else {
		//TODO check about box mode / banner mode
		if (self.preMode) {
			res = [self makeArrayWithRegularOutputElement:@" "];
		} else {
			if (self.shouldWriteQuotedSpace) {
				NSString *ws = @" ";
				[self.linebreakHandler2 handleText:ws];
				res = [self makeArrayWithRegularOutputElement:ws];
				self.shouldWriteWhitespace = NO;
			}
			self.shouldWriteQuotedSpace = YES;
		}
	}
	
	return res;
}

- (NSArray *)handleNonbreakingSpace:(XTHtmlNonbreakingSpace *)nonbreakingSpace
{
	XT_TRACE_ENTRY;
	//TODO? nbsp variations: https://en.wikipedia.org/wiki/Non-breaking_space - repl. by simlr width breaking ones

	NSArray *res = nil;
	
	if (self.receivingGameTitle) {
		res = [self makeArrayWithGameTitleElement:@" "];
	} else {
		//TODO check about box mode / banner mode
		//TODO use premade objs instead of "[self makeArrayWith" for common cases
		if (self.preMode) {
			res = [self makeArrayWithRegularOutputElement:@" "];
		} else {
			if (self.lastElementFormattedEndedInWhitespace) {
				// to avoid unwanted indents at beginning of line
				res = [self makeArrayWithRegularOutputElement:@" "];
			} else {
				res = [self makeArrayWithRegularOutputElement:@"\u00A0"];
			}
		}
	}
	
	return res;
}

//------- TODO ---------

- (NSArray *)makeArrayWithGameTitleElement:(NSString *)string
{
	XTFormattedOutputElement *outputElement;
	NSMutableAttributedString *attrString = [[NSMutableAttributedString new] initWithString:string];
	outputElement = [XTFormattedOutputElement gameTitleElement:attrString];
	NSArray *res = [NSArray arrayWithObject:outputElement];
	return res;
}

- (NSArray *)makeArrayWithRegularOutputElement:(NSString *)string
{
	NSArray *res = [NSArray arrayWithObject:[self makeRegularOutputElement:string]];
	return res;
}

- (NSArray *)makeArrayWithListItemPrefixElement:(NSString *)string
{
	NSArray *res = [NSArray arrayWithObject:[self makeListItemPrefixElement:string]];
	return res;
}

//------

- (XTFormattedOutputElement *)makeRegularOutputElement:(NSString *)string
{
	//TODO flags... fonts...
	
	NSMutableAttributedString *attrString = [self makeAttributedStringForOutput:string];
	XTFormattedOutputElement *outputElement = [XTFormattedOutputElement regularOutputElement:attrString];
	return outputElement;
}

- (NSMutableAttributedString *)makeAttributedStringForOutput:(NSString *)string
{
   NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:string
																				  attributes:[self getTextAttributesDictionaryForOutput]];
   return attrString;
}

- (XTFormattedOutputElement *)makeListItemPrefixElement:(NSString *)string
{
	//TODO flags... fonts...
	
	NSMutableAttributedString *attrString = [self makeAttributedStringForListItemPrefix:string];
	XTFormattedOutputElement *outputElement = [XTFormattedOutputElement regularOutputElement:attrString];
	return outputElement;
}

- (NSMutableAttributedString *)makeAttributedStringForListItemPrefix:(NSString *)string
{
	NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:string
																				   attributes:[self getTextAttributesDictionaryForListItemPrefix]];
	return attrString;
}

- (NSMutableAttributedString *)makeAttributedStringForInput:(NSString *)string
{
	NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:string
																				   attributes:[self getTextAttributesDictionaryForInput]];
	return attrString;
}

//------

- (NSDictionary *)getTextAttributesDictionaryForOutput
{
	XT_DEF_SELNAME;

	NSMutableDictionary *dict = [self getTextAttributesDictionaryCommon];
	NSMutableParagraphStyle *pgStyle = dict[NSParagraphStyleAttributeName];

	[pgStyle setTabStops:[self getDefaultTabStops:pgStyle]];
	
	NSTextAlignment alignment = NSLeftTextAlignment;
	
	if (self.textAlignMode == XT_TEXT_ALIGN_LEFT) {
		alignment = NSLeftTextAlignment;
	} else if (self.textAlignMode == XT_TEXT_ALIGN_CENTER) {
		alignment = NSCenterTextAlignment;
	} else if (self.textAlignMode == XT_TEXT_ALIGN_RIGHT) {
		alignment = NSRightTextAlignment;
	} else {
		XT_ERROR_1(@"unknown textAlignMode %d", self.textAlignMode);
	}
	
	//TODO test:
	//TODO doesn't work for "Text1<tab align=right>Right?<br>Text2<br>Text3";
		// !!! "<br><tab align=right>Right?" works - i.e. just after newline
		// cannot mix alignments in same pg - seems we must use right-aligned tabs...
		/*
			idea sketch:
				add enough tab chars to bring use to The Right-aligned One
				take into account window width < combined width of tabs
				...but how to define The Right-aligned One?
		 */
	/*
	switch (self.tabAlignMode) {
		case XT_TEXT_ALIGN_UNSPECIFIED:
			// Nothing - don't override textAlignMode
			break;
		case XT_TEXT_ALIGN_LEFT:
			alignment = NSLeftTextAlignment;
			dict[XT_OUTPUT_FORMATTER_ATTR_TAB_ALIGNMENT_SET] = @"true";
			break;
		case XT_TEXT_ALIGN_CENTER:
			alignment = NSCenterTextAlignment;
			dict[XT_OUTPUT_FORMATTER_ATTR_TAB_ALIGNMENT_SET] = @"true";
			break;
		case XT_TEXT_ALIGN_RIGHT:
			alignment = NSRightTextAlignment;
			dict[XT_OUTPUT_FORMATTER_ATTR_TAB_ALIGNMENT_SET] = @"true";
			break;
		default:
			XT_ERROR_1(@"unknown tabAlignMode %d", self.tabAlignMode);
			break;
	}*/
	
	[pgStyle setAlignment:alignment];
	
	//TODO all "indenting" tags should contribute to actual indent size
	CGFloat firstLineHeadIndent = [self getTabStopColumnWidthInPoints];
	CGFloat headIndent = firstLineHeadIndent;
	if (self.blockquoteLevel >= 1) {
		CGFloat indent = firstLineHeadIndent * self.blockquoteLevel;
		[pgStyle setFirstLineHeadIndent:indent];
		[pgStyle setHeadIndent:indent];
	} else if (self.unorderedListMode) {
		[pgStyle setFirstLineHeadIndent:firstLineHeadIndent];
		[pgStyle setHeadIndent:headIndent];
	} else if (self.orderedListMode) {
		[pgStyle setFirstLineHeadIndent:firstLineHeadIndent];
		[pgStyle setHeadIndent:headIndent];
	}
	
	if (self.underlineMode) {
		dict[NSUnderlineStyleAttributeName] = [NSNumber numberWithInt:1];
	}
	
	/*
	//TODO exp!
	//TODO test collapsing behaviour -- doesn't seem to work :-(
	if (self.h1Mode) {
		CGFloat spacingBefore = 12.0; //TODO dep on font size
		CGFloat spacingAfter = 12.0; //TODO dep on font size
		[pgStyle setParagraphSpacingBefore:spacingBefore];
		[pgStyle setParagraphSpacing:spacingAfter];
	}
	//TODO other h* etc.
	*/
	
	if (self.activeTagA != nil) {
		NSString *href = [self.activeTagA attributeAsString:@"href"];
		if (href == nil) {
			href = @"";
		}
		dict[NSLinkAttributeName] = href;
		if ([self.activeTagA hasAttribute:@"append"]) {
			dict[XT_OUTPUT_FORMATTER_ATTR_CMDLINK_APPEND] = @"true";
		}
		if ([self.activeTagA hasAttribute:@"noenter"]) {
			dict[XT_OUTPUT_FORMATTER_ATTR_CMDLINK_NOENTER] = @"true";
		}
	}

	NSDictionary *temporaryAttrsDict = [self getTextTemporaryAttributesDictionaryForOutput];
	dict[XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT] = temporaryAttrsDict;
	
	return dict;
}

- (NSDictionary *)getTextAttributesDictionaryForListItemPrefix
{
    NSMutableDictionary *dict = [self getTextAttributesDictionaryCommon];
	NSMutableParagraphStyle *pgStyle = dict[NSParagraphStyleAttributeName];

	[pgStyle setTabStops:[self getListItemPrefixTabStops:pgStyle]];
	
	CGFloat firstLineHeadIndent = [self getTabStopColumnWidthInPoints];
	[pgStyle setFirstLineHeadIndent:firstLineHeadIndent];

	CGFloat headIndent = firstLineHeadIndent + [self getListItemPrefixColumnWidthInPoints];
	[pgStyle setHeadIndent:headIndent];

	return dict;
}

- (NSDictionary *)getTextTemporaryAttributesDictionaryForOutput
{
	XT_DEF_SELNAME;
	
	NSMutableDictionary *dict = nil;
	
	if (self.activeTagA != nil) {
		dict = [NSMutableDictionary dictionary];
		dict[NSForegroundColorAttributeName] = self.prefs.linksTextColor;
		if (! self.underlineMode) {
			if (! self.prefs.linksUnderline.boolValue) {
				dict[NSUnderlineStyleAttributeName] = [NSNumber numberWithInt:NSUnderlineStyleNone];
			}
		}
		if ([self.activeTagA hasAttribute:@"plain"]) {
			// "plain" link - set temp attrs so it looks like regular text
			dict[NSForegroundColorAttributeName] = [self getOutputTextColor];
			if (! self.underlineMode) {
				dict[NSUnderlineStyleAttributeName] = [NSNumber numberWithInt:NSUnderlineStyleNone];
			}
		}
	}
	
	return dict;
}

//TODO for output only - rename
- (NSMutableDictionary *)getTextAttributesDictionaryCommon
{
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
	
	dict[NSFontAttributeName] = [self getCurrentFontForOutput];
	
	dict[NSForegroundColorAttributeName] = [self getOutputTextColor];
		//TODO reconsider wrt prefs
	
	NSMutableParagraphStyle *pgStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
	dict[NSParagraphStyleAttributeName] = pgStyle;
	
	return dict;
}

- (NSColor *)getOutputTextColor
{
	NSColor *res;
	if (self.isForBanner) {
		res = self.prefs.statusLineTextColor;
	} else {
		res = self.prefs.outputAreaTextColor;
	}
	return res;
	//TODO when game can set
}

//TODO make less wastefull!!
//TODO used?
- (CGFloat)getOutputTextWidth:(NSString *)string
{
	// https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/TextLayout/Tasks/StringHeight.html

	NSFont *myFont = [self getCurrentFontForOutput];
	float myWidth = 1000; //TODO really should be from NSTextView...
	
	NSTextStorage *textStorage = [[NSTextStorage alloc] initWithString:string];
	NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize: NSMakeSize(myWidth, FLT_MAX)];
	NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
	
	[layoutManager addTextContainer:textContainer];
	[textStorage addLayoutManager:layoutManager];
	
	[textStorage addAttribute:NSFontAttributeName value:myFont range:NSMakeRange(0, [textStorage length])];
	[textContainer setLineFragmentPadding:0.0];
	
	(void) [layoutManager glyphRangeForTextContainer:textContainer];
	
	return [layoutManager usedRectForTextContainer:textContainer].size.width;
}

- (NSDictionary *)getTextAttributesDictionaryForInput
{
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
	
	dict[NSFontAttributeName] = [self getCurrentFontForInput];
	dict[NSForegroundColorAttributeName] = self.prefs.inputTextColor;

	return dict;
}

- (NSFont *)getCurrentFontForOutput
{
	//TODO adapt for prefs' banner font
	
	BOOL bold = (self.boldFaceMode | self.hiliteMode | self.h1Mode | self.h2Mode | self.h3Mode | self.h4Mode);
	BOOL italics = self.italicsMode;
	
	NSString *parameterizedFontName;
	if (self.ttMode || self.preMode) {
		parameterizedFontName = [self.fontManager xtadsFixedWidthParameterizedFontName];
	} else {
		parameterizedFontName = [self.fontManager xtadsDefaultParameterizedFontName];
	}
	
	NSArray *fontNames;
	if (self.htmlFontFaceList != nil && self.htmlFontFaceList.count >= 1) {
		fontNames = self.htmlFontFaceList;
	} else {
		fontNames = [NSArray arrayWithObject:parameterizedFontName];
	}

	XTParameterizedFont *parameterizedFont = [self.fontManager getParameterizedFontWithName:parameterizedFontName];
	CGFloat defaultPointSize = parameterizedFont.size;

	NSNumber *pointSize = nil;
	if (self.h1Mode) {
		pointSize = [NSNumber numberWithFloat:(defaultPointSize + 14.0)];
	} else if (self.h2Mode) {
		pointSize = [NSNumber numberWithFloat:(defaultPointSize + 9.0)];
	} else if (self.h3Mode) {
		pointSize = [NSNumber numberWithFloat:(defaultPointSize + 4.0)];
	} else if (self.h4Mode) {
		pointSize = [NSNumber numberWithFloat:(defaultPointSize + 2.0)];
	}
	
	NSFont *res = [self.fontManager getFontWithName:fontNames
										  pointSize:pointSize
										   htmlSize:self.htmlFontSize
											   bold:bold
											italics:italics];
	
	return res;
}

- (NSFont *)getCurrentFontForInput
{
	// Remember, this is only used for programmatically added input text
	
	XTPrefs *prefs = [XTPrefs prefs];
	NSString *parameterizedFontName;
	
	if (prefs.inputFontIsSameAsDefaultFont.boolValue) {
		parameterizedFontName = [self.fontManager xtadsDefaultParameterizedFontName];
	} if (prefs.inputFontUsedEvenIfNotRequestedByGame.boolValue) {
		parameterizedFontName = [self.fontManager tadsInputParameterizedFontName];
	} else {
		//TODO how to handle?
		parameterizedFontName = [self.fontManager xtadsDefaultParameterizedFontName];
	}
	
	NSArray *fontNames = [NSArray arrayWithObject:parameterizedFontName];
	
	NSFont *res = [self.fontManager getFontWithName:fontNames
										  pointSize:nil
										   htmlSize:nil
											   bold:NO
											italics:NO];
	//TODO bold/italics should be determined by param font?
	
	return res;
}

- (NSArray *)getDefaultTabStops:(NSMutableParagraphStyle *)pgStyle
{
	if (self.defaultTabStops == nil) {
		
		NSUInteger numTabStops = 6; //TODO: 30;
		NSMutableArray *tempTabStops = [NSMutableArray arrayWithCapacity:numTabStops];
		CGFloat columnWidthInPoints = [self getTabStopColumnWidthInPoints];
		
		for(NSInteger tabCounter = 0; tabCounter < numTabStops; tabCounter++) {
			NSTextTab * aTab = [[NSTextTab alloc] initWithType:NSLeftTabStopType location:((tabCounter + 1) * columnWidthInPoints)];
			[tempTabStops addObject:aTab];
		}

		//TODO exp:
		NSTextTab * aTab = [[NSTextTab alloc] initWithType:NSRightTabStopType
												  location:(15 * columnWidthInPoints)];
		[tempTabStops addObject:aTab];
		
		self.defaultTabStops = tempTabStops;
	}
	
	return self.defaultTabStops;
}

//TODO what about tabs in list item text itself?
//TODO clean up / cache
- (NSArray *)getListItemPrefixTabStops:(NSMutableParagraphStyle *)pgStyle
{
	if (self.listItemPrefixTabStops == nil) {
		
		NSUInteger numTabStops = 2;
		NSMutableArray *tempTabStops = [NSMutableArray arrayWithCapacity:numTabStops];
		
		CGFloat prefixStartLocInPoints = [self getTabStopColumnWidthInPoints];
		NSTextTab * prefixStartTab = [[NSTextTab alloc] initWithType:NSLeftTabStopType location:prefixStartLocInPoints];
		[tempTabStops addObject:prefixStartTab];
		
		CGFloat textStartLocInPoints = prefixStartLocInPoints + [self getListItemPrefixColumnWidthInPoints];
		NSTextTab * textStartTab = [[NSTextTab alloc] initWithType:NSLeftTabStopType location:textStartLocInPoints];
		[tempTabStops addObject:textStartTab];

		self.listItemPrefixTabStops = tempTabStops;
	}
	
	return self.listItemPrefixTabStops;
}

- (CGFloat)getTabStopColumnWidthInPoints
{
	//TODO wip!!!
	
	float columnWidthInPoints = 32; // ((self.fontSize / 2) + 1) * 4;
		//TODO real value. dep on relevant font size?
	//TODO user option?
	//- En (typography), a unit of width in typography, equivalent to half the height of a given font. (see also en dash)

	return columnWidthInPoints;
}

- (CGFloat)getListItemPrefixColumnWidthInPoints
{
	CGFloat res = [self getTabStopColumnWidthInPoints] * 0.7;
	return res;
}

//TODO mv down?
- (BOOL)isInTabOppressingTag
{
	BOOL res = (self.h1Mode || self.h2Mode || self.h3Mode || self.h4Mode);
	return res;
}

- (BOOL)isInListMode
{
	BOOL res = (self.orderedListMode || self.unorderedListMode);
	return res;
}

//-------------------------------------------------------------------------------------------

#pragma mark XTOutputFormatterProtocol

- (NSArray *)handleHtmlTagQ:(XTHtmlTagQ *)tag
{
	NSString *quote = @"\"";
	//TODO diff char for open / close?
	[self.linebreakHandler2 handleText:quote];
	return [self makeArrayWithRegularOutputElement:quote];
}

- (NSArray *)handleHtmlTagTab:(XTHtmlTagTab *)tag
{
	XT_DEF_SELNAME;
	
	//TODO completely overhault handling of this tab
	
	if ([tag hasAttribute:@"id"]) {
		// a tab definition, so no output
		return self.emptyArray;
	}
	
	//TODO test:
	/*
	NSString *attrNameAlign = @"align";
	if ([tag hasAttribute:attrNameAlign]) {
		if ([tag hasAttribute:attrNameAlign withCaseInsensitiveValue:@"right"]) {
			self.tabAlignMode = XT_TEXT_ALIGN_RIGHT;
		} else if ([tag hasAttribute:attrNameAlign withCaseInsensitiveValue:@"center"]) {
			self.tabAlignMode = XT_TEXT_ALIGN_CENTER;
		} else if ([tag hasAttribute:attrNameAlign withCaseInsensitiveValue:@"left"]) {
			self.tabAlignMode = XT_TEXT_ALIGN_LEFT;
		} else {
			NSString *alignAttr = [tag attributeAsString:attrNameAlign];
			XT_ERROR_1(@"unknown align attr \"%@\"", alignAttr)
			self.tabAlignMode = XT_TEXT_ALIGN_UNSPECIFIED;
		}
		//TODO tabAlignMode should only lasts until next linebreak
		return self.emptyArray;
	}*/

	NSUInteger multiple = 1;
		//TODO -1 ?
	if ([tag hasAttribute:@"multiple"]) {
		multiple = [tag attributeAsUInt:@"multiple"];
	}

	if (! [self isInTabOppressingTag]) {
		self.shouldWriteWhitespace = NO;
		[self.linebreakHandler2 handleTagTab];
		return [self makeArrayWithRegularOutputElement:tabString];
	} else {
		return self.emptyArray;
	}
}

//TODO unit test newline gen:
- (NSArray *)handleHtmlTagBlockQuote:(XTHtmlTagBlockQuote *)tag
{
	//TODO consider interaction / flags with h*, list modes, etc.

	if (! tag.closing) {
		self.blockquoteLevel += 1;
	} else {
		if (self.blockquoteLevel >= 1) {
			self.blockquoteLevel -= 1;
		}
	}
	
	return self.emptyArray;
}

//TODO make block level?
- (NSArray *)handleHtmlTagBr:(XTHtmlTagBr *)tag
{
	NSInteger height = -1;
	if ([tag hasAttribute:@"height"]) {
	 	height = [tag attributeAsUInt:@"height"];
	}

	NSString *s = [self.linebreakHandler2 handleTagBr:height];
	self.shouldWriteWhitespace = (self.linebreakHandler2.state != XT_LINEBREAKHANDLER2_AT_START_OF_LINE);
	
	//self.tabAlignMode = XT_TEXT_ALIGN_UNSPECIFIED;
		//TODO too coarse-grained?

	NSMutableArray *res = [NSMutableArray arrayWithCapacity:2];

	[res addObject:[XTFormattedOutputElement removeTabsToStartOfLineElement]];
	
	if (s != nil && s.length >= 1) {
		NSMutableAttributedString *attrString = [self makeAttributedStringForOutput:s];
		[res addObject:[XTFormattedOutputElement regularOutputElement:attrString]];
	}

	return res;
}

- (NSArray *)handleHtmlTagDiv:(XTHtmlTagDiv *)tag
{
	[self setTextAlignModeFromAlignAttribute:tag];
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagP:(XTHtmlTagP *)tag
{
	[self setTextAlignModeFromAlignAttribute:tag];

	return self.emptyArray;
}

- (void)setTextAlignModeFromAlignAttribute:(XTHtmlTag *)tag
{
	XT_DEF_SELNAME;

	if (! tag.closing) {
		NSString *attrNameAlign = @"align";
		if (! [tag hasAttribute:attrNameAlign]) {
			self.textAlignMode = XT_TEXT_ALIGN_LEFT;
		} else if ([tag hasAttribute:attrNameAlign withCaseInsensitiveValue:@"right"]) {
			self.textAlignMode = XT_TEXT_ALIGN_RIGHT;
		} else if ([tag hasAttribute:attrNameAlign withCaseInsensitiveValue:@"center"]) {
			self.textAlignMode = XT_TEXT_ALIGN_CENTER;
		} else if ([tag hasAttribute:attrNameAlign withCaseInsensitiveValue:@"left"]) {
			self.textAlignMode = XT_TEXT_ALIGN_LEFT;
		} else if ([tag hasAttribute:attrNameAlign]) {
			NSString *alignAttr = [tag attributeAsString:@"align"];
			XT_ERROR_1(@"unknown align attr \"%@\"", alignAttr)
			self.textAlignMode = XT_TEXT_ALIGN_LEFT;
		}
	} else {
		self.textAlignMode = XT_TEXT_ALIGN_LEFT;
	}
}

- (NSArray *)handleHtmlTagTitle:(XTHtmlTagTitle *)tag
{
	NSArray *res;
	
	if (! tag.closing) {
		//[stream startTitle];
		self.receivingGameTitle = YES;
		//self.gameTitle = [NSMutableString stringWithString:@""];
		res = [self makeArrayWithGameTitleElement:@"{{clear}}"];
			//TODO hack
	} else {
		//[stream endTitle];
		self.receivingGameTitle = NO;
		res = self.emptyArray;
	}
	
	return res;
}

- (NSArray *)handleHtmlTagHr:(XTHtmlTagHr *)tag
{
	//TODO ideally: adjust length acc to window width
	//TODO ...or at least centre a fixed text string
	
	self.textAlignMode = XT_TEXT_ALIGN_LEFT;
	
	NSString *s = @"–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––";

	[self.linebreakHandler2 handleText:s]; // so we're "in text"
	
	NSMutableArray *res = [NSMutableArray array];

	[res addObject:[self makeRegularOutputElement:s]];
	//TODO exp rm: [res addObject:[self makeRegularOutputElement:@"\n"]];
	
	return res;
}

- (NSArray *)handleHtmlTagA:(XTHtmlTagA *)tag
{
	if (! tag.closing) {
		self.activeTagA = tag;
	} else {
		self.activeTagA = nil;
	}
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagB:(XTHtmlTagB *)tag
{
	self.boldFaceMode = (! tag.closing);
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagI:(XTHtmlTagI *)tag
{
	self.italicsMode = (! tag.closing);
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagU:(XTHtmlTagU *)tag
{
	self.underlineMode = (! tag.closing);
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagAboutBox:(XTHtmlTagAboutBox *)tag
{
	/*TODO the presence/absence of the below code doesn't seem to matter, all of a sudden ?!?! BUG NOT SHOWING!
	//TODO exp!!! ... blighted isle ting! space bug
	[self.linebreakHandler2 resetForNextCommand];
		//TODO really: [self.linebreakHandler2 handleTagAboutBox:tag]
	self.shouldWriteWhitespace = (self.linebreakHandler2.state != XT_LINEBREAKHANDLER2_AT_START_OF_LINE);
	// ...exp
	 */
	
	self.aboutBoxMode = (! tag.closing);
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagBanner:(XTHtmlTagBanner *)tag;
{
	// "Common Ground", "help" calls this
	//TODO temp comt'd out - 1893 startup menu not showing
	//TODO ignore for id=StatusLine ?
	//[stream setBannerMode:(! self.closing)];
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagCenter:(XTHtmlTagCenter *)tag
{
	self.textAlignMode = (tag.closing ? XT_TEXT_ALIGN_LEFT : XT_TEXT_ALIGN_CENTER);
	
	return self.emptyArray;
		//TODO might be at cmd input, so should really mv cursor to left of line - return "non-printing space"?
}

- (NSArray *)handleHtmlTagH1:(XTHtmlTagH1 *)tag
{
	self.h1Mode = (! [self isInListMode] && ! tag.closing);
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagH2:(XTHtmlTagH2 *)tag
{
	self.h2Mode = (! [self isInListMode] && ! tag.closing);
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagH3:(XTHtmlTagH3 *)tag
{
	self.h3Mode = (! [self isInListMode] && ! tag.closing);
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagH4:(XTHtmlTagH4 *)tag
{
	self.h4Mode = (! [self isInListMode] && ! tag.closing);
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagOl:(XTHtmlTagOl *)tag
{
	self.orderedListMode = (! tag.closing);
	self.orderedListIndex = 0;
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagUl:(XTHtmlTagUl *)tag
{
	self.unorderedListMode = (! tag.closing);
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagLi:(XTHtmlTagLi *)tag
{
	NSArray *res = nil;
	
	//TODO simplify
	
	if (self.orderedListMode) {
		NSString *s = nil;
		if (! tag.closing) {
			self.orderedListIndex += 1;
			s = [NSString stringWithFormat:@"%lu.\t", self.orderedListIndex];
		} else {
			//s = [self.linebreakHandler2 handleEndLi]; TODO rm handleEndLi
		}
		if (s != nil) {
			res = [self makeArrayWithListItemPrefixElement:s];
		}

	} else if (self.unorderedListMode) {
		NSString *s = nil;
		if (! tag.closing) {
			s = [NSString stringWithFormat:@"\u2022\t"];
		} else {
			//s = [self.linebreakHandler2 handleEndLi];
		}
		if (s != nil) {
			res = [self makeArrayWithListItemPrefixElement:s];
		}

	}
	
	if (res == nil) {
		res = self.emptyArray;
	}

	return res;
}

- (NSArray *)handleHtmlTagNoop:(XTHtmlTagNoop *)tag
{
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagQuestionMarkT2:(XTHtmlTagQuestionMarkT2 *)tag
{
	/* TODO handle:
	 *   Write out the special <?T2> HTML sequence, in case we're on an HTML
	 *   system.  This tells the HTML parser to use the parsing rules for
	 *   TADS 2 callers.
	 */
	 //outformat("\\H+<?T2>\\H-");
	
	 return self.emptyArray;
}

- (NSArray *)handleHtmlTagQuestionMarkT3:(XTHtmlTagQuestionMarkT3 *)tag
{
	/* TODO handle, but for T3:
	 *   Write out the special <?T2> HTML sequence, in case we're on an HTML
	 *   system.  This tells the HTML parser to use the parsing rules for
	 *   TADS 2 callers.
	 */
	//outformat("\\H+<?T2>\\H-");
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagTt:(XTHtmlTagTt *)tag
{
	self.ttMode = (! tag.closing);
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagTable:(XTHtmlTagTable *)tag
{
	self.textAlignMode = XT_TEXT_ALIGN_LEFT;
	//TODO hacky? clear more flags?
	//TODO also do such clearing for other blk lvl tags? ul ol ...?
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagTr:(XTHtmlTagTr *)tag
{
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagTh:(XTHtmlTagTh *)tag
{
	//TODO pass thru some "filter" wrt. markup ws?
	
	NSArray *res;
	if (tag.closing) {
		res = [self makeArrayWithRegularOutputElement:tableCellSeparator];
			//TODO tab?
	} else {
		res = self.emptyArray;
	}
	
	return res;
}

- (NSArray *)handleHtmlTagTd:(XTHtmlTagTd *)tag
{
	//TODO pass thru some "filter" wrt. markup ws?
	
	NSArray *res;
	if (tag.closing) {
		res = [self makeArrayWithRegularOutputElement:tableCellSeparator];
		//TODO tab?
	} else {
		res = self.emptyArray;
	}
	
	return res;
}

- (NSArray *)handleHtmlTagCite:(XTHtmlTagCite *)tag
{
	self.italicsMode = (! tag.closing);
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagFont:(XTHtmlTagFont *)tag
{
	BOOL allow = self.prefs.allowGamesToSetFonts.boolValue;
	
	if (allow && ! tag.closing) {
		
		NSInteger sign;
		NSUInteger tempHtmlFontSize;
		NSInteger htmlFontSize = 3; // default
		
		// size: A number from 1 to 7 that defines the size of the text. Browser default is 3.
		if ([tag attribute:@"size" asOptionalSign:&sign andUint:&tempHtmlFontSize]) {
			if (sign == 0) {
				htmlFontSize = tempHtmlFontSize;
			} else {
				htmlFontSize += (sign * tempHtmlFontSize);
			}
			if (htmlFontSize < 1) {
				htmlFontSize = 1;
			} else if (htmlFontSize > 7) {
				htmlFontSize = 7;
			}
			self.htmlFontSize = [NSNumber numberWithUnsignedInteger:htmlFontSize];
		}

		NSArray *htmlFontFaceList = [tag attributeAsCommaSeparatedStrings:@"face"];
		if (htmlFontFaceList != nil && htmlFontFaceList.count >= 1) {
			self.htmlFontFaceList = htmlFontFaceList;
		}

		//TODO handle attr COLOR (dep on prefs)
		
	} else {

		self.htmlFontSize = nil;
		self.htmlFontFaceList = nil;
	}
	
	return self.emptyArray;
}

- (NSArray *)handleHtmlTagPre:(XTHtmlTagPre *)tag
{
	self.preMode = (! tag.closing);

	return self.emptyArray;
}

- (NSArray *)handleHtmlTagImg:(XTHtmlTagImg *)tag
{
	NSArray *res = nil;
	
	if ([tag hasAttribute:@"alt"]) {
		NSString *altText = [tag attributeAsString:@"alt"];
		if (altText != nil && altText.length >= 1) {
			//NSString *s = [NSString stringWithFormat:@"[Image \"%@\" not shown]\n\n", altText];
			NSString *s = [NSString stringWithFormat:@"[Image \"%@\" not shown]\n", altText];
			res = [self makeArrayWithRegularOutputElement:s];
		}
	}
	if (res == nil) {
		res = self.emptyArray;
	}

	return res;
}

- (NSArray *)handleHtmlTagTads2Hilite:(XTHtmlTagT2Hilite *)tag
{
	self.hiliteMode = (! tag.closing);
	
	return self.emptyArray;
}

@end
