/* -- macosxCGS.c -- */

/*
 * We need to keep this separate from nearly everything else, e.g. rfb.h
 * and the other stuff, otherwise it does not work properly, mouse drags
 * will not work!!
 */

#if (defined(__MACH__) && defined(__APPLE__))

#include <ApplicationServices/ApplicationServices.h>
#include <Cocoa/Cocoa.h>
#include <Carbon/Carbon.h>

extern CGDirectDisplayID displayID;

void macosxCGS_get_all_windows(void);
int macosxCGS_get_qlook(int);
void macosxGCS_set_pasteboard(char *str, int len);

typedef CGError       CGSError;
typedef long          CGSWindowCount;
typedef void *        CGSConnectionID;
typedef int           CGSWindowID;
typedef CGSWindowID*  CGSWindowIDList;
typedef CGWindowLevel CGSWindowLevel;
typedef NSRect        CGSRect;

extern CGSConnectionID _CGSDefaultConnection ();

extern CGSError CGSGetOnScreenWindowList (CGSConnectionID cid,
    CGSConnectionID owner, CGSWindowCount listCapacity,
    CGSWindowIDList list, CGSWindowCount *listCount);

extern CGSError CGSGetWindowList (CGSConnectionID cid,
    CGSConnectionID owner, CGSWindowCount listCapacity,
    CGSWindowIDList list, CGSWindowCount *listCount);

extern CGSError CGSGetScreenRectForWindow (CGSConnectionID cid,
    CGSWindowID wid, CGSRect *rect);

extern CGWindowLevel CGSGetWindowLevel (CGSConnectionID cid,
    CGSWindowID wid, CGSWindowLevel *level);

typedef enum _CGSWindowOrderingMode {
    kCGSOrderAbove                =  1, /* Window is ordered above target. */
    kCGSOrderBelow                = -1, /* Window is ordered below target. */
    kCGSOrderOut                  =  0  /* Window is removed from the on-screen window list. */
} CGSWindowOrderingMode;

extern OSStatus CGSOrderWindow(const CGSConnectionID cid,
    const CGSWindowID wid, CGSWindowOrderingMode place, CGSWindowID relativeToWindowID);

static CGSConnectionID cid = NULL;

int macwinmax = 0; 
typedef struct windat {
	int win;
	int x, y;
	int width, height;
	int level;
	int mapped;
	int clipped;
	int ncache_only;
} windat_t;

extern int ncache;

#define MAXWINDAT 4096
windat_t macwins[MAXWINDAT]; 
static CGSWindowID _wins_all[MAXWINDAT]; 
static CGSWindowID _wins_mapped[MAXWINDAT]; 
static CGSWindowCount _wins_all_cnt, _wins_mapped_cnt; 
static int _wins_int[MAXWINDAT]; 

#define WINHISTNUM 32768
#define WINHISTMAX 4
char whist[WINHISTMAX][WINHISTNUM];
int whist_idx = -1;
int qlook[WINHISTNUM];

char is_exist     = 0x1;
char is_mapped    = 0x2;
char is_clipped   = 0x4;
char is_offscreen = 0x8;

extern double dnow(void);
extern double dnowx(void);

extern int dpy_x, dpy_y;
extern int macosx_icon_anim_time;

extern void macosx_add_mapnotify(int, int, int);
extern void macosx_add_create(int, int);
extern void macosx_add_destroy(int, int);
extern void macosx_add_visnotify(int, int, int);

int CGS_levelmax;
int CGS_levels[16];

int macosxCGS_get_qlook(int w) {
	if (w >= WINHISTNUM) {
		return -1;
	}
	return qlook[w];
}

int macosxCGS_find_index(int w) {
	static int last_index = -1;
	int idx;

	if (last_index >= 0) {
		if (macwins[last_index].win == w) {
			return last_index;
		}
	}

	idx = macosxCGS_get_qlook(w);
	if (idx >= 0) {
		if (macwins[idx].win == w) {
			last_index = idx;
			return idx;
		}
	}

	for (idx=0; idx < macwinmax; idx++) {
		if (macwins[idx].win == w) {
			last_index = idx;
			return idx;
		}
	}
	return -1;
}

int macosxCGS_follow_animation_win(int win, int idx, int grow) {
	double t = dnow();
	int diffs = 0;
	int x, y, w, h;
	int xp, yp, wp, hp;
	CGSRect rect;
	CGSError err; 

	int reps = 0;

	if (cid == NULL) {
		cid = _CGSDefaultConnection();
		if (cid == NULL) {
			return 0;
		}
	}

	if (idx < 0) {
		idx = macosxCGS_find_index(win); 
	}
	if (idx < 0) {
		return 0;
	}

	while (dnow() < t + 0.001 * macosx_icon_anim_time)  {
		err = CGSGetScreenRectForWindow(cid, win, &rect);
		if (err != 0) {
			break;
		}
		x = (int) rect.origin.x;
		y = (int) rect.origin.y;
		w = (int) rect.size.width;
		h = (int) rect.size.height;

		if (grow) {
			macwins[idx].x      = x;
			macwins[idx].y      = y;
			macwins[idx].width  = w;
			macwins[idx].height = h;
		}
	
		fprintf(stderr, " chase: %03dx%03d+%03d+%03d  %d\n", w, h, x, y, win);
		if (x == xp && y == yp && w == wp && h == hp)  {
			reps++;
			if (reps >= 2) {
				break;
			}
		} else {
			diffs++;
			reps = 0;
		}
		xp = x;
		yp = y;
		wp = w;
		hp = h;
		usleep(50 * 1000);
	}
	if (diffs >= 2) {
		return 1;
	} else {
		return 0;
	}
}

extern int macosx_check_clipped(int win, int *list, int n);
extern int macosx_check_offscreen(int win);

static int check_clipped(int win) {
	int i, n = 0, win2;
	for (i = 0; i < (int) _wins_mapped_cnt; i++) {
		win2 = (int) _wins_mapped[i];
		if (win2 == win) {
			break;
		}
		_wins_int[n++] = win2;
	}
	return macosx_check_clipped(win, _wins_int, n);
}

static int check_offscreen(int win) {
	return macosx_check_offscreen(win);
}

extern int macosx_ncache_macmenu;

void macosxCGS_get_all_windows(void) {
	static double last = 0.0;
	static int totcnt = 0;
	double dt = 0.0, now = dnow();
	int i, db = 0, whist_prv, maxwin = 0, whist_skip = 0;
	CGSWindowCount cap = (CGSWindowCount) MAXWINDAT;
	CGSWindowCount cnt = 0;
	CGSError err; 

	CGS_levelmax = 0;
	CGS_levels[CGS_levelmax++] = (int) kCGDraggingWindowLevel;	/* 500 ? */
	if (0) CGS_levels[CGS_levelmax++] = (int) kCGHelpWindowLevel;		/* 102 ? */
	if (macosx_ncache_macmenu) CGS_levels[CGS_levelmax++] = (int) kCGPopUpMenuWindowLevel;	/* 101 pulldown menu */
	CGS_levels[CGS_levelmax++] = (int) kCGMainMenuWindowLevelKey;	/*  24 ? */
	CGS_levels[CGS_levelmax++] = (int) kCGModalPanelWindowLevel;	/*   8 open dialog box */
	CGS_levels[CGS_levelmax++] = (int) kCGFloatingWindowLevel;	/*   3 ? */
	CGS_levels[CGS_levelmax++] = (int) kCGNormalWindowLevel;	/*   0 regular window */

	if (cid == NULL) {
		cid = _CGSDefaultConnection();
		if (cid == NULL) {
			return;
		}
	}

	if (dt > 0.0 && now < last + dt) {
		return;
	}

	last = now;

	macwinmax = 0; 

	totcnt++;

	if (ncache > 0) {
		whist_prv = whist_idx++;
		if (whist_prv < 0) {
			whist_skip = 1;
			whist_prv = 0;
		}
		whist_idx = whist_idx % WINHISTMAX;
		for (i=0; i < WINHISTNUM; i++) {
			whist[whist_idx][i] = 0;
			qlook[i] = -1;
		}
	}

	err = CGSGetWindowList(cid, NULL, cap, _wins_all, &_wins_all_cnt);

if (db) fprintf(stderr, "cnt: %d err: %d\n", _wins_all_cnt, err);

	if (err != 0) {
		return;
	}
	
	for (i=0; i < (int) _wins_all_cnt; i++) {
		CGSRect rect;
		CGSWindowLevel level;
		int j, keepit = 0;
		err = CGSGetScreenRectForWindow(cid, _wins_all[i], &rect);
		if (err != 0) {
			continue;
		}
		if (rect.origin.x == 0 && rect.origin.y == 0) {
			if (rect.size.width == dpy_x) {
				if (rect.size.height == dpy_y) {
					continue;
				}
			}
		}
		err = CGSGetWindowLevel(cid, _wins_all[i], &level);
		if (err != 0) {
			continue;
		}
		for (j=0; j<CGS_levelmax; j++) {
			if ((int) level == CGS_levels[j]) {
				keepit = 1;
				break;
			}
		}
		if (! keepit) {
			continue;
		}

		macwins[macwinmax].level  = (int) level;
		macwins[macwinmax].win    = (int) _wins_all[i];
		macwins[macwinmax].x      = (int) rect.origin.x;
		macwins[macwinmax].y      = (int) rect.origin.y;
		macwins[macwinmax].width  = (int) rect.size.width;
		macwins[macwinmax].height = (int) rect.size.height;
		macwins[macwinmax].mapped = 0;
		macwins[macwinmax].clipped = 0;
		macwins[macwinmax].ncache_only = 0;
		if (level == kCGPopUpMenuWindowLevel) {
			macwins[macwinmax].ncache_only = 1;
		}

if (0 || db) fprintf(stderr, "i=%03d ID: %06d  x: %03d  y: %03d  w: %03d h: %03d level: %d\n", i, _wins_all[i],
    (int) rect.origin.x, (int) rect.origin.y,(int) rect.size.width, (int) rect.size.height, (int) level);

		if (macwins[macwinmax].win < WINHISTNUM) {
			qlook[macwins[macwinmax].win] = macwinmax;
			if (macwins[macwinmax].win > maxwin) {
				maxwin = macwins[macwinmax].win;
			}
		}

		macwinmax++;
	}

	err = CGSGetOnScreenWindowList(cid, NULL, cap, _wins_mapped, &_wins_mapped_cnt);

if (db) fprintf(stderr, "cnt: %d err: %d\n", _wins_mapped_cnt, err);

	if (err != 0) {
		return;
	}
	
	for (i=0; i < (int) _wins_mapped_cnt; i++) {
		int j, idx = -1;
		int win = (int) _wins_mapped[i];

		if (0 <= win && win < WINHISTNUM) {
			j = qlook[win];
			if (j >= 0 && macwins[j].win == win) {
				idx = j; 
			}
		}
		if (idx < 0) {
			for (j=0; j < macwinmax; j++) {
				if (macwins[j].win == win) {
					idx = j; 
					break;
				}
			}
		}
		if (idx >= 0) {
			macwins[idx].mapped = 1;
		}
	}

	if (ncache > 0) {
		int nv= 0, NBMAX = 64;
		int nv_win[64];
		int nv_lvl[64];
		int nv_vis[64];

		for (i=0; i < macwinmax; i++) {
			int win = macwins[i].win;
			char prev, curr;

			if (win >= WINHISTNUM) {
				continue;
			}

			whist[whist_idx][win] |= is_exist;
			if (macwins[i].mapped) {
				whist[whist_idx][win] |= is_mapped;
				if (check_clipped(win)) {
					whist[whist_idx][win] |= is_clipped;
					macwins[i].clipped = 1;
				}
				if (check_offscreen(win)) {
					whist[whist_idx][win] |= is_offscreen;
				}
			} else {
				whist[whist_idx][win] |= is_offscreen;
			}

			curr = whist[whist_idx][win];
			prev = whist[whist_prv][win];

			if (whist_skip) {
				;
			} else if ( !(prev & is_mapped) && (curr & is_mapped)) {
				/* MapNotify */
				fprintf(stderr, "MapNotify:   %d/%d  %d               %.4f %d tot=%d\n", prev, curr, win, dnowx(), totcnt); 
				macosx_add_mapnotify(win, macwins[i].level, 1);
				//macosxCGS_follow_animation_win(win, i, 1);

			} else if ( !(curr & is_mapped) && (prev & is_mapped)) {
				/* UnmapNotify */
				fprintf(stderr, "UnmapNotify: %d/%d  %d               %.4f A tot=%d\n", prev, curr, win, dnowx(), totcnt); 
				macosx_add_mapnotify(win, macwins[i].level, 0);
			} else if ( !(prev & is_exist) && (curr & is_exist)) {
				/* CreateNotify */
				fprintf(stderr, "CreateNotify:%d/%d  %d               %.4f whist: %d/%d 0x%x tot=%d\n", prev, curr, win, dnowx(), whist_prv, whist_idx, win, totcnt); 
				macosx_add_create(win, macwins[i].level);
				if (curr & is_mapped) {
					fprintf(stderr, "MapNotify:   %d/%d  %d               %.4f tot=%d\n", prev, curr, win, dnowx(), totcnt); 
					macosx_add_mapnotify(win, macwins[i].level, 1);
				}
			}
			if (whist_skip) {
				;
			} else if (nv >= NBMAX) {
				;
			} else if (!(curr & is_mapped)) {
				;
			} else if (!(prev & is_mapped)) {
				if (1) {
					;
				} else if (curr & is_clipped) {
					fprintf(stderr, "VisibNotify: %d/%d  %d               OBS tot=%d\n", prev, curr, win, totcnt); 
					nv_win[nv] = win;
					nv_lvl[nv] = macwins[i].level;
					nv_vis[nv++] = 1;
				} else {
					fprintf(stderr, "VisibNotify: %d/%d  %d               UNOBS tot=%d\n", prev, curr, win, totcnt); 
					nv_win[nv] = win;
					nv_lvl[nv] = macwins[i].level;
					nv_vis[nv++] = 0;
				}
			} else {
				if        ( !(prev & is_clipped) &&  (curr & is_clipped) ) {
					fprintf(stderr, "VisibNotify: %d/%d  %d               OBS tot=%d\n", prev, curr, win, totcnt); 
					nv_win[nv] = win;
					nv_lvl[nv] = macwins[i].level;
					nv_vis[nv++] = 1;
				} else if (  (prev & is_clipped) && !(curr & is_clipped) ) {
					fprintf(stderr, "VisibNotify: %d/%d  %d               UNOBS tot=%d\n", prev, curr, win, totcnt); 
					nv_win[nv] = win;
					nv_lvl[nv] = macwins[i].level;
					nv_vis[nv++] = 0;
				}
			}
		}
		for (i=0; i < maxwin; i++) {
			char prev, curr;
			int win = i;
			int q = qlook[i];
			int lvl = 0;

			if (whist_skip) {
				break;
			}

			if (q >= 0) {
				lvl = macwins[q].level;	
			}
			curr = whist[whist_idx][win];
			prev = whist[whist_prv][win];
			if (!(curr & is_exist) && (prev & is_exist)) {
				if (prev & is_mapped) {
					fprintf(stderr, "UnmapNotify: %d/%d  %d               %.4f B tot=%d\n", prev, curr, win, dnowx(), totcnt); 
					macosx_add_mapnotify(win, lvl, 0);
				}
				/* DestroyNotify */
				fprintf(stderr, "DestroNotify:%d/%d  %d               %.4f tot=%d\n", prev, curr, win, dnowx(), totcnt); 
				macosx_add_destroy(win, lvl);
			}
		}
		if (nv) {
			int k;
			for (k = 0; k < nv; k++) {
				macosx_add_visnotify(nv_win[k], nv_lvl[k], nv_vis[k]);
			}
		}
	}
}

#if 1
NSLock *pblock = nil;
NSString *pbstr = nil;
NSString *cuttext = nil;

int pbcnt = -1;
NSStringEncoding pbenc = NSWindowsCP1252StringEncoding;

void macosxGCS_initpb(void) {
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
	pblock = [[NSLock alloc] init];
	if (![NSPasteboard generalPasteboard]) {
		fprintf(stderr, "macosxGCS_initpb: pasteboard inaccessible.\n");
		pbcnt = 0;
		pbstr = [[NSString alloc] initWithString:@"\e<PASTEBOARD INACCESSIBLE>\e"]; 
	}
	[pool release];
}

void macosxGCS_set_pasteboard(char *str, int len) {
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
	if (pbcnt != 0) {
		[pblock lock];
		[cuttext release];
		cuttext = [[NSString alloc] initWithData:[NSData dataWithBytes:str length:len] encoding: pbenc];
		if ([[NSPasteboard generalPasteboard] declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil]) {
			NS_DURING
				[[NSPasteboard generalPasteboard] setString:cuttext forType:NSStringPboardType];
			NS_HANDLER
				fprintf(stderr, "macosxGCS_set_pasteboard: problem writing to pasteboard\n");
			NS_ENDHANDLER
		} else {
			fprintf(stderr, "macosxGCS_set_pasteboard: problem writing to pasteboard\n");
		}
		[cuttext release];
		cuttext = nil;
		[pblock unlock];
	}
	[pool release];
}

extern void macosx_send_sel(char *, int);

void macosxGCS_poll_pb(void) {

	static double dlast = 0.0;
	double now = dnow();

	if (now < dlast + 0.2) {
		return;
	}
	dlast = now;

	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
	[pblock lock];
	if (pbcnt != [[NSPasteboard generalPasteboard] changeCount]) {
		pbcnt = [[NSPasteboard generalPasteboard] changeCount];
		[pbstr release];
		pbstr = nil;
		if ([[NSPasteboard generalPasteboard] availableTypeFromArray:[NSArray arrayWithObject:NSStringPboardType]]) {
			pbstr = [[[NSPasteboard generalPasteboard] stringForType:NSStringPboardType] copy];
			if (pbstr) {
				NSData *str = [pbstr dataUsingEncoding:pbenc allowLossyConversion:YES];
				if ([str length]) {
					macosx_send_sel((char *) [str bytes], [str length]);
				}
			}
		}
	}
	[pblock unlock];
	[pool release];
}
#endif

#endif	/* __APPLE__ */