/*
 * magnify - a X utility for magnifying screen image
 *
 * 2000/5/21  Alfredo K. Kojima
 *
 * This program is in the Public Domain.
 */
#include <config.h>

#include <X11/Xproto.h>

#include <WINGs/WINGs.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

/*
 * TODO:
 * - lens that shows where it's magnifying
 *
 *
 */

int refreshrate = 200;

typedef struct {
	Drawable d;
	XRectangle *rects;
	int rectP;
	unsigned long lastpixel;
	unsigned long *buffer;
	int width, height;
	int rwidth, rheight;	/* size of window in real pixels */
	int magfactor;
	int refreshrate;

	WMWindow *win;
	WMLabel *label;
	WMPixmap *pixmap;

	WMWindow *dlg;

	WMSlider *speed;
	WMSlider *magnify;
	WMButton *okB;
	WMButton *cancelB;
	WMButton *newB;

	int x, y;
	Bool frozen;
	Bool firstDraw;
	Bool markPointerHotspot;

	WMHandlerID tid;
} BufferData;

static BufferData *newWindow(int magfactor);

int windowCount = 0;

int rectBufferSize = 32;
Display *dpy, *vdpy;
WMScreen *scr;
unsigned int black;
WMColor *cursorColor1;
WMColor *cursorColor2;


static BufferData *makeBufferData(WMWindow * win, WMLabel * label, int width, int height, int magfactor)
{
	BufferData *data;

	data = wmalloc(sizeof(BufferData));

	data->rwidth = width;
	data->rheight = height;

	data->refreshrate = refreshrate;

	data->firstDraw = True;

	data->magfactor = magfactor;

	data->rects = wmalloc(sizeof(XRectangle) * rectBufferSize);
	data->rectP = 0;

	data->win = win;
	data->label = label;

	data->pixmap = WMCreatePixmap(scr, width, height, WMScreenDepth(scr), False);
	WMSetLabelImage(data->label, data->pixmap);

	data->d = WMGetPixmapXID(data->pixmap);

	data->frozen = False;

	width /= magfactor;
	height /= magfactor;
	data->buffer = wmalloc(sizeof(unsigned long) * width * height);
	data->width = width;
	data->height = height;

	return data;
}

static void resizeBufferData(BufferData * data, int width, int height, int magfactor)
{
	int w = width / magfactor;
	int h = height / magfactor;

	data->rwidth = width;
	data->rheight = height;
	data->firstDraw = True;
	data->magfactor = magfactor;
	data->buffer = wrealloc(data->buffer, sizeof(unsigned long) * w * h);
	data->width = w;
	data->height = h;
	memset(data->buffer, 0, w * h * sizeof(unsigned long));

	WMResizeWidget(data->label, width, height);

	WMReleasePixmap(data->pixmap);
	data->pixmap = WMCreatePixmap(scr, width, height, WMScreenDepth(scr), False);
	WMSetLabelImage(data->label, data->pixmap);

	data->d = WMGetPixmapXID(data->pixmap);
}

static int drawpoint(BufferData * data, unsigned long pixel, int x, int y)
{
	static GC gc = NULL;
	Bool flush = (x < 0);

	if (!flush) {
		if (data->buffer[x + data->width * y] == pixel && !data->firstDraw)
			return 0;

		data->buffer[x + data->width * y] = pixel;
	}
	if (gc == NULL) {
		gc = XCreateGC(dpy, DefaultRootWindow(dpy), 0, NULL);
	}

	if (!flush && data->lastpixel == pixel && data->rectP < rectBufferSize) {
		data->rects[data->rectP].x = x * data->magfactor;
		data->rects[data->rectP].y = y * data->magfactor;
		data->rects[data->rectP].width = data->magfactor;
		data->rects[data->rectP].height = data->magfactor;
		data->rectP++;

		return 0;
	}
	XSetForeground(dpy, gc, data->lastpixel);
	XFillRectangles(dpy, data->d, gc, data->rects, data->rectP);
	data->rectP = 0;
	data->rects[data->rectP].x = x * data->magfactor;
	data->rects[data->rectP].y = y * data->magfactor;
	data->rects[data->rectP].width = data->magfactor;
	data->rects[data->rectP].height = data->magfactor;
	data->rectP++;

	data->lastpixel = pixel;

	return 1;
}

static inline unsigned long getpix(XImage * image, int x, int y, int xoffs, int yoffs)
{
	if (x < xoffs || y < yoffs || x >= xoffs + image->width || y >= yoffs + image->height) {
		return black;
	}
	return XGetPixel(image, x - xoffs, y - yoffs);
}

static void updateImage(BufferData * data, int rx, int ry)
{
	int gx, gy, gw, gh;
	int x, y;
	int xoffs, yoffs;
	int changedPixels = 0;
	XImage *image;

	gw = data->width;
	gh = data->height;

	gx = rx - gw / 2;
	gy = ry - gh / 2;

	xoffs = yoffs = 0;
	if (gx < 0) {
		xoffs = abs(gx);
		gw += gx;
		gx = 0;
	}
	if (gx + gw >= WidthOfScreen(DefaultScreenOfDisplay(vdpy))) {
		gw = WidthOfScreen(DefaultScreenOfDisplay(vdpy)) - gx;
	}
	if (gy < 0) {
		yoffs = abs(gy);
		gh += gy;
		gy = 0;
	}
	if (gy + gh >= HeightOfScreen(DefaultScreenOfDisplay(vdpy))) {
		gh = HeightOfScreen(DefaultScreenOfDisplay(vdpy)) - gy;
	}

	image = XGetImage(vdpy, DefaultRootWindow(vdpy), gx, gy, gw, gh, AllPlanes, ZPixmap);

	for (y = 0; y < data->height; y++) {
		for (x = 0; x < data->width; x++) {
			unsigned long pixel;

			pixel = getpix(image, x, y, xoffs, yoffs);

			if (drawpoint(data, pixel, x, y))
				changedPixels++;
		}
	}
	/* flush the point cache */
	drawpoint(data, 0, -1, -1);

	XDestroyImage(image);

	if (data->markPointerHotspot && !data->frozen) {
		XRectangle rects[4];

		rects[0].x = (data->width / 2 - 3) * data->magfactor;
		rects[0].y = (data->height / 2) * data->magfactor;
		rects[0].width = 2 * data->magfactor;
		rects[0].height = data->magfactor;

		rects[1].x = (data->width / 2 + 2) * data->magfactor;
		rects[1].y = (data->height / 2) * data->magfactor;
		rects[1].width = 2 * data->magfactor;
		rects[1].height = data->magfactor;

		XFillRectangles(dpy, data->d, WMColorGC(cursorColor1), rects, 2);

		rects[2].y = (data->height / 2 - 3) * data->magfactor;
		rects[2].x = (data->width / 2) * data->magfactor;
		rects[2].height = 2 * data->magfactor;
		rects[2].width = data->magfactor;

		rects[3].y = (data->height / 2 + 2) * data->magfactor;
		rects[3].x = (data->width / 2) * data->magfactor;
		rects[3].height = 2 * data->magfactor;
		rects[3].width = data->magfactor;

		XFillRectangles(dpy, data->d, WMColorGC(cursorColor2), rects + 2, 2);
	}

	if (changedPixels > 0) {
		WMRedisplayWidget(data->label);
	}

	data->firstDraw = False;
}

static void update(void *d)
{
	BufferData *data = (BufferData *) d;
	Window win;
	int rx, ry;
	int bla;
	unsigned ubla;

	if (data->frozen) {
		rx = data->x;
		ry = data->y;
	} else {
		XQueryPointer(dpy, DefaultRootWindow(dpy), &win, &win, &rx, &ry, &bla, &bla, &ubla);
	}
	updateImage(data, rx, ry);

	data->tid = WMAddTimerHandler(data->refreshrate, update, data);
}

static void resizedWindow(void *d, WMNotification * notif)
{
	BufferData *data = (BufferData *) d;
	WMView *view = (WMView *) WMGetNotificationObject(notif);
	WMSize size;

	size = WMGetViewSize(view);

	resizeBufferData(data, size.width, size.height, data->magfactor);
}

static void closeWindow(WMWidget * w, void *d)
{
	BufferData *data = (BufferData *) d;

	windowCount--;
	if (windowCount == 0) {
		WMReleaseApplication();
		exit(0);
	} else {
		WMDeleteTimerHandler(data->tid);
		WMDestroyWidget(w);
		wfree(data->buffer);
		wfree(data->rects);
		WMReleasePixmap(data->pixmap);
		wfree(data);
	}
}

static void keyHandler(XEvent * event, void *d)
{
	BufferData *data = (BufferData *) d;
	char buf[32];
	KeySym ks;
	WMView *view = WMWidgetView(data->win);
	WMSize size;

	size = WMGetViewSize(view);

	if (XLookupString(&event->xkey, buf, 31, &ks, NULL) > 0) {
		switch (buf[0]) {
		case 'n':
			newWindow(data->magfactor);
			break;
		case 'm':
			data->markPointerHotspot = !data->markPointerHotspot;
			break;
		case 'f':
		case ' ':
			data->frozen = !data->frozen;
			if (data->frozen) {
				data->x = event->xkey.x_root;
				data->y = event->xkey.y_root;
				sprintf(buf, "[Magnify %ix]", data->magfactor);
			} else {
				sprintf(buf, "Magnify %ix", data->magfactor);
			}
			WMSetWindowTitle(data->win, buf);
			break;
		case '1':
		case '2':
		case '3':
		case '4':
		case '5':
		case '6':
		case '7':
		case '8':
		case '9':
			resizeBufferData(data, size.width, size.height, buf[0] - '0');
			if (data->frozen) {
				sprintf(buf, "[Magnify %ix]", data->magfactor);
			} else {
				sprintf(buf, "Magnify %ix", data->magfactor);
			}
			WMSetWindowTitle(data->win, buf);
			break;
		}
	}
}

static BufferData *newWindow(int magfactor)
{
	WMWindow *win;
	WMLabel *label;
	BufferData *data;
	char buf[32];

	windowCount++;

	win = WMCreateWindow(scr, "magnify");
	WMResizeWidget(win, 300, 200);
	sprintf(buf, "Magnify %ix", magfactor);
	WMSetWindowTitle(win, buf);
	WMSetViewNotifySizeChanges(WMWidgetView(win), True);

	label = WMCreateLabel(win);
	WMResizeWidget(label, 300, 200);
	WMMoveWidget(label, 0, 0);
	WMSetLabelRelief(label, WRSunken);
	WMSetLabelImagePosition(label, WIPImageOnly);

	data = makeBufferData(win, label, 300, 200, magfactor);

	WMCreateEventHandler(WMWidgetView(win), KeyReleaseMask, keyHandler, data);

	WMAddNotificationObserver(resizedWindow, data, WMViewSizeDidChangeNotification, WMWidgetView(win));

	WMRealizeWidget(win);

	WMMapSubwidgets(win);
	WMMapWidget(win);

	WMSetWindowCloseAction(win, closeWindow, data);
	data->tid = WMAddTimerHandler(refreshrate, update, data);

	return data;
}

int main(int argc, char **argv)
{
	int i;
	char *display = "";
	char *vdisplay = NULL;
	int magfactor = 2;

	WMInitializeApplication("Magnify", &argc, argv);

	for (i = 1; i < argc; i++) {
		if (strcmp(argv[i], "-display") == 0) {
			i++;
			if (i >= argc)
				goto help;
			display = argv[i];
		} else if (strcmp(argv[i], "-vdisplay") == 0) {
			i++;
			if (i >= argc)
				goto help;
			vdisplay = argv[i];
		} else if (strcmp(argv[i], "-m") == 0) {
			i++;
			if (i >= argc)
				goto help;
			magfactor = atoi(argv[i]);
			if (magfactor < 1 || magfactor > 32) {
				printf("%s:invalid magnification factor ``%s''\n", argv[0], argv[i]);
				exit(1);
			}
		} else if (strcmp(argv[i], "-r") == 0) {
			i++;
			if (i >= argc)
				goto help;
			refreshrate = atoi(argv[i]);
			if (refreshrate < 1) {
				printf("%s:invalid refresh rate ``%s''\n", argv[0], argv[i]);
				exit(1);
			}
		} else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
 help:

			printf("Usage: %s [options]\n", argv[0]);
			puts("Options:");
			puts("  -display <display>	display that should be used");
			puts("  -m <number>		change magnification factor (default 2)");
			puts("  -r <number>		change refresh delay, in milliseconds (default 200)");
			puts("Keys:");
			puts("  1,2,3,4,5,6,7,8,9	change the magnification factor");
			puts("  <space>, f		freeze the 'camera', making it magnify only the current\n"
			     "			position");
			puts("  n			create a new window");
			puts("  m			show/hide the pointer hotspot mark");
			exit(0);
		}
	}

	dpy = XOpenDisplay(display);
	if (!dpy) {
		puts("could not open display");
		exit(1);
	}

	if (vdisplay) {
		vdpy = XOpenDisplay(vdisplay);
		if (!vdpy) {
			puts("could not open display to be viewed");
			exit(1);
		}
	} else {
		vdpy = dpy;
	}

	/* calculate how many rectangles we can send in a trip to the server */
	rectBufferSize = XMaxRequestSize(dpy) - 128;
	rectBufferSize /= sizeof(XRectangle);
	if (rectBufferSize < 1)
		rectBufferSize = 1;

	black = BlackPixel(dpy, DefaultScreen(dpy));

	scr = WMCreateScreen(dpy, 0);

	cursorColor1 = WMCreateNamedColor(scr, "#ff0000", False);
	cursorColor2 = WMCreateNamedColor(scr, "#00ff00", False);

	newWindow(magfactor);

	WMScreenMainLoop(scr);

	return 0;
}
