#define Point OSXPoint
#define Rect OSXRect
#define Cursor OSXCursor
#include <Carbon/Carbon.h>
#import <Foundation/Foundation.h>
#ifdef MULTITOUCH
#include <IOKit/IOKitLib.h>
#include <IOKit/hidsystem/IOHIDShared.h>
#endif
#undef Rect
#undef Point
#undef Cursor
#undef offsetof
#undef nil

#include "u.h"
#include "libc.h"
#include <thread.h>
#include <draw.h>
#include <memdraw.h>
#include <keyboard.h>
#include "mouse.h"
#include <cursor.h>
#include "osx-screen.h"
#include "osx-keycodes.h"
#include "devdraw.h"
#include "glendapng.h"

AUTOFRAMEWORK(Carbon)
AUTOFRAMEWORK(Cocoa)

#ifdef MULTITOUCH
AUTOFRAMEWORK(MultitouchSupport)
AUTOFRAMEWORK(IOKit)
#endif

#define panic sysfatal

extern Rectangle mouserect;

struct {
	char *label;
	char *winsize;
	QLock labellock;

	Rectangle fullscreenr;
	Rectangle screenr;
	Memimage *screenimage;
	int isfullscreen;
	ulong fullscreentime;
	
	Point xy;
	int buttons;
	int kbuttons;

	CGDataProviderRef provider;
	MenuRef wmenu;
	MenuRef vmenu;
	WindowRef window;
	CGImageRef image;
	CGContextRef windowctx;
	PasteboardRef snarf;
	int needflush;
	QLock flushlock;
	int active;
	int infullscreen;
	int kalting;		// last keystroke was Kalt
	int touched;		// last mouse event was touchCallback
	int collapsed;		// parked in dock
	int flushing;		// flushproc has started
	NSMutableArray* devicelist;
} osx;

/* 
 These structs are required, in order to handle some parameters returned from the 
 Support.framework 
 */ 
typedef struct { 
	float x; 
	float y; 
}mtPoint; 

typedef struct { 
	mtPoint position; 
	mtPoint velocity; 
}mtReadout; 

/* 
 Some reversed engineered informations from MultiTouchSupport.framework 
 */ 
typedef struct 
{ 
	int frame; //the current frame 
	double timestamp; //event timestamp 
	int identifier; //identifier guaranteed unique for life of touch per device 
	int state; //the current state (not sure what the values mean) 
	int unknown1; //no idea what this does 
	int unknown2; //no idea what this does either 
	mtReadout normalized; //the normalized position and vector of the touch (0,0 to 1,1) 
	float size; //the size of the touch (the area of your finger being tracked) 
	int unknown3; //no idea what this does 
	float angle; //the angle of the touch -| 
	float majorAxis; //the major axis of the touch -|-- an ellipsoid. you can track the angle of each finger! 
	float minorAxis; //the minor axis of the touch -| 
	mtReadout unknown4; //not sure what this is for 
	int unknown5[2]; //no clue 
	float unknown6; //no clue 
}Touch; 

//a reference pointer for the multitouch device 
typedef void *MTDeviceRef; 

//the prototype for the callback function 
typedef int (*MTContactCallbackFunction)(int,Touch*,int,double,int); 

//returns a pointer to the default device (the trackpad?) 
MTDeviceRef MTDeviceCreateDefault(void); 

//returns a CFMutableArrayRef array of all multitouch devices 
CFMutableArrayRef MTDeviceCreateList(void); 

//registers a device's frame callback to your callback function 
void MTRegisterContactFrameCallback(MTDeviceRef, MTContactCallbackFunction); 
void MTUnregisterContactFrameCallback(MTDeviceRef, MTContactCallbackFunction); 

//start sending events 
void MTDeviceStart(MTDeviceRef, int); 
void MTDeviceStop(MTDeviceRef); 

MTDeviceRef MTDeviceCreateFromService(io_service_t);
io_service_t MTDeviceGetService(MTDeviceRef);

#define kNTracks 10
struct TouchTrack {
	int id;
	float firstThreshTime;
	mtPoint pos;
} tracks[kNTracks];

#define kSizeSensitivity 1.25f
#define kTimeSensitivity 0.03f /* seconds */
#define kButtonLimit 0.6f /* percentage from base of pad */

int
findTrack(int id)
{
	int i;
	for(i = 0; i < kNTracks; ++i)
		if(tracks[i].id == id)
			return i;
	return -1;
}

#define kMoveSensitivity 0.05f

int
moved(mtPoint a, mtPoint b)
{
	if(fabs(a.x - b.x) > kMoveSensitivity)
		return 1;
	if(fabs(a.y - b.y) > kMoveSensitivity)
		return 1;
	return 0;
}

int
classifyTouch(Touch *t)
{
	mtPoint p;
	int i;

	p = t->normalized.position;

	i = findTrack(t->identifier);
	if(i == -1) {
		i = findTrack(-1);
		if(i == -1)
			return 0;	// No empty tracks.
		tracks[i].id = t->identifier;
		tracks[i].firstThreshTime = t->timestamp;
		tracks[i].pos = p;
		// we don't have a touch yet - we wait kTimeSensitivity before reporting it.
		return 0;
	}
		
	if(t->size == 0) { // lost touch
		tracks[i].id = -1;
		return 0;
	}
	if(t->size < kSizeSensitivity) {
		tracks[i].firstThreshTime = t->timestamp;
	}
	if((t->timestamp - tracks[i].firstThreshTime) < kTimeSensitivity) {
		return 0;
	}
	if(p.y > kButtonLimit && t->size > kSizeSensitivity) {
		if(p.x < 0.35)
			return 1;
		if(p.x > 0.65)
			return 4;
		if(p.x > 0.35 && p.x < 0.65)
			return 2;
	}
	return 0;
}

static ulong msec(void);

int
touchCallback(int device, Touch *data, int nFingers, double timestamp, int frame)
{
#ifdef MULTITOUCH
	int buttons, delta, i;
	static int obuttons;
	CGPoint p;
	CGEventRef e;

	p.x = osx.xy.x+osx.screenr.min.x;
	p.y = osx.xy.y+osx.screenr.min.y;
	if(!ptinrect(Pt(p.x, p.y), osx.screenr))
		return 0;
	osx.touched = 1;
	buttons = 0;
	for(i = 0; i < nFingers; ++i)
		buttons |= classifyTouch(data+i);
	delta = buttons ^ obuttons;
	obuttons = buttons;
	if(delta & 1) {
		e = CGEventCreateMouseEvent(NULL, 
			(buttons & 1) ? kCGEventOtherMouseDown : kCGEventOtherMouseUp, 
			p,
			29);
		CGEventPost(kCGSessionEventTap, e);
		CFRelease(e);
	}
	if(delta & 2) {
		e = CGEventCreateMouseEvent(NULL,
			(buttons & 2) ? kCGEventOtherMouseDown : kCGEventOtherMouseUp, 
			p,
			30);
		CGEventPost(kCGSessionEventTap, e);
		CFRelease(e);
	}
	if(delta & 4){
		e = CGEventCreateMouseEvent(NULL, 
			(buttons & 4) ? kCGEventOtherMouseDown : kCGEventOtherMouseUp, 
			p,
			31);
		CGEventPost(kCGSessionEventTap, e);
		CFRelease(e);
	}
	return delta != 0;
#else
	return 0;
#endif
}

extern int multitouch;

enum
{
	WindowAttrs =
		kWindowCloseBoxAttribute |
		kWindowCollapseBoxAttribute |
		kWindowResizableAttribute |
		kWindowStandardHandlerAttribute |
		kWindowFullZoomAttribute
};

enum
{
	P9PEventLabelUpdate = 1
};

static void screenproc(void*);
static void eresized(int);
static void fullscreen(int);
static void seticon(void);
static void activated(int);

static OSStatus quithandler(EventHandlerCallRef, EventRef, void*);
static OSStatus eventhandler(EventHandlerCallRef, EventRef, void*);
static OSStatus cmdhandler(EventHandlerCallRef, EventRef, void*);

enum
{
	CmdFullScreen = 1,
};

void screeninit(void);
void _flushmemscreen(Rectangle r);

#ifdef MULTITOUCH
static void
RegisterMultitouch(void *ctx, io_iterator_t iter)
{
	io_object_t io;
	MTDeviceRef dev;

	while((io = IOIteratorNext(iter)) != 0){
		dev = MTDeviceCreateFromService(io);
		if (dev != nil){
			MTRegisterContactFrameCallback(dev, touchCallback);
			[osx.devicelist addObject:dev];
			if(osx.active)
				MTDeviceStart(dev, 0);
		}
		
		IOObjectRelease(io);
	}
}

static void
UnregisterMultitouch(void *ctx, io_iterator_t iter)
{
	io_object_t io;
	MTDeviceRef dev;
	int i;
	
	while((io = IOIteratorNext(iter)) != 0){
		for(i = 0; i < [osx.devicelist count]; i++){
			dev = [osx.devicelist objectAtIndex:i];
			if(IOObjectIsEqualTo(MTDeviceGetService(dev), io)){
				if(osx.active)
					MTDeviceStop(dev);
				MTUnregisterContactFrameCallback(dev, touchCallback);
				[osx.devicelist removeObjectAtIndex:i];
				break;
			}
		}

		IOObjectRelease(io);
	}
}

#endif /*MULTITOUCH*/

static void
InitMultiTouch()
{
#ifdef MULTITOUCH
	IONotificationPortRef port;
	CFRunLoopSourceRef source;
	io_iterator_t iter;
	kern_return_t kr;
	io_object_t obj;
	int i;

	if(!multitouch)
		return;

	osx.devicelist = [[NSMutableArray alloc] init];

	for(i = 0; i < kNTracks; ++i)
		tracks[i].id = -1;

	port = IONotificationPortCreate(kIOMasterPortDefault);
	if(port == nil){
		fprint(2, "failed to get an IO notification port\n");
		return;
	}

	source = IONotificationPortGetRunLoopSource(port);
	if(source == nil){
		fprint(2, "failed to get loop source for port");
		return;
	}

	CFRunLoopAddSource(
		(CFRunLoopRef)GetCFRunLoopFromEventLoop(GetMainEventLoop()), 
		source, 
		kCFRunLoopDefaultMode);

	kr = IOServiceAddMatchingNotification(
		port, kIOTerminatedNotification,
		IOServiceMatching("AppleMultitouchDevice"), 
		&UnregisterMultitouch,
		nil, &iter);

	if(kr != KERN_SUCCESS){
		fprint(2, "failed to add termination notification\n");
		return;
	}

	/* Arm the notification */
	while((obj = IOIteratorNext(iter)) != 0)
		IOObjectRelease(obj);

	kr = IOServiceAddMatchingNotification(
		port, kIOMatchedNotification,
		IOServiceMatching("AppleMultitouchDevice"),
		&RegisterMultitouch,
		nil, &iter);

	if(kr != KERN_SUCCESS){
		fprint(2, "failed to add matching notification\n");
		return;
	}

	RegisterMultitouch(nil, iter);
#endif
}

Memimage*
attachscreen(char *label, char *winsize)
{
	if(label == nil)
		label = "gnot a label";
	osx.label = strdup(label);
	osx.winsize = winsize;
	if(osx.screenimage == nil){
		screeninit();
		if(osx.screenimage == nil)
			panic("cannot create OS X screen");
	}
	return osx.screenimage;
}

extern int multitouch;

void
_screeninit(void)
{
	CGRect cgr;
	OSXRect or;
	Rectangle r;
	int havemin;

	memimageinit();

	ProcessSerialNumber psn = { 0, kCurrentProcess };
	TransformProcessType(&psn, kProcessTransformToForegroundApplication);
	SetFrontProcess(&psn);

	cgr = CGDisplayBounds(CGMainDisplayID());
	osx.fullscreenr = Rect(0, 0, cgr.size.width, cgr.size.height);
	
	InitCursor();
	
	// Create minimal menu with full-screen option.
	ClearMenuBar();
	CreateStandardWindowMenu(0, &osx.wmenu);
	InsertMenu(osx.wmenu, 0);
	MenuItemIndex ix;
	CreateNewMenu(1004, 0, &osx.vmenu);	// XXX 1004?
	SetMenuTitleWithCFString(osx.vmenu, CFSTR("View"));
	AppendMenuItemTextWithCFString(osx.vmenu,
		CFSTR("Full Screen"), 0, CmdFullScreen, &ix);
	SetMenuItemCommandKey(osx.vmenu, ix, 0, 'F');
	AppendMenuItemTextWithCFString(osx.vmenu,
		CFSTR("Cmd-F exits full screen"),
		kMenuItemAttrDisabled, CmdFullScreen, &ix);
	InsertMenu(osx.vmenu, GetMenuID(osx.wmenu));
	DrawMenuBar();

	// Create the window.
	r = Rect(0, 0, Dx(osx.fullscreenr)*2/3, Dy(osx.fullscreenr)*2/3);
	havemin = 0;
	if(osx.winsize && osx.winsize[0]){
		if(parsewinsize(osx.winsize, &r, &havemin) < 0)
			sysfatal("%r");
	}
	if(!havemin)
		r = rectaddpt(r, Pt((Dx(osx.fullscreenr)-Dx(r))/2, (Dy(osx.fullscreenr)-Dy(r))/2));
	or.left = r.min.x;
	or.top = r.min.y;
	or.right = r.max.x;
	or.bottom = r.max.y;
	CreateNewWindow(kDocumentWindowClass, WindowAttrs, &or, &osx.window);
	setlabel(osx.label);
	seticon();
	
	// Set up the clip board.
	if(PasteboardCreate(kPasteboardClipboard, &osx.snarf) != noErr)
		panic("pasteboard create");

	// Explain in great detail which events we want to handle.
	// Why can't we just have one handler?
	const EventTypeSpec quits[] = {
		{ kEventClassApplication, kEventAppQuit }
	};
	const EventTypeSpec cmds[] = {
		{ kEventClassWindow, kEventWindowClosed },
		{ kEventClassWindow, kEventWindowBoundsChanged },
		{ kEventClassWindow, kEventWindowDrawContent },
		{ kEventClassCommand, kEventCommandProcess },
		{ kEventClassWindow, kEventWindowActivated },
		{ kEventClassWindow, kEventWindowDeactivated },
		{ kEventClassWindow, kEventWindowCollapsed },
		{ kEventClassWindow, kEventWindowExpanded },
	};
	const EventTypeSpec events[] = {
		{ kEventClassApplication, kEventAppShown },
		{ kEventClassKeyboard, kEventRawKeyDown },
		{ kEventClassKeyboard, kEventRawKeyModifiersChanged },
		{ kEventClassKeyboard, kEventRawKeyRepeat },
		{ kEventClassMouse, kEventMouseDown },
		{ kEventClassMouse, kEventMouseUp },
		{ kEventClassMouse, kEventMouseMoved },
		{ kEventClassMouse, kEventMouseDragged },
		{ kEventClassMouse, kEventMouseWheelMoved },
		{ 'P9PE', P9PEventLabelUpdate}
	};

	InstallApplicationEventHandler(
		NewEventHandlerUPP(quithandler),
		nelem(quits), quits, nil, nil);

 	InstallApplicationEventHandler(
 		NewEventHandlerUPP(eventhandler),
		nelem(events), events, nil, nil);

	InstallWindowEventHandler(osx.window,
		NewEventHandlerUPP(cmdhandler),
		nelem(cmds), cmds, osx.window, nil);

	// Finally, put the window on the screen.
	ShowWindow(osx.window);
	ShowMenuBar();
	eresized(0);
	SelectWindow(osx.window);

	if(multitouch)
		InitMultiTouch();

	// CoreGraphics pins mouse events to the destination point of a
	// CGWarpMouseCursorPosition (see setmouse) for an interval of time
	// following the move. Disable this by setting the interval to zero
	// seconds.
	CGSetLocalEventsSuppressionInterval(0.0);

	InitCursor();
}

static Rendez scr;
static QLock slock;

void
screeninit(void)
{
	scr.l = &slock;
	qlock(scr.l);
	proccreate(screenproc, nil, 256*1024);
	while(osx.window == nil)
		rsleep(&scr);
	qunlock(scr.l);
}

static void
screenproc(void *v)
{
	qlock(scr.l);
	_screeninit();
	rwakeup(&scr);
	qunlock(scr.l);
	RunApplicationEventLoop();
}

static OSStatus kbdevent(EventRef);
static OSStatus mouseevent(EventRef);

static OSStatus
cmdhandler(EventHandlerCallRef next, EventRef event, void *arg)
{
	return eventhandler(next, event, arg);
}

static OSStatus
quithandler(EventHandlerCallRef next, EventRef event, void *arg)
{
	exit(0);
	return 0;
}

static OSStatus
eventhandler(EventHandlerCallRef next, EventRef event, void *arg)
{
	OSStatus result;

	result = CallNextEventHandler(next, event);

	switch(GetEventClass(event)){

	case 'P9PE':
		if(GetEventKind(event) == P9PEventLabelUpdate) {
			qlock(&osx.labellock);
			setlabel(osx.label);
			qunlock(&osx.labellock);
			return noErr;
		} else
			return eventNotHandledErr;

	case kEventClassApplication:;
		Rectangle r = Rect(0, 0, Dx(osx.screenr), Dy(osx.screenr));
		_flushmemscreen(r);
		return eventNotHandledErr;

	case kEventClassKeyboard:
		return kbdevent(event);
	
	case kEventClassMouse:
		return mouseevent(event);
	
	case kEventClassCommand:;
		HICommand cmd;
		GetEventParameter(event, kEventParamDirectObject,
			typeHICommand, nil, sizeof cmd, nil, &cmd);
		switch(cmd.commandID){
		case kHICommandQuit:
			exit(0);
		
		case CmdFullScreen:
			fullscreen(1);
			break;
		
		default:
			return eventNotHandledErr;
		}
		break;
	
	case kEventClassWindow:
		switch(GetEventKind(event)){
		case kEventWindowClosed:
			exit(0);
		
		case kEventWindowBoundsChanged:;
			// We see kEventWindowDrawContent
			// if we grow a window but not if we shrink it.
			UInt32 flags;
			GetEventParameter(event, kEventParamAttributes,
				typeUInt32, 0, sizeof flags, 0, &flags);
			int new = (flags & kWindowBoundsChangeSizeChanged) != 0;
			eresized(new);
			break;
		
		case kEventWindowDrawContent:
			// Tried using just flushmemimage here, but
			// it causes an odd artifact in which making a window
			// bigger in both width and height can then only draw
			// on the new border: it's like the old window is stuck
			// floating on top.  Doing a full "get a new window"
			// seems to solve the problem.
			eresized(1);
			break;

		case kEventWindowActivated:
			if(!osx.collapsed)
				activated(1);
			return eventNotHandledErr;
					
		case kEventWindowDeactivated:
			activated(0);
			return eventNotHandledErr;

		case kEventWindowCollapsed:
			osx.collapsed = 1;
			activated(0);
			return eventNotHandledErr;

		case kEventWindowExpanded:
			osx.collapsed = 0;
			activated(1);
			return eventNotHandledErr;

		default:
			return eventNotHandledErr;
		}
		break;
	}
	
	return result;
}

static ulong
msec(void)
{
	return nsec()/1000000;
}

static OSStatus
mouseevent(EventRef event)
{
	int wheel;
	OSXPoint op;
	
	GetEventParameter(event, kEventParamMouseLocation,
		typeQDPoint, 0, sizeof op, 0, &op);

	osx.xy = subpt(Pt(op.h, op.v), osx.screenr.min);
	wheel = 0;

	switch(GetEventKind(event)){
	case kEventMouseWheelMoved:;
		SInt32 delta;
		GetEventParameter(event, kEventParamMouseWheelDelta,
			typeSInt32, 0, sizeof delta, 0, &delta);
		
		// if I have any active touches in my region, I need to ignore the wheel motion.
		//int i;
		//for(i = 0; i < kNTracks; ++i) {
		//	if(tracks[i].id != -1 && tracks[i].pos.y > kButtonLimit) break;
		//}
		//if(i == kNTracks) { // No active touches, go ahead and scroll.
			if(delta > 0)
				wheel = 8;
			else
				wheel = 16;
		//}
		break;
	
	case kEventMouseDown:
	case kEventMouseUp:;
		UInt32 but, mod;
		GetEventParameter(event, kEventParamMouseChord,
			typeUInt32, 0, sizeof but, 0, &but);
		GetEventParameter(event, kEventParamKeyModifiers,
			typeUInt32, 0, sizeof mod, 0, &mod);
		
		// OS X swaps button 2 and 3
		but = (but & ~6) | ((but & 4)>>1) | ((but&2)<<1);
		but = (but & ~((1<<10)-1)) | mouseswap(but & ((1<<10)-1));
		if(osx.touched) {
			// in multitouch we use the clicks down to enable our 
			// virtual buttons.
			if(but & 0x7) {
				if(but>>29)
					but = but >> 29;
			} else 
				but = 0;
			osx.touched = 0;
		}

		// Apply keyboard modifiers and pretend it was a real mouse button.
		// (Modifiers typed while holding the button go into kbuttons,
		// but this one does not.)
		if(but == 1){
			if(mod & optionKey) {
				// Take the ALT away from the keyboard handler.
				if(osx.kalting) {
					osx.kalting = 0;
					keystroke(Kalt);
				}
				but = 2;
			}
			else if(mod & cmdKey)
				but = 4;
		}
		osx.buttons = but;
		break;

	case kEventMouseMoved:
	case kEventMouseDragged:
		break;
	
	default:
		return eventNotHandledErr;
	}

	mousetrack(osx.xy.x, osx.xy.y, osx.buttons|osx.kbuttons|wheel, msec());
	return noErr;	
}

static int keycvt[] =
{
	[QZ_IBOOK_ENTER] '\n',
	[QZ_RETURN] '\n',
	[QZ_ESCAPE] 27,
	[QZ_BACKSPACE] '\b',
	[QZ_LALT] Kalt,
	[QZ_LCTRL] Kctl,
	[QZ_LSHIFT] Kshift,
	[QZ_F1] KF+1,
	[QZ_F2] KF+2,
	[QZ_F3] KF+3,
	[QZ_F4] KF+4,
	[QZ_F5] KF+5,
	[QZ_F6] KF+6,
	[QZ_F7] KF+7,
	[QZ_F8] KF+8,
	[QZ_F9] KF+9,
	[QZ_F10] KF+10,
	[QZ_F11] KF+11,
	[QZ_F12] KF+12,
	[QZ_INSERT] Kins,
	[QZ_DELETE] 0x7F,
	[QZ_HOME] Khome,
	[QZ_END] Kend,
	[QZ_KP_PLUS] '+',
	[QZ_KP_MINUS] '-',
	[QZ_TAB] '\t',
	[QZ_PAGEUP] Kpgup,
	[QZ_PAGEDOWN] Kpgdown,
	[QZ_UP] Kup,
	[QZ_DOWN] Kdown,
	[QZ_LEFT] Kleft,
	[QZ_RIGHT] Kright,
	[QZ_KP_MULTIPLY] '*',
	[QZ_KP_DIVIDE] '/',
	[QZ_KP_ENTER] '\n',
	[QZ_KP_PERIOD] '.',
	[QZ_KP0] '0',
	[QZ_KP1] '1',
	[QZ_KP2] '2',
	[QZ_KP3] '3',
	[QZ_KP4] '4',
	[QZ_KP5] '5',
	[QZ_KP6] '6',
	[QZ_KP7] '7',
	[QZ_KP8] '8',
	[QZ_KP9] '9',
};

static OSStatus
kbdevent(EventRef event)
{
	char ch;
	UInt32 code;
	UInt32 mod;
	int k;

	GetEventParameter(event, kEventParamKeyMacCharCodes,
		typeChar, nil, sizeof ch, nil, &ch);
	GetEventParameter(event, kEventParamKeyCode,
		typeUInt32, nil, sizeof code, nil, &code);
	GetEventParameter(event, kEventParamKeyModifiers,
		typeUInt32, nil, sizeof mod, nil, &mod);

	switch(GetEventKind(event)){
	case kEventRawKeyDown:
	case kEventRawKeyRepeat:
		osx.kalting = 0;
		if(mod == cmdKey){
			if(ch == 'F' || ch == 'f'){
				if(osx.isfullscreen && msec() - osx.fullscreentime > 500)
					fullscreen(0);
				return noErr;
			}
			
			// Pass most Cmd keys through as Kcmd + ch.
			// OS X interprets a few no matter what we do,
			// so it is useless to pass them through as keystrokes too.
			switch(ch) {
			case 'm':	// minimize window
			case 'h':	// hide window
			case 'H':	// hide others
			case 'q':	// quit
				return eventNotHandledErr;
			}
			if(' ' <= ch && ch <= '~') {
				keystroke(Kcmd + ch);
				return noErr;
			}
			return eventNotHandledErr;
		}
		k = ch;
		if(code < nelem(keycvt) && keycvt[code])
			k = keycvt[code];
		if(k == 0)
			return noErr;
		if(k > 0)
			keystroke(k);
		else{
			UniChar uc;
			OSStatus s;

			s = GetEventParameter(event, kEventParamKeyUnicodes,
				typeUnicodeText, nil, sizeof uc, nil, &uc);
			if(s == noErr)
				keystroke(uc);
		}
		break;

	case kEventRawKeyModifiersChanged:
		if(!osx.buttons && !osx.kbuttons){
			if(mod == optionKey) {
				osx.kalting = 1;
				keystroke(Kalt);
			}
			break;
		}
		
		// If the mouse button is being held down, treat 
		// changes in the keyboard modifiers as changes
		// in the mouse buttons.
		osx.kbuttons = 0;
		if(mod & optionKey)
			osx.kbuttons |= 2;
		if(mod & cmdKey)
			osx.kbuttons |= 4;
		mousetrack(osx.xy.x, osx.xy.y, osx.buttons|osx.kbuttons, msec());
		break;
	}
	return noErr;
}

static void
eresized(int new)
{
	Memimage *m;
	OSXRect or;
	ulong chan;
	Rectangle r;
	int bpl;
	CGDataProviderRef provider;
	CGImageRef image;
	CGColorSpaceRef cspace;

	GetWindowBounds(osx.window, kWindowContentRgn, &or);
	r = Rect(or.left, or.top, or.right, or.bottom);
	if(Dx(r) == Dx(osx.screenr) && Dy(r) == Dy(osx.screenr) && !new){
		// No need to make new image.
		osx.screenr = r;
		return;
	}

	chan = XBGR32;
	m = allocmemimage(Rect(0, 0, Dx(r), Dy(r)), chan);
	if(m == nil)
		panic("allocmemimage: %r");
	if(m->data == nil)
		panic("m->data == nil");
	bpl = bytesperline(r, 32);
	provider = CGDataProviderCreateWithData(0,
		m->data->bdata, Dy(r)*bpl, 0);
	//cspace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
	cspace = CGColorSpaceCreateDeviceRGB();
	image = CGImageCreate(Dx(r), Dy(r), 8, 32, bpl,
		cspace,
		kCGImageAlphaNoneSkipLast,
		provider, 0, 0, kCGRenderingIntentDefault);
	CGColorSpaceRelease(cspace);
	CGDataProviderRelease(provider);	// CGImageCreate did incref
	
	mouserect = m->r;
	if(new){
		mouseresized = 1;
		mousetrack(osx.xy.x, osx.xy.y, osx.buttons|osx.kbuttons, msec());
	}
//	termreplacescreenimage(m);
	_drawreplacescreenimage(m);	// frees old osx.screenimage if any
	if(osx.image)
		CGImageRelease(osx.image);
	osx.image = image;
	osx.screenimage = m;
	osx.screenr = r;
	
	if(new){
		qlock(&osx.flushlock);
		QDEndCGContext(GetWindowPort(osx.window), &osx.windowctx);
		osx.windowctx = nil;
		qunlock(&osx.flushlock);
	}
}

void
flushproc(void *v)
{
	for(;;){
		if(osx.needflush && osx.windowctx && canqlock(&osx.flushlock)){
			if(osx.windowctx){
				CGContextFlush(osx.windowctx);
				osx.needflush = 0;
			}
			qunlock(&osx.flushlock);
		}
		usleep(33333);
	}
}

void
_flushmemscreen(Rectangle r)
{
	CGRect cgr;
	CGImageRef subimg;

	qlock(&osx.flushlock);
	if(osx.windowctx == nil){
		QDBeginCGContext(GetWindowPort(osx.window), &osx.windowctx);
		if(!osx.flushing) {
			proccreate(flushproc, nil, 256*1024);
			osx.flushing = 1;
		}
	}
	
	cgr.origin.x = r.min.x;
	cgr.origin.y = r.min.y;
	cgr.size.width = Dx(r);
	cgr.size.height = Dy(r);
	subimg = CGImageCreateWithImageInRect(osx.image, cgr);
	cgr.origin.y = Dy(osx.screenr) - r.max.y; // XXX how does this make any sense?
	CGContextDrawImage(osx.windowctx, cgr, subimg);
	osx.needflush = 1;
	qunlock(&osx.flushlock);
	CGImageRelease(subimg);
}

void
activated(int active)
{
#ifdef MULTITOUCH
	int i;
	if(active) {
		for(i = 0; i<[osx.devicelist count]; i++) { //iterate available devices 
			MTDeviceStart([osx.devicelist objectAtIndex:i], 0); //start sending events 
		} 
	} else {
		osx.xy.x = -10000;
		for(i = 0; i<[osx.devicelist count]; i++) { //iterate available devices 
			MTDeviceStop([osx.devicelist objectAtIndex:i]); //stop sending events 
		} 
		for(i = 0; i<kNTracks; ++i) {
			tracks[i].id = -1;
		}
	}
#endif
	osx.active = active;
}

void
fullscreen(int wascmd)
{
	static OSXRect oldrect;
	GDHandle device;
	OSXRect dr;

	if(!wascmd)
		return;
	
	if(!osx.isfullscreen){
		GetWindowGreatestAreaDevice(osx.window,
			kWindowTitleBarRgn, &device, nil);
		dr = (*device)->gdRect;
		if(dr.top == 0 && dr.left == 0)
			HideMenuBar();
		GetWindowBounds(osx.window, kWindowContentRgn, &oldrect);
		ChangeWindowAttributes(osx.window,
			kWindowNoTitleBarAttribute,
			kWindowResizableAttribute);
		MoveWindow(osx.window, 0, 0, 1);
		MoveWindow(osx.window, dr.left, dr.top, 0);
		SizeWindow(osx.window,
			dr.right - dr.left,
			dr.bottom - dr.top, 0);
		osx.isfullscreen = 1;
	}else{
		ShowMenuBar();
		ChangeWindowAttributes(osx.window,
			kWindowResizableAttribute,
			kWindowNoTitleBarAttribute);
		SizeWindow(osx.window,
			oldrect.right - oldrect.left,
			oldrect.bottom - oldrect.top, 0);
		MoveWindow(osx.window, oldrect.left, oldrect.top, 0);
		osx.isfullscreen = 0;
	}
	eresized(1);
}
		
void
setmouse(Point p)
{
	CGPoint cgp;
	
	cgp.x = p.x + osx.screenr.min.x;
	cgp.y = p.y + osx.screenr.min.y;
	CGWarpMouseCursorPosition(cgp);
	osx.xy = p;
}

void
setcursor(Cursor *c)
{
	OSXCursor oc;
	int i;

	if(c == nil){
		InitCursor();
		return;
	}
	
	// SetCursor is deprecated, but what replaces it?
	for(i=0; i<16; i++){
		oc.data[i] = ((ushort*)c->set)[i];
		oc.mask[i] = oc.data[i] | ((ushort*)c->clr)[i];
	}
	oc.hotSpot.h = - c->offset.x;
	oc.hotSpot.v = - c->offset.y;
	SetCursor(&oc);
}

void
getcolor(ulong i, ulong *r, ulong *g, ulong *b)
{
	ulong v;
	
	v = 0;
	*r = (v>>16)&0xFF;
	*g = (v>>8)&0xFF;
	*b = v&0xFF;
}

int
setcolor(ulong i, ulong r, ulong g, ulong b)
{
	/* no-op */
	return 0;
}


int
hwdraw(Memdrawparam *p)
{
	return 0;
}

struct {
	QLock lk;
	char buf[SnarfSize];
	Rune rbuf[SnarfSize];
	PasteboardRef apple;
} clip;

char*
getsnarf(void)
{
	char *s;
	CFArrayRef flavors;
	CFDataRef data;
	CFIndex nflavor, ndata, j;
	CFStringRef type;
	ItemCount nitem;
	PasteboardItemID id;
	PasteboardSyncFlags flags;
	UInt32 i;
	u16int *u;
	Fmt fmt;
	Rune r;

/*	fprint(2, "applegetsnarf\n"); */
	qlock(&clip.lk);
	clip.apple = osx.snarf;
	if(clip.apple == nil){
		if(PasteboardCreate(kPasteboardClipboard, &clip.apple) != noErr){
			fprint(2, "apple pasteboard create failed\n");
			qunlock(&clip.lk);
			return nil;
		}
	}
	flags = PasteboardSynchronize(clip.apple);
	if(flags&kPasteboardClientIsOwner){
		s = strdup(clip.buf);
		qunlock(&clip.lk);
		return s;
	}
	if(PasteboardGetItemCount(clip.apple, &nitem) != noErr){
		fprint(2, "apple pasteboard get item count failed\n");
		qunlock(&clip.lk);
		return nil;
	}
	for(i=1; i<=nitem; i++){
		if(PasteboardGetItemIdentifier(clip.apple, i, &id) != noErr)
			continue;
		if(PasteboardCopyItemFlavors(clip.apple, id, &flavors) != noErr)
			continue;
		nflavor = CFArrayGetCount(flavors);
		for(j=0; j<nflavor; j++){
			type = (CFStringRef)CFArrayGetValueAtIndex(flavors, j);
			if(!UTTypeConformsTo(type, CFSTR("public.utf16-plain-text")))
				continue;
			if(PasteboardCopyItemFlavorData(clip.apple, id, type, &data) != noErr)
				continue;
			qunlock(&clip.lk);
			ndata = CFDataGetLength(data)/2;
			u = (u16int*)CFDataGetBytePtr(data);
			fmtstrinit(&fmt);
			// decode utf-16.  what was apple thinking?
			for(i=0; i<ndata; i++) {
				r = u[i];
				if(0xd800 <= r && r < 0xdc00 && i+1 < ndata && 0xdc00 <= u[i+1] && u[i+1] < 0xe000) {
					r = (((r - 0xd800)<<10) |  (u[i+1] - 0xdc00)) + 0x10000;
					i++;
				}
				else if(0xd800 <= r && r < 0xe000)
					r = Runeerror;
				if(r == '\r')
					r = '\n';
				fmtrune(&fmt, r);
			}
			CFRelease(flavors);
			CFRelease(data);
			return fmtstrflush(&fmt);
		}
		CFRelease(flavors);
	}
	qunlock(&clip.lk);
	return nil;		
}

void
putsnarf(char *s)
{
	CFDataRef cfdata;
	PasteboardSyncFlags flags;
	u16int *u, *p;
	Rune r;
	int i;

/*	fprint(2, "appleputsnarf\n"); */

	if(strlen(s) >= SnarfSize)
		return;
	qlock(&clip.lk);
	strcpy(clip.buf, s);
	runesnprint(clip.rbuf, nelem(clip.rbuf), "%s", s);
	clip.apple = osx.snarf;
	if(PasteboardClear(clip.apple) != noErr){
		fprint(2, "apple pasteboard clear failed\n");
		qunlock(&clip.lk);
		return;
	}
	flags = PasteboardSynchronize(clip.apple);
	if((flags&kPasteboardModified) || !(flags&kPasteboardClientIsOwner)){
		fprint(2, "apple pasteboard cannot assert ownership\n");
		qunlock(&clip.lk);
		return;
	}
	u = malloc(runestrlen(clip.rbuf)*4);
	p = u;
	for(i=0; clip.rbuf[i]; i++) {
		r = clip.rbuf[i];
		// convert to utf-16
		if(0xd800 <= r && r < 0xe000)
			r = Runeerror;
		if(r >= 0x10000) {
			r -= 0x10000;
			*p++ = 0xd800 + (r>>10);
			*p++ = 0xdc00 + (r & ((1<<10)-1));
		} else
			*p++ = r;
	}
	cfdata = CFDataCreate(kCFAllocatorDefault, 
		(uchar*)u, (p-u)*2);
	free(u);
	if(cfdata == nil){
		fprint(2, "apple pasteboard cfdatacreate failed\n");
		qunlock(&clip.lk);
		return;
	}
	if(PasteboardPutItemFlavor(clip.apple, (PasteboardItemID)1,
		CFSTR("public.utf16-plain-text"), cfdata, 0) != noErr){
		fprint(2, "apple pasteboard putitem failed\n");
		CFRelease(cfdata);
		qunlock(&clip.lk);
		return;
	}
	CFRelease(cfdata);
	qunlock(&clip.lk);
}

void
setlabel(char *label)
{
	CFStringRef cs;

	cs = CFStringCreateWithBytes(nil, (uchar*)label, strlen(label), kCFStringEncodingUTF8, false);
	SetWindowTitleWithCFString(osx.window, cs);
	CFRelease(cs);
}

void
kicklabel(char *label)
{
	char *p;
	EventRef e;

	p = strdup(label);
	if(p == nil)
		return;
	qlock(&osx.labellock);
	free(osx.label);
	osx.label = p;
	qunlock(&osx.labellock);
	
	CreateEvent(nil, 'P9PE', P9PEventLabelUpdate, 0, kEventAttributeUserEvent, &e);
	PostEventToQueue(GetMainEventQueue(), e, kEventPriorityStandard);
	
}

static void
seticon(void)
{
	CGImageRef im;
	CGDataProviderRef d;

	d = CGDataProviderCreateWithData(nil, glenda_png, sizeof glenda_png, nil);
	im = CGImageCreateWithPNGDataProvider(d, nil, true, kCGRenderingIntentDefault);
	if(im)
		SetApplicationDockTileImage(im);
	CGImageRelease(im);
	CGDataProviderRelease(d);
}