// 9 september 2015
#import "uipriv_darwin.h"

// We are basically cloning NSScrollView here, managing scrolling ourselves.
// TODOs
// - is the page increment set up right?
// - do we need to draw anything in the empty corner?
// - autohiding scrollbars

// NSScrollers have no intrinsic size; here we give it one
@interface areaScroller : NSScroller {
	BOOL libui_vertical;
}
- (id)initWithFrame:(NSRect)r vertical:(BOOL)v;
@end

@implementation areaScroller

- (id)initWithFrame:(NSRect)r vertical:(BOOL)v
{
	self = [super initWithFrame:r];
	if (self)
		self->libui_vertical = v;
	return self;
}

- (NSSize)intrinsicContentSize
{
	NSSize s;
	CGFloat scrollerWidth;

	s = [super intrinsicContentSize];
	scrollerWidth = [NSScroller scrollerWidthForControlSize:[self controlSize]
		scrollerStyle:[self scrollerStyle]];
	if (self->libui_vertical)
		s.width = scrollerWidth;
	else
		s.height = scrollerWidth;
	return s;
}

- (void)setControlSize:(NSControlSize)size
{
	[super setControlSize:size];
	[self invalidateIntrinsicContentSize];
}

- (void)setScrollerStyle:(NSScrollerStyle)style
{
	[super setScrollerStyle:style];
	[self invalidateIntrinsicContentSize];
}

@end

@interface areaDrawingView : NSView {
	uiArea *libui_a;
}
- (id)initWithFrame:(NSRect)r area:(uiArea *)a;
- (uiModifiers)parseModifiers:(NSEvent *)e;
- (void)doMouseEvent:(NSEvent *)e;
- (int)sendKeyEvent:(uiAreaKeyEvent *)ke;
- (int)doKeyDownUp:(NSEvent *)e up:(int)up;
- (int)doKeyDown:(NSEvent *)e;
- (int)doKeyUp:(NSEvent *)e;
- (int)doFlagsChanged:(NSEvent *)e;
@end

@interface areaView : NSView {
	uiArea *libui_a;
	areaDrawingView *drawingView;
	areaScroller *hscrollbar;
	areaScroller *vscrollbar;
	intmax_t hscrollpos;
	intmax_t vscrollpos;
}
- (id)initWithFrame:(NSRect)r area:(uiArea *)a;
- (void)dvFrameSizeChanged:(NSNotification *)note;
- (IBAction)hscrollEvent:(id)sender;
- (IBAction)vscrollEvent:(id)sender;
- (intmax_t)hscrollPos;
- (intmax_t)vscrollPos;
// scroll utilities
- (intmax_t)hpagesize;
- (intmax_t)vpagesize;
- (intmax_t)hscrollmax;
- (intmax_t)vscrollmax;
- (intmax_t)hscrollbarPosition;
- (intmax_t)vscrollbarPosition;
- (void)hscrollTo:(intmax_t)pos;
- (void)vscrollTo:(intmax_t)pos;
@end

struct uiArea {
	uiDarwinControl c;
	areaView *view;
	uiAreaHandler *ah;
};

uiDarwinDefineControl(
	uiArea,								// type name
	uiAreaType,							// type function
	view									// handle
)

@implementation areaDrawingView

- (id)initWithFrame:(NSRect)r area:(uiArea *)a
{
	self = [super initWithFrame:r];
	if (self)
		self->libui_a = a;
	return self;
}

- (void)drawRect:(NSRect)r
{
	CGContextRef c;
	uiAreaDrawParams dp;
	areaView *av;

	c = (CGContextRef) [[NSGraphicsContext currentContext] graphicsPort];
	dp.Context = newContext(c);

	// TODO frame or bounds?
	dp.ClientWidth = [self frame].size.width;
	dp.ClientHeight = [self frame].size.height;

	dp.ClipX = r.origin.x;
	dp.ClipY = r.origin.y;
	dp.ClipWidth = r.size.width;
	dp.ClipHeight = r.size.height;

	av = (areaView *) [self superview];
	dp.HScrollPos = [av hscrollPos];
	dp.VScrollPos = [av vscrollPos];

	// no need to save or restore the graphics state to reset transformations; Cocoa creates a brand-new context each time
	(*(self->libui_a->ah->Draw))(self->libui_a->ah, self->libui_a, &dp);

	freeContext(dp.Context);
}

- (BOOL)isFlipped
{
	return YES;
}

- (BOOL)acceptsFirstResponder
{
	return YES;
}

- (uiModifiers)parseModifiers:(NSEvent *)e
{
	NSEventModifierFlags mods;
	uiModifiers m;

	m = 0;
	mods = [e modifierFlags];
	if ((mods & NSControlKeyMask) != 0)
		m |= uiModifierCtrl;
	if ((mods & NSAlternateKeyMask) != 0)
		m |= uiModifierAlt;
	if ((mods & NSShiftKeyMask) != 0)
		m |= uiModifierShift;
	if ((mods & NSCommandKeyMask) != 0)
		m |= uiModifierSuper;
	return m;
}

// capture on drag is done automatically on OS X
- (void)doMouseEvent:(NSEvent *)e
{
	uiAreaMouseEvent me;
	NSPoint point;
	areaView *av;
	uintmax_t buttonNumber;
	NSUInteger pmb;
	unsigned int i, max;

	av = (areaView *) [self superview];

	// this will convert point to drawing space
	// thanks swillits in irc.freenode.net/#macdev
	point = [self convertPoint:[e locationInWindow] fromView:nil];
	me.X = point.x;
	me.Y = point.y;

	// TODO frame or bounds?
	me.ClientWidth = [self frame].size.width;
	me.ClientHeight = [self frame].size.height;
	me.HScrollPos = [av hscrollPos];
	me.VScrollPos = [av vscrollPos];

	buttonNumber = [e buttonNumber] + 1;
	// swap button numbers 2 and 3 (right and middle)
	if (buttonNumber == 2)
		buttonNumber = 3;
	else if (buttonNumber == 3)
		buttonNumber = 2;

	me.Down = 0;
	me.Up = 0;
	me.Count = 0;
	switch ([e type]) {
	case NSLeftMouseDown:
	case NSRightMouseDown:
	case NSOtherMouseDown:
		me.Down = buttonNumber;
		me.Count = [e clickCount];
		break;
	case NSLeftMouseUp:
	case NSRightMouseUp:
	case NSOtherMouseUp:
		me.Up = buttonNumber;
		break;
	case NSLeftMouseDragged:
	case NSRightMouseDragged:
	case NSOtherMouseDragged:
		// we include the button that triggered the dragged event in the Held fields
		buttonNumber = 0;
		break;
	}

	me.Modifiers = [self parseModifiers:e];

	pmb = [NSEvent pressedMouseButtons];
	me.Held1To64 = 0;
	if (buttonNumber != 1 && (pmb & 1) != 0)
		me.Held1To64 |= 1;
	if (buttonNumber != 2 && (pmb & 4) != 0)
		me.Held1To64 |= 2;
	if (buttonNumber != 3 && (pmb & 2) != 0)
		me.Held1To64 |= 4;
	// buttons 4..64
	max = 32;
	// TODO are the upper 32 bits just mirrored?
//	if (sizeof (NSUInteger) == 8)
//		max = 64;
	for (i = 4; i <= max; i++) {
		uint64_t j;

		if (buttonNumber == i)
			continue;
		j = 1 << (i - 1);
		if ((pmb & j) != 0)
			me.Held1To64 |= j;
	}

	(*(self->libui_a->ah->MouseEvent))(self->libui_a->ah, self->libui_a, &me);
}

#define mouseEvent(name) \
	- (void)name:(NSEvent *)e \
	{ \
		[self doMouseEvent:e]; \
	}
// TODO set up tracking events
mouseEvent(mouseMoved)
mouseEvent(mouseDragged)
mouseEvent(rightMouseDragged)
mouseEvent(otherMouseDragged)
mouseEvent(mouseDown)
mouseEvent(rightMouseDown)
mouseEvent(otherMouseDown)
mouseEvent(mouseUp)
mouseEvent(rightMouseUp)
mouseEvent(otherMouseUp)

// note: there is no equivalent to WM_CAPTURECHANGED on Mac OS X; there literally is no way to break a grab like that
// even if I invoke the task switcher and switch processes, the mouse grab will still be held until I let go of all buttons
// therefore, no DragBroken()

- (int)sendKeyEvent:(uiAreaKeyEvent *)ke
{
	return (*(self->libui_a->ah->KeyEvent))(self->libui_a->ah, self->libui_a, ke);
}

- (int)doKeyDownUp:(NSEvent *)e up:(int)up
{
	uiAreaKeyEvent ke;

	ke.Key = 0;
	ke.ExtKey = 0;
	ke.Modifier = 0;

	ke.Modifiers = [self parseModifiers:e];

	ke.Up = up;

	if (!fromKeycode([e keyCode], &ke))
		return 0;
	return [self sendKeyEvent:&ke];
}

- (int)doKeyDown:(NSEvent *)e
{
	return [self doKeyDownUp:e up:0];
}

- (int)doKeyUp:(NSEvent *)e
{
	return [self doKeyDownUp:e up:1];
}

- (int)doFlagsChanged:(NSEvent *)e
{
	uiAreaKeyEvent ke;
	uiModifiers whichmod;

	ke.Key = 0;
	ke.ExtKey = 0;

	// Mac OS X sends this event on both key up and key down.
	// Fortunately -[e keyCode] IS valid here, so we can simply map from key code to Modifiers, get the value of [e modifierFlags], and check if the respective bit is set or not — that will give us the up/down state
	if (!keycodeModifier([e keyCode], &whichmod))
		return 0;
	ke.Modifier = whichmod;
	ke.Modifiers = [self parseModifiers:e];
	ke.Up = (ke.Modifiers & ke.Modifier) == 0;
	// and then drop the current modifier from Modifiers
	ke.Modifiers &= ~ke.Modifier;
	return [self sendKeyEvent:&ke];
}

@end

// called by subclasses of -[NSApplication sendEvent:]
// by default, NSApplication eats some key events
// this prevents that from happening with uiArea
// see http://stackoverflow.com/questions/24099063/how-do-i-detect-keyup-in-my-nsview-with-the-command-key-held and http://lists.apple.com/archives/cocoa-dev/2003/Oct/msg00442.html
int sendAreaEvents(NSEvent *e)
{
	NSEventType type;
	id focused;
	areaDrawingView *view;

	type = [e type];
	if (type != NSKeyDown && type != NSKeyUp && type != NSFlagsChanged)
		return 0;
	focused = [[e window] firstResponder];
	if (focused == nil)
		return 0;
	if (![focused isKindOfClass:[areaDrawingView class]])
		return 0;
	view = (areaDrawingView *) focused;
	switch (type) {
	case NSKeyDown:
		return [view doKeyDown:e];
	case NSKeyUp:
		return [view doKeyUp:e];
	case NSFlagsChanged:
		return [view doFlagsChanged:e];
	}
	return 0;
}

@implementation areaView

- (id)initWithFrame:(NSRect)r area:(uiArea *)a
{
	NSScrollerStyle style;
	CGFloat swidth;
	NSMutableDictionary *views;
	NSLayoutConstraint *constraint;

	self = [super initWithFrame:r];
	if (self) {
		self->libui_a = a;

		self->drawingView = [[areaDrawingView alloc] initWithFrame:NSZeroRect area:self->libui_a];
		[self->drawingView setTranslatesAutoresizingMaskIntoConstraints:NO];

		style = [NSScroller preferredScrollerStyle];
		swidth = [NSScroller scrollerWidthForControlSize:NSRegularControlSize
			scrollerStyle:style];

		self->hscrollbar = [[areaScroller alloc]
			initWithFrame:NSMakeRect(0, 0, swidth * 5, swidth)
			vertical:NO];
		[self->hscrollbar setScrollerStyle:style];
		[self->hscrollbar setKnobStyle:NSScrollerKnobStyleDefault];
		[self->hscrollbar setControlTint:NSDefaultControlTint];
		[self->hscrollbar setControlSize:NSRegularControlSize];
		[self->hscrollbar setArrowsPosition:NSScrollerArrowsDefaultSetting];
		[self->hscrollbar setTranslatesAutoresizingMaskIntoConstraints:NO];

		self->vscrollbar = [[areaScroller alloc]
			initWithFrame:NSMakeRect(0, 0, swidth, swidth * 5)
			vertical:YES];
		[self->vscrollbar setScrollerStyle:style];
		[self->vscrollbar setKnobStyle:NSScrollerKnobStyleDefault];
		[self->vscrollbar setControlTint:NSDefaultControlTint];
		[self->vscrollbar setControlSize:NSRegularControlSize];
		[self->vscrollbar setArrowsPosition:NSScrollerArrowsDefaultSetting];
		[self->vscrollbar setTranslatesAutoresizingMaskIntoConstraints:NO];

		[self addSubview:self->drawingView];
		[self addSubview:self->hscrollbar];
		[self addSubview:self->vscrollbar];

		// use visual constraints to arrange:
		// - the drawing view and vertical scrollbar horizontally
		// - the drawing view and horizontal scrollbar vertically
		// - the horizontal scrollbar flush left
		// - the vertical scrollbar flush top
		views = [NSMutableDictionary new];
		[views setObject:self->drawingView forKey:@"drawingView"];
		[views setObject:self->hscrollbar forKey:@"hscrollbar"];
		[views setObject:self->vscrollbar forKey:@"vscrollbar"];
		addConstraint(self, @"H:|[drawingView][vscrollbar]|", nil, views);
		addConstraint(self, @"V:|[drawingView][hscrollbar]|", nil, views);
		addConstraint(self, @"H:|[hscrollbar]", nil, views);
		addConstraint(self, @"V:|[vscrollbar]", nil, views);
		[views release];

		// use explicit layout constraints to line up
		// - the bottom edge of the drawing view with the bottom edge of the vertical scrollbar
		// - the right edge of the drawing view with the right edge of the horizontal scrollbar
		constraint = [NSLayoutConstraint constraintWithItem:self->drawingView
			attribute:NSLayoutAttributeBottom
			relatedBy:NSLayoutRelationEqual
			toItem:self->vscrollbar
			attribute:NSLayoutAttributeBottom
			multiplier:1
			constant:0];
		[self addConstraint:constraint];
		[constraint release];
		constraint = [NSLayoutConstraint constraintWithItem:self->drawingView
			attribute:NSLayoutAttributeRight
			relatedBy:NSLayoutRelationEqual
			toItem:self->hscrollbar
			attribute:NSLayoutAttributeRight
			multiplier:1
			constant:0];
		[self addConstraint:constraint];
		[constraint release];

		self->hscrollpos = 0;
		self->vscrollpos = 0;

		// now set up events
		// first we need to monitor when the drawing view frame size has changed, as we need to recalculate all the scrollbar parameters in that case
		[[NSNotificationCenter defaultCenter]
			addObserver:self
			selector:@selector(dvFrameSizeChanged:)
			name:NSViewFrameDidChangeNotification
			object:self->drawingView];
		// and this will trigger a frame changed event to kick us off
		[self->drawingView setPostsFrameChangedNotifications:YES];

		// and the scrollbar events
		[self->hscrollbar setTarget:self];
		[self->hscrollbar setAction:@selector(hscrollEvent:)];
		[self->vscrollbar setTarget:self];
		[self->vscrollbar setAction:@selector(vscrollEvent:)];

		// TODO notification on preferred style change
	}
	return self;
}

- (void)dealloc
{
	[self->vscrollbar setTarget:nil];
	[self->hscrollbar setTarget:nil];
	[[NSNotificationCenter defaultCenter] removeObserver:self];
	[super dealloc];
}

// TODO reduce code duplication

// TODO if the proportion becomes 1 we should disable the scrollbar
- (void)dvFrameSizeChanged:(NSNotification *)note
{
	intmax_t max;
	double proportion;

	max = [self hscrollmax];
	if (max == 0) {
		[self->hscrollbar setKnobProportion:0];
		// this hides the knob
		[self->hscrollbar setEnabled:NO];
	} else {
		proportion = [self hpagesize];
		proportion /= max;
		[self->hscrollbar setKnobProportion:proportion];
		[self->hscrollbar setEnabled:YES];
	}

	max = [self vscrollmax];
	if (max == 0) {
		[self->vscrollbar setKnobProportion:0];
		// this hides the knob
		[self->vscrollbar setEnabled:NO];
	} else {
		proportion = [self vpagesize];
		proportion /= max;
		[self->vscrollbar setKnobProportion:proportion];
		[self->vscrollbar setEnabled:YES];
	}

	// and update the scrolling position
	[self hscrollTo:self->hscrollpos];
	[self vscrollTo:self->vscrollpos];

	if ((*(self->libui_a->ah->RedrawOnResize))(self->libui_a->ah, self->libui_a))
		[self->drawingView setNeedsDisplay:YES];
}

- (IBAction)hscrollEvent:(id)sender
{
	uintmax_t pos;

	pos = self->hscrollpos;
	switch ([self->hscrollbar hitPart]) {
	case NSScrollerNoPart:
		// do nothing
		break;
	case NSScrollerDecrementPage:
		pos -= [self hpagesize];
		break;
	case NSScrollerKnob:
	case NSScrollerKnobSlot:
		pos = [self hscrollbarPosition];
		break;
	case NSScrollerIncrementPage:
		pos += [self hpagesize];
		break;
	case NSScrollerDecrementLine:
		pos--;
		break;
	case NSScrollerIncrementLine:
		pos++;
		break;
	}
	[self hscrollTo:pos];
}

- (IBAction)vscrollEvent:(id)sender
{
	uintmax_t pos;

	pos = self->vscrollpos;
	switch ([self->vscrollbar hitPart]) {
	case NSScrollerNoPart:
		// do nothing
		break;
	case NSScrollerDecrementPage:
		pos -= [self vpagesize];
		break;
	case NSScrollerKnob:
	case NSScrollerKnobSlot:
		pos = [self vscrollbarPosition];
		break;
	case NSScrollerIncrementPage:
		pos += [self vpagesize];
		break;
	case NSScrollerDecrementLine:
		pos--;
		break;
	case NSScrollerIncrementLine:
		pos++;
		break;
	}
	[self vscrollTo:pos];
}

- (intmax_t)hscrollPos
{
	return self->hscrollpos;
}

- (intmax_t)vscrollPos
{
	return self->vscrollpos;
}

// scroll utilities

- (intmax_t)hpagesize
{
	return [self->drawingView frame].size.width;
}

- (intmax_t)vpagesize
{
	return [self->drawingView frame].size.height;
}

- (intmax_t)hscrollmax
{
	intmax_t n;

	n = (*(self->libui_a->ah->HScrollMax))(self->libui_a->ah, self->libui_a);
	n -= [self hpagesize];
	if (n < 0)
		n = 0;
	return n;
}

- (intmax_t)vscrollmax
{
	intmax_t n;

	n = (*(self->libui_a->ah->VScrollMax))(self->libui_a->ah, self->libui_a);
	n -= [self vpagesize];
	if (n < 0)
		n = 0;
	return n;
}

- (intmax_t)hscrollbarPosition
{
	return [self->hscrollbar doubleValue] * [self hscrollmax];
}

- (intmax_t)vscrollbarPosition
{
	return [self->vscrollbar doubleValue] * [self vscrollmax];
}

- (void)hscrollTo:(intmax_t)pos
{
	double doubleVal;
	CGFloat by;
	NSRect update;

	if (pos > [self hscrollmax])
		pos = [self hscrollmax];
	if (pos < 0)
		pos = 0;

	by = -(pos - self->hscrollpos);
	[self->drawingView scrollRect:[self->drawingView bounds]
		by:NSMakeSize(by, 0)];
	update = [self->drawingView bounds];
	if (by < 0) {		// right of bounds needs updating
		// + by since by is negative and we need to subtract its absolute value from the width
		update.origin.x += update.size.width + by;
		update.size.width = -by;
	} else			// left of bounds needs updating
		update.size.width = by;
	[self->drawingView setNeedsDisplayInRect:update];

	self->hscrollpos = pos;
	doubleVal = ((double) (self->hscrollpos)) / [self hscrollmax];
	[self->hscrollbar setDoubleValue:doubleVal];
}

- (void)vscrollTo:(intmax_t)pos
{
	double doubleVal;
	CGFloat by;
	NSRect update;

	if (pos > [self vscrollmax])
		pos = [self vscrollmax];
	if (pos < 0)
		pos = 0;

	by = -(pos - self->vscrollpos);
	[self->drawingView scrollRect:[self->drawingView bounds]
		by:NSMakeSize(0, by)];
	update = [self->drawingView bounds];
	if (by < 0) {		// bottom of bounds needs updating
		// + by since by is negative and we need to subtract its absolute value from the height
		update.origin.y += update.size.height + by;
		update.size.height = -by;
	} else			// top of bounds needs updating
		update.size.height = by;
	[self->drawingView setNeedsDisplayInRect:update];

	self->vscrollpos = pos;
	doubleVal = ((double) (self->vscrollpos)) / [self vscrollmax];
	[self->vscrollbar setDoubleValue:doubleVal];
}

@end

void uiAreaUpdateScroll(uiArea *a)
{
/* TODO
	NSRect frame;

	frame.origin = NSMakePoint(0, 0);
	frame.size.width = (*(a->ah->HScrollMax))(a->ah, a);
	frame.size.height = (*(a->ah->VScrollMax))(a->ah, a);
	[a->documentView setFrame:frame];
*/
}

void uiAreaQueueRedrawAll(uiArea *a)
{
	[a->view setNeedsDisplay:YES];
}

uiArea *uiNewArea(uiAreaHandler *ah)
{
	uiArea *a;

	a = (uiArea *) uiNewControl(uiAreaType());

	a->ah = ah;

	a->view = [[areaView alloc] initWithFrame:NSZeroRect area:a];

	uiDarwinFinishNewControl(a, uiArea);

	return a;
}
