/*
 * Copyright (C) 2000, Matias Atria
 *
 * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <stdarg.h>

#include "mdvi.h"
#include "private.h"
#include "x11.h"

#define WIN_BORDER	0
#define STATUS_BORDER	0

#define PAGEMOVE_TINY	0
#define PAGEMOVE_SMALL	1
#define PAGEMOVE_NORMAL	2
#define PAGEMOVE_LARGE	3

#include <X11/keysym.h>
#include <X11/cursorfont.h>

typedef struct {
	Window	win;
	Uint	w;
	Uint	h;
	int	xorig;
	int	yorig;
	int	minx;
	int	miny;
	int	maxx;
	int	maxy;
	int	dirty;
} WinArea;

#define MAX_MARKS	16

typedef struct {
	DrawArea area;
	char	**strings;
	int	size;
	int	top;
} StatusLine;

typedef void (*RulerDrawer) __PROTO((DviContext *, int, int, int, int));

typedef struct {
	TopWindow dd_top;
	WinArea   dd_dvi;
	Ulong     dd_fg;
	Ulong     dd_bg;
	Uint      dd_page_w;
	Uint	  dd_page_h;
	Uint	  dd_paper_w;
	Uint	  dd_paper_h;
	double	  dd_hruler_inches;
	double	  dd_vruler_inches;
	short	  dd_hsteps;
	short	  dd_vsteps;
	Uint	  dd_base_x;
	Uint	  dd_base_y;
	short	  dd_marks[MAX_MARKS];
	GC        dd_gc;
	short     dd_busy;
	int     dd_ruler_xorig;
	int     dd_ruler_yorig;
	int     dd_ruler_x;
	int     dd_ruler_y;
	int     dd_inruler;
	RulerDrawer dd_ruler_draw;
	int	dd_reset_origin;
	DviAppConfig *dd_app;
} DviDrawData;

/* these are constant after initialization */
static Cursor	busy, point;
static GC	rulergc;
static GC	scrollgc;

extern DviAppConfig *mdvi_app;

static DviOrientation pos_rotate_sequence[] = {
	MDVI_ORIENT_RP90,
	MDVI_ORIENT_IRP90,
	MDVI_ORIENT_IRM90,
	MDVI_ORIENT_RM90,
	MDVI_ORIENT_BTRL,
	MDVI_ORIENT_TBLR,
	MDVI_ORIENT_BTLR,
	MDVI_ORIENT_TBRL
};

static DviOrientation neg_rotate_sequence[] = {
	MDVI_ORIENT_RM90,
	MDVI_ORIENT_IRM90,
	MDVI_ORIENT_IRP90,
	MDVI_ORIENT_RP90,
	MDVI_ORIENT_TBLR,
	MDVI_ORIENT_BTRL,
	MDVI_ORIENT_TBRL,
	MDVI_ORIENT_BTLR
};

static DviOrientation h_flip_sequence[] = {
	MDVI_ORIENT_TBRL,
	MDVI_ORIENT_TBLR,
	MDVI_ORIENT_BTRL,
	MDVI_ORIENT_BTLR,
	MDVI_ORIENT_IRM90,
	MDVI_ORIENT_IRP90,
	MDVI_ORIENT_RM90,
	MDVI_ORIENT_RP90
};

static DviOrientation v_flip_sequence[] = {
	MDVI_ORIENT_BTLR,
	MDVI_ORIENT_BTRL,
	MDVI_ORIENT_TBLR,
	MDVI_ORIENT_TBRL,
	MDVI_ORIENT_IRP90,
	MDVI_ORIENT_IRM90,
	MDVI_ORIENT_RP90,
	MDVI_ORIENT_RM90
};

/* 
 * I know this is poor style, but it saves a lot of routine
 * typing.
 */
#define GET_DRAW_DATA	\
	DviDrawData *dd = (DviDrawData *)dvi->device.device_data

#define top	dd->dd_top
#define dviwin	dd->dd_dvi
#define page_w	dd->dd_page_w
#define page_h	dd->dd_page_h
#define paper_w	dd->dd_paper_w
#define paper_h	dd->dd_paper_h
#define hruler_inches	dd->dd_hruler_inches
#define vruler_inches	dd->dd_vruler_inches
#define hsteps	dd->dd_hsteps
#define vsteps	dd->dd_vsteps
#define base_x	dd->dd_base_x
#define base_y	dd->dd_base_y
#define marks	dd->dd_marks
#define drawgc	dd->dd_gc
#define inruler dd->dd_inruler
#define reset_origin dd->dd_reset_origin
#define dviapp	dd->dd_app

#define NORULER	0
#define HRULER	1
#define VRULER	2
#define VHRULER	3

#define SET_RULER_ORIGIN(t,x,y) do {                                 \
	XSync(dpy, False);                                           \
	dd->dd_ruler_xorig = dd->dd_ruler_x = (x);                   \
	dd->dd_ruler_yorig = dd->dd_ruler_y = (y);                   \
	switch(t) {                                                  \
		case NORULER: dd->dd_ruler_draw = NULL; break;       \
		case HRULER: dd->dd_ruler_draw = draw_hruler; break; \
		case VRULER: dd->dd_ruler_draw = draw_vruler; break; \
		case VHRULER: dd->dd_ruler_draw = draw_ruler; break; \
	}                                                            \
	dd->dd_inruler = (t);                                        \
	} while(0)

#define UNSET_RULER() do {                                           \
	dd->dd_ruler_draw = NULL; dd->dd_inruler = NORULER;          \
	} while(0)

#define DRAW_RULER() do {                                            \
	if(dd->dd_ruler_draw)                                        \
		dd->dd_ruler_draw(dvi,                               \
			dd->dd_ruler_xorig,                          \
			dd->dd_ruler_yorig,                          \
			dd->dd_ruler_x,                              \
			dd->dd_ruler_y);                             \
	} while(0)

#define MOVE_RULER(x,y) do {                                         \
	DRAW_RULER();                                                \
	dd->dd_ruler_x = (x);                                        \
	dd->dd_ruler_y = (y);                                        \
	DRAW_RULER();                                                \
	} while(0)

#define WORKING() do { \
	if(++dd->dd_busy == 1 && busy != None) \
	  XDefineCursor(dpy, top.win, busy); \
	} while(0)
#define RESTING() do { \
	if(--dd->dd_busy == 0 && busy != None) \
	  XUndefineCursor(dpy, top.win); \
	} while(0)

int	mdvi_create_window(DviContext *dvi, const char *geometry);
void    view_page __PROTO((DviContext *dvi, int pageno));

static void print_key_help __PROTO((void));
static void handle_special_key __PROTO((DviContext *dvi, int argno, int den, KeySym sym, int state));
static void handle_key __PROTO((DviContext *dvi, XKeyEvent *ev));
static void do_redraw __PROTO((DviContext *dvi));
static void do_expose __PROTO((DviContext *dvi, int x, int y, Uint w, Uint h));
static void handle_dvi __PROTO((DviContext *dvi, XEvent *ev));
static void x11_draw_glyph __PROTO((DviContext *dvi, DviFontChar *ch, int x0, int y0));
static void x11_draw_rule __PROTO((DviContext *dvi, int x, int y, Uint w, Uint h, int));
static void draw_hticks __PROTO((DviContext *dvi, int x, int y, int x2));
static void draw_vticks __PROTO((DviContext *dvi, int x, int y, int y2));
static void draw_hruler __PROTO((DviContext *dvi, int x1, int y1, int x2, int y2));
static void draw_vruler __PROTO((DviContext *dvi, int x1, int y1, int x2, int y2));
static void draw_ruler __PROTO((DviContext *dvi, int x, int y, int x2, int y2));
static void reset_page __PROTO((DviContext *dvi));
static void reset_view_origin __PROTO((DviContext *dvi));
static void position_windows __PROTO((DviContext *dvi));
static int configure_window __PROTO((DviContext *dvi));
static void scroll_window __PROTO((DviContext *dvi, int newx, int newy));
static void pageup __PROTO((DviContext *dvi, int n, int mode));
static void pageleft __PROTO((DviContext *dvi, int n, int mode));
static int  set_pageno __PROTO((DviContext *dvi, int page));
static int  set_tex_pageno __PROTO((DviContext *dvi, int page));
static void top_events __PROTO((DviContext *dvi, XEvent *ev));

extern void font_print __PROTO((void));

/* functions that are used as DviDevice in the DVI context */

static void x11_draw_glyph(DviContext *dvi, DviFontChar *ch, int x0, int y0)
{
	int	rx, ry;
	int	x, y, w, h;
	int	isbitmap;
	int	isbox;
	DviGlyph *glyph;
	GET_DRAW_DATA;
	
	/* coordinates into the page */
	rx = base_x + x0;
	ry = base_y + y0;

	isbitmap = 1;
	if(dvi->params.hshrink == 1 && dvi->params.vshrink == 1)
		glyph = &ch->glyph;
	else if(dvi->params.flags & MDVI_PARAM_ANTIALIASED) {
		glyph = &ch->grey;
		isbitmap = 0;
	} else
		glyph = &ch->shrunk;

	isbox = (glyph->data == NULL || 
		(dvi->params.flags & MDVI_PARAM_CHARBOXES));
	
	x = glyph->x;
	y = glyph->y;
	w = glyph->w;
	h = glyph->h;

	switch(dvi->params.orientation) {
	case MDVI_ORIENT_TBLR:
		break;
	case MDVI_ORIENT_TBRL:
		rx = page_w - rx;
		break;
	case MDVI_ORIENT_BTLR:
		ry = page_h - ry;
		break;
	case MDVI_ORIENT_BTRL:
		rx = page_w - rx;
		ry = page_h - ry;
		break;
	case MDVI_ORIENT_RP90:
		SWAPINT(rx, ry);
		ry = page_h - ry;
		break;
	case MDVI_ORIENT_RM90:
		SWAPINT(rx, ry);
		rx = page_w - rx;
		break;
	case MDVI_ORIENT_IRM90:
		SWAPINT(rx, ry);
		rx = page_w - rx;
		ry = page_h - ry;
		break;
	case MDVI_ORIENT_IRP90:
		SWAPINT(rx, ry);
		break;
	default:
		break;
	}

	rx -= x + dviwin.xorig;
	ry -= y + dviwin.yorig;
		 
	/* 
	 * drawing is allowed only in the rectangle inside 
	 * [minx, miny] - [maxx, maxy]
	 * of dviwin
	 */
	if(rx > dviwin.maxx || dviwin.minx > rx + w ||
	   ry > dviwin.maxy || dviwin.miny > ry + h)
	   	return;
	if(isbox)
		XDrawRectangle(dpy, dviwin.win, drawgc,
			rx, ry, w, h);
	else if(isbitmap)
		draw_bitmap(dviwin.win, (BITMAP *)glyph->data, rx, ry);
	else
		put_image(dviwin.win, glyph->data, rx, ry, w, h);
}

static void x11_draw_rule(DviContext *dvi, int x, int y, Uint w, Uint h, int fill)
{
	int	rx, ry;
	GET_DRAW_DATA;

	rx = base_x + x;
	ry = base_y + y;
	
	switch(dvi->params.orientation) {
	case MDVI_ORIENT_TBLR:
		break;
	case MDVI_ORIENT_TBRL:
		rx = page_w - rx - w;
		break;
	case MDVI_ORIENT_BTLR:
		ry = page_h - ry - h;
		break;
	case MDVI_ORIENT_BTRL:
		rx = page_w - rx - w;
		ry = page_h - ry - h;
		break;
	case MDVI_ORIENT_RP90:
		SWAPINT(rx, ry);
		SWAPINT(w, h);
		ry = page_h - ry - h;
		break;
	case MDVI_ORIENT_RM90:
		SWAPINT(rx, ry);
		SWAPINT(w, h);
		rx = page_w - rx - w;
		break;
	case MDVI_ORIENT_IRM90:
		SWAPINT(rx, ry);
		SWAPINT(w, h);
		rx = page_w - rx - w;
		ry = page_h - ry - h;
		break;
	case MDVI_ORIENT_IRP90:
		SWAPINT(rx, ry);
		SWAPINT(w, h);
		break;
	}
	rx -= dviwin.xorig;
	ry -= dviwin.yorig;
		 
	if(rx <= dviwin.maxx && dviwin.minx <= rx + w &&
	   ry <= dviwin.maxy && dviwin.miny <= ry + h) {
	   	if(fill == 0)
	   		XDrawRectangle(dpy, dviwin.win, image_gc,
	   			rx, ry, w ? w : 1, h ? h : 1);
	   	else
	   		XFillRectangle(dpy, dviwin.win, image_gc, 
	   			rx, ry, w ? w : 1, h ? h : 1);
	}
}

static int x11_interpolate_colors(void *device_data,
	Ulong *pixels, int nlevels, Ulong fg, Ulong bg, double g, int density)
{
	double	frac;
	XColor	xc, xfg, xbg;
	int	i, n;
	
	xfg.pixel = fg;
	xbg.pixel = bg;
	XQueryColor(dpy, colormap, &xfg);
	XQueryColor(dpy, colormap, &xbg);

	n = nlevels - 1;
	for(i = 0; i < nlevels; i++) {
		if(g > 0)
			frac = pow((double)i / n, 1 / g);
		else
			frac = 1 - pow((double)(n - i) / n, -g);
		xc.red = frac * ((double)xfg.red - xbg.red) + xbg.red;
		xc.green = frac * ((double)xfg.green - xbg.green) + xbg.green;
		xc.blue = frac * ((double)xfg.blue - xbg.blue) + xbg.blue;
		if(XAllocColor(dpy, colormap, &xc))
			pixels[i] = xc.pixel;
		else
			pixels[i] = (i * 100 >= density * 15) ?
				xfg.pixel : xbg.pixel;
	}
	return nlevels;
}

static void *x11_create_image(void *device_data, Uint w, Uint h, Uint bpp)
{
	XImage *image;
	
	image = XCreateImage(dpy, visual, depth, ZPixmap, 0,
		(char *)0, w, h, bpp, 0);
	image->data = xcalloc(image->bytes_per_line, h);
	return image;
}

static void x11_free_image(void *ptr)
{
	XImage *image = (XImage *)ptr;
	char	*data;
	
	data = image->data;
	image->data = NULL;
	XDestroyImage(image);
	xfree(data);
}

static void x11_put_pixel(void *image, int x, int y, Ulong color)
{
	XPutPixel((XImage *)image, x, y, color);
}

static void x11_refresh(DviContext *dvi, void *device_data)
{
	GET_DRAW_DATA;
	
	configure_window(dvi);
	if(!reset_origin)
		set_pageno(dvi, dvi->currpage);
}

static void x11_set_color(void *device_data, Ulong fg, Ulong bg)
{
	DviDrawData *dd = (DviDrawData *)device_data;
	
	if(fg != dd->dd_fg) {
		DEBUG((DBG_DEVICE, "foreground %lu -> %lu\n",
			dd->dd_fg, fg));
		XSetForeground(dpy, image_gc, fg);
		dd->dd_fg = fg;
	}
	if(bg != dd->dd_bg) {
		DEBUG((DBG_DEVICE, "background %lu -> %lu\n",
			dd->dd_bg, bg));
		XSetBackground(dpy, image_gc, bg);
		dd->dd_bg = bg;
	}
}

/* draws tick marks for horizontal rulers */
static void draw_hticks(DviContext *dvi, int x, int y, int x2)
{
	int	i, n;
	double	dh;
	GET_DRAW_DATA;
	
	dh = hruler_inches * dvi->params.dpi / (hsteps * dvi->params.hshrink);
	if(x > x2) 
		dh = -dh;
	for(n = 0, i = x; (x < x2 ? i < x2 : i > x2); ) {
		if(hsteps > 1 && (n % hsteps) == 0)
			XDrawLine(dpy, dviwin.win, rulergc, i, y+6, i, y-6);
		else if((hsteps & 1) || (n % (hsteps >> 1)))
			XDrawLine(dpy, dviwin.win, rulergc, i, y+2, i, y-2);
		else
			XDrawLine(dpy, dviwin.win, rulergc, i, y+4, i, y-4);
		i = x + FROUND(++n * dh);
	}	
}

/* draws tick marks for vertical rulers */
static void draw_vticks(DviContext *dvi, int x, int y, int y2)
{
	int	i, n;
	double	dh;
	GET_DRAW_DATA;
	
	dh = vruler_inches * dvi->params.vdpi / (vsteps * dvi->params.vshrink);
	if(y > y2) 
		dh = -dh;
	for(n = 0, i = y; (y < y2 ? i < y2 : i > y2); ) {
		if(vsteps > 1 && (n % vsteps) == 0)
			XDrawLine(dpy, dviwin.win, rulergc, x+6, i, x-6, i);
		else if((vsteps & 1) || (n % (vsteps >> 1)))
			XDrawLine(dpy, dviwin.win, rulergc, x+2, i, x-2, i);
		else
			XDrawLine(dpy, dviwin.win, rulergc, x+4, i, x-4, i);
		i = y + FROUND(++n * dh);
	}	
}

static void draw_hruler(DviContext *dvi, int x1, int y1, int x2, int y2)
{
	int	tx, ty, n, d;
	double	fraction;
	char	str[64];
	GET_DRAW_DATA;
	
	/* grab the server */
	XRaiseWindow(dpy, dviwin.win);
	XSync(dpy, True);
	XGrabServer(dpy);
	
	/* draw a horizontal line */
	XDrawLine(dpy, dviwin.win, rulergc, 
		x1, y1, x2, y1);

	/* draw tick marks */
	draw_hticks(dvi, x1, y1, x2);

	/* draw a small rectangle at the end of the ruler */
	XFillRectangle(dpy, dviwin.win, rulergc, x2-1, y1-1, 3, 3); 
	XDrawLine(dpy, dviwin.win, rulergc, x2, y1, x2, y2);

	/* draw text */
	if(x1 != x2) {
		/* make the text centered under the ruler */
		fraction = (double)abs(x1 - x2) * dvi->params.hshrink / 
			(dvi->params.dpi * hruler_inches);
		d = sprintf(str, "%.2f", fraction);
		n = XTextWidth(default_font, str, d);
		tx = x2 + 15;
		ty = y2 - FONT_HEIGHT(default_font) / 2 + 
			default_font->max_bounds.ascent;
		if(ty - 2 < default_font->max_bounds.ascent)
			ty = default_font->max_bounds.ascent + 2;
		if(ty + default_font->max_bounds.descent + 2 > dviwin.h)
			ty = dviwin.h - default_font->max_bounds.descent - 2;
		if(tx < 2)
			tx = 2;
		if(tx + n + 2 > dviwin.w)
			tx = Min(x2, dviwin.w) - n - 10;
		XDrawString(dpy, dviwin.win, rulergc, tx, ty, str, d);
	}
	XUngrabServer(dpy);
}

static void draw_vruler(DviContext *dvi, int x1, int y1, int x2, int y2)
{
	int	tx, ty, n, d;
	char	str[64];
	double	fraction;
	GET_DRAW_DATA;

	/* grab the server */
	XGrabServer(dpy);
	
	/* draw a horizontal line */
	XDrawLine(dpy, dviwin.win, rulergc, x1, y1, x1, y2);

	/* draw tick marks */
	draw_vticks(dvi, x1, y1, y2);
	
	/* draw a small rectangle at the end of the ruler */
	XFillRectangle(dpy, dviwin.win, rulergc, x1-1, y2-1, 3, 3); 
	XDrawLine(dpy, dviwin.win, rulergc, x1, y2, x2, y2);
	
	/* draw text */
	if(y1 != y2) {
		/* make the text centered under the ruler */
		fraction = (double)abs(y1 - y2) * dvi->params.vshrink /
			(dvi->params.vdpi * vruler_inches);
		d = sprintf(str, "%.2f", fraction);
		n = XTextWidth(default_font, str, d);
		if(n % 2) n++;
		tx = x2 - n / 2;
		ty = y2 + default_font->max_bounds.ascent + 10;
		/* make sure the text is visible */
		if(tx < 2) 
			tx = 2;
		if(tx + n + 2 > dviwin.w)
			tx = dviwin.w - n - 2;
		if(ty > 0 && ty > dviwin.h - 2)
			ty = Min(y2, dviwin.h) - default_font->max_bounds.descent - 10;
		if(ty < default_font->max_bounds.ascent + 2)
			ty = default_font->max_bounds.ascent + 2;
		XDrawString(dpy, dviwin.win, rulergc, tx, ty, str, d);
	}
	XUngrabServer(dpy);
}

/*ARGSUSED*/
static void draw_ruler(DviContext *dvi, int x0, int y0, int x, int y)
{
	GET_DRAW_DATA;
	
	/* Grab the server */
	XGrabServer(dpy);

	/* Draw horizontal line */
	XDrawLine(dpy, dviwin.win, rulergc, 0, y, dviwin.w, y);

	/* Draw vertical line */
	XDrawLine(dpy, dviwin.win, rulergc, x, 0, x, dviwin.h);

	/* hand back the server */
	XUngrabServer(dpy);
	
	/* draw ticks */

	/* horizontal ticks */
	draw_hticks(dvi, x, y, dviwin.w);
	draw_hticks(dvi, x, y, 0);
	
	/* vertical ticks */
	draw_vticks(dvi, x, y, dviwin.h);
	draw_vticks(dvi, x, y, 0);

}

static void reset_page(DviContext *dvi)
{
	GET_DRAW_DATA;
	
	dviwin.minx = 0;
	dviwin.miny = 0;
	dviwin.maxx = dviwin.w;
	dviwin.maxy = dviwin.h;
	dviwin.dirty = 1;
}

static void reset_view_origin(DviContext *dvi)
{
	GET_DRAW_DATA;
	
	switch(dvi->params.orientation) {
	case MDVI_ORIENT_TBLR:
	case MDVI_ORIENT_IRP90:
		dviwin.xorig = 0;
		dviwin.yorig = 0;
		break;
	case MDVI_ORIENT_TBRL:
	case MDVI_ORIENT_RM90:
		dviwin.xorig = page_w - dviwin.w;
		dviwin.yorig = 0;
		break;
	case MDVI_ORIENT_BTLR:
	case MDVI_ORIENT_RP90:
		dviwin.xorig = 0;
		dviwin.yorig = page_h - dviwin.h;
		break;
	case MDVI_ORIENT_BTRL:
	case MDVI_ORIENT_IRM90:
		dviwin.xorig = page_w - dviwin.w;
		dviwin.yorig = page_h - dviwin.h;
		break;	
	}
	if(dviwin.xorig < 0)
		dviwin.xorig = 0;
	if(dviwin.yorig < 0)
		dviwin.yorig = 0;

}

static void position_windows(DviContext *dvi)
{
	int	tw, th;
	GET_DRAW_DATA;
	
	tw = top.w - 2*WIN_BORDER;
	th = top.h - 2*WIN_BORDER;
	XMoveResizeWindow(dpy, dviwin.win, 0, 0, tw, th);
	dviwin.w = tw;
	dviwin.h = th;
	reset_page(dvi);
}

static int configure_window(DviContext *dvi)
{
	Uint	w, h;
	double	ratio;
	int	hs, vs;
	int	changed = 0;
	GET_DRAW_DATA;
	
	ratio = dvi->paper.inches_tall / dvi->paper.inches_wide;
	base_x = unit2pix(dvi->params.dpi, dviapp->hmargin);
	base_y = unit2pix(dvi->params.vdpi, dviapp->vmargin);
	page_w = FROUND(dvi->dviconv * dvi->dvi_page_w);
	page_h = FROUND(dvi->dvivconv * dvi->dvi_page_h);
	hs = dvi->params.hshrink;
	vs = dvi->params.vshrink;
	if(!hs && !vs) {
		/* compute both */
		w = root_w;
		h = FROUND(w * ratio);
		if(h > root_h) {
			h = root_h;
			w = FROUND(h / ratio);	
		}
		/* determine shrinking factors */
		hs = ROUND(page_w + 2*base_x, w);
		vs = ROUND(page_h + 2*base_y, h);
		if(hs < 1) hs = 1;
		if(vs < 1) vs = 1;
	} else if(!hs) {
		h = FROUND(dvi->params.vdpi * dvi->paper.inches_tall /
			dvi->params.vshrink) + 2;
		w = FROUND(h / ratio);
		hs = ROUND(page_w + 2*base_x, w);
	} else if(!vs) {
		w = FROUND(dvi->params.dpi * dvi->paper.inches_wide / 
			dvi->params.hshrink) + 2;
		h = FROUND(w * ratio);
		vs = ROUND(page_h + 2*base_y, h);
	} else {
		w = FROUND(dvi->params.dpi * dvi->paper.inches_wide /
			dvi->params.hshrink) + 2;
		h = FROUND(dvi->params.vdpi * dvi->paper.inches_tall /
			dvi->params.vshrink) + 2;
	}

	/* convert page dimensions */
	if(!dvi->params.hshrink) {
		dvi->params.hshrink = hs;
		dvi->params.conv /= hs;
	}
	if(!dvi->params.vshrink) {
		dvi->params.vshrink = vs;
		dvi->params.vconv /= vs;
	}
	
	/* fix margins */
	base_x = unit2pix(dvi->params.dpi, dviapp->hmargin) / dvi->params.hshrink;
	base_y = unit2pix(dvi->params.vdpi, dviapp->vmargin) / dvi->params.vshrink;
	
	/* recompute things now that we have all the information */
	page_w = pixel_round(dvi, dvi->dvi_page_w) + base_x;
	page_h = vpixel_round(dvi, dvi->dvi_page_h) + base_y;
	
	/* get paper dimensions */
	paper_w = FROUND(dvi->params.dpi * dvi->paper.inches_wide /
		dvi->params.hshrink) + 2;
	paper_h = FROUND(dvi->params.vdpi * dvi->paper.inches_tall /
		dvi->params.vshrink) + 2;
	
	/* this is the new window size */
	if(page_w < paper_w)
		page_w = paper_w;
	if(page_h < paper_h)
		page_h = paper_h;
		
	switch(dvi->params.orientation) {
	case MDVI_ORIENT_TBLR:
	case MDVI_ORIENT_TBRL:
	case MDVI_ORIENT_BTLR:
	case MDVI_ORIENT_BTRL:
		break;
	case MDVI_ORIENT_RP90:
	case MDVI_ORIENT_RM90:
	case MDVI_ORIENT_IRP90:
	case MDVI_ORIENT_IRM90:
		SWAPINT(paper_w, paper_h);
		SWAPINT(page_w, page_h);
		break;
	}

	w = Min(page_w, root_w);
	h = Min(page_h, root_h);

	changed = 1;	
	if(top.win == None) {
		create_top_window(&top, dvi->filename, w, h);
		expose_window(top.win, False);
		get_win_size(top.win, &top.w, &top.h);
	} else if(w != top.w || h != top.h)
		XResizeWindow(dpy, top.win, w, h);
	else
		changed = 0;
		
	/* setup ruler units */
	hruler_inches = unit2pix_factor(dviapp->hrule_units);
	vruler_inches = unit2pix_factor(dviapp->vrule_units);
	ratio = hruler_inches * dvi->params.dpi;
	if(ratio < 10)
		hsteps = 1;
	else if(ratio < 100)
		hsteps = 5;
	else
		hsteps = 10;
	ratio = vruler_inches * dvi->params.vdpi;
	if(ratio < 10)
		vsteps = 1;
	else if(ratio < 100)
		vsteps = 5;
	else
		vsteps = 10;
	
	return changed;
}

int	mdvi_create_window(DviContext *dvi, const char *geometry)
{
	DviDrawData	*dd;
	int	i;	
	
	dd = xalloc(DviDrawData);
	memzero(dd, sizeof(DviDrawData));
	dvi->device.device_data = dd;
	
	/* if this looks weird, see the comment about bad style above */
	dviapp = mdvi_app;
	top.win = None;
	configure_window(dvi);
	if(top.win == None)
		return -1;
				
	XSelectInput(dpy, top.win, 
		ExposureMask|StructureNotifyMask|KeyPressMask);

	/* GC for drawing */
	drawgc = create_gc(GXcopy, top.win, dvi->params.fg, dvi->params.bg);

	/* GC for rulers */	
	rulergc = create_gc(GXxor, top.win, white, black);

	/* create our child window */
	dviwin.win = create_window(top.win, dvi->params.bg, 
		top.w - 2*WIN_BORDER, 
		top.h - 2*WIN_BORDER, WIN_BORDER);
	XSelectInput(dpy, dviwin.win,
		ExposureMask|ButtonPressMask|ButtonReleaseMask|ButtonMotionMask);	
	get_win_size(dviwin.win, &dviwin.w, &dviwin.h);
	dviwin.minx = 0;
	dviwin.miny = 0;
	dviwin.maxx = dviwin.w;
	dviwin.maxy = dviwin.h;

	reset_view_origin(dvi);
	add_window_events(dviwin.win, ButtonMotionMask);

	/* GC for scrolling */
	scrollgc = create_gc(GXcopy, dviwin.win, dvi->params.fg, dvi->params.bg);
	XSetGraphicsExposures(dpy, scrollgc, True);	
	
	/* setup the functions in the dvi context */
	dvi->device.draw_glyph   = x11_draw_glyph;
	dvi->device.draw_rule    = x11_draw_rule;
	dvi->device.alloc_colors = x11_interpolate_colors;
	dvi->device.create_image = x11_create_image;
	dvi->device.free_image   = x11_free_image;
	dvi->device.put_pixel    = x11_put_pixel;
	dvi->device.set_color    = x11_set_color;
	dvi->device.refresh      = NULL; /*x11_refresh;*/

	/* initialize marks */
	for(i = 0; i < MAX_MARKS; i++)
		marks[i] = -1;

	position_windows(dvi);

	printf("Type `q' to quit, `?' to see keyboard and mouse commands\n");

	return 0;
}

static void scroll_window(DviContext *dvi, int newx, int newy)
{
	int	x, y, w, h;
	int	src_x, dest_x;
	int	src_y, dest_y;
	GET_DRAW_DATA;
	
	x = newx - dviwin.xorig;
	y = newy - dviwin.yorig;
	dviwin.xorig = newx;
	dviwin.yorig = newy;
	dviwin.minx -= x;
	dviwin.maxx -= x;
	dviwin.miny -= y;
	dviwin.maxy -= y;
	if(dviwin.minx < 0)
		dviwin.minx = 0;
	if(dviwin.miny < 0)
		dviwin.miny = 0;
	if(dviwin.maxx > dviwin.w)
		dviwin.maxx = dviwin.w;
	if(dviwin.maxy > dviwin.h)
		dviwin.maxy = dviwin.h;

	if(x > 0) {
		src_x = x;
		dest_x = 0;
	} else {
		src_x = 0;
		dest_x = -x;
	}
	if(y > 0) {
		src_y = y;
		dest_y = 0;
	} else {
		src_y = 0;
		dest_y = -y;
	}
	w = dviwin.w - abs(x);
	h = dviwin.h - abs(y);
	if(w <= 0 || h <= 0) {
		/* need to refresh the entire window */
		reset_page(dvi);
		XClearArea(dpy, dviwin.win, 0, 0, dviwin.w, dviwin.h, True);
	} else {
		XCopyArea(dpy, dviwin.win, dviwin.win, scrollgc,
			src_x, src_y, w, h, dest_x, dest_y);
		if(dest_x > 0 && h)
			XClearArea(dpy, dviwin.win,
				0, dest_y, dviwin.w - w, h, True);
		else if(dest_x == 0 && h)
			XClearArea(dpy, dviwin.win,
				w, dest_y, dviwin.w - w, h, True);
		if(dest_y > 0 && w)
			XClearArea(dpy, dviwin.win,
				dest_x, 0, w, dviwin.h - h, True);
		else if(dest_y == 0 && w)
			XClearArea(dpy, dviwin.win,
				dest_x, h, w, dviwin.h - h, True);
		if(w < dviwin.w && h < dviwin.h) {
			XClearArea(dpy, dviwin.win,
				dest_x ? 0 : w,
				dest_y ? 0 : h,
				dviwin.w - w, dviwin.h - h, True);
		}
	}
}

static void pageup(DviContext *dvi, int n, int mode)
{
	int	z;
	int	maxh;
	GET_DRAW_DATA;

	n = n * (int)page_h;
	switch(mode) {
	case PAGEMOVE_TINY: n /= 80; break;
	case PAGEMOVE_SMALL: n /= 40; break;
	case PAGEMOVE_NORMAL: n /= 10; break;
	case PAGEMOVE_LARGE: break;
	}

	z = dviwin.yorig + n;
	maxh = page_h - dviwin.h;
	if(z > maxh)
		z = maxh;
	if(z < 0)
		z = 0;
	if(z != dviwin.yorig)
		scroll_window(dvi, dviwin.xorig, z);
	else
		beep_sound();
}

static void pageleft(DviContext *dvi, int n, int mode)
{
	int	z;
	int	maxw;
	GET_DRAW_DATA;

	n = n * (int)page_w;
	switch(mode) {
	case PAGEMOVE_TINY: n /= 80; break;
	case PAGEMOVE_SMALL: n /= 40; break;
	case PAGEMOVE_NORMAL: n /= 10; break;
	case PAGEMOVE_LARGE: break;
	}
	z = dviwin.xorig + n;
	maxw = page_w - dviwin.w;
	if(z > maxw)
		z = maxw;
	if(z < 0)
		z = 0;
	if(z != dviwin.xorig)
		scroll_window(dvi, z, dviwin.yorig);
	else
		beep_sound();
}

static int set_pageno(DviContext *dvi, int page)
{
	GET_DRAW_DATA;
	
	if(page < 0 || page > dvi->npages - 1)
		return -1;
	dvi->currpage = page;
	reset_page(dvi);
	XClearArea(dpy, dviwin.win, 0, 0, dviwin.w, dviwin.h, True);
	return 0;
}

static int set_tex_pageno(DviContext *dvi, int tex_page)
{
	int	page;
	
	page = mdvi_find_tex_page(dvi, tex_page);
	if(page == dvi->npages)
		return -1;
	return set_pageno(dvi, page);
}

void	print_key_help(void)
{
	printf(
"Keystroke commands: (S=Shift, C=Control)\n"
"  PageUp/PageDown        go to previous/next page\n"
"  C-PageUp/PageDown      go to first/last page\n"
"  [S/C] Left/Right       scroll document horizontally\n"
"  Up/Down                scroll document vertically\n"
"  q                      quit\n"
"  b                      switch between your eyes and TeX's\n"
"  a                      toggle antialiasing\n"
"  l                      redraw page\n"
"  L                      reload the DVI file from disk\n"
"  R/r                    rotate page clockwise/counter-clockwise\n"
"  F/f                    flip page vertically/horizontally\n"
"  ?                      this help\n"
"  (NUM) p                go to current page + NUM (-\\infty < NUM < \\infty)\n"
"  (NUM) j                go to output page number NUM (last+NUM if NUM < 0)\n"
"  (NUM) t                go to TeX's (i.e. document's) page number NUM\n"
"  (NUM) d                change resolution\n"
"  (NUM) m                change magnification\n"
"  (NUM) s                change shrinking factor (both set to NUM)\n"
"  (NUM) x                change horizontal shrinking factor\n"
"  (NUM) y                change vertical shrinking factor\n"
"  (NUM) n                change pixel density (for bitmap shrinking)\n"
"  (NUM) g                change gamma correction factor\n"
"  C-Fn                   set bookmark\n"
"  Fn                     goto bookmark\n"
"Mouse usage:\n"
"  Button 1               display ruler\n"
"  Button 1 + Shift       display horizontal ruler\n"
"  Button 1 + Control     display vertical ruler\n"
	"");
}

void	handle_special_key(DviContext *dvi, int argno, int den, KeySym sym, int state)
{
	int	arg;
	GET_DRAW_DATA;
	
	switch(sym) {
		case XK_Page_Down:
			if(state & ControlMask)
				arg = dvi->npages - 1;
			else
				arg = dvi->currpage + 1;
			if(arg != dvi->currpage && set_pageno(dvi, arg) < 0)
				beep_sound();
			break;
		case XK_Page_Up:
			if(state & ControlMask)
				arg = 0;
			else
				arg = dvi->currpage - 1;
			if(arg != dvi->currpage && set_pageno(dvi, arg) < 0)
				beep_sound();
			break;
		case XK_Left:
			if(state & ControlMask)
				arg = PAGEMOVE_TINY;
			else if(state & ShiftMask)
				arg = PAGEMOVE_SMALL;
			else
				arg = PAGEMOVE_NORMAL;
			pageleft(dvi, -1, arg);
			break;
		case XK_Right:
			if(state & ControlMask)
				arg = PAGEMOVE_TINY;
			else if(state & ShiftMask)
				arg = PAGEMOVE_SMALL;
			else
				arg = PAGEMOVE_NORMAL;
			pageleft(dvi, 1, arg);
			break;
		case XK_Up:
			if(state & ControlMask)
				arg = PAGEMOVE_TINY;
			else if(state & ShiftMask)
				arg = PAGEMOVE_SMALL;
			else
				arg = PAGEMOVE_NORMAL;
			pageup(dvi, -1, arg);
			break;
		case XK_Down:
			if(state & ControlMask)
				arg = PAGEMOVE_TINY;
			else if(state & ShiftMask)
				arg = PAGEMOVE_SMALL;
			else
				arg = PAGEMOVE_NORMAL;
			pageup(dvi, 1, arg);
			break;
		case XK_F1:  arg =  0; goto setmark;
		case XK_F2:  arg =  1; goto setmark;
		case XK_F3:  arg =  2; goto setmark;
		case XK_F4:  arg =  3; goto setmark;
		case XK_F5:  arg =  4; goto setmark;
		case XK_F6:  arg =  5; goto setmark;
		case XK_F7:  arg =  6; goto setmark;
		case XK_F8:  arg =  7; goto setmark;
		case XK_F9:  arg =  8; goto setmark;
		case XK_F10: arg =  9; goto setmark;
		case XK_F11: arg = 10; goto setmark;
		case XK_F12: arg = 11; goto setmark;
		case XK_F13: arg = 12; goto setmark;
		case XK_F14: arg = 13; goto setmark;
		case XK_F15: arg = 14; goto setmark;
		case XK_F16: arg = 15; goto setmark;
		setmark:
			if(arg < 0 || arg >= MAX_MARKS) {
				beep_sound();
				break;
			}
			if(state & ControlMask) {
				/* set a mark */
				marks[arg] = dvi->currpage;
			} else if(marks[arg] >= 0) {
				if(set_pageno(dvi, marks[arg]) < 0)
					beep_sound();
			}
			break;
		default:
			break;
	}
}

#define NEWNUMBER \
	(has_number = 1, has_dot = 0, number = 0, logten = 1, sign = 1)
#define RESETNUMBER \
	(has_number = has_dot = 0, number = 0, sign = 1)
	
void	handle_key(DviContext *dvi, XKeyEvent *ev)
{
	char	num_buf[256];
	int	length;
	char	ch;
	KeySym	sym;
	XComposeStatus unused;
	int	reconfigure = 0;
	static int has_number = 0;
	static int sign = 1;
	static int number;
	static int has_dot = 0;
	static double logten = 1;
	GET_DRAW_DATA;
	
	length  = XLookupString(ev, num_buf, 256, &sym, &unused);
	if(length == 0) {
		sym = XLookupKeysym(ev, 0);
		if(sym != NoSymbol) {
			handle_special_key(dvi, 
				has_number ? number : 0,
				has_dot ? logten : 1,
				sym, ev->state);
		}
		RESETNUMBER;
		return;
	}
	if(length == 1)
		ch = num_buf[0];
	else
		ch = 0;

	/* if user is typing a number, update our state */	
	if(ch >= '0' && ch <= '9') {
		has_number = 1;
		number = number * 10 + sign * (ch - '0');
		if(has_dot) logten *= 10;
		return;
	} else if(ch == '-') {
		NEWNUMBER;
		sign = -1;
		return;
	} else if(ch == '+') {
		NEWNUMBER;
		return;
	} else if(ch == '.' && !has_dot) {
		/* it's a floating point number */
		has_number = 1;
		has_dot = 1;
		logten = 1;
		return;
	}

	switch(ch) {
	case 8:
		if(has_number)
			dvi->params.layer = FROUND((double)number / logten);
		else if(dvi->params.layer)
			dvi->params.layer--;
		set_pageno(dvi, dvi->currpage);
		break;
	case 9:
		if(has_number)
			dvi->params.layer = FROUND((double)number / logten);
		else
			dvi->params.layer++;
		reset_page(dvi);
		break;
	case 'q':
		top.active = 0;
		break;
	case 'b':
		dvi->params.flags ^= MDVI_PARAM_CHARBOXES;
		set_pageno(dvi, dvi->currpage);
		break;
	case 'a':
		dvi->params.flags ^= MDVI_PARAM_ANTIALIASED;
		set_pageno(dvi, dvi->currpage);
		break;
	case 'p':
		if(!has_number)
			break;
		number += dvi->currpage;
		if(number < 0)
			number = 0;
		if(number > dvi->npages-1)
			number = dvi->npages-1;
		set_pageno(dvi, number);
		break;
	case 'j':
		if(!has_number)
			break;
		if(number < 0)
			has_number = (set_pageno(dvi, dvi->npages+number) < 0);
		else
			has_number = (set_pageno(dvi, number) < 0);
		if(has_number)
			beep_sound();
		break;
	case 't':
		if(!has_number)
			break;
		if(set_tex_pageno(dvi, number) < 0)
			beep_sound();
		break;
	case 'l':
		set_pageno(dvi, dvi->currpage);
		break;
	case 'L':
		/* reload! */
		WORKING();
		if(mdvi_reload(dvi, NULL) == -1)
			beep_sound();
		else
			reconfigure = 1;
		RESTING();
		break;
	case 'd':
		/* change DPI */
		if(!has_number || number == dvi->params.dpi)
			break;
		WORKING();
		reconfigure = mdvi_set_dpi(dvi, number);
		RESTING();
		break;
	case 's':
		if(!has_number || number < 0)
			break;
		WORKING();
		reconfigure = mdvi_set_shrink(dvi, number, number);
		RESTING();
		break;
	case 'x':
		if(!has_number || number < 0)
			break;
		WORKING();
		reconfigure = mdvi_set_hshrink(dvi, number);
		RESTING();
		break;
	case 'y':
		if(!has_number || number < 0)
			break;
		WORKING();
		reconfigure = mdvi_set_vshrink(dvi, number);
		RESTING();
		break;
	case 'm': {
		double	x;

		if(!has_number)
			break;
		x = has_dot ? (double)number / logten : (double)number;
		if(x <= 0) {
			warning(_("%s: magnification must be positive\n"),
				dvi->filename);
			break;
		}
		WORKING();
		reconfigure = mdvi_set_mag(dvi, x);
		RESTING();
		break;
	}
	case '?':
		print_key_help();
		break;
	case 'r':
		/* rotate +90 */
		reconfigure = mdvi_set_orientation(dvi, 
			pos_rotate_sequence[dvi->params.orientation]);
		reset_origin = 1;
		break;
	case 'R':
		/* rotate -90 */
		reconfigure = mdvi_set_orientation(dvi,
			neg_rotate_sequence[dvi->params.orientation]);
		reset_origin = 1;
		break;
	case 'f':
		/* flip horizontally */
		reconfigure = mdvi_set_orientation(dvi,
			h_flip_sequence[dvi->params.orientation]);
		reset_origin = 1;
		break;
	case 'F':
		/* flip vertically */
		reconfigure = mdvi_set_orientation(dvi,
			v_flip_sequence[dvi->params.orientation]);
		reset_origin = 1;
		break;
	case 'g': {
		double	x;
		
		if(!has_number)
			break;
		x = has_dot ? (double)number / logten : (double)number;
		reconfigure = mdvi_set_gamma(dvi, x);
		break;
	}
	case 'n':
		if(!has_number)
			break;
		if(number < 0)
			beep_sound();
		else
			reconfigure = mdvi_set_density(dvi, number);
		break;
	case '/':
		font_print();
		break;
	}

	RESETNUMBER;
	
	if(reconfigure) {
		configure_window(dvi);
		if(!reset_origin)
			set_pageno(dvi, dvi->currpage);
	}
}

static void top_events(DviContext *dvi, XEvent *ev)
{
	int	tw, th;
	GET_DRAW_DATA;
	
	tw = top.w; th = top.h;
	
	if(handle_top_events(&top, ev))
		return;
	switch(ev->type) {
	case ConfigureNotify:
		if(reset_origin || 
		   ev->xconfigure.width != tw || ev->xconfigure.height != th) {
			position_windows(dvi);
			if(reset_origin) {
				reset_view_origin(dvi);
				reset_origin = 0;
				set_pageno(dvi, dvi->currpage);
			}
		}
		break;
	case KeyPress:
		handle_key(dvi, &ev->xkey);
		break;
	}
}

void	do_redraw(DviContext *dvi)
{
	GET_DRAW_DATA;
	
	WORKING();
	if(mdvi_dopage(dvi, dvi->currpage) == -1)
		fatal("%s: failed to display page\n", dvi->filename);
	RESTING();
	XDrawRectangle(dpy, dviwin.win, normal_gc,
		-dviwin.xorig, -dviwin.yorig, paper_w-1, paper_h-1);
	reset_page(dvi);
	dviwin.dirty = 0;
}

void	do_expose(DviContext *dvi, int x, int y, Uint w, Uint h)
{
	GET_DRAW_DATA;
	
	if(!dviwin.dirty) {
		dviwin.minx = x;
		dviwin.miny = y;
		dviwin.maxx = x + w;
		dviwin.maxy = y + h;
	} else {
		if(x < dviwin.minx)
			dviwin.minx = x;
		if(y < dviwin.miny)
			dviwin.miny = y;
		if(x + w > dviwin.maxx)
			dviwin.maxx = x + w;
		if(y + h > dviwin.maxy)
			dviwin.maxy = y + h;
	}
}

void	handle_dvi(DviContext *dvi, XEvent *ev)
{
	GET_DRAW_DATA;

	switch(ev->type) {
	case ButtonPress:
		if(inruler == NORULER && ev->xbutton.button == Button1) {
			int	type;
			
			/* Raise our window */
			XRaiseWindow(dpy, top.win);
			XFlush(dpy);
			
			/* rulers */
			if(ev->xbutton.state & ControlMask)
				type = VRULER;
			else if(ev->xbutton.state & ShiftMask)
				type = HRULER;
			else
				type = VHRULER;
			SET_RULER_ORIGIN(type, ev->xbutton.x, ev->xbutton.y);
			DRAW_RULER();
			XDefineCursor(dpy, top.win, point);
		}
		break;
	case ButtonRelease:
		if(inruler != NORULER) {
			XUndefineCursor(dpy, top.win);
			DRAW_RULER();
			UNSET_RULER();
		}
		break;
	case MotionNotify:
		if(inruler != NORULER)
			MOVE_RULER(ev->xmotion.x, ev->xmotion.y);
		break;
	case GraphicsExpose:
	case Expose:
		do_expose(dvi, ev->xexpose.x, ev->xexpose.y,
			ev->xexpose.width, ev->xexpose.height);
		dviwin.dirty = 1;
		break;
	}
}

void	view_page(DviContext *dvi, int pageno)
{
	GET_DRAW_DATA;
	
	if(busy == None)
		busy = XCreateFontCursor(dpy, XC_watch);
	if(point == None)
		point = XCreateFontCursor(dpy, XC_crosshair);
	do_expose(dvi, 0, 0, dviwin.w, dviwin.h);
	set_pageno(dvi, pageno);
	expose_window(dviwin.win, True);
	while(top.active) {
		XEvent	ev;

		if(!XPending(dpy)) {
			if(dviwin.dirty)
				do_redraw(dvi);
			else if(reset_origin) {
				position_windows(dvi);
				reset_view_origin(dvi);
				set_pageno(dvi, dvi->currpage);
				reset_origin = 0;
			}
		}

		XNextEvent(dpy, &ev);
		if(ev.xany.window == top.win)
			top_events(dvi, &ev);
		else if(ev.xany.window == dviwin.win)
			handle_dvi(dvi, &ev);
	}
	/* destroy everything */
	XFreeGC(dpy, drawgc);
	XFreeGC(dpy, rulergc);
	XFreeGC(dpy, scrollgc);
	XDestroyWindow(dpy, dviwin.win);
	/* destroy our private data */
	xfree(dvi->device.device_data);
	dvi->device.device_data = NULL;
}
