//
//  XTTabStopHelper.m
//  XTads
//
//  Created by Rune Berg on 18/07/2020.
//  Copyright © 2020 Rune Berg. All rights reserved.
//

#import "XTTabStopHelper.h"
#import "XTTabStopModelEntry.h"
#import "XTLogger.h"
#import "XTAllocDeallocCounter.h"
#import "XTFontUtils.h"
#import "XTStringUtils.h"
#import "XTCharConstants.h"
#import "XTTabStopModel.h"
#import "XTTabStopUtils.h"
#import "XTMutableAttributedStringHelper.h"


@interface XTTabStopHelper ()

@property XTTabStopModel *tabStopModel;
@property NSArray<XTRecalcTabStopCommand *> *tabStopsNeedingRecalc;

@property NSArray *emptyArray;

@end


@implementation XTTabStopHelper

static XTLogger* logger;

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

OVERRIDE_ALLOC_FOR_COUNTER
OVERRIDE_DEALLOC_FOR_COUNTER

- (instancetype)init
{
	self = [super init];
	if (self) {
		_textView = nil;
		_tabStopModel = [XTTabStopModel new];
		_emptyArray = [NSArray array];
	}
	return self;
}

- (NSArray *)getDefaultTabStops
{
	return self.emptyArray;
}

- (XTTextTab *)createTabStopAtRhsOfViewForOptTextTableBlock:(XTTextTableBlock *)textTableBlock
{
	[self.textView ensureLayoutForTextContainer];
	
	NSTextAlignment alignment = NSTextAlignmentRight;
	CGFloat locRhsOfView;
	
	if (textTableBlock != nil) {
		XTRect *contentRect = [textTableBlock getlatestContentRect];
		if (contentRect == nil) {
			//XT_WARN_1(@"contentRect == nil for textTableBlock %@", textTableBlock);
			locRhsOfView = 20;
			// temporary, placeholder value - otherwise it would be 0, which causes a problem with attr.string width measurements
			// gets recalc'd later
			//XT_WARN_1(@"created tab at pos %lf (contentRect == nil)", tabLoc);
		} else {
			CGFloat contentRectWidth = contentRect.rect.size.width;
			locRhsOfView = contentRectWidth;
		}

	} else {
		locRhsOfView = [self.textView findCoordOfRhsOfView];
	}

	NSDictionary<NSString *,id> *customOptions = [NSDictionary dictionaryWithObject:XT_TAB_TYPE_RHS_OF_VIEW forKey:XT_TAB_TYPE_KEY];
	XTTextTab *tab = [XTTextTab withTextAlignment:alignment location:locRhsOfView options:nil customOptions:customOptions];
	
	//XT_DEF_SELNAME;
	//XT_WARN_1(@"loc=%lf", locRhsOfView);
	
	return tab;
}

- (XTTextTab *)createTabStopAtHalfwayToRhsOfViewFromLoc:(CGFloat)fromLoc textTableBlock:(XTTextTableBlock *)textTableBlock
{
	//XT_DEF_SELNAME;
	
	[self.textView ensureLayoutForTextContainer];
	
	NSTextAlignment alignment = NSTextAlignmentCenter;
	CGFloat tabLoc;
	
	if (textTableBlock != nil) {
		XTRect *contentRect = [textTableBlock getlatestContentRect];
		if (contentRect == nil) {
			//XT_WARN_1(@"contentRect == nil for textTableBlock %@", textTableBlock);
			tabLoc = 20;
				// temporary, placeholder value - otherwise it would be 0, which causes a problem with attr.string width measurements.
				// gets recalc'd later
			//XT_WARN_1(@"created tab at pos %lf (contentRect == nil)", tabLoc);
		} else {
			CGFloat contentRectWidth = contentRect.rect.size.width;
			if ((contentRectWidth - fromLoc) < 0.0) {
				XT_DEF_SELNAME;
				XT_WARN_0(@"(contentRectWidth - fromLoc) < 0.0");
			}
			tabLoc = fromLoc + ((contentRectWidth - fromLoc) / 2.0);
			tabLoc = floor(tabLoc);
			//XT_WARN_2(@"created tab at pos %lf (contentRect width %lf)", tabLoc, contentRect.rect.size.width);
			
			if (tabLoc >= contentRectWidth) {
				tabLoc = contentRectWidth - 1;
			}
		}
	} else {
		tabLoc = [self.textView findCoordOfTabAtHalfwayToRhsOfViewFromLoc:fromLoc];
	}
	
	NSDictionary<NSString *,id> *customOptions = [NSDictionary dictionaryWithObject:XT_TAB_TYPE_HALFWAY_TO_RHS_OF_VIEW forKey:XT_TAB_TYPE_KEY];
	XTTextTab *tab = [XTTextTab withTextAlignment:alignment location:tabLoc options:nil customOptions:customOptions];
	
	//XT_WARN_1(@"--> tabLoc=%lf", tabLoc);

	return tab;
}

- (XTTextTab *)createTabFromModelEntry:(XTTabStopModelEntry *)modelEntry
						   minLocation:(CGFloat)minLocation
							  resizing:(BOOL)resizing
					 optTextTableBlock:(XTTextTableBlock *)textTableBlock
{
	//XT_DEF_SELNAME;
	//XT_TRACE_3(@"modelEntry.ident=%@ modelEntry.position=%f minLocation=%f", modelEntry.ident, modelEntry.position.doubleValue, minLocation);
	
	[self.textView ensureLayoutForTextContainer];
	
	NSTextAlignment alignment = modelEntry.nsTextAlignment;
	NSDictionary<NSString *,id> *options = modelEntry.nsTextAlignmentOptions; // NB! r/o property (not a member field)
	NSDictionary<NSString *,id> *customOptions = [modelEntry getCustomOptions];
	//...clear options entry "xtads.tabModelEntry" ???

	CGFloat pos = modelEntry.position.doubleValue;

	pos -= modelEntry.positionAdjustmentForDecimalPoint;
	if (pos < 0.0) {
		pos = 0.0;
	}
	
	if (! resizing) {
		//TODO !!! exp rm:
		//pos += 0.01;
	} else {
		int brkpt = 1;
	}
	
	if (pos < minLocation) {
		// make sure new tab isn't at or before minLocation (typically == insertion pos.)
		pos = minLocation + 0.01;
	}
	
	//TODO !!! adapt: refactor vs other similar cases
	CGFloat viewWidth;
	if (textTableBlock != nil) {
		XTRect *contentRect = [textTableBlock getlatestContentRect];
		if (contentRect == nil) {
			//XT_WARN_1(@"contentRect == nil for textTableBlock %@", textTableBlock);
			viewWidth = 20;
			// temporary, placeholder value - otherwise it would be 0, which causes a problem with attr.string width measurements
			// gets recalc'd later
			//XT_WARN_1(@"created tab at pos %lf (contentRect == nil)", tabLoc);
		} else {
			CGFloat contentRectWidth = contentRect.rect.size.width;
			viewWidth = contentRectWidth;
		}
	} else {
		viewWidth = [self.textView findCoordOfRhsOfView];
	}
	
	if (pos >= viewWidth) {
		if (resizing) {
			int brkpt = 1;
		} else {
			pos = minLocation + 0.01;
		}
	}
	
	//TODO !!! rm if not needed
	if (pos == 0.0) {
		pos = 0.01;
	}
	
	if (pos >= viewWidth) {
		int brkpt = 1;
	}

	XTTextTab *tab = [XTTextTab withTextAlignment:alignment location:pos options:options customOptions:customOptions];
	
	//XT_WARN_4(@"modelEntry.ident=%@ modelEntry.position=%f minLocation=%f -> %@", modelEntry.ident, modelEntry.position.doubleValue, minLocation, tab);

	return tab;
}

- (XTTextTab *)createTabStopAtPosition:(CGFloat)posNewTab
{
	NSMutableDictionary<NSString *,id> *customOptions = [NSMutableDictionary dictionaryWithCapacity:2];
	[customOptions setObject:XT_TAB_TYPE_FIXED_POS forKey:XT_TAB_TYPE_KEY];
	
	XTTextTab *textTab = [XTTextTab withTextAlignment:NSTextAlignmentLeft location:posNewTab options:nil customOptions:customOptions];
	return textTab;
}

- (XTTextTab *)createTabStopFor:(NSAttributedString *)attrString
			   atNextMultipleOf:(NSNumber *)multiple
				   fromLocation:(CGFloat)location
{
	//XT_DEF_SELNAME;
	
	[self.textView ensureLayoutForTextContainer];
	
	CGFloat enSpaceWidth = [self getHtmlModeEnSpaceTabStopColumnWidthInPointsFor:attrString];
	
	CGFloat locNewTab = [self.tabStopModel findPositionOfNextTabWithMultiple:multiple
																fromPosition:location
																enSpaceWidth:enSpaceWidth];
	
	NSMutableDictionary<NSString *,id> *customOptions = [NSMutableDictionary dictionaryWithCapacity:2];
	[customOptions setObject:XT_TAB_TYPE_MULTIPLE forKey:XT_TAB_TYPE_KEY];
	[customOptions setObject:multiple forKey:XT_TAB_MULTIPLE_KEY];
	
	XTTextTab *textTab = [XTTextTab withTextAlignment:NSTextAlignmentLeft location:locNewTab options:nil customOptions:customOptions];
	//XT_WARN_3(@"fromLocation=%lf multiple=%lu --> loc=%lf", location, multiple.unsignedIntegerValue, locNewTab);
	return textTab;
}

- (CGFloat)getHtmlModeEnSpaceTabStopColumnWidthInPointsFor:(NSAttributedString *)attrString
{
	NSDictionary *dict = [attrString attributesAtIndex:0 effectiveRange:nil];
	NSFont *font = dict[NSFontAttributeName];
	
	NSString *enSpaceString;
	CGFloat scale;
	if (font.fixedPitch) {
		enSpaceString = EFFECTIVE_EN_SPACE_FOR_TABS_MONO_FONT;
		scale = 0.85;
	} else {
		enSpaceString = EFFECTIVE_EN_SPACE_FOR_TABS_PROP_FONT;
		scale = 1.0;
	}
	
	NSAttributedString *attrStringEnSpace = [[NSAttributedString alloc] initWithString:enSpaceString attributes:dict];
	
	NSSize size = [XTFontUtils requiredRectForText:attrStringEnSpace forViewOfSize:NSMakeSize(10000, 10000) suppressCenterAndRightTabs:YES];
	
	CGFloat res = size.width;
	res = res * scale;
	return res;
}

- (void)resetTabStopModel
{
	[self.tabStopModel reset];
}

- (XTTabStopModelEntry *)findModelTabWithId:(NSString *)tabId
{
	XTTabStopModelEntry *res = [self.tabStopModel findTabWithId:tabId];
	return res;
}

- (XTTabStopAlignment)alignmentFromString:(NSString *)s
{
	XTTabStopAlignment res = [self.tabStopModel alignmentFromString:s];
	return res;
}

- (void)addModelTabStopWithId:(NSString *)ident
					 position:(CGFloat)position
					alignment:(NSString *)alignment
				  decimalChar:(NSString *)decimalChar
{
	[self.tabStopModel addTabStopWithId:ident
							   position:position
							  alignment:alignment
							decimalChar:decimalChar];
}

- (XTTabStopModelEntry *)createOverridenModelTabStop:(XTTabStopModelEntry *)tabStopModelEntry
											   align:(NSString *)alignment
										 decimalChar:(NSString *)decimalChar
{
	XTTabStopModelEntry *res = [self.tabStopModel createOverridenTabStop:tabStopModelEntry
																   align:alignment
															 decimalChar:decimalChar];
	return res;
}

- (CGFloat)getPlainTextModeTabStopColumnWidthInPoints:(NSFont *)font
{
	CGFloat fontSize = font.pointSize;
	//CGFloat columnWidthInPoints = ((fontSize / 2.0) + 1.0) * 4.0;
	//CGFloat columnWidthInPoints = ((fontSize / 2.0)) * 4.0;
	//CGFloat columnWidthInPoints = ((fontSize / 2.0) + 1.0) * 3.0;
	//CGFloat columnWidthInPoints = ((fontSize / 2.0)) * 4.0;
	//CGFloat columnWidthInPoints = ((fontSize / 2.0) + 2.0) * 3.0;
	CGFloat columnWidthInPoints = ceil(fontSize / 2.0);
	columnWidthInPoints *= 4.0;
	columnWidthInPoints *= 0.9;
		//TODO make factor dep on fontSize?
	columnWidthInPoints = ceil(columnWidthInPoints);

	//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;
}

- (NSString *)stringForTabIndent:(NSUInteger)indent foldLeadingSpace:(BOOL)foldLeadingSpace
{
	NSMutableString *mutString = [NSMutableString stringWithCapacity:(indent * 2)];
	for (NSInteger idx = 0; idx < indent; idx++) {
		[mutString appendString:EFFECTIVE_EN_SPACE_FOR_TABS_PROP_FONT];
		//[mutString appendString:UNICHAR_EN_SPACE]; // too narrow. 2 regular spaces looks like on qtads.
	}
	NSString *res = mutString;
	if (foldLeadingSpace) {
		if (mutString.length >= 1) {
			res = [XTStringUtils withoutLastChar:mutString];
		}
	}
	return res;
}

- (BOOL)paragraphStyleHasTabStopAtRhsOfWindow:(NSParagraphStyle *)pgStyle
{
   BOOL res = NO;
   NSArray *tabStops = pgStyle.tabStops;
   if (tabStops.count >= 1) {
	   NSTextTab *lastTabStop = tabStops[tabStops.count - 1];
	   if ([lastTabStop isKindOfClass:[XTTextTab class]]) {
		   XTTextTab *castLastTabStop = (XTTextTab *)lastTabStop;
		   res = [XTTabStopUtils tabStopIsAtRhsOfView:castLastTabStop];
	   }
   }
   return res;
}

- (void)prepareForRecalcAllOfTabStops
{
	//XT_WARN_ENTRY;
	
	NSTextStorage *textStorage = [self.textView textStorage];
	NSRange range = NSMakeRange(0, textStorage.length);
	[self prepareForRecalcAllOfTabStopsInRange:range];
}

- (void)prepareForRecalcAllOfTabStopsInRange:(NSRange)range
{
	//XT_WARN_ENTRY;
	
	if (self.tabStopsNeedingRecalc.count >= 1) {
		return;
	}
	
	NSTextStorage *textStorage = [self.textView textStorage];
	self.tabStopsNeedingRecalc = [XTTabStopUtils limitTabStopsSensitiveToResizing:textStorage range:range];
}

- (void)recalcAllTabStops
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"entry")

	//XTTimer *timer = [XTTimer fromNow];
	
	NSTextStorage *textStorage = [self.textView textStorage];
	NSString *textStorageString = textStorage.string;
	
	[self.textView ensureLayoutForTextContainer];
	
	for (XTRecalcTabStopCommand *recalcTabStopCommand in self.tabStopsNeedingRecalc) {
		NSUInteger tabIdxInTextStorage = recalcTabStopCommand.idxInTextStorage;
		NSUInteger tabStopInParaIndex = recalcTabStopCommand.tabIndexInParagraph;
		
		NSRange paraRange = [XTStringUtils rangeOfParagraphIn:textStorageString atLoc:tabIdxInTextStorage];
		//NSAttributedString *attrStringForParaRange = [textStorage attributedSubstringFromRange:paraRange];
		NSMutableParagraphStyle *oldParaStyle = [textStorage attribute:NSParagraphStyleAttributeName atIndex:tabIdxInTextStorage effectiveRange:nil];
		
		if (oldParaStyle == nil) {
			//XT_WARN_0(@"oldParaStyle == nil");
			// This can happen sometimes. Not a problem.
			int brkpt = 1;
		} else {
			NSArray *oldTabStops = oldParaStyle.tabStops;
			NSMutableParagraphStyle *newParaStyle = [oldParaStyle mutableCopy];
			NSMutableArray *newTabStops = [oldTabStops mutableCopy];
			NSTextTab *oldTabStop = oldTabStops[tabStopInParaIndex];

			if ([oldTabStop isKindOfClass:[XTTextTab class]]) {
				XTTextTab *castOldTabStop = (XTTextTab *)oldTabStop;
				NSInteger posBeforeTab = ((NSInteger)tabIdxInTextStorage) - 1;
				XTTextTab *newTabStop = [self recalcTabStop:castOldTabStop fromInsertionLoc:posBeforeTab];
				
				newTabStops[tabStopInParaIndex] = newTabStop;
				newParaStyle.tabStops = newTabStops;
				[textStorage addAttribute:NSParagraphStyleAttributeName value:newParaStyle range:paraRange];
			}
			
			//XTTimer *timerLayout = [XTTimer fromNow];
			//TODO !!! adapt? [self.textView ensureLayoutForTextContainer];
		}
		
		if (recalcTabStopCommand.reinsertTabForPlaceholder) {
			[XTTabStopUtils restoreTabStopSensitiveToResizing:textStorage atPos:tabIdxInTextStorage];
		}
	}

	self.tabStopsNeedingRecalc = nil;

	//if (! self.isForBanner) {
	//	XT_WARN_4(@"took %lf secs for %lu chars, %lu tabstops, idxStart=%lu", [timer timeElapsed], textStorageLen, countTabstopsRecalcd, idxStart);
	//}

	//XT_WARN_0(@"exit")
}

- (XTTextTab *)recalcTabStop:(XTTextTab *)oldTabStop fromInsertionLoc:(NSUInteger)insLoc
{
	XT_DEF_SELNAME;
	//XT_WARN_0(@"");
	
	XTTextTab *newTabStop = nil;
	
	if ([XTTabStopUtils tabStopIsAtRhsOfView:oldTabStop]) {
		NSRange range = NSMakeRange(insLoc + 1, 1); // +1 to get to tab itself -- insLoc is idx _before_ tab char
		XTTextTableBlock *textTableBlock = [self tableCellForTextInRange:range];
		newTabStop = [self createTabStopAtRhsOfViewForOptTextTableBlock:textTableBlock];

	} else if ([XTTabStopUtils tabStopIsAtHalfwayToRhsOfView:oldTabStop]) {
		NSUInteger tabStopLoc = insLoc + 1;  // +1 to get to tab itself -- insLoc is idx _before_ tab char
		NSRange rangeOfTabStop = NSMakeRange(tabStopLoc, 1);
		XTTextTableBlock *textTableBlock = [self tableCellForTextInRange:rangeOfTabStop];

		NSTextStorage *textStorage = [self.textView textStorage];
		NSString *string = [textStorage string];
		NSRange rangeOfParagraph = [XTStringUtils rangeOfParagraphIn:string atLoc:tabStopLoc];
		NSUInteger numCharsUpToTabInParagraph = tabStopLoc - rangeOfParagraph.location;
		NSRange rangeOfParagraphUpToTab = NSMakeRange(rangeOfParagraph.location, numCharsUpToTabInParagraph);
		NSAttributedString *attrAtringParagraphUpToTab = [textStorage attributedSubstringFromRange:rangeOfParagraphUpToTab];
			//TODO !!! rm? should not be necessary now
		attrAtringParagraphUpToTab = [XTStringUtils withoutTrailingNewline:attrAtringParagraphUpToTab]; // trailing newline makes width of para == width of viewSize
		NSSize viewSize = NSMakeSize(10000.0, 10000.0); //TODO !!! use tTB's size?
		NSSize requiredSizeForParagraph = [XTFontUtils requiredRectForText:attrAtringParagraphUpToTab
															 forViewOfSize:viewSize
												suppressCenterAndRightTabs:NO];
		CGFloat xCoord = requiredSizeForParagraph.width;
		
		newTabStop = [self createTabStopAtHalfwayToRhsOfViewFromLoc:xCoord textTableBlock:textTableBlock];

	} else if ([XTTabStopUtils tabStopIsToId:oldTabStop]) {
		NSRange range = NSMakeRange(insLoc + 1, 1); // +1 to get to tab itself -- insLoc is idx _before_ tab char
		XTTextTableBlock *textTableBlock = [self tableCellForTextInRange:range];
		XTTabStopModelEntry *modelTabStop = (XTTabStopModelEntry *)[oldTabStop getCustomOptionForKey:XT_TAB_MODEL_ENTRY_KEY];
		newTabStop = [self createTabFromModelEntry:modelTabStop
									   minLocation:oldTabStop.location
										  resizing:YES
								 optTextTableBlock:textTableBlock];
		
	} else if ([XTTabStopUtils tabStopIsAtMultiple:oldTabStop]) {
		newTabStop = oldTabStop;
		//XT_WARN_1(@"newTabStop.location=%lf", newTabStop.location);

	} else if ([XTTabStopUtils tabStopIsAtFixedPos:oldTabStop]) {
		// used for list items
		newTabStop = oldTabStop;
		
	} else {
		XT_ERROR_0(@"Unexpected tab type");
	}
	
	return newTabStop;
}

- (XTTextTableBlock *)tableCellForTextInRange:(NSRange)range
{
	XTTextTableBlock *res = [XTStringUtils tableCellForAttrString:self.textView.textStorage range:range];
	return res;
}

- (BOOL)textStorageEndsWithARegularSpace
{
	//TODO !!! check if last space was from tab#indent
	BOOL res = NO;
	NSTextStorage *textStorage = self.textView.textStorage;
	NSString *textStorageString = textStorage.string;
	if (textStorageString.length >= 1) {
		unichar lastCh = [XTStringUtils lastChar:textStorageString];
		if (lastCh == ' ') {
			NSUInteger lastChIdx = textStorageString.length - 1;
			NSRange range = NSMakeRange(lastChIdx, 1);
			NSAttributedString *lastChAttrString = [textStorage attributedSubstringFromRange:range];
			if (! [XTMutableAttributedStringHelper isFromExpandedTabTagWithIndent:lastChAttrString]) {
				res = YES;
			}
		}
	}
	return res;
}

@end
