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

#import "XTColorationHelper.h"
#import "XTHtmlColor.h"
#import "XTPrefs.h"
#import "XTAllocDeallocCounter.h"
#import "XTTextRecolorationTemplate.h"
#import "XTTextRecolorationHelper.h"
#import "XTTextView.h"
#import "XTTriple.h"
#import "XTLogger.h"


@interface XTColorationHelper ()

@property BOOL isForBanner;
@property BOOL isForGridBanner;

@property XTTextView *textView;
@property XTPrefs *prefs;
@property XTTextRecolorationTemplate *textRecolorationTemplate;
@property XTTextRecolorationHelper *textRecolorationHelper;

@property XTHtmlColor *lastUsedTextHtmlColor;
@property XTHtmlColor *lastUsedInputHtmlColor;
@property NSNumber *lastUsedColorsForBodyTag;

@end


@implementation XTColorationHelper

OVERRIDE_ALLOC_FOR_COUNTER
OVERRIDE_DEALLOC_FOR_COUNTER

@synthesize gridModeScreenColor = _gridModeScreenColor;

static XTLogger* logger;

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

+ (instancetype)forTextView:(XTTextView *)textView
					isForT3:(BOOL)isForT3
				isForBanner:(BOOL)isForBanner
			isForGridBanner:(BOOL)isForGridBanner
{
	XTColorationHelper *res = [XTColorationHelper new];

	res.isForT3 = isForT3;
	res.isForBanner = isForBanner;
	res.isForGridBanner = isForGridBanner;

	res.textView = textView;
	res.textRecolorationTemplate = [XTTextRecolorationTemplate forTextStorage:textView.textStorage];
	res.textRecolorationHelper = [XTTextRecolorationHelper forTextStorage:textView.textStorage isForBanner:isForBanner];

	return res;
}

- (id)init
{
    self = [super init];
    if (self) {
		_isForT3 = NO;
		_isForBanner = NO;
		_isForGridBanner = NO;
		_htmlMode = NO;

		_textView = nil;
		_prefs = [XTPrefs prefs];
		_textRecolorationTemplate = nil;
		_textRecolorationHelper = nil;
		
		_lastUsedTextHtmlColor = nil;
		_lastUsedInputHtmlColor = nil;
		_lastUsedColorsForBodyTag = nil;

		_gridModeForegroundColor = nil;
		_gridModeBackgroundColor = nil;
		_gridModeScreenColor = nil;
	}
    return self;
}

- (void)setGridModeScreenColor:(XTHtmlColor *)color
{
	_gridModeScreenColor = color;
	[self refreshGridModeScreenColor];
}

- (XTHtmlColor *)gridModeScreenColor
{
	return _gridModeScreenColor;
}

//TODO !!! adaot: inline in only caller
- (void)refreshGridModeScreenColor
{
	self.textView.backgroundColor = [self getOutputBackgroundColorForTextView];
}


- (void)applyPrefsTextAndInputColors
{
	NSColor *textColor = [self getPrefsOutputTextColor];
	XTHtmlColor *textHtmlColor = [XTHtmlColor forNSColor:textColor];
	
	NSColor *inputColor = self.prefs.inputTextColor.value;
	XTHtmlColor *inputHtmlColor = [XTHtmlColor forNSColor:inputColor];
	
	[self applyTextHtmlColor:textHtmlColor inputHtmlColor:inputHtmlColor forBodyTag:NO forceApply:NO];
}

- (void)applyTextAndInputColorsForAllowGameToSetColors
{
	if ([self allowGameToSetColors]) {
		[self.textRecolorationTemplate recolorForAttribute:XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT callback:^(NSRange range, XTOutputTextColorResult *colorResult, XTColorSource colorSource) {
			[self.textRecolorationHelper restoreTextColorsSetByGameForRange:range colorResult:colorResult colorSource:colorSource];
		}];
		[self.textRecolorationTemplate recolorForAttribute:XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT_BACKGROUND callback:^(NSRange range, XTOutputTextColorResult *colorResult, XTColorSource colorSource) {
			[self.textRecolorationHelper restoreTextBackgroundColorsSetByGameForRange:range colorResult:colorResult colorSource:colorSource];
		}];
	} else {
		[self.textRecolorationTemplate recolorForAttribute:XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT callback:^(NSRange range, XTOutputTextColorResult *colorResult, XTColorSource colorSource) {
			[self.textRecolorationHelper removeTextColorsSetByGameForRange:range colorResult:colorResult colorSource:colorSource];
		}];
		[self.textRecolorationTemplate recolorForAttribute:XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT_BACKGROUND callback:^(NSRange range, XTOutputTextColorResult *colorResult, XTColorSource colorSource) {
			[self.textRecolorationHelper removeTextBackgroundColorsSetByGameForRange:range colorResult:colorResult colorSource:colorSource];
		}];
	}
}

//TODO !!! mv:
- (void)applyBodyTextAndInputColorsForceApply:(BOOL)forceApply
{
	//XT_DEF_SELNAME;

	XTHtmlColor *textHtmlColor = self.bodyTextColor;
	XTHtmlColor *inputHtmlColor = self.bodyInputColor;
	[self applyTextHtmlColor:textHtmlColor inputHtmlColor:inputHtmlColor forBodyTag:YES forceApply:forceApply];
}

- (void)applyBodyLinkColor
{
	[self.textRecolorationTemplate recolorForAttribute4:XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT callback:^(NSRange range, id attrValue) {
		
		NSDictionary *tempAttrDict = attrValue;
		NSMutableDictionary *newTempAttrDict = [NSMutableDictionary dictionaryWithDictionary:tempAttrDict];
		
		NSNumber *isPlainLinkObj = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_PLAIN_LINK];
		BOOL isPlainLink = isPlainLinkObj.boolValue;
		
		//TODO !!! adapt: recalc dict[NSUnderlineStyleAttributeName]
			// sep. method?
			// when? non-plain link without underline from fmt spec? XT_OUTPUT_FORMATTER_ATTR_UNDERLINE_FROM_PREFS
				
		NSColor *color = nil;
		XTOutputTextColorResult *colorResult = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT];
		if (! isPlainLink) {
			if ([self allowGameToSetColors]) {
				BOOL shouldColorVisitedLink = colorResult.htmlColor.isForAttributeValueVLink && self.bodyLinkVisitedColor != nil;
				if (shouldColorVisitedLink) {
					color = self.bodyLinkVisitedColor.color;
				} else {
					colorResult = [self getLinkColorResult];
					color = colorResult.htmlColor.color;
				}
			}
			if (color == nil) {
				color = self.prefs.linksTextColor.value;
			}
		} else {
			colorResult = [self getOutputTextColorFromBodyOrPrefs];
			color = colorResult.htmlColor.color;
		}
		newTempAttrDict[NSForegroundColorAttributeName] = color;
		newTempAttrDict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT] = colorResult;
		
		[self.textView.textStorage addAttribute:XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT value:newTempAttrDict range:range];
		[self.textView.layoutManager addTemporaryAttributes:newTempAttrDict forCharacterRange:range];
	}];
}

- (void)updateLinkColors
{
	[self.textRecolorationTemplate recolorForAttribute4:XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT callback:^(NSRange range, id attrValue) {
		
		NSDictionary *tempAttrDict = attrValue;
		NSMutableDictionary *newTempAttrDict = [NSMutableDictionary dictionaryWithDictionary:tempAttrDict];
		
		NSNumber *isPlainLinkObj = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_PLAIN_LINK];
		BOOL isPlainLink = isPlainLinkObj.boolValue;

		NSNumber *isHiddenLinkObj = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_HIDDEN_LINK];
		BOOL isHiddenLink = isHiddenLinkObj.boolValue;

		//TODO !!! adapt: recalc dict[NSUnderlineStyleAttributeName]
			// sep. method?
			// when? non-plain link without underline from fmt spec? XT_OUTPUT_FORMATTER_ATTR_UNDERLINE_FROM_PREFS
				
		NSColor *color = nil;
		XTOutputTextColorResult *colorResult = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT];
		
		if ((! isPlainLink) && (! isHiddenLink)) {
			if ([self allowGameToSetColors]) {
				BOOL shouldColorVisitedLink = colorResult.htmlColor.isForAttributeValueVLink && self.bodyLinkVisitedColor != nil;
				if (shouldColorVisitedLink) {
					color = self.bodyLinkVisitedColor.color;
				} else {
					//if (! shouldColorVisitedLink) {
						colorResult = [self getLinkColorResult];
					//}
					color = colorResult.htmlColor.color;
				}
			}
			/*TODO !!! rm:
			 if (! shouldColorVisitedLink) {
				 colorResult = [self getLinkColorResult];
			 }
			 if ([self allowGameToSetColors]) {
				 if (shouldColorVisitedLink) {
					 color = self.bodyLinkVisitedColor.color;
				 }
				 if (color == nil) {
					 color = colorResult.htmlColor.color;
				 }
			 }
			 if (color == nil) {
				 color = self.prefs.linksTextColor.value;
			 }
			*/
		}
		if (color == nil) {
			if ((! isPlainLink) && (! isHiddenLink)) {
				color = self.prefs.linksTextColor.value;
			} else {
				color = [self.textView.textStorage attribute:NSForegroundColorAttributeName atIndex:range.location effectiveRange:nil];
			}
		}
		
		newTempAttrDict[NSForegroundColorAttributeName] = color;
		
		[self.textView.textStorage addAttribute:XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT value:newTempAttrDict range:range];
		[self.textView.layoutManager addTemporaryAttributes:newTempAttrDict forCharacterRange:range];
	}];
}

- (void)updateTableColors
{
	[self.textRecolorationTemplate recolorForAttribute4:NSParagraphStyleAttributeName callback:^(NSRange range, id attrValue) {
		
		[self.textRecolorationHelper updateTableColors:attrValue];
	}];
}

- (void)updateCursorColor:(XTFormattingSpecificationForHtmlTag *)formattingSpecForHtmlTag
{
	NSColor *textColor;
	if (! [self allowGameToSetColors]) {
		textColor = self.prefs.outputAreaTextColor.value;
	} else {
		XTOutputTextColorResult *textColorResult = [self getOutputTextColor:formattingSpecForHtmlTag];
		textColor = textColorResult.htmlColor.color;
	}
	if (textColor != nil) {
		[self.textView setInsertionPointColor:textColor];
	}
}

- (void)resetBodyColors
{
	self.bodyBackgroundColor = nil;
	self.textView.backgroundColor = [self getOutputBackgroundColorForTextView];
	self.bodyTextColor = nil;
	self.bodyInputColor = nil;
	self.bodyLinkColor = nil;
	self.bodyLinkHoverColor = nil;
	self.bodyLinkActiveColor = nil;
}

- (void)applyTextHtmlColor:(XTHtmlColor *)textHtmlColor
			inputHtmlColor:(XTHtmlColor *)inputHtmlColor
				forBodyTag:(BOOL)forBodyTag
				forceApply:(BOOL)forceApply
{
	if (! forceApply) {
		if (! [self shouldApplyTextHtmlColor:textHtmlColor inputHtmlColor:inputHtmlColor forBodyTag:forBodyTag]) {
			return;
		}
	}
	
	BOOL allowGameToSetColors = [self allowGameToSetColors];
	
	[self.textRecolorationTemplate recolorForAttribute:XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT callback:^(NSRange range, XTOutputTextColorResult *colorResult, XTColorSource colorSource) {
		[self.textRecolorationHelper applyTextHtmlColor:range
										colorResult:colorResult
										colorSource:colorSource
									  textHtmlColor:textHtmlColor
									 inputHtmlColor:inputHtmlColor
										 forBodyTag:forBodyTag
										 forceApply:forceApply
							   allowGameToSetColors:allowGameToSetColors];
	}];
	
	self.lastUsedTextHtmlColor = textHtmlColor;
	self.lastUsedInputHtmlColor = inputHtmlColor;
	self.lastUsedColorsForBodyTag = [NSNumber numberWithBool:forBodyTag];

	if ((! forBodyTag) || forceApply) {
		// prefs changed
		[self.textRecolorationTemplate recolorForAttribute:XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT_BACKGROUND callback:^(NSRange range, XTOutputTextColorResult *colorResult, XTColorSource colorSource) {
			[self.textRecolorationHelper applyTextHtmlBackgroundColor:range
													  colorResult:colorResult
													  colorSource:colorSource
													   forceApply:forceApply
											 allowGameToSetColors:allowGameToSetColors];
		}];
	}
}

- (BOOL)shouldApplyTextHtmlColor:(XTHtmlColor *)textHtmlColor inputHtmlColor:(XTHtmlColor *)inputHtmlColor forBodyTag:(BOOL)forBodyTag
{
	if (! forBodyTag) {
		// prefs changed
		return YES;
	}
	
	BOOL newTextHtmlColor = ! [XTHtmlColor safeCompare:textHtmlColor to:self.lastUsedTextHtmlColor];
	if (newTextHtmlColor) {
		return YES;
	}
	
	if (! self.isForBanner) {
		BOOL newInputHtmlColor = ! [XTHtmlColor safeCompare:inputHtmlColor to:self.lastUsedInputHtmlColor];
		if (newInputHtmlColor) {
			return YES;
		}
	}
	
	BOOL newForBodyTag = (self.lastUsedColorsForBodyTag == nil || self.lastUsedColorsForBodyTag.boolValue != forBodyTag);
	if (newForBodyTag) {
		return YES;
	}

	return NO;
}

- (NSColor *)getPrefsOutputTextColor
{
	NSColor *res;
	if (self.isForBanner) {
		res = self.prefs.statusLineTextColor.value;
	} else {
		res = self.prefs.outputAreaTextColor.value;
	}
	return res;
}

- (BOOL)allowGameToSetColors
{
	BOOL res;
	
	if ([self isForT2PlainTextBanner]) {
		res = NO;
	} else {
		NSNumber *valueObj = self.prefs.allowGamesToSetColors.value;
		res = (valueObj == nil || valueObj.boolValue);
	}
	
	return res;
}

- (XTOutputTextColorResult *)getLinkColorResult
{
	XTColorSource colorSource;
	XTHtmlColor *htmlColor;
	
	if (self.bodyLinkColor != nil) {
		colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_BODY;
		htmlColor = [XTHtmlColor forAttributeValue:self.bodyLinkColor.attrValue];
	} else {
		colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_PREFS;
		htmlColor = [XTHtmlColor forAttributeValueLink];
	}
	
	XTOutputTextColorResult *res = [XTOutputTextColorResult forHtmlColor:htmlColor colorSource:colorSource];
	
	return res;
}

//TODO !!! adapt: a mess - streamline
- (XTOutputTextColorResult *)getOutputTextColor:(XTFormattingSpecificationForHtmlTag *)formattingSpecForHtmlTag
{
	XTHtmlColor *htmlColor = nil;
	XTColorSource colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_PREFS;
	BOOL bannerTextColorFromMainAreaPrefs = NO;
	
	if (self.isForGridBanner) {
		if (self.gridModeForegroundColor != nil) {
			if (self.gridModeForegroundColor.color != nil) {
				htmlColor = self.gridModeForegroundColor;
				colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_FONT;
			} else {
				NSColor *color;
				if ([self allowGameToSetColors]) {
					color = self.prefs.outputAreaTextColor.value; // yup, that's what mjr terp and qtads do
				} else {
					color = [self getPrefsOutputTextColor];
				}
				htmlColor = [XTHtmlColor forNSColor:color];
				colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_PREFS;
				bannerTextColorFromMainAreaPrefs = YES;
			}
		} else {
			htmlColor = [self getGridModeDefaultTextColor];
			colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_FONT;
		}
	}
	
	if (htmlColor == nil) {
		XTFormattingSpecification *formattingSpec = formattingSpecForHtmlTag.formattingSpec;
		if (formattingSpec.fontColor != nil) {
			htmlColor = formattingSpec.fontColor;
			colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_FONT;
			if (htmlColor.color == nil) {
				htmlColor = [XTHtmlColor forAttributeValue:@"black"];
			}
		}
	}
	
	if (htmlColor == nil) {
		XTOutputTextColorResult *tempRes = [self getOutputTextColorFromBodyOrPrefs];
		htmlColor = tempRes.htmlColor;
		colorSource = tempRes.colorSource;
		bannerTextColorFromMainAreaPrefs = tempRes.bannerTextColorFromMainAreaPrefs;
	}
	
	XTOutputTextColorResult *res = [XTOutputTextColorResult forHtmlColor:htmlColor
															 colorSource:colorSource
										bannerTextColorFromMainAreaPrefs:bannerTextColorFromMainAreaPrefs];
	return res;
}

- (XTOutputTextColorResult *)getOutputTextColorFromBodyOrPrefs
{
	XTHtmlColor *htmlColor = nil;
	XTColorSource colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_PREFS;
	BOOL bannerTextColorFromMainAreaPrefs = NO;

	if (self.bodyTextColor != nil) {
		htmlColor = self.bodyTextColor;
		colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_BODY;
	}
	if (htmlColor.color == nil) {
		if (self.isForBanner && ! [self isForT2PlainTextBanner] && ! self.isForT3) {
			htmlColor = [XTHtmlColor forAttributeValue:@"black"];
			colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_BODY;
		} else {
			NSColor *color;
			if (self.isForBanner && self.isForT3) {
				if ([self allowGameToSetColors]) {
					color = self.prefs.outputAreaTextColor.value; // yup, that's what mjr terp and qtads do
				} else {
					color = [self getPrefsOutputTextColor];
				}
				bannerTextColorFromMainAreaPrefs = YES;
			} else {
				color = [self getPrefsOutputTextColor];
			}
			htmlColor = [XTHtmlColor forNSColor:color];
			colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_PREFS;
		}
	}
	
	XTOutputTextColorResult *res = [XTOutputTextColorResult forHtmlColor:htmlColor
															 colorSource:colorSource
										bannerTextColorFromMainAreaPrefs:bannerTextColorFromMainAreaPrefs];
	return res;
}

- (XTOutputTextColorResult *)getOutputBackgroundColor:(XTFormattingSpecificationForHtmlTag *)formattingSpecForHtmlTag // for text
{
	XTHtmlColor *htmlColor = nil;
	XTColorSource colorSource = XT_COLOR_SOURCE_OUTPUT_FROM_PREFS;
	
	if (self.gridModeBackgroundColor != nil) {
		htmlColor = self.gridModeBackgroundColor;
		colorSource = XT_COLOR_SOURCE_BACKGROUND_FROM_FONT;
	} else {
		XTFormattingSpecification *formattingSpec = formattingSpecForHtmlTag.formattingSpec;
		if (formattingSpec.backgroundColor.color != nil) {
			htmlColor = formattingSpec.backgroundColor;
			colorSource = XT_COLOR_SOURCE_BACKGROUND_FROM_FONT;
		}
	}
	
	XTOutputTextColorResult *res = [XTOutputTextColorResult forHtmlColor:htmlColor colorSource:colorSource];
	return res;
}

- (void)updateLinksUnderline
{
	NSNumber *underlineStyleSingle = [NSNumber numberWithInt:NSUnderlineStyleSingle];
	NSNumber *underlineStyleNone = [NSNumber numberWithInt:NSUnderlineStyleNone];
	BOOL underlineLinks = self.prefs.linksUnderline.value.boolValue;
	
	[self.textRecolorationTemplate recolorForAttribute4:XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT callback:^(NSRange range, id attrValue) {
		
		NSDictionary *tempAttrDict = attrValue;
		
		NSNumber *underlineIsFromPrefsObj = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_UNDERLINE_FROM_PREFS];
		BOOL underlineIsFromPrefs = underlineIsFromPrefsObj.boolValue;

		if (underlineIsFromPrefs) {
			NSMutableDictionary *newTempAttrDict = [NSMutableDictionary dictionaryWithDictionary:tempAttrDict];

			if (underlineLinks) {
				newTempAttrDict[NSUnderlineStyleAttributeName] = underlineStyleSingle;
			} else {
				newTempAttrDict[NSUnderlineStyleAttributeName] = underlineStyleNone;
			}
							
			[self.textView.textStorage addAttribute:XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT value:newTempAttrDict range:range];
			[self.textView.layoutManager addTemporaryAttributes:newTempAttrDict forCharacterRange:range];
		}
	}];
}

//TODO !!! adapt: simplify logic!!!
- (NSColor *)getOutputBackgroundColorForTextView
{
	NSColor *res;
	BOOL allowGameToSetColors = [self allowGameToSetColors];
	
	if (self.isForGridBanner) {
		if (self.gridModeScreenColor != nil) {
			if (allowGameToSetColors) {
				res = self.gridModeScreenColor.color;
			} else {
				res = [self getPrefsOutputBackgroundColorForTextView];
			}
		} else {
			res = [self getPrefsOutputBackgroundColorForTextBannerTextView];
		}
		return res;
	}
	
	if (allowGameToSetColors && self.bodyBackgroundColor.color != nil) {
		res = self.bodyBackgroundColor.color;
	} else if (allowGameToSetColors && self.isForBanner /*isForTagBanner*/ && ! self.isForT3) {
		res = [self getPrefsOutputBackgroundColorForTextBannerTextView];
	} else {
		if (self.isForBanner && self.isForT3 && allowGameToSetColors) {
			res = self.prefs.outputAreaBackgroundColor.value; // yup, that's what mjr terp and qtads do
		} else {
			res = [self getPrefsOutputBackgroundColorForTextView];
		}
	}
	
	return res;
}

- (NSColor *)getPrefsOutputBackgroundColorForTextView
{
	NSColor *res;
	if (self.isForBanner) {
		res = self.prefs.statusLineBackgroundColor.value;
	} else {
		res = self.prefs.outputAreaBackgroundColor.value;
	}
	return res;
}

- (NSColor *)getPrefsOutputBackgroundColorForTextBannerTextView
{
	NSColor *res;
	if ([self allowGameToSetColors]) {
		res = self.prefs.outputAreaBackgroundColor.value; // yup, main area's screen color - this is what mjr's terp does
	} else {
		res = [self getPrefsOutputBackgroundColorForTextView];
	}
	return res;
}

- (XTHtmlColor *)getGridModeDefaultTextColor
{
	XTHtmlColor *res = [XTHtmlColor forAttributeValue:@"black"];
	return res;
}

- (BOOL)isForT2PlainTextBanner
{
	BOOL res = self.isForBanner && (! self.isForT3) && (! self.htmlMode);
	return res;
}

- (void)applyMouseOverLink:(XTMouseOnLinkState *)mouseOverLinkState
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");
	
	NSRange range = mouseOverLinkState.range;
	NSDictionary *attrDict = [self.textView.textStorage attributesAtIndex:range.location effectiveRange:nil];
	NSDictionary *tempAttrDict = [self.textView.textStorage attribute:XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT atIndex:range.location effectiveRange:nil];

	NSNumber *isPlainLinkObj = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_PLAIN_LINK];
	BOOL isPlainLink = isPlainLinkObj.boolValue;
	if (isPlainLink) {
		return;
	}
	
	NSNumber *isHiddenLinkObj = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_HIDDEN_LINK];
	BOOL isHiddenLink = isHiddenLinkObj.boolValue;

	NSString *hover = attrDict[XT_OUTPUT_FORMATTER_ATTR_CMDLINK_HOVER_COLORS];
	XTTriple *hoverProps = [self resolveLinkHoverPropertiesFromAttrValue:hover];

	if ([hoverProps valueCount] >= 1) {
		NSMutableDictionary *usedTempAttrDict = [NSMutableDictionary dictionaryWithCapacity:5];
		if ([self allowGameToSetColors]) {
			NSColor *fgColor = (NSColor *)hoverProps.firstObject;
			if (fgColor != nil) {
				usedTempAttrDict[NSForegroundColorAttributeName] = fgColor;
			} else {
				if (self.prefs.linksHighlightHovering.value.boolValue) {
					usedTempAttrDict[NSForegroundColorAttributeName] = self.prefs.linksHoveringColor.value;
				}
			}
			NSColor *bgColor = (NSColor *)hoverProps.secondObject;
			if (bgColor != nil) {
				usedTempAttrDict[NSBackgroundColorAttributeName] = bgColor;
			}
		}
		if (hoverProps.thirdObject != nil) {
			usedTempAttrDict[NSUnderlineStyleAttributeName] = [NSNumber numberWithInteger:NSUnderlineStyleSingle];
		}
		[self.textView.layoutManager addTemporaryAttributes:usedTempAttrDict forCharacterRange:range];
	} else if (isHiddenLink) {
		NSColor *color;
		if (self.prefs.linksHighlightHovering.value.boolValue) {
			color = self.prefs.linksHoveringColor.value;
		} else {
			color = self.prefs.linksTextColor.value;
		}
		[self.textView.layoutManager addTemporaryAttribute:NSForegroundColorAttributeName value:color forCharacterRange:range];
		// No underline
	} else {
		BOOL highlightHover = self.prefs.linksHighlightHovering.value.boolValue;
		if (highlightHover) {
			NSColor *color = self.prefs.linksHoveringColor.value;
			[self.textView.layoutManager addTemporaryAttribute:NSForegroundColorAttributeName value:color forCharacterRange:range];
		}
	}
}

//TODO !!! refactor this mess:
- (void)cancelMouseOverLink:(XTMouseOnLinkState *)mouseOverLinkState
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");
	
	NSRange range = mouseOverLinkState.range;
	NSDictionary *attrDict = [self.textView.textStorage attributesAtIndex:range.location effectiveRange:nil];
	NSDictionary *tempAttrDict = [self.textView.textStorage attribute:XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT atIndex:range.location effectiveRange:nil];

	NSNumber *isPlainLinkObj = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_PLAIN_LINK];
	BOOL isPlainLink = isPlainLinkObj.boolValue;
	if (isPlainLink) {
		//XT_WARN_0(@"isPlainLink");
		return;
	}
	
	NSNumber *isHiddenLinkObj = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_HIDDEN_LINK];
	BOOL isHiddenLink = isHiddenLinkObj.boolValue;

	NSString *hover = attrDict[XT_OUTPUT_FORMATTER_ATTR_CMDLINK_HOVER_COLORS];
	XTTriple *hoverProps = [self resolveLinkHoverPropertiesFromAttrValue:hover];
	
	if ([hoverProps valueCount] >= 1 || isHiddenLink) {
		//XT_WARN_0(@"[hoverProps valueCount] >= 1 || isHiddenLink");

		NSColor *color = nil;
		XTOutputTextColorResult *colorResult = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT];
		BOOL shouldColorVisitedLink = colorResult.htmlColor.isForAttributeValueVLink && self.bodyLinkVisitedColor != nil;

		if ((! isHiddenLink) && (! shouldColorVisitedLink)) {
			colorResult = [self getLinkColorResult];
		}
		if (isHiddenLink) {
			if ([self allowGameToSetColors]) {
				XTOutputTextColorResult *permColorResult = attrDict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT];
				color = permColorResult.htmlColor.color;
			} else {
				color = [self getPrefsOutputTextColor];
			}
		} else if ([self allowGameToSetColors]) {
			if (shouldColorVisitedLink) {
				color = self.bodyLinkVisitedColor.color;
			}
			if (color == nil) {
				color = colorResult.htmlColor.color;
			}
		}
		if (color == nil) {
			color = self.prefs.linksTextColor.value;
		}
		BOOL prefsLinksUnderLine = self.prefs.linksUnderline.value.boolValue;
		if (((! prefsLinksUnderLine) && (hoverProps.thirdObject != nil)) || isHiddenLink) {
			[self.textView.layoutManager addTemporaryAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleNone] forCharacterRange:range];
		}
		[self.textView.layoutManager addTemporaryAttribute:NSForegroundColorAttributeName value:color forCharacterRange:range];
		[self.textView.layoutManager removeTemporaryAttribute:NSBackgroundColorAttributeName forCharacterRange:range];
		
	} else {
		
		NSColor *color = nil;
		XTOutputTextColorResult *colorResult = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT];
		BOOL shouldColorVisitedLink = colorResult.htmlColor.isForAttributeValueVLink && self.bodyLinkVisitedColor != nil;

		if (! shouldColorVisitedLink) {
			colorResult = [self getLinkColorResult];
		}
		if ([self allowGameToSetColors]) {
			if (shouldColorVisitedLink) {
				color = self.bodyLinkVisitedColor.color;
			}
			if (color == nil) {
				color = colorResult.htmlColor.color;
			}
		}
		if (color == nil) {
			color = self.prefs.linksTextColor.value;
		}
		
		[self.textView.layoutManager addTemporaryAttribute:NSForegroundColorAttributeName value:color forCharacterRange:range];
	}
}

- (void)applyMouseDownOnLink:(XTMouseOnLinkState *)mouseOverLinkState
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");
	
	NSRange range = mouseOverLinkState.range;
	NSDictionary *tempAttrDict = [self.textView.textStorage attribute:XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT atIndex:range.location effectiveRange:nil];

	NSNumber *isPlainLinkObj = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_PLAIN_LINK];
	BOOL isPlainLink = isPlainLinkObj.boolValue;
	if (isPlainLink) {
		return;
	}

	NSColor *color = [self resolveLinkActiveColor];
	if (color != nil) {
		[self.textView.layoutManager addTemporaryAttribute:NSForegroundColorAttributeName value:color forCharacterRange:range];
	}
}

- (void)cancelMouseDownOnLink:(XTMouseOnLinkState *)mouseOverLinkState
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");
	
	NSRange range = mouseOverLinkState.range;
	NSDictionary *tempAttrDict = [self.textView.textStorage attribute:XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT atIndex:range.location effectiveRange:nil];

	NSNumber *isPlainLinkObj = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_PLAIN_LINK];
	BOOL isPlainLink = isPlainLinkObj.boolValue;
	if (isPlainLink) {
		//XT_WARN_0(@"isPlainLink");
		return;
	}
	
	NSNumber *isHiddenLinkObj = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_HIDDEN_LINK];
	BOOL isHiddenLink = isHiddenLinkObj.boolValue;

	NSColor *color = nil;
	XTOutputTextColorResult *colorResult = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT];
	BOOL shouldColorVisitedLink = colorResult.htmlColor.isForAttributeValueVLink && self.bodyLinkVisitedColor != nil;
	if ((! isHiddenLink) && (! shouldColorVisitedLink)) {
		//XT_WARN_0(@"(! isHiddenLink) && (! shouldColorVisitedLink)");
		colorResult = [self getLinkColorResult];
	}
	if (isHiddenLink) {
		//XT_WARN_0(@"isHiddenLink -> [self getPrefsOutputTextColor]");
		color = [self getPrefsOutputTextColor];
	} else if ([self allowGameToSetColors]) {
		if (shouldColorVisitedLink) {
			//XT_WARN_0(@"shouldColorVisitedLink -> self.bodyLinkVisitedColor.color");
			color = self.bodyLinkVisitedColor.color;
		}
		if (color == nil) {
			//XT_WARN_0(@"colorResult.htmlColor.color");
			color = colorResult.htmlColor.color;
		}
	}
	if (color == nil) {
		//XT_WARN_0(@"self.prefs.linksTextColor.value");
		color = self.prefs.linksTextColor.value;
	}

	[self.textView.layoutManager addTemporaryAttribute:NSForegroundColorAttributeName value:color forCharacterRange:range];
}

- (XTTriple *)resolveLinkHoverPropertiesFromAttrValue:(NSString *)attrValue
{
	NSColor *fgColor = nil;
	NSColor *bgColor = nil;
	NSNumber *underline = nil;

	if (attrValue.length >= 1) {
		NSArray<NSString *> *hoverColors = [attrValue componentsSeparatedByString:@","];
		if (hoverColors.count >= 1) {

			NSString *fgColorStr = hoverColors[0];
			XTHtmlColor *fgHtmlColor = [XTHtmlColor forAttributeValue:fgColorStr];
			fgColor = fgHtmlColor.color;
	
			if (hoverColors.count >= 2) {
				NSString *bgColorStr = hoverColors[1];
				XTHtmlColor *bgHtmlColor = [XTHtmlColor forAttributeValue:bgColorStr];
				bgColor = bgHtmlColor.color;
			}
			
			if (hoverColors.count >= 3) {
				NSString *underlineStr = hoverColors[2];
				underlineStr = [underlineStr lowercaseString];
				if ([underlineStr isEqualToString:@"underline"]) {
					underline = [NSNumber numberWithBool:YES];
				}
			}
		}
	}
	
	if (fgColor == nil) {
		fgColor = self.bodyLinkHoverColor.color;
	}

	XTTriple *res = nil;
	if (fgColor != nil || bgColor != nil || underline != nil) {
		res = [XTTriple tripleWithFirstObject:fgColor
								secondObject:bgColor
								 thirdObject:underline];
	}

	return res;
}

- (NSColor *)resolveLinkActiveColor
{
	NSColor *res = nil;

	if ([self allowGameToSetColors]) {
		if (self.bodyLinkActiveColor != nil) {
			res = self.bodyLinkActiveColor.color;
		}
	}
	if (res == nil) {
		res = self.prefs.linksClickedColor.value;
	}
	
	return res;
}

- (void)setLinkClickedColorAtCharIndex:(NSUInteger)charIndex
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");
	
	NSAttributedString *attrString = self.textView.textStorage;
	if (charIndex >= attrString.length) {
		return;
	}
	
	NSRange linkRange;
	NSRange attrStringRange = NSMakeRange(0, attrString.length);
	[attrString attribute:NSLinkAttributeName
				  atIndex:charIndex
	 longestEffectiveRange:&linkRange
				  inRange:attrStringRange];
	
	NSDictionary *tempAttrDict = [self.textView.textStorage attribute:XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT
														   atIndex:linkRange.location
													 effectiveRange:nil];
	XTOutputTextColorResult *tempColorResult = tempAttrDict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT];

	XTHtmlColor *newColor = [XTHtmlColor forAttributeValueVLink];
	XTOutputTextColorResult *newTempColorResult = [XTOutputTextColorResult forHtmlColor:newColor
																		colorSource:tempColorResult.colorSource];

	NSMutableDictionary *newTempAttrDict = [NSMutableDictionary dictionaryWithDictionary:tempAttrDict];
	newTempAttrDict[XT_OUTPUT_FORMATTER_ATTR_RECOLORABLE_TEXT] = newTempColorResult;
	
	[self.textView.textStorage addAttribute:XT_OUTPUT_FORMATTER_ATTR_TEMPATTRSDICT value:newTempAttrDict range:linkRange];
	[self.textView.layoutManager addTemporaryAttributes:newTempAttrDict forCharacterRange:linkRange];
}

@end
