/*
   pspresent: PostScript presentation tool
   Copyright (C) Matthew Chapman 2001-2003

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#include "pspresent.h"
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <X11/keysym.h>

#ifdef HAVE_LIBXINERAMA
# include <X11/extensions/Xinerama.h>
#endif

Display *display;
Screen *screen;

static unsigned int width, height;
static Window draw_wnd, display_wnd;
static Pixmap draw_pix, current_pix, prev_pix;
static GC gc;

static int num_pages, num_real_pages;
static struct pspage *pages;
static int *real_pages;
static char *page_start;
static char *page_end;

static Bool gs_ready = False;
static Bool skip_overlays_mode = False;
static int current_dir = +1;	/* current direction */
static int current_page = -1;	/* page displayed on screen */
static int prev_page = -1;	/* last page displayed on screen */
static int gs_page = 0;		/* page being rendered by gs */
static int requested_page = 0;	/* page that user wants displayed */
static int warp_page;

static void UpdateWindow(void)
{
	XCopyArea(display, current_pix, display_wnd, gc, 0, 0, width, height, 0, 0);
}

static void RenderPage(int page)
{
	page_start = pages[page].data;
	page_end = pages[page+1].data;
	gs_page = page;
	gs_ready = False;
	GSNextPage();
}

static int GetNextPage(void)
{
	int new_page;

	if (skip_overlays_mode)
	{
		new_page = current_page;
		do {
			new_page += current_dir;
			if ((new_page >= num_pages) || (new_page < 0))
				return -1;
		} while (!pages[new_page].is_last);
	}
	else
	{
		new_page = current_page + current_dir;
		if ((new_page >= num_pages) || (new_page < 0))
			return -1;
	}

	return new_page;
}

static void GotoPage(int page)
{
	int next_page;

	requested_page = page;

	if (!gs_ready)
		return;

	if (page == prev_page)
	{
		/* Still have this page cached */
		XCopyArea(display, prev_pix, draw_pix, gc, 0, 0, width, height, 0, 0);
	}
	else if (page == gs_page)
	{
		/* Excellent - the page that GhostScript just rendered is
		 * the one the user wants */
	}
	else
	{
		RenderPage(page);
		return;
	}

	requested_page = -1;
	prev_page = current_page;
	current_page = page;

	XCopyArea(display, current_pix, prev_pix, gc, 0, 0, width, height, 0, 0);
	XCopyArea(display, draw_pix, current_pix, gc, 0, 0, width, height, 0, 0);
	UpdateWindow();

	/* Prefetch the next page */
	next_page = GetNextPage();
	if (next_page != -1)
		RenderPage(next_page);
}

static void NextPage(int dir, Bool skip_overlays)
{
	int next_page;

	/* Assume user will keep going in that direction */
	current_dir = dir;
	skip_overlays_mode = skip_overlays;

	next_page = GetNextPage();
	if (next_page != -1)
		GotoPage(next_page);
}

static void WarpPage(int page)
{
	current_dir = (page == num_pages-1) ? -1 : +1;
	skip_overlays_mode = False;

	GotoPage(page);
}

static Bool ProcessEvents(void)
{
	XEvent event;
	KeySym key;

	while (XPending(display) > 0)
	{
		XNextEvent(display, &event);
		switch (event.type)
		{
			case ClientMessage: /* message from Ghostscript */
				if (!GSProcessMessage(&event.xclient))
					return False;
				gs_ready = True;
				if (requested_page != -1)
					GotoPage(requested_page);
				break;

			case Expose:
				UpdateWindow();
				break;

			case KeyPress:
				key = XKeycodeToKeysym(display, event.xkey.keycode, 0);

				switch (key)
				{
				case XK_space:
				case XK_Next:
				case XK_KP_Next:
				case XK_Right:
				case XK_KP_Right:
				case XK_Down:
				case XK_KP_Down:
					NextPage(+1, event.xkey.state & ShiftMask);
					break;

				case XK_BackSpace:
				case XK_Prior:
				case XK_KP_Prior:
				case XK_Left:
				case XK_KP_Left:
				case XK_Up:
				case XK_KP_Up:
					NextPage(-1, event.xkey.state & ShiftMask);
					break;

				case XK_Home:
				case XK_KP_Home:
					WarpPage(0);
					break;
				case XK_End:
				case XK_KP_End:
					WarpPage(num_pages-1);
					break;

				case XK_0 ... XK_9:
					warp_page *= 10;
					warp_page += key - XK_0;
					break;
				case XK_KP_0 ... XK_KP_9:
					warp_page *= 10;
					warp_page += key - XK_KP_0;
					break;
				case XK_Return:
				case XK_KP_Enter:
					warp_page--; /* pages start at 0 */
					if ((warp_page >= 0) && (warp_page < num_real_pages))
						WarpPage(real_pages[warp_page]);
					else
						XBell(display, 0);
					warp_page = 0;
					break;

				case XK_Escape:
				case XK_q:
					return False;
				}
				break;

			case ButtonPress:
				switch (event.xbutton.button)
				{
				case Button1:
					NextPage(+1, event.xbutton.state & ShiftMask);
					break;

				case Button3:
					NextPage(-1, event.xbutton.state & ShiftMask);
					break;
				}
				break;

			case ButtonRelease:
				/* We don't exit until the ButtonRelease to prevent another
				 * application getting the ButtonRelease and pasting
				 */
				switch (event.xbutton.button)
				{
				case Button2:
					return False;
				}
				break;
		}
	}

	return True;
}

static void MainLoop(int gs_fd)
{
	fd_set readset;
	fd_set writeset;
	int x_fd = ConnectionNumber(display);
	int max_fd = (gs_fd > x_fd) ? gs_fd : x_fd;

	FD_ZERO(&readset);
	FD_ZERO(&writeset);
	while (True)
	{
		if (!ProcessEvents())
			return;

		FD_SET(x_fd, &readset);
		if (page_start == page_end)
			FD_CLR(gs_fd, &writeset);
		else
			FD_SET(gs_fd, &writeset);

		if (select(max_fd+1, &readset, &writeset, NULL, NULL) == -1)
		{
			perror("select");
			return;
		}

		if (FD_ISSET(gs_fd, &writeset))
			page_start = GSWrite(gs_fd, page_start, page_end);
	}
}

/* try to hide decorations using MWM hints */
static void HideDecorations(Window wnd)
{
	PropMotifWmHints motif_hints;
	Atom atom;

	atom = XInternAtom(display, "_MOTIF_WM_HINTS", False);
	if (!atom)
		return;

	motif_hints.flags = MWM_HINTS_DECORATIONS;
	motif_hints.decorations = 0;
	XChangeProperty(display, wnd, atom, atom, 32, PropModeReplace,
			(unsigned char *) &motif_hints, sizeof(motif_hints)/4);
}

static size_t MapFile(char *filename, char **ptr)
{
	struct stat st;
	size_t size, pagesize;
	int fd;

	fd = open(filename, O_RDONLY);
	if (fd == -1)
		return -1;

	if (fstat(fd, &st) == -1)
	{
		close(fd);
		return -1;
	}

	/* round size up to page size */
	pagesize = getpagesize();
	size = (st.st_size + pagesize-1) & ~(pagesize-1);
	*ptr = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
	if (*ptr == MAP_FAILED)
		size = -1;

	close(fd);
	return size;
}

static void usage(char *program)
{
	fprintf(stderr, "pspresent: PostScript presentation tool\n"
			"Version " VERSION ". Copyright (C) 2001-2006 Matt Chapman.\n\n"
			"Usage: %s [options] psfile\n"
			"   -h: Display this usage message\n"
			"   -o: Do not override window manager\n"
#ifdef HAVE_LIBXINERAMA
			"   -s: Use only head n of a XINERAMA display\n"
#endif
			"   -O: Override orientation (Portrait|Landscape|Upside-Down|Seascape)\n"
			, program);
}

int main(int argc, char *argv[])
{
	XSetWindowAttributes attribs;
	Bool override_redirect = True, force_orientation = False;
	char *filename, *document;
	int gs_fd, depth, c;
	int orientation, arg_orientation;
	int bounds[4];
	size_t size;
	int x, y, head = -1;
#ifdef HAVE_LIBXINERAMA
	XineramaScreenInfo *head_info;
	int heads;
#endif

	while ((c = getopt(argc, argv, "os:O:hv?")) != -1)
	{
		switch (c)
		{
			case 'o':
				override_redirect = False;
				break;
			case 's':
				head = atoi(optarg);
				break;
			case 'O':
				if (!PSGetOrientation(optarg, &arg_orientation))
				{
					fprintf(stderr, "ERROR: orientation should be one of Portrait|Landscape|Upside-Down|Seascape\n");
					return EXIT_FAILURE;
				}
				force_orientation = True;
				break;
			default:
				usage(argv[0]);
				return EXIT_FAILURE;
		}
	}

	if (argc - optind != 1)
	{
		usage(argv[0]);
		return EXIT_FAILURE;
	}

	filename = argv[optind];
	size = MapFile(filename, &document);
	if (size == -1)
	{
		perror(filename);
		return EXIT_FAILURE;
	}

	if (!PSScanDocument(document, document+size, bounds, &orientation, 
			    &num_pages, &pages,
			    &num_real_pages, &real_pages))
	{
		return EXIT_FAILURE;
	}

	if (force_orientation)
		orientation = arg_orientation;

	display = XOpenDisplay(NULL);
	if (display == NULL)
	{
		fprintf(stderr, "ERROR: Failed to open display: %s\n", XDisplayName(NULL));
		return EXIT_FAILURE;
	}

	screen = DefaultScreenOfDisplay(display);
	depth = DefaultDepthOfScreen(screen);

	if (head != -1)
	{
		/* user wants a specific screen head */
#ifdef HAVE_LIBXINERAMA
		head_info = XineramaQueryScreens(display, &heads);
		if (head_info == NULL)
		{
			fprintf(stderr, "ERROR: XINERAMA not available\n");
			return EXIT_FAILURE;
		}

		if ((head > heads) || (heads < 0))
		{
			fprintf(stderr, "ERROR: Head %d requested, but only %d heads available\n", head, heads);
			return EXIT_FAILURE;
		}

		x = (int) head_info[head].x_org;
		y = (int) head_info[head].y_org;
		width = (unsigned int) head_info[head].width;
		height = (unsigned int) head_info[head].height;
#else
		fprintf(stderr, "ERROR: XINERAMA support not compiled in\n");
		return EXIT_FAILURE;
#endif
	}
	else
	{
		x = 0;
		y = 0;
		width = WidthOfScreen(screen);
		height = HeightOfScreen(screen);
	}

	/* create the real window */
	attribs.override_redirect = override_redirect;
        display_wnd = XCreateWindow(display, DefaultRootWindow(display), x, y,
			    width, height, 0, depth, InputOutput,
			    DefaultVisualOfScreen(screen), CWOverrideRedirect, &attribs);

	/* create the drawing window */
        draw_wnd = XCreateWindow(display, DefaultRootWindow(display), 0, 0,
			    width, height, 0, depth, InputOutput,
			    DefaultVisualOfScreen(screen), 0, NULL);

	/* create the drawing pixmap */
	draw_pix = XCreatePixmap(display, draw_wnd, width, height, depth);

	/* create a pixmap for saving current slide */
	current_pix = XCreatePixmap(display, draw_wnd, width, height, depth);

	/* create a pixmap for saving previous slide */
	prev_pix = XCreatePixmap(display, draw_wnd, width, height, depth);

	gc = XCreateGC(display, display_wnd, 0, NULL);

	HideDecorations(display_wnd);
	XStoreName(display, display_wnd, filename);
	XMapWindow(display, display_wnd);
	XSelectInput(display, display_wnd, KeyPressMask | ButtonPressMask | ButtonReleaseMask | ExposureMask);

	ProcessEvents();

	if (override_redirect)
		XSetInputFocus(display, display_wnd, RevertToParent, CurrentTime);
	else
		XMoveWindow(display, display_wnd, 0, 0);

	gs_fd = GSStart(draw_wnd, draw_pix, width, height, bounds, orientation);

	/* start off with prologue and page 1 */
	page_start = document;
	page_end = pages[1].data;
	MainLoop(gs_fd);

	GSStop(gs_fd);

	/* clean up */
	XFreeGC(display, gc);
	XFreePixmap(display, prev_pix);
	XFreePixmap(display, current_pix);
	XFreePixmap(display, draw_pix);
	XDestroyWindow(display, draw_wnd);
	XDestroyWindow(display, display_wnd);
	XCloseDisplay(display);
	munmap(document, size);

	return EXIT_SUCCESS;
}
