//
//  XTTextView.m
//  XTads
//
//  Created by Rune Berg on 28/01/2017.
//  Copyright © 2017 Rune Berg. All rights reserved.
//

#import "XTTextView.h"
#import "XTLogger.h"
#import "XTNotifications.h"
#import "XTTableColumnWidthTracker.h"
#import "XTTimer.h"
#import "XTFontUtils.h"
#import "XTStringUtils.h"
#import "XTTabStopUtils.h"
#import "XTOutputFormatter.h"


@interface XTTextView ()

@property BOOL hasDoneCustomInit;
@property BOOL isObservingPrefs;

@property NSTimeInterval totalTimeInEnsureLayoutForTextContainer;
@property NSTimeInterval totalTimeInForceNewLayoutForTextContainer;

@end


@implementation XTTextView

static XTLogger* logger;

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

- (void)customInit
{
	if (_hasDoneCustomInit) {
		return;
	}

	_leftRightInset = 8.0;
	_topBottomInset = 8.0;
	
	_prefs = [XTPrefs prefs];
	[self setupReceptionOfAppLevelNotifications];
	_isObservingPrefs = YES;
	
	NSSize insetOutput = NSMakeSize(_leftRightInset, _topBottomInset);
	[self setTextContainerInset:insetOutput];
	
	_totalTimeInEnsureLayoutForTextContainer = 0.0;

	_hasDoneCustomInit = YES;
}

- (void)teardown
{
	if (_isObservingPrefs) {
		_isObservingPrefs = NO;
		[self teardownReceptionOfAppLevelNotifications];
	}

	[XTStringUtils removeXTadsParagraphStyles:self.textStorage];
	
	NSAttributedString *noText = [NSAttributedString new];
	[self.textStorage setAttributedString:noText];
	
	self.delegate = nil;
}

- (void)setupReceptionOfAppLevelNotifications
{
	[[NSNotificationCenter defaultCenter] addObserver:self
											 selector:@selector(handleNotifyPrefsChanged:)
												 name:XTadsNotifyPrefsChanged
											   object:nil]; // nil means "for any sender"
}

- (void)teardownReceptionOfAppLevelNotifications
{
	[[NSNotificationCenter defaultCenter] removeObserver:self
													name:XTadsNotifyPrefsChanged
												  object:nil];
}

- (void)handleNotifyPrefsChanged:(NSNotification *)notification
{
	[self syncWithPrefs];
}

- (void)syncWithPrefs
{
}

- (CGFloat)findTotalWidthAdjustedForInset
{
	CGFloat res = self.frame.size.width - (2 * self.leftRightInset);
	return res;
}

- (void)viewWillStartLiveResize
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");
	
	BOOL shouldRecalcTabStops = ! [self recalcsTabsInLiveResize];
	
	if (shouldRecalcTabStops) {
		[self.outputFormatter prepareForRecalcAllOfTabStops];
	}
	
	[super viewWillStartLiveResize];
}

- (void)resizeWithOldSuperviewSize:(NSSize)oldSize
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");
	
	BOOL shouldRecalcTabStops = ((! self.inLiveResize) || [self recalcsTabsInLiveResize]);
	
	if (shouldRecalcTabStops) {
		[self.outputFormatter prepareForRecalcAllOfTabStops];
	}

	[super resizeWithOldSuperviewSize:oldSize];
	
	//TODO !!! too costly? maybe guard with a "tables used" flag
	[self forceNewLayoutForTextContainer]; // or else table cells might not be resized correctly
	
	if (shouldRecalcTabStops) {
		[self.outputFormatter recalcAllTabStops];
	}
}

- (void)viewDidEndLiveResize
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");

	[super viewDidEndLiveResize];
	
	BOOL shouldRecalcTabStops = ! [self recalcsTabsInLiveResize];
	
	if (shouldRecalcTabStops) {
		[self.outputFormatter prepareForRecalcAllOfTabStops];
	}

	[self forceNewLayoutForTextContainer]; // or else table cells might not be resized correctly

	if (shouldRecalcTabStops) {
		[self.outputFormatter recalcAllTabStops];
	}
}

- (void)copy:(id)sender
{
	[super copy:sender];
	
	NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
	NSArray<Class> *classArray = [NSArray arrayWithObject:[NSAttributedString class]];
	NSDictionary<NSPasteboardReadingOptionKey, id> *options = [NSDictionary dictionary];
	
	NSArray *attrStringArray = [pasteboard readObjectsForClasses:classArray options:options];
	if (attrStringArray.count == 1) {
		NSAttributedString *filteredAttrString = [XTStringUtils filterZwsp:attrStringArray[0]];
		NSArray *filteredAttrStringArray = [NSArray arrayWithObject:filteredAttrString];
		[pasteboard clearContents]; //TODO this is a bit crude - should really only replace attr.string item and leave the rest alone
		[pasteboard writeObjects:filteredAttrStringArray];
	}
}

- (CGFloat)findCoordOfRhsOfView
{
	CGFloat res = [self findTotalWidthAdjustedForInset];
	CGFloat lfPadding = self.textContainer.lineFragmentPadding;
	res -= (2.0 * lfPadding);
	return res;
}

- (CGFloat)findCoordOfTabAtHalfwayToRhsOfViewFromLoc:(CGFloat)fromLoc
{
	//XT_DEF_SELNAME;
	
	CGFloat coordRhsOfView = [self findCoordOfRhsOfView];
	CGFloat pointsToHalfway = (coordRhsOfView - fromLoc) / 2.0;
	if (pointsToHalfway < 0.0) {
		pointsToHalfway = 0.0;
	}
	CGFloat res = fromLoc + pointsToHalfway;
	return res;
}

//
// Find lower end of insertion point, on "flipped" y-axis (0 being top of view)
//
- (CGFloat)findYCoordOfInsertionPoint
{
	XT_DEF_SELNAME;
	
	// http://stackoverflow.com/questions/7554472/gettting-window-coordinates-of-the-insertion-point-cocoa
	
	// -firstRectForCharacterRange:actualRange returns a frame rectangle
	// containing the insertion point or, in case there's a selection,
	// the first line of that selection. The origin of the frame rectangle is
	// in screen coordinates
	NSRange ipRange = [self selectedRange];
	NSRect ipRectScreen = [self firstRectForCharacterRange:ipRange actualRange:NULL];

	CGFloat res;
	
	if (ipRectScreen.origin.y == 0.0) {
		
		// 2017-10-28: this comment is suspect / outdated
		// Sometimes, after certain keystrokes at the more prompt (space, arrow-*, page-*), we get
		// an empty ipRectScreen. I haven't managed to understand why, but this is a way to compensate for that.
		// adhoc test game:
		//		only seems to happen at end of output, when called via resetForGameHasEndedMsg
		//		seems ins pos is somehow affected by those keys...
		//			visually, they behave different too...
		// dearbrian startup
		//		for ditto keys
		
		NSUInteger tsLen = self.textStorage.length;
		if (tsLen == 0) {
			res = self.topBottomInset + 14; // 14 being default font height
		} else {
			// This can happen when a banner's physical layout hasn't happened yet,
			// or the layout constraints make the banner's view too small for proper layout.
			// Fall back to view's height.
			// Not ideal, but shouldn't be a problem in practical use.
			if ([self shouldLogDodgyReturnFromFindYCoordOfInsertionPoint]) {
				XT_WARN_0(@"has text but got empty rectangle from firstRectForCharacterRange, using frame height - 2");
			}
			CGFloat selfHeight = self.frame.size.height;
			res = selfHeight - 2.0;
		}
		
	} else {
		
		// Normal case.

		//if (self.textStorage.length == 0) {
		//	int brkpt = 1;
		//}

		NSPoint ipPoint = [self getInsertionPointNormalCase:ipRectScreen];
		res = ipPoint.y;
	}
	
	res = ceil(res);

	XT_TRACE_1(@"--> %f", res);
	return res;
}

- (CGFloat)findXCoordOfInsertionPointRaw
{
	XT_DEF_SELNAME;
	
	CGFloat res = [self findXCoordOfInsertionPointInternal];
	// *** No rounding of res ***
	
	XT_TRACE_1(@"--> %f", res);
	return res;
}

- (CGFloat)findXCoordOfInsertionPointInternal
{
	//XT_DEF_SELNAME;

	[self ensureLayoutForTextContainer];
	
	NSRange ipRange = [self selectedRange];
		// which we assume is at the very end of our textview's text
	
	//TODO use this approach in y coord case too:
	
	CGFloat res = [self findStartXCoordOfTextInRange:ipRange];
	return res;
}

- (CGFloat)findStartXCoordOfTextInRange:(NSRange)range
{
	XT_DEF_SELNAME;

	/* [self firstRectForCharacterRange:...] is unstable when text scrolls, so use approach suggested by:
	 http://webcache.googleusercontent.com/search?q=cache:E7qpW4SPJZAJ:www.cocoabuilder.com/archive/cocoa/158114-nstextview-firstrectforcharacterrange-returns-empty-rect.html+&cd=3&hl=en&ct=clnk&gl=no
	 */
	NSRect ipRectAlt = [self.layoutManager boundingRectForGlyphRange:range inTextContainer:self.textContainer];
	if (ipRectAlt.origin.x == 0) {
		XT_WARN_0(@"got empty rectangle from boundingRectForGlyphRange");
	}
	NSSize tCInset = self.textContainerInset;
	CGFloat resAlt = ipRectAlt.origin.x + tCInset.width;
	
	XT_TRACE_1(@"--> %f", resAlt);
	return resAlt; //was: res;
}

- (CGFloat)findEndXCoordOfTextInRange:(NSRange)range
{
	XT_DEF_SELNAME;
	
	NSRect ipRectAlt = [self.layoutManager boundingRectForGlyphRange:range inTextContainer:self.textContainer];
	if (ipRectAlt.origin.x == 0) {
		XT_WARN_0(@"got empty rectangle from boundingRectForGlyphRange");
	}
	CGFloat resAlt = ipRectAlt.origin.x;
	resAlt += ipRectAlt.size.width;  // !
	NSSize tCInset = self.textContainerInset;
	resAlt += tCInset.width;
	
	XT_TRACE_1(@"--> %f", resAlt);
	return resAlt;
}

- (NSPoint)getInsertionPointNormalCase:(NSRect)ipRectScreen
{
	// -convertRectFromScreen: converts from screen coordinates to window coordinates
	NSRect ipRectWindow = [self.window convertRectFromScreen:ipRectScreen];
	
	// The origin is the lower left corner of the frame rectangle containing the insertion point
	NSPoint ipPointWindow = ipRectWindow.origin;
	
	NSPoint ipPointSelf = [self convertPoint:ipPointWindow fromView:nil];
	// Note: NSTextView uses a flipped y-axis, so .y is wrt top of frame
	
	return ipPointSelf;
}

- (void)setInsertionPointAtEndOfText
{
	NSUInteger index = self.textStorage.length;
	[self setSelectedRange:NSMakeRange(index, 0)];
}

- (NSUInteger)endOfOutputPosition
{
	return self.textStorage.length;
}

- (void)ensureLayoutForTextContainer
{
	//XT_DEF_SELNAME;
	//XTTimer *timer = [XTTimer fromNow];

	NSTextContainer* textContainer = [self textContainer];
	NSLayoutManager* layoutManager = [self layoutManager];
	
	[layoutManager ensureLayoutForTextContainer:textContainer];
	
	//NSTimeInterval elapsed = [timer timeElapsed];
	//self.totalTimeInEnsureLayoutForTextContainer += elapsed;
	//XT_WARN_2(@"took %lf secs, total %lf secs", elapsed, self.totalTimeInEnsureLayoutForTextContainer);
}

- (void)forceNewLayoutForTextContainer
{
	//XT_DEF_SELNAME;
	//XTTimer *timer = [XTTimer fromNow];
	
	NSTextContainer* textContainer = [self textContainer];
	NSLayoutManager* layoutManager = [self layoutManager];
	[layoutManager textContainerChangedGeometry:textContainer];
	[layoutManager ensureLayoutForTextContainer:textContainer];
	
	//NSTimeInterval elapsed = [timer timeElapsed];
	//self.totalTimeInForceNewLayoutForTextContainer += elapsed;
	//XT_WARN_2(@"took %lf secs, total %lf secs", elapsed, self.totalTimeInForceNewLayoutForTextContainer);
}

- (CGFloat)findTotalHeight
{
	CGFloat res = self.frame.size.height;
	return res;
}

- (CGFloat)findVisibleHeight
{
	[self ensureLayoutForTextContainer];

	CGFloat res = self.enclosingScrollView.documentVisibleRect.size.height;

	if (res == 0.0) {
		NSUInteger textStorageLength = self.textStorage.length;
		if (textStorageLength != 0) {
			XT_DEF_SELNAME;
			XT_TRACE_1(@"res == 0.0 for textStorageLength %lu - fall back on alt. method", textStorageLength);
			res = [XTFontUtils heightOfText:self];
			CGFloat totalVerticalInset = [self totalVerticalInset:NO];
			res += totalVerticalInset;
		}
	}
	
	return res;
}

- (CGFloat)totalHorizontalInset:(BOOL)respectInsetsIfNoText
{
	//TODO use respectInsetsIfNoText
	//TODO but write test game a la cruise's banner
	
	CGFloat inset = self.textContainerInset.width;
	CGFloat lfPadding = self.textContainer.lineFragmentPadding;
	inset += lfPadding;
	
	inset = (2 * inset);
	
	//if (self.style & OS_BANNER_STYLE_VSCROLL) {
	NSScrollView *scrollView = self.enclosingScrollView;
	if (scrollView != nil) {
		NSScroller *scroller = scrollView.verticalScroller;
		if (scroller != nil) {
			CGFloat size = [self widthOfScroller:scroller];
			inset += size;
		}
	}
	
	return inset;
}

- (CGFloat)totalVerticalInset:(BOOL)respectInsetsIfNoText
{
	CGFloat inset = 0;
	BOOL respectInsets;
	if (respectInsetsIfNoText) {
		respectInsets = YES;
	} else {
		NSUInteger textLen = self.string.length;
		respectInsets = (textLen >= 1);
	}
	
	if (respectInsets) {
		inset = self.textContainerInset.height;
		inset = (2 * inset);
	}
	
	//if (self.style & OS_BANNER_STYLE_HSCROLL) {
	NSScrollView *scrollView = self.enclosingScrollView;
	if (scrollView != nil) {
		NSScroller *scroller = scrollView.horizontalScroller;
		if (scroller != nil) {
			CGFloat size = [self widthOfScroller:scroller];
			inset += size;
		}
	}
	
	return inset;
}

- (CGFloat)widthOfScroller:(NSScroller *)scroller
{
	CGFloat res = 0.0;
	
	if (scroller != nil) {
		NSControlSize ctrlSize = [scroller controlSize];
		NSScrollerStyle scrollerStyle = [scroller scrollerStyle];
		CGFloat size = [NSScroller scrollerWidthForControlSize:ctrlSize scrollerStyle:scrollerStyle];
		res = size;
	}
	
	return res;
}

- (void)scrollToBottom
{
	NSUInteger len = self.string.length;
	[self scrollRangeToVisible:NSMakeRange(len, 1)];
}

- (void)removeText:(NSRange)range
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");

	NSTextStorage *ts = [self textStorage];
	[ts deleteCharactersInRange:range];
}

- (void)removeLastChar
{
	//XT_DEF_SELNAME;
	//XT_WARN_0(@"");

	NSTextStorage *ts = [self textStorage];
	NSUInteger tsLen = [ts length];
	if (tsLen >= 1) {
		NSRange rangeToRemove = NSMakeRange(tsLen - 1, 1);
		[ts deleteCharactersInRange:rangeToRemove];
	}
}

- (void)removeAttributedStringFromEnd:(NSAttributedString *)attrString
{
	//XT_DEF_SELNAME;
	//XT_WARN_1(@"attrString=\"%@\"", attrString.string);
	
	NSTextStorage *ts = [self textStorage];
	NSUInteger tsLen = [ts length];
	NSUInteger numToRemove = [attrString length];
	NSRange rangeToRemove = NSMakeRange(tsLen - numToRemove, numToRemove);
	[ts deleteCharactersInRange:rangeToRemove];
}

- (void)removeTextFromEnd:(NSUInteger)numCharsToRemove
{
	NSTextStorage *ts = [self textStorage];
	NSUInteger tsLen = [ts length];
	NSRange rangeToRemove = NSMakeRange(tsLen - numCharsToRemove, numCharsToRemove);
	[ts deleteCharactersInRange:rangeToRemove];
}

- (CGFloat)removeTextFromStart:(NSUInteger)numCharsToRemove
{
	XT_DEF_SELNAME;
	//XT_WARN_0(@"");

	NSTextStorage *ts = [self textStorage];

	CGFloat oldTextViewHeight = [self findTotalHeight];
	NSRange rangeToDelete = NSMakeRange(0, numCharsToRemove);
	[ts deleteCharactersInRange:rangeToDelete];
	//https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/AttributedStrings/Tasks/ChangingAttrStrings.html
	NSRange rangeToFix = NSMakeRange(0, ts.length);
	[ts fixAttributesInRange:rangeToFix];
	[self ensureLayoutForTextContainer]; // ...or else call to findTotalHeight below doesn't work right

	// deleting from the text store affects state used for pagination,
	// so calc a value to adjust that state (in caller):
	CGFloat newTextViewHeight = [self findTotalHeight];
	CGFloat trimmedTextViewHeight = (oldTextViewHeight - newTextViewHeight);
		// this isn't 100% correct in all cases, but let's stay on the sane side ;-)
	if (trimmedTextViewHeight < 0.0) {
		XT_ERROR_1(@"trimmedTextViewHeight was %f, setting it to 0.0", trimmedTextViewHeight);
		trimmedTextViewHeight = 0.0;
	}

	return trimmedTextViewHeight;
}

- (NSString *)stringInRange:(NSRange)range
{
	NSTextStorage *ts = [self textStorage];
	NSAttributedString *ats = [ts attributedSubstringFromRange:range];
	NSString *res = ats.string;
	return res;
}

- (BOOL)shouldLogDodgyReturnFromFindYCoordOfInsertionPoint
{
	return YES;
}

@end
