//
//  XTStringUtils.m
//  TadsTerp
//
//  Created by Rune Berg on 24/04/14.
//  Copyright (c) 2014 Rune Berg. All rights reserved.
//

#import "XTStringUtils.h"
#import "XTLogger.h"
#import "XTConstants.h"
#import "XTCharConstants.h"
#import "XTTabStopModelEntry.h"
#import "XTTextTab.h"
#import "XTTextTable.h"
#import "XTTextTableBlock.h"


@implementation XTStringUtils

static XTLogger* logger;

static NSCharacterSet *whitespaceCharSet;
static NSCharacterSet *whitespaceToDeleteToLastNewlineCharSet;
static NSCharacterSet *breakingWhitespaceCharSet;
static NSArray *internetLinkPrefixes;

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

	whitespaceCharSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
	
	NSMutableCharacterSet *tempWSTDTLNCharSet = [NSMutableCharacterSet whitespaceCharacterSet]; // does not incl. NL / CR
	whitespaceToDeleteToLastNewlineCharSet = tempWSTDTLNCharSet;
	
	NSMutableCharacterSet *tempBreakingWhitespaceCharSet = [NSMutableCharacterSet whitespaceAndNewlineCharacterSet];
		//TODO ? rm non-breaking ones?
	[tempBreakingWhitespaceCharSet addCharactersInString:@"-"];
	breakingWhitespaceCharSet = tempBreakingWhitespaceCharSet;

	internetLinkPrefixes = @[@"http:", @"https:", @"ftp:", @"news:", @"mailto:", @"telnet:"];
}

+ (BOOL)string:(NSString *)string endsWithChar:(unichar)ch
{
	//TODO unit test
	BOOL res = NO;
	NSUInteger stringLen = string.length;
	if (string != nil && stringLen >= 1) {
		unichar lastCh = [string characterAtIndex:(stringLen - 1)];
		res = (lastCh == ch);
	}
	return res;
}

+ (BOOL)string:(NSString *)string endsWith:(NSString *)end
{
	BOOL res = [XTStringUtils internalString:string endsWith:end];
	return res;
}

+ (BOOL)string:(NSString *)string endsWithCaseInsensitive:(NSString *)end
{
	if (string != nil && string.length >= 1) {
		string = [string lowercaseString];
	}
	if (end != nil && end.length >= 1) {
		end = [end lowercaseString];
	}
	BOOL res = [XTStringUtils internalString:string endsWith:end];
	return res;
}

+ (NSUInteger)lengthOfTrailingEffectiveNewlineNotInTableCell:(NSAttributedString *)attrString
{
	NSUInteger res = 0;
	NSUInteger length = attrString.length;
	
	if (length == 0) {
		return res;
	}

	NSUInteger lengthOfTrailingEffectiveNewline = 0;
	if ([attrString.string hasSuffix:@"\n" ZERO_WIDTH_SPACE]) {
		lengthOfTrailingEffectiveNewline = 2;
	} else if ([attrString.string hasSuffix:@"\n"]) {
		lengthOfTrailingEffectiveNewline = 1;
	}
	
	if (lengthOfTrailingEffectiveNewline >= 1) {
		NSDictionary *attrs = [attrString attributesAtIndex:0 effectiveRange:NULL];
		NSMutableParagraphStyle *pgStyle = attrs[NSParagraphStyleAttributeName];
		NSArray *blocks = pgStyle.textBlocks; // table cell blocks
		if (blocks == nil || blocks.count == 0) {
			// we're not in a table cell
			res = lengthOfTrailingEffectiveNewline;
		}
	}
	
	return res;
}

//TODO unit test
+ (BOOL)string:(NSString *)string startsWith:(NSString *)start
{
	BOOL res = [string hasPrefix:start];
	return res;
}

//TODO use!
+ (BOOL)string:(NSString *)string1 isEqualToCaseInsensitive:(NSString *)string2
{
	BOOL res;
	if (string1 == nil || string2 == nil) {
		res = NO;
	} else {
		string1 = [string1 lowercaseString];
		string2 = [string2 lowercaseString];
		res = [string1 isEqualToString:string2];
	}
	return res;
}

+ (BOOL)internalString:(NSString *)string endsWith:(NSString *)end
{
	BOOL res = NO;
	if (string != nil && end != nil && string.length >= 1 && end.length >= 1) {
		NSRange rangeOfString = [string rangeOfString:end options:NSBackwardsSearch];
		if (rangeOfString.location != NSNotFound) {
			NSUInteger expectedLocation = (string.length - end.length);
			res = (rangeOfString.location == expectedLocation);
		}
	}
	return res;
}

+ (NSString *)filterRepeatedNewlines:(NSString *)string
{
	NSMutableString *res = nil;
	
	if (string != nil) {
		NSUInteger stringLength = string.length;
		res  = [NSMutableString stringWithCapacity:stringLength];
		unichar prevCh = 'z';
		for (NSUInteger i = 0; i < stringLength; i++) {
			unichar ch = [string characterAtIndex:i];
			if (ch != '\n' || prevCh != '\n') { //TODO !!! handle soft newline
				NSString *newChStr = [NSString stringWithCharacters:&ch length:1];
				[res appendString:newChStr];
			}
			prevCh = ch;
		}
	}
	
	return res;
}

+ (NSRange)findRangeOfTrailingWhitespaceInLastParagraph:(NSString *)string
{
	NSUInteger resLoc = NSNotFound;
	NSUInteger resLen = 0;
	
	if (string != nil) {
		
		NSUInteger trailingWhitespaceCount = 0;
		NSInteger index = string.length - 1;

		while (index >= 0) {
			unichar ch = [string characterAtIndex:index];
			if (! [whitespaceToDeleteToLastNewlineCharSet characterIsMember:[string characterAtIndex:index]]) {
				if (ch != ZERO_WIDTH_SPACE_CHAR) {
					break;
				} else {
					//TODO !!! rm if not used
					// zero width space - only count it if not before newline (paragraph break)
					if (index >= 1) {
						unichar charBefore = [string characterAtIndex:(index - 1)];
						if (charBefore == '\n') {
							// keep - it contains paragraph style info
							break;
						}
					}
				}
			}
			trailingWhitespaceCount += 1;
			index -= 1;
		}
		
		if (trailingWhitespaceCount >= 1) {
			resLoc = index + 1;
			resLen = trailingWhitespaceCount;
		}
	}
	
	NSRange res = NSMakeRange(resLoc, resLen);
	return res;
}

+ (NSRange)findRangeOfLastParagraph:(NSString *)string
{
	NSUInteger resLoc = NSNotFound;
	NSUInteger resLen = 0;
	
	if (string != nil && string.length >= 1) {
		NSInteger stringLen = string.length;
		resLoc = 0;
		resLen = stringLen;
		NSUInteger newLineCount = 0;
		unichar lastCh = [string characterAtIndex:(stringLen - 1)];
		BOOL lastCharIsNewline = [XTStringUtils isEffectiveNewline: lastCh];

		for (NSInteger idx = stringLen - 1; idx >= 0; idx -= 1) {
			unichar ch = [string characterAtIndex:idx];
			if ([XTStringUtils isEffectiveNewline: ch]) {
				newLineCount += 1;
				if ((! lastCharIsNewline) || newLineCount == 2) {
					resLoc = idx + 1;
					resLen = stringLen - resLoc;
					break;
				}
			}
		}
	}
	
	NSRange res = NSMakeRange(resLoc, resLen);
	return res;
}

//TODO unit test
+ (NSRange)findRangeOfOngoingParagraph:(NSString *)string
{
	NSUInteger resLoc = NSNotFound;
	NSUInteger resLen = 0;
	
	if (string != nil && string.length >= 1) {
		NSInteger stringLen = string.length;
		NSUInteger idxParagraphStart = 0;
		for (NSInteger idx = stringLen - 1; idx >= 0; idx -= 1) {
			unichar ch = [string characterAtIndex:idx];
			if ([XTStringUtils isEffectiveNewline: ch]) {
				// idx is now at last newline
				idxParagraphStart = idx + 1;
				break;
			}
		}
		resLoc = idxParagraphStart;
		resLen = stringLen - resLoc;
	 }
		
	NSRange res = NSMakeRange(resLoc, resLen);
	return res;
}

//TODO unit test
+ (NSString *)trimLeadingAndTrailingWhitespace:(NSString *)string
{
	NSString *res = nil;
	if (string != nil) {
		res = [string stringByTrimmingCharactersInSet:whitespaceCharSet];
	}
	return res;
}

//TODO unit test
+ (NSString *)removeLeadingZwsp:(NSString *)string
{
	NSString *res = string;
	
	if (string.length >= 1) {
		unichar firstCh = [string characterAtIndex:0];
		if (firstCh == ZERO_WIDTH_SPACE_CHAR) {
			NSMutableString *mutRes = [NSMutableString stringWithString:string];
			[mutRes deleteCharactersInRange:NSMakeRange(0, 1)];
			res = mutRes;
		}
	}

	return res;
}

//TODO unit test
+ (NSString *)emptyIfNull:(NSString *)string {
	
	NSString *res = string;
	if (res == nil) {
		res = @"";
	}
	return res;
}

//TODO unit test
+ (BOOL)isEmptyOrNull:(NSString *)string
{
	BOOL res = (string == nil || string.length == 0);
	return res;
}

//TODO unit test
+ (BOOL)isInternetLink:(NSString *)string
{
	// See http://www.tads.org/t3doc/doc/htmltads/deviate.htm#Achanges
	
	BOOL res = NO;
	if (string != nil) {
		string = [XTStringUtils trimLeadingAndTrailingWhitespace:string];
		string = [string lowercaseString];
		for (NSString* prefix in internetLinkPrefixes) {
			if ([string hasPrefix:prefix]) {
				res = YES;
				break;
			}
		}
	}
	return res;
}

+ (NSString *)stringOf:(NSUInteger)n string:(NSString *)s
{
	NSMutableString *res = [NSMutableString stringWithCapacity:50];
	
	for (NSUInteger i = 0; i < n; i++) {
		[res appendString:s];
	}
	
	return res;
}

+ (NSCharacterSet *)breakingWhitespaceCharSet
{
	return breakingWhitespaceCharSet;
}

+ (NSUInteger)indexInString:(NSString *)string ofCharAtRow:(NSUInteger)row column:(NSUInteger)column;
{
	//XT_DEF_SELNAME;
	
	NSUInteger sLen = string.length;
	NSUInteger currRow = 0;
	NSUInteger currColumn = 0;
	NSUInteger i;
	
	for (i = 0; i < sLen && currRow < row; i++) {
		unichar ch = [string characterAtIndex:i];
		if (ch != '\n') {
			currColumn += 1;
		} else {
			currRow += 1;
			currColumn = 0;
		}
	}
	if (currRow < row) {
		//XT_WARN_2(@"only has %lu rows, asked for %lu", currRow, row);
		// It's ok - we get a flush after banner_goto
	} else {
		for (; i < sLen && currColumn < column; i++) {
			unichar ch = [string characterAtIndex:i];
			if (ch != '\n') {
				currColumn += 1;
			} else {
				// we're on the right row, but it's not wide enough
				break;
			}
		}
		if (currColumn < column) {
			//XT_WARN_3(@"row %lu has only %lu cols, asked for %lu", currRow, currColumn, column);
			// It's ok - we get a flush after banner_goto
		}
	}
	
	return i;
}

+ (NSRange)rangeOfLongestLineIn:(NSString *)string
{
	NSUInteger lenString = string.length;
	NSUInteger startLongest = 0;
	NSUInteger startCurrent = 0;
	NSUInteger lenLongest = 0;
	NSUInteger lenCurrent = 0;
	NSUInteger i;
	
	for (i = 0; i < lenString; i++) {
		unichar ch = [string characterAtIndex:i];
		if (ch == '\n') { //TODO !!! or UNICHAR_LINE_SEPARATOR
			if (lenCurrent > lenLongest) {
				startLongest = startCurrent;
				lenLongest = i - startCurrent;
			}
			startCurrent = i + 1;
			lenCurrent = 0;
		} else {
			lenCurrent += 1;
		}
	}
	
	if (lenCurrent > lenLongest) {
		startLongest = startCurrent;
		lenLongest = i - startCurrent;
	}
	
	NSRange res = NSMakeRange(startLongest, lenLongest);
	return res;
}

+ (unichar)removeLastCharFrom:(NSMutableString *)string
{
	XT_DEF_SELNAME;
	XT_WARN_1(@"\"%@\"", string);

	unichar lastChar = 0;
	
	NSUInteger len = string.length;
	if (string != nil && len >= 1) {
		lastChar = [string characterAtIndex:(len - 1)];
		NSRange range = NSMakeRange(len - 1, 1);
		[string deleteCharactersInRange:range];
	} else {
		XT_DEF_SELNAME;
		XT_ERROR_0(@"string was nil or empty");
	}
	
	return lastChar;
}

+ (NSString *)numericPrefix:(NSString *)string
{
	NSMutableString *res = nil;
	
	if (string != nil) {
		res = [NSMutableString string];
		NSUInteger len = string.length;
		for (NSUInteger i = 0; i < len; i++) {
			unichar ch = [string characterAtIndex:i];
			if (ch >= '0' && ch <= '9') {
				NSString *newChStr = [NSString stringWithCharacters:&ch length:1];
				[res appendString:newChStr];
			} else {
				break;
			}
		}
	}
	
	return res;
}

+ (unichar)lastChar:(NSString *)string
{
	unichar res = [string characterAtIndex:(string.length - 1)];
	return res;
}

+ (NSArray *)splitString:(NSString *)s
			 bySeparator:(NSString *)sep
 includeSeparatorEntries:(BOOL)includeSepEntries
{
	NSMutableArray *tempRes;
	
	if (sep == nil) {
		tempRes = [NSMutableArray arrayWithCapacity:1];
		if (s != nil) {
			[tempRes addObject:s];
		}
	} else {
		NSArray *comps = [s componentsSeparatedByString:sep];
		tempRes = [NSMutableArray arrayWithCapacity:comps.count * 2];
		BOOL useSep = NO;
		for (NSString *s in comps) {
			if (useSep) {
				[tempRes addObject:sep];
			}
			if (includeSepEntries) {
				useSep = YES;
			}
			[tempRes addObject:s];
		}
	}

	NSArray *res = [NSArray arrayWithArray:tempRes];
	return res;
}

+ (NSRange)rangeOfNextParagraphIn:(NSString *)string fromLoc:(NSUInteger)loc
{
	NSRange res = NSMakeRange(NSNotFound, 0);
	
	if (loc < string.length) {
		//NSString *paraSep = @"\n";
		NSRange rangeSearch = NSMakeRange(loc, string.length - loc);
		
		//TODO !!! handle soft nl
		//NSRange rangeParaSep = [string rangeOfString:paraSep options:0 range:rangeSearch];
		NSInteger firstNewlineIdx = -1;
		NSUInteger stringLen = string.length;
		for (NSUInteger idx = loc; idx < stringLen; idx++) {
			unichar ch = [string characterAtIndex:idx];
			if ([XTStringUtils isEffectiveNewline:ch]) {
				firstNewlineIdx = idx;
				break;
			}
		}
		
		NSUInteger paraLen;
		if (firstNewlineIdx == -1) {
			paraLen = rangeSearch.length; // i.e. the rest of string
		} else {
			paraLen = firstNewlineIdx - loc + 1;
		}
			
		res = NSMakeRange(loc, paraLen);
	}
	
	return res;
}

//TODO !!! unit test
+ (NSRange)rangeOfParagraphIn:(NSString *)string atLoc:(NSUInteger)loc
{
	NSRange res = NSMakeRange(NSNotFound, 0);
	
	if (loc < string.length && string.length >= 1) {
		
		unichar charAtLoc = [string characterAtIndex:loc];
		
		NSUInteger newlinesToFindBackward = (charAtLoc == '\n' ? 2 : 1);
		NSUInteger startLoc = loc;
		for (NSInteger idx = loc; idx >= 0 && newlinesToFindBackward >= 1; idx -= 1) {
			unichar ch = [string characterAtIndex:idx];
			if (ch != '\n') {
				startLoc = idx;
			} else {
				newlinesToFindBackward -= 1;
			}
		}
		
		NSUInteger newlinesToFindForward = (charAtLoc == '\n' ? 0 : 1);
		NSUInteger endLoc = loc;
		NSUInteger stringLen = string.length;
		for (NSInteger idx = loc; idx < stringLen && newlinesToFindForward >= 1; idx += 1) {
			unichar ch = [string characterAtIndex:idx];
			if (ch == '\n') {
				newlinesToFindForward -= 1;
			}
			endLoc = idx;
		}
		
		res = NSMakeRange(startLoc, endLoc - startLoc + 1);
	}
	
	return res;
}

+ (NSArray *)splitAttributedString:(NSAttributedString *)attrString
					   bySeparator:(NSString *)separator
{
	NSArray *res = [self splitAttributedString:attrString bySeparator:separator includeSeparatorEntries:YES];	
	return res;
}

+ (NSArray<NSAttributedString *>*)splitIntoParagraphs:(NSAttributedString *)attrString
{
	NSMutableArray<NSAttributedString *> *res = [NSMutableArray arrayWithCapacity:10];
	NSString *string = attrString.string;
	NSUInteger attrStringLen = string.length;
	
	NSUInteger idx = 0;
	NSUInteger idxParaStart = 0;
	while (idx < attrStringLen) {
		unichar ch = [string characterAtIndex:idx];
		idx += 1;
		if (ch == '\n') {
			NSRange range = NSMakeRange(idxParaStart, idx - idxParaStart);
			NSAttributedString * para = [attrString attributedSubstringFromRange:range];
			[res addObject:para];
			idxParaStart = idx;
		}
	}
	if (idx > idxParaStart) {
		NSRange range = NSMakeRange(idxParaStart, idx - idxParaStart);
		NSAttributedString * para = [attrString attributedSubstringFromRange:range];
		[res addObject:para];
	}
	
	return res;
}

+ (NSArray *)splitAttributedString:(NSAttributedString *)attrString
					   bySeparator:(NSString *)separator
		   includeSeparatorEntries:(BOOL)includeSeparatorEntries
{
	NSArray *plainStrings = [XTStringUtils splitString:attrString.string bySeparator:separator includeSeparatorEntries:includeSeparatorEntries];
	
	NSMutableArray *res = [NSMutableArray arrayWithCapacity:plainStrings.count];
	
	NSUInteger idx = 0;
	for (NSString *s in plainStrings) {
		if (s.length >= 1) {
			NSRange range = NSMakeRange(idx, s.length);
			NSAttributedString *as = [attrString attributedSubstringFromRange:range];
			[res addObject:as];
			idx += range.length;
		}
	}
	
	return res;
}

+ (NSString *)safeNameForEncoding:(NSStringEncoding)encoding
{
	NSString *encName = [NSString localizedNameOfStringEncoding:encoding];
	encName = [encName stringByReplacingOccurrencesOfString:@" " withString:@"-"];
	encName = [encName stringByReplacingOccurrencesOfString:@"(" withString:@"-"];
	encName = [encName stringByReplacingOccurrencesOfString:@")" withString:@"-"];
	return encName;
}

+ (BOOL)isTads2GameUrl:(NSURL *)url
{
	BOOL res = [XTStringUtils url:url endsWith:@"." XT_FILE_EXTENSION_TADS2_GAME];
	return res;
}
	
+ (BOOL)isTads3GameUrl:(NSURL *)url
{
	BOOL res = [XTStringUtils url:url  endsWith:@"." XT_FILE_EXTENSION_TADS3_GAME];
	return res;
}

+ (BOOL)url:(NSURL *)url endsWith:(NSString *)dotFileExtension
{
	BOOL res = NO;
	if (url != nil) {
		NSString *urlString = [url absoluteString];
		res = [XTStringUtils string:urlString endsWithCaseInsensitive:dotFileExtension];
	}
	return res;
}

+ (BOOL)isEffectiveNewline:(unichar)ch
{
	BOOL res = (ch == '\n' /*|| ch == UNICHAR_LINE_SEPARATOR_CHAR*/);
		//TODO also u2029?
	return res;
}

+ (void)trimTrailingNewlines:(NSMutableAttributedString *)mutAttrString
{
	//XT_WARN_ENTRY;
	
	NSString *string = mutAttrString.string;
	NSInteger idx = [mutAttrString length] - 1;
	
	while (idx >= 0 && [self isEffectiveNewline:[string characterAtIndex:idx]]) {
		[mutAttrString deleteCharactersInRange:NSMakeRange(idx, 1)];
		idx -= 1;
	}
}

+ (void)removeTrailingString:(NSString *)string from:(NSMutableAttributedString *)mutAttrString
{
	//XT_DEF_SELNAME;
	//XT_WARN_1(@"%@", mutAttrString.string);
	
	NSString *fromString = mutAttrString.string;
	
	if ([XTStringUtils string:fromString endsWith:string]) {
		NSInteger rmFromIdx = fromString.length - string.length;
		[mutAttrString deleteCharactersInRange:NSMakeRange(rmFromIdx, string.length)];
	}
}

//TODO !!! unit test
+ (NSArray<XTRange *>*)findRangesOfLinesExcludingNewlines:(NSString *)string
{
	NSMutableArray<XTRange *> *res = [NSMutableArray arrayWithCapacity:50];
	
	NSUInteger stringLen = string.length;
	NSUInteger idxLineStart = 0;
	
	while (idxLineStart < stringLen) {
		NSUInteger idxLineEnd = idxLineStart;
		while (idxLineEnd < stringLen) {
			unichar ch = [string characterAtIndex:idxLineEnd];
			if ([self isEffectiveNewline:ch]) {
				break;
			} else {
				idxLineEnd += 1;
			}
		}
		NSUInteger lineLen = idxLineEnd - idxLineStart;
		NSUInteger lineLenForRange = lineLen;
		if (lineLenForRange == 0) {
			//...lineLenForRange = 1; // empty line TODO !!! why is this needed?!
		}
		XTRange *range = [XTRange rangeWithLocation:idxLineStart length:lineLenForRange];
		//NSString *stringForRange = [string substringWithRange:range.range];
		[res addObject:range];
		idxLineStart += (lineLen + 1);
	}
	
	return res;
}

//TODO !!! unit test
+ (NSUInteger)numberOfTrailingNewlinesIn:(NSString *)string
{
	NSUInteger res = 0;
	
	NSUInteger len = string.length;
	for (NSInteger idx = len - 1; idx >= 0; idx--) {
		unichar ch = [string characterAtIndex:idx];
		if (ch == '\n') {
			res += 1;
		} else {
			break;
		}
	}
	
	return res;
}

+ (NSAttributedString *)withoutTrailingNewline:(NSAttributedString *)attrString
{
	NSAttributedString *res = attrString;
	
	NSUInteger attrStringLength = attrString.length;
	if (attrStringLength >= 1) {
		unichar lastCh = [attrString.string characterAtIndex:(attrStringLength - 1)];
		if (lastCh == '\n') {
			NSRange range = NSMakeRange(0, attrStringLength - 1);
			res = [attrString attributedSubstringFromRange:range];
		}
	}
	
	return res;
}

+ (NSAttributedString *)withOnlyLastChar:(NSAttributedString *)attrString
{
	NSAttributedString *res = attrString;
	
	if (attrString != nil && attrString.length >= 1) {
		NSRange range = NSMakeRange(attrString.length - 1, 1);
		res = [attrString attributedSubstringFromRange:range];
	}
	
	return res;
}

//TODO !!! unit test
+ (NSString *)withoutLastChar:(NSString *)string
{
	NSString *res = string;
	
	if (string != nil && string.length >= 1) {
		res = [string substringToIndex:(string.length - 1)];
	}
	return res;
}

+ (BOOL)stringContainsNewlines:(NSString *)string
{
	BOOL res = [string containsString:@"\n"];
	return res;
}

+ (NSMutableParagraphStyle *)mutableParagraphStyleFor:(NSAttributedString *)attrString
{
	NSMutableParagraphStyle *res = [XTStringUtils mutableParagraphStyleFor:attrString atIndex:0];
	return res;
}

+ (NSMutableParagraphStyle *)mutableParagraphStyleFor:(NSAttributedString *)attrString atIndex:(NSUInteger)index
{
	NSDictionary *attrsDict = [attrString attributesAtIndex:index effectiveRange:nil];
	NSParagraphStyle *pgStyle = (NSParagraphStyle *) [attrsDict objectForKey:NSParagraphStyleAttributeName];
	NSMutableParagraphStyle *res = [pgStyle mutableCopy];
	return res;
}

+ (NSAttributedString *)filterZwsp:(NSAttributedString *)attrString
{
	NSMutableAttributedString *mutAttrString = [[NSMutableAttributedString alloc] initWithAttributedString:attrString];

	for (NSInteger idx = [mutAttrString length] - 1; idx >= 0; idx--) {
		unichar ch = [mutAttrString.string characterAtIndex:idx];
		if (ch == ZERO_WIDTH_SPACE_CHAR) {
			[mutAttrString deleteCharactersInRange:NSMakeRange(idx, 1)];
		}
	}
	
	return mutAttrString;
}

+ (void)applyParagraphStyle:(NSParagraphStyle *)paragraphStyle toAttrString:(NSMutableAttributedString *)mutAttrString
{
	if (mutAttrString.length >= 1) {
		NSRange range = NSMakeRange(0, mutAttrString.length);
		[mutAttrString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range];
	}
}

+ (NSMutableAttributedString *)concatenateAttributedStringArray:(NSArray<NSAttributedString *> *)attrStringArray
{
	NSMutableAttributedString *res = [NSMutableAttributedString new];
	
	for (NSAttributedString *attrString in attrStringArray) {
		[res appendAttributedString:attrString];
	}
	
	return res;
}

//TODO !!! adapt: mv to formetter / handler?
+ (void)removeXTadsParagraphStyles:(NSMutableAttributedString *)mutAttrString
{
	NSString *string = mutAttrString.string;
	NSUInteger stringLen = string.length;
	NSUInteger idxLineStart = 0;
	
	while (idxLineStart < stringLen) {
		NSRange lineRange;
		NSMutableParagraphStyle *paraStyle = [mutAttrString attribute:NSParagraphStyleAttributeName atIndex:idxLineStart effectiveRange:&lineRange];
		
		NSArray *oldTabStops = paraStyle.tabStops;
		if (oldTabStops.count >= 1) {
			for (XTTextTab *oldTabStop in oldTabStops) {
				if ([oldTabStop isKindOfClass:[XTTextTab class]]) {
					XTTextTab *castOldTabStop = (XTTextTab *)oldTabStop;
					[castOldTabStop removeCustomOptions];
				}
			}
		}

		// Needed to prevent memory leaks:
		NSArray *textBlocks = paraStyle.textBlocks;
		if (textBlocks.count >= 1) {
			for (NSTextBlock *block in textBlocks) {
				if ([block isKindOfClass:[XTTextTableBlock class]]) {
					XTTextTableBlock *castBlock = (XTTextTableBlock *)block;
					if ([castBlock.table isKindOfClass:[XTTextTable class]]) {
						XTTextTable *castTable = (XTTextTable *)castBlock.table;
						[castTable clear];
					}
				}
			}
		}
		
		idxLineStart += lineRange.length;
	}

	//TODO !!! exp: - rm if not needed
	if (stringLen >= 1) {
		NSRange fullRange = NSMakeRange(0, stringLen);
		[mutAttrString removeAttribute:@"XTads_tabStops" range:fullRange];
	}
}

+ (XTTextTableBlock *)tableCellForAttrString:(NSAttributedString *)attrString range:(NSRange)range
{
	NSParagraphStyle *paragraphStyle = [self paragraphStyleOfAttrString:attrString range:range];
	XTTextTableBlock *res = [self tableCellForParagraphStyle:paragraphStyle];
	return res;
}

+ (XTTextTableBlock *)tableCellForAttrString:(NSAttributedString *)attrString atIndex:(NSUInteger)idx
{
	NSParagraphStyle *paragraphStyle = [attrString attribute:NSParagraphStyleAttributeName atIndex:idx effectiveRange:nil];
	XTTextTableBlock *res = [self tableCellForParagraphStyle:paragraphStyle];
	return res;
}

+ (XTTextTableBlock *)tableCellForParagraphStyle:(NSParagraphStyle *)paragraphStyle
{
	XTTextTableBlock *res = nil;

	//TODO !!! when nested tables...
	NSArray<NSTextBlock *> *textBlockArray = paragraphStyle.textBlocks;
	if ((textBlockArray != nil) && (textBlockArray.count >= 1)) {
		NSTextBlock *tableCell = textBlockArray[0];
		if ([tableCell isKindOfClass:[XTTextTableBlock class]]) {
			res = (XTTextTableBlock *)tableCell;
		}
	}

	return res;
}

+ (NSParagraphStyle *)paragraphStyleOfAttrString:(NSAttributedString *)attrString range:(NSRange)range
{
	NSParagraphStyle *res = nil;
	
	if (range.location != NSNotFound && range.length >= 1) {
		// There is some text in current paragraph
		NSDictionary *attrs = [attrString attributesAtIndex:range.location effectiveRange:nil];
		res = [attrs objectForKey:NSParagraphStyleAttributeName];
	}

	return res;
}

+ (void)removeAttributeForNewlines:(NSMutableAttributedString *)mutAttrString attrName:(NSString *)attrName
{
	NSString *string = mutAttrString.string;
	NSUInteger stringLen = string.length;
	
	for (NSUInteger idx = 0; idx < stringLen; idx++) {
		unichar ch = [string characterAtIndex:idx];
		if (ch == '\n') {
			NSRange range = NSMakeRange(idx, 1);
			[mutAttrString removeAttribute:attrName range:range];
		}
	}
}

+ (NSString *)stringForSpecialSpaceWhenMonoFont:(unichar)specialSpace
{
	// See http://www.tads.org/t3doc/doc/sysman/fmt.htm
	
	NSString *res;
	
	switch (specialSpace) {
		case 0x2003: // emsp
			res = @"   ";
			break;
		case 0x2002: // ensp
			// fall through
		case 0x2004: // tpmsp
			res = @"  ";
			break;
		case 0x2008: // puncsp
			// fall through
		case 0x200A: // hairsp
			res = @"";
			break;
		default:
			res = @" ";
			break;
	}
	
	return res;
}

+ (void)applyParagraphStyle:(NSMutableParagraphStyle *)pgStyle toLastParagraphOf:(NSMutableAttributedString *)mutAttrString
{
	NSString *string = [mutAttrString string];
	NSRange rangeOfLastPara = [XTStringUtils findRangeOfLastParagraph:string];
	
	[mutAttrString addAttribute:NSParagraphStyleAttributeName value:pgStyle range:rangeOfLastPara];
}

+ (NSMutableParagraphStyle *)getParagraphStyleAtStartOfOngoingParagraphOf:(NSAttributedString *)attrString
{
	//XT_DEF_SELNAME;
	
	NSString *string = [attrString string];
	NSRange range = [XTStringUtils findRangeOfOngoingParagraph:string];

	NSMutableParagraphStyle *res = nil;
	if (range.length >= 1) {
		NSUInteger indexStart = range.location;
		res = [XTStringUtils mutableParagraphStyleFor:attrString atIndex:indexStart];
	}
	
	return res;
}

+ (NSMutableParagraphStyle *)getParagraphStyleAtStartOfLastParagraphOf:(NSAttributedString *)attrString
{
	NSString *string = [attrString string];

	NSRange rangeOfLastPara = [XTStringUtils findRangeOfLastParagraph:string];
	if (rangeOfLastPara.length == 0) {
		XT_DEF_SELNAME;
		XT_ERROR_0(@"rangeOfLastPara.length == 0 -- this should not happen");
	}
	
	NSUInteger indexStartOfLastPara = rangeOfLastPara.location;
	NSMutableParagraphStyle *res = [XTStringUtils mutableParagraphStyleFor:attrString atIndex:indexStartOfLastPara];
	
	return res;
}

+ (void)removeTrailingWhitespaceInLastParagraph:(NSMutableAttributedString *)mutAttrString
{
	NSString *string = mutAttrString.string;
	NSRange rangeOfTrailingWhitespace = [XTStringUtils findRangeOfTrailingWhitespaceInLastParagraph:string];
	if (rangeOfTrailingWhitespace.location != NSNotFound && rangeOfTrailingWhitespace.length >= 1) {
		//XT_DEF_SELNAME;
		//XT_WARN_1(@"rangeOfTrailingWhitespace.length=%lu", rangeOfTrailingWhitespace.length);
		[mutAttrString deleteCharactersInRange:rangeOfTrailingWhitespace];
	}
}

@end
