//
//  XTFontUtils.m
//  XTads
//
//  Created by Rune Berg on 24/04/15.
//  Copyright (c) 2015 Rune Berg. All rights reserved.
//

#import "XTFontUtils.h"
#import "XTStringUtils.h"
#import "XTLogger.h"
#import "XTTimer.h"
#import "XTRequiredRectForTextCache.h"
#import "XTTabStopUtils.h"


@implementation XTFontUtils

static XTLogger* logger;

static NSLayoutManager *scratchLayoutManager;
static NSTextContainer *scratchTextContainer;
static NSTextStorage *scratchTextStorage;

static double totalTimeInRequiredRectForText = 0.0;
+ (double)getTotalTimeInRequiredRectForText
{
	return totalTimeInRequiredRectForText;
}

+ (void)initialize
{
	logger = [XTLogger loggerForClass:[XTFontUtils class]];
	
	scratchLayoutManager = [[NSLayoutManager alloc] init];
	NSSize scratchViewSize = NSMakeSize(10000.0, 10000.0);
	scratchTextContainer = [[NSTextContainer alloc] initWithSize:scratchViewSize];
	scratchTextContainer.lineFragmentPadding = 0;
	[scratchLayoutManager addTextContainer:scratchTextContainer];
	scratchTextStorage = [NSTextStorage new];
	[scratchTextStorage addLayoutManager:scratchLayoutManager];
}

+ (NSFont *)boldVersionOfFont:(NSFont *)font
{
	// See http://www.cocoabuilder.com/archive/cocoa/133618-nsfont-bold.html
	
	NSFontManager *nsfontManager = [NSFontManager sharedFontManager];
	NSFont* boldFont = [nsfontManager convertFont:font toHaveTrait:NSBoldFontMask];
	if (boldFont == font) {
		// original font has no bold version, so try a different approach
		boldFont = [nsfontManager convertWeight:YES ofFont:font];
	}
	return boldFont;
}

+ (CGFloat)defaultTextLineHeight:(NSFont *)font;
{
	NSLayoutManager *layoutMgr = [NSLayoutManager new];
	CGFloat res = [layoutMgr defaultLineHeightForFont:font];

	return res;
}

+ (CGFloat)defaultWidthOfString:(NSString *)string inFont:(NSFont *)font;
{
	// See http://stackoverflow.com/questions/3692143/cocoa-get-string-width-in-pixels-from-font
	
	NSDictionary *attrsDict = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
	NSSize size = [string sizeWithAttributes:attrsDict];
	CGFloat width = size.width;
	if (! font.isFixedPitch) {
		//TODO exp rm'd: width = width * (1.185);  // because sizeWithAttributes doesn't seem to work quite right
	}
	
	width = ceil(width); // round up to a whole number
	
	return width;
}

//TODO !!! add range param to method below
+ (CGFloat)widthOfLongestIndivisibleWordInTextStorage:(NSTextStorage *)textStorage
								   	numCharsInLongest:(NSUInteger *)numCharsInLongest
{
	NSRange range = NSMakeRange(0, textStorage.length);
	CGFloat res = [XTFontUtils widthOfLongestIndivisibleWordInTextStorage:textStorage
																	range:range
														numCharsInLongest:numCharsInLongest];
	return res;
}

//TODO test w/ nbsp
+ (CGFloat)widthOfLongestIndivisibleWordInTextStorage:(NSTextStorage *)textStorage
												range:(NSRange)range
									numCharsInLongest:(NSUInteger *)numCharsInLongest
{
	XT_DEF_SELNAME;
	
	NSSize viewSize = NSMakeSize(10000.0, 10000.0);
	
	CGFloat widthOfLongest = 0.0;
	
	NSString *string = [textStorage string];
	NSCharacterSet *breakingWhitespaceCharSet = [XTStringUtils breakingWhitespaceCharSet]; //TODO !!! reconsider for HTML tables
	
	BOOL isInWord = NO;
	
	NSUInteger currentWordStartIdx = 0;
	NSUInteger currentWordEndIdx = 0;
	NSUInteger lengthOfWidest = 0;
	
	for (NSUInteger i = range.location; i < (range.location + range.length); i++) {
		unichar ch = [string characterAtIndex:i];
		BOOL chIsBreakingWhitespace = [breakingWhitespaceCharSet characterIsMember:ch];
		if (! isInWord) {
			if (! chIsBreakingWhitespace) {
				// starting a word
				currentWordStartIdx = i;
				currentWordEndIdx = i;
				isInWord = YES;
			}
		} else {
			if (chIsBreakingWhitespace) {
				// ending a word
				NSUInteger lengthOfCurrent = currentWordEndIdx - currentWordStartIdx + 1;
				
				NSSize size = [self requiredRectForTextStorage:textStorage
														 range:NSMakeRange(currentWordStartIdx, lengthOfCurrent)
												 forViewOfSize:viewSize];
							   
				CGFloat widthOfCurrent = size.width;
				if (widthOfCurrent > widthOfLongest) {
					widthOfLongest = widthOfCurrent;
					lengthOfWidest = lengthOfCurrent;
				}
				isInWord = NO;
			} else {
				currentWordEndIdx = i;
			}
		}
	}
	
	if (isInWord) {
		NSUInteger lengthOfCurrent = currentWordEndIdx - currentWordStartIdx + 1;
		
		NSSize size = [self requiredRectForTextStorage:textStorage
												 range:NSMakeRange(currentWordStartIdx, lengthOfCurrent)
										 forViewOfSize:viewSize];
		
		CGFloat widthOfCurrent = size.width;
		if (widthOfCurrent > widthOfLongest) {
			widthOfLongest = widthOfCurrent;
			lengthOfWidest = lengthOfCurrent;
		}
	}

	widthOfLongest = ceil(widthOfLongest);
	XT_TRACE_2(@"-> %f (%lu)", widthOfLongest, lengthOfWidest);
	
	if (numCharsInLongest != nil) {
		*numCharsInLongest = lengthOfWidest;
	}
	return widthOfLongest;
}

+ (NSSize)sizeOfTextInTextStorage:(NSTextStorage *)textStorage
							range:(NSRange)range
						 viewSize:(NSSize)viewSize
{
	NSSize size = [self requiredRectForTextStorage:textStorage
											 range:range
									 forViewOfSize:viewSize];

	return size;
}

+ (NSSize)sizeOfAttrString:(NSAttributedString *)attrString
{
	NSSize res = NSMakeSize(0.0, 0.0);
	if (attrString.length >= 1) {
		NSSize viewSize = NSMakeSize(10000.0, 10000.0);
		res = [self requiredRectForText:attrString forViewOfSize:viewSize suppressCenterAndRightTabs:YES roundWidthToCeil:NO];
	}
	return res;
}

+ (CGFloat)widthOfLongestLineInTextStorage:(NSTextStorage *)textStorage
{
	//XT_DEF_SELNAME;
	
	NSString *string = [textStorage string];
	
	NSSize size = [self requiredRectForTextStorage:textStorage
											 range:[XTStringUtils rangeOfLongestLineIn:string]
									 forViewOfSize:NSMakeSize(10000.0, 10000.0)];
	return size.width;
}

+ (CGFloat)heightOfText:(NSTextView *)textView
{
	CGFloat fullWidth = textView.bounds.size.width;
	CGFloat usableWidth = textView.bounds.size.width;
	NSSize inset = textView.textContainerInset;
	CGFloat totalInsetWidth = inset.width * 2.0;  // both lhs and rhs
	usableWidth = fullWidth - totalInsetWidth;
	if (usableWidth < totalInsetWidth) {
		usableWidth = totalInsetWidth;
	}
	if (usableWidth <= 0.0) {
		usableWidth = 16.0;
	}
	
	NSTextStorage *textStorage = [textView textStorage];

	NSSize size = [self requiredRectForTextStorage:[textView textStorage]
											 range:NSMakeRange(0, textStorage.length)
									 forViewOfSize:NSMakeSize(usableWidth, 10000.0)];
	
	return size.height;
}

+ (NSSize)requiredRectForTextStorage:(NSTextStorage *)textStorage
							   range:(NSRange)range
					   forViewOfSize:(NSSize)viewSize {
	
	//XTTimer *timer = [XTTimer fromNow];
	
	NSAttributedString *attrStringForRange = [textStorage attributedSubstringFromRange:range];
	
	NSSize res = NSZeroSize;
	NSArray<XTRange *>* rangesOfLinesExcludingNewlines = [XTStringUtils findRangesOfLinesExcludingNewlines:attrStringForRange.string];

	for (XTRange *rangeObj in rangesOfLinesExcludingNewlines) {
		[rangeObj offsetLocationBy:range.location];
	}

	for (XTRange *rangeObj in rangesOfLinesExcludingNewlines) {
		NSRange rangeUsedForCalc = rangeObj.range;
		if (rangeUsedForCalc.length == 0) {
			// Empty line (newline only) - do calc *with* newline to get correct height
			rangeUsedForCalc.length = 1;
		}
		NSString *stringForRange = [textStorage.string substringWithRange:rangeUsedForCalc];
		NSAttributedString *attrStringForRange = [textStorage attributedSubstringFromRange:rangeUsedForCalc];
		NSSize sizeOfLine = [self requiredRectForTextStorageLine:textStorage range:rangeUsedForCalc forViewOfSize:viewSize];
		res.height += sizeOfLine.height;
		if ((rangeObj.range.length >= 1) && (sizeOfLine.width > res.width)) { // only consider non-empty lines for width
			res.width = sizeOfLine.width;
		}
		
		if (res.width == 10000.0) {
			XT_DEF_SELNAME;
			XT_WARN_0(@"res.width == 10000.0");
			//NSSize sizeOfLine = [self requiredRectForTextStorageLine:textStorage range:rangeUsedForCalc forViewOfSize:viewSize];
		} else {
			int brkpt = 1;
		}
	}
	
	//totalTimeInRequiredRectForTextStorage += [timer timeElapsed];

	if (res.width == 10000.0) {
		XT_DEF_SELNAME;
		XT_ERROR_0(@"res.width == 10000.0");
	}

	return res;
}

+ (NSSize)requiredRectForTextStorageLine:(NSTextStorage *)textStorage
								   range:(NSRange)range
						   forViewOfSize:(NSSize)viewSize {
	
	if (range.length == 0) {
		return NSZeroSize;
	}
	
	// Find the attr string we're calc'ing for:

	NSAttributedString *subString = [textStorage attributedSubstringFromRange:range];
	
	// Find the required size

	NSSize res = [self requiredRectForText:subString forViewOfSize:viewSize suppressCenterAndRightTabs:YES];
	return res;
}

+ (NSSize)requiredRectForText:(NSAttributedString *)attrAtring
				forViewOfSize:(NSSize)viewSize
   suppressCenterAndRightTabs:(BOOL)suppressCenterAndRightTabs {
	
	NSSize res = [self requiredRectForText:attrAtring
							 forViewOfSize:viewSize
				suppressCenterAndRightTabs:suppressCenterAndRightTabs
						  roundWidthToCeil:YES];
	return res;
}
 

+ (NSSize)requiredRectForText:(NSAttributedString *)attrAtring
				forViewOfSize:(NSSize)viewSize
   suppressCenterAndRightTabs:(BOOL)suppressCenterAndRightTabs
			 roundWidthToCeil:(BOOL)roundWidthToCeil{
	
	//XT_DEF_SELNAME;roundWidthToCeil:
	//XT_WARN_1(@"attrAtring=\"%@\"", attrAtring);

	XTTimer *timer = [XTTimer fromNow];
	
	attrAtring = [self attributedStringForMeasuringSize:attrAtring suppressCenterAndRightTabs:suppressCenterAndRightTabs];
	//XT_WARN_1(@"attrAtring(meas.)=\"%@\"", attrAtring.string);
	
	//NSParagraphStyle *pgStyleImmutable = [attrAtring attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:nil];
	//NSArray * tabArray = pgStyleImmutable.tabStops;
	
	XTRequiredRectForTextCache *reqRectForTextCache = [XTRequiredRectForTextCache singletonInstance];
	NSSize cachedSize = [reqRectForTextCache getSizeForText:attrAtring viewSize:viewSize];
	if (! NSEqualSizes(cachedSize, NSZeroSize)) {
		return cachedSize;
	}
	
	[scratchTextContainer setSize:viewSize];
	
	[scratchTextStorage setAttributedString:attrAtring];
	
	NSRange rangeAttrString = NSMakeRange(0, attrAtring.length);
	NSRect res = [scratchLayoutManager boundingRectForGlyphRange:rangeAttrString inTextContainer:scratchTextContainer];
	if (roundWidthToCeil) {
		res.size.width = ceil(res.size.width);
	}
	res.size.height = ceil(res.size.height);
	
	totalTimeInRequiredRectForText += [timer timeElapsed];
	
	[reqRectForTextCache noteSize:res.size forText:attrAtring viewSize:viewSize];
	
	//XT_DEF_SELNAME;
	//XT_WARN_2(@"--> (w=%lf, h=%lf)", res.size.width, res.size.height);
	
	return res.size;
}

+ (NSAttributedString *)attributedStringForMeasuringSize:(NSAttributedString *)attrString
							  suppressCenterAndRightTabs:(BOOL)suppressCenterAndRightTabs
{
	NSAttributedString *res = [XTFontUtils attributedStringWithoutBlocks:attrString];
	if (suppressCenterAndRightTabs) {
		res = [XTFontUtils attributedStringWithoutCenterOrRightAlignedTabs:res];
	}
	return res;
}

//TODO !!! mv to stringutils? or too specialized for that?
+ (NSAttributedString *)attributedStringWithoutBlocks:(NSAttributedString *)attrString
{
	NSMutableAttributedString *res = [NSMutableAttributedString new];
	[res setAttributedString:attrString];

	NSUInteger length = attrString.length;
	NSUInteger idx = 0;

	while (idx < length) {
		NSRange range;
		NSDictionary *attrsForSubString = [res attributesAtIndex:idx effectiveRange:&range];
		NSParagraphStyle *pgStyleImmutable = attrsForSubString[NSParagraphStyleAttributeName];

		if (pgStyleImmutable.textBlocks.count >= 1) {

			NSMutableParagraphStyle *pgStyleWithoutBlocks = [NSMutableParagraphStyle new];
			[pgStyleWithoutBlocks setParagraphStyle:pgStyleImmutable];
			pgStyleWithoutBlocks.textBlocks = [NSArray array];

			NSMutableDictionary *attrsWithoutBlocks = [NSMutableDictionary dictionaryWithDictionary:attrsForSubString];
			attrsWithoutBlocks[NSParagraphStyleAttributeName] = pgStyleWithoutBlocks;

			[res setAttributes:attrsWithoutBlocks range:range];
		}

		idx += range.length;
	}
	
	return res;
}

//TODO !!! mv to stringutils? or too specialized for that?
+ (NSAttributedString *)attributedStringWithoutCenterOrRightAlignedTabs:(NSAttributedString *)attrString
{
	NSMutableAttributedString *res = [NSMutableAttributedString new];
	[res setAttributedString:attrString];

	NSUInteger tabCharCount = 0;
	
	for (NSInteger idx = res.length - 1; idx >= 0; idx--) {
		unichar ch = [res.string characterAtIndex:idx];
		if (ch == '\t') {
			NSRange rangeOfAttrs;
			NSDictionary *attrsOriginal = [attrString attributesAtIndex:idx effectiveRange:&rangeOfAttrs];
			NSParagraphStyle *pgStyleOriginal = attrsOriginal[NSParagraphStyleAttributeName];
			NSArray *tabStopsOriginal = pgStyleOriginal.tabStops;
			tabCharCount += 1;
			NSUInteger idxTab = tabStopsOriginal.count - tabCharCount;
			NSTextTab *textTab = tabStopsOriginal[idxTab];
			if ([textTab isKindOfClass:[XTTextTab class]]) {
				XTTextTab *castTextTab = (XTTextTab *)textTab;
				BOOL tabIsCenterAligneded = [XTTabStopUtils tabStopIsAtHalfwayToRhsOfView:castTextTab];
				BOOL tabIsRightAligned = [XTTabStopUtils tabStopIsAtRhsOfView:castTextTab];

				if (tabIsCenterAligneded || tabIsRightAligned) {
					NSMutableArray *tabStopsNew = [NSMutableArray arrayWithArray:tabStopsOriginal];
					[tabStopsNew removeObjectAtIndex:idxTab];

					NSMutableParagraphStyle *pgStyleNew = [NSMutableParagraphStyle new];
					[pgStyleNew setParagraphStyle:pgStyleOriginal];
					pgStyleNew.tabStops = tabStopsNew;

					NSRange rangeOfParagraphWithTab = [XTStringUtils rangeOfParagraphIn:attrString.string atLoc:idx];
					[res addAttribute:NSParagraphStyleAttributeName value:pgStyleNew range:rangeOfParagraphWithTab];

					NSRange range = NSMakeRange(idx, 1);
					[res replaceCharactersInRange:range withString:@"  "];
				}
			}
		} else if (ch == '\n') {
			// entering next paragraph
			tabCharCount = 0;
		}
	}
	
	return res;
}

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

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

@end
