#include "pdfapp.h"

#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/cursorfont.h>
#include <X11/keysym.h>
#include <X11/XF86keysym.h>

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>

#define mupdf_icon_bitmap_16_width 16
#define mupdf_icon_bitmap_16_height 16
static unsigned char mupdf_icon_bitmap_16_bits[] = {
    0x00, 0x00, 0x00, 0x1e, 0x00, 0x2b, 0x80, 0x55, 0x8c, 0x62, 0x8c, 0x51,
    0x9c, 0x61, 0x1c, 0x35, 0x3c, 0x1f, 0x3c, 0x0f, 0xfc, 0x0f, 0xec, 0x0d,
    0xec, 0x0d, 0xcc, 0x0c, 0xcc, 0x0c, 0x00, 0x00 };

#define mupdf_icon_bitmap_16_mask_width 16
#define mupdf_icon_bitmap_16_mask_height 16
static unsigned char mupdf_icon_bitmap_16_mask_bits[] = {
    0x00, 0x1e, 0x00, 0x3f, 0x80, 0x7f, 0xce, 0xff, 0xde, 0xff, 0xde, 0xff,
    0xfe, 0xff, 0xfe, 0x7f, 0xfe, 0x3f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
    0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xce, 0x1c };

#ifndef timeradd
#define timeradd(a, b, result) \
    do { \
        (result)->tv_sec = (a)->tv_sec + (b)->tv_sec; \
        (result)->tv_usec = (a)->tv_usec + (b)->tv_usec; \
        if ((result)->tv_usec >= 1000000) \
        { \
            ++(result)->tv_sec; \
            (result)->tv_usec -= 1000000; \
        } \
    } while (0)
#endif

#ifndef timersub
#define timersub(a, b, result) \
    do { \
        (result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \
        (result)->tv_usec = (a)->tv_usec - (b)->tv_usec; \
        if ((result)->tv_usec < 0) { \
            --(result)->tv_sec; \
            (result)->tv_usec += 1000000; \
        } \
    } while (0)
#endif

extern int ximage_init(Display *display, int screen, Visual *visual);
extern int ximage_get_depth(void);
extern Visual *ximage_get_visual(void);
extern Colormap ximage_get_colormap(void);
extern void ximage_blit(Drawable d, GC gc, int dstx, int dsty,
    unsigned char *srcdata,
    int srcx, int srcy, int srcw, int srch, int srcstride);

void windrawstringxor(pdfapp_t *app, int x, int y, char *s);
void cleanup(pdfapp_t *app);

static Display *xdpy;
static Atom XA_CLIPBOARD;
static Atom XA_TARGETS;
static Atom XA_TIMESTAMP;
static Atom XA_UTF8_STRING;
static Atom WM_DELETE_WINDOW;
static Atom NET_WM_NAME;
static Atom NET_WM_STATE;
static Atom NET_WM_STATE_FULLSCREEN;
static Atom WM_RELOAD_PAGE;
static int x11fd;
static int xscr;
static Window xwin;
static Pixmap xicon, xmask;
static GC xgc;
static XEvent xevt;
static int mapped = 0;
static Cursor xcarrow, xchand, xcwait, xccaret;
static int justcopied = 0;
static int dirty = 0;
static int transition_dirty = 0;
static int dirtysearch = 0;
static char *password = "";
static XColor xbgcolor;
static XColor xshcolor;
static int reqw = 0;
static int reqh = 0;
static char copylatin1[1024 * 16] = "";
static char copyutf8[1024 * 48] = "";
static Time copytime;
static char *filename;
static char message[1024] = "";

static pdfapp_t gapp;
static int closing = 0;
static int reloading = 0;
static int showingpage = 0;
static int showingmessage = 0;

static int advance_scheduled = 0;
static struct timeval tmo;
static struct timeval tmo_advance;
static struct timeval tmo_at;

/*
 * Dialog boxes
 */
static void showmessage(pdfapp_t *app, int timeout, char *msg)
{
    struct timeval now;

    showingmessage = 1;
    showingpage = 0;

    fz_strlcpy(message, msg, sizeof message);

    if ((!tmo_at.tv_sec && !tmo_at.tv_usec) || tmo.tv_sec < timeout)
    {
        tmo.tv_sec = timeout;
        tmo.tv_usec = 0;
        gettimeofday(&now, NULL);
        timeradd(&now, &tmo, &tmo_at);
    }
}

void winerror(pdfapp_t *app, char *msg)
{
    fprintf(stderr, "mupdf: error: %s\n", msg);
    cleanup(app);
    exit(1);
}

void winwarn(pdfapp_t *app, char *msg)
{
    char buf[1024];
    snprintf(buf, sizeof buf, "warning: %s", msg);
    showmessage(app, 10, buf);
    fprintf(stderr, "mupdf: %s\n", buf);
}

void winalert(pdfapp_t *app, pdf_alert_event *alert)
{
    char buf[1024];
    snprintf(buf, sizeof buf, "Alert %s: %s", alert->title, alert->message);
    fprintf(stderr, "%s\n", buf);
    switch (alert->button_group_type)
    {
    case PDF_ALERT_BUTTON_GROUP_OK:
    case PDF_ALERT_BUTTON_GROUP_OK_CANCEL:
        alert->button_pressed = PDF_ALERT_BUTTON_OK;
        break;
    case PDF_ALERT_BUTTON_GROUP_YES_NO:
    case PDF_ALERT_BUTTON_GROUP_YES_NO_CANCEL:
        alert->button_pressed = PDF_ALERT_BUTTON_YES;
        break;
    }
}

void winprint(pdfapp_t *app)
{
    fprintf(stderr, "The MuPDF library supports printing, but this application currently does not\n");
}

char *winpassword(pdfapp_t *app, char *filename)
{
    char *r = password;
    password = NULL;
    return r;
}

char *wintextinput(pdfapp_t *app, char *inittext, int retry)
{
    static char buf[256];

    if (retry)
        return NULL;

    printf("> [%s] ", inittext);
    fgets(buf, sizeof buf, stdin);
    return buf;
}

int winchoiceinput(pdfapp_t *app, int nopts, char *opts[], int *nvals, char *vals[])
{
    /* FIXME: temporary dummy implementation */
    return 0;
}

/*
 * X11 magic
 */

static void winopen(void)
{
    XWMHints *wmhints;
    XClassHint *classhint;

    xdpy = XOpenDisplay(NULL);
    if (!xdpy)
        fz_throw(gapp.ctx, FZ_ERROR_GENERIC, "cannot open display");

    XA_CLIPBOARD = XInternAtom(xdpy, "CLIPBOARD", False);
    XA_TARGETS = XInternAtom(xdpy, "TARGETS", False);
    XA_TIMESTAMP = XInternAtom(xdpy, "TIMESTAMP", False);
    XA_UTF8_STRING = XInternAtom(xdpy, "UTF8_STRING", False);
    WM_DELETE_WINDOW = XInternAtom(xdpy, "WM_DELETE_WINDOW", False);
    NET_WM_NAME = XInternAtom(xdpy, "_NET_WM_NAME", False);
    NET_WM_STATE = XInternAtom(xdpy, "_NET_WM_STATE", False);
    NET_WM_STATE_FULLSCREEN = XInternAtom(xdpy, "_NET_WM_STATE_FULLSCREEN", False);
    WM_RELOAD_PAGE = XInternAtom(xdpy, "_WM_RELOAD_PAGE", False);

    xscr = DefaultScreen(xdpy);

    ximage_init(xdpy, xscr, DefaultVisual(xdpy, xscr));

    xcarrow = XCreateFontCursor(xdpy, XC_left_ptr);
    xchand = XCreateFontCursor(xdpy, XC_hand2);
    xcwait = XCreateFontCursor(xdpy, XC_watch);
    xccaret = XCreateFontCursor(xdpy, XC_xterm);

    xbgcolor.red = 0x7000;
    xbgcolor.green = 0x7000;
    xbgcolor.blue = 0x7000;

    xshcolor.red = 0x4000;
    xshcolor.green = 0x4000;
    xshcolor.blue = 0x4000;

    XAllocColor(xdpy, DefaultColormap(xdpy, xscr), &xbgcolor);
    XAllocColor(xdpy, DefaultColormap(xdpy, xscr), &xshcolor);

    xwin = XCreateWindow(xdpy, DefaultRootWindow(xdpy),
        10, 10, 200, 100, 0,
        ximage_get_depth(),
        InputOutput,
        ximage_get_visual(),
        0,
        NULL);
    if (xwin == None)
        fz_throw(gapp.ctx, FZ_ERROR_GENERIC, "cannot create window");

    XSetWindowColormap(xdpy, xwin, ximage_get_colormap());
    XSelectInput(xdpy, xwin,
        StructureNotifyMask | ExposureMask | KeyPressMask |
        PointerMotionMask | ButtonPressMask | ButtonReleaseMask);

    mapped = 0;

    xgc = XCreateGC(xdpy, xwin, 0, NULL);

    XDefineCursor(xdpy, xwin, xcarrow);

    wmhints = XAllocWMHints();
    if (wmhints)
    {
        wmhints->flags = IconPixmapHint | IconMaskHint;
        xicon = XCreateBitmapFromData(xdpy, xwin,
            (char*)mupdf_icon_bitmap_16_bits,
            mupdf_icon_bitmap_16_width,
            mupdf_icon_bitmap_16_height);
        xmask = XCreateBitmapFromData(xdpy, xwin,
            (char*)mupdf_icon_bitmap_16_mask_bits,
            mupdf_icon_bitmap_16_mask_width,
            mupdf_icon_bitmap_16_mask_height);
        if (xicon && xmask)
        {
            wmhints->icon_pixmap = xicon;
            wmhints->icon_mask = xmask;
            XSetWMHints(xdpy, xwin, wmhints);
        }
        XFree(wmhints);
    }

    classhint = XAllocClassHint();
    if (classhint)
    {
        classhint->res_name = "mupdf";
        classhint->res_class = "MuPDF";
        XSetClassHint(xdpy, xwin, classhint);
        XFree(classhint);
    }

    XSetWMProtocols(xdpy, xwin, &WM_DELETE_WINDOW, 1);

    x11fd = ConnectionNumber(xdpy);
}

void winclose(pdfapp_t *app)
{
    if (pdfapp_preclose(app))
    {
        closing = 1;
    }
}

int winsavequery(pdfapp_t *app)
{
    fprintf(stderr, "mupdf: discarded changes to document\n");
    /* FIXME: temporary dummy implementation */
    return DISCARD;
}

int wingetsavepath(pdfapp_t *app, char *buf, int len)
{
    /* FIXME: temporary dummy implementation */
    return 0;
}

void winreplacefile(char *source, char *target)
{
    rename(source, target);
}

void wincopyfile(char *source, char *target)
{
    char *buf = malloc(strlen(source)+strlen(target)+5);
    if (buf)
    {
        sprintf(buf, "cp %s %s", source, target);
        system(buf);
        free(buf);
    }
}

void cleanup(pdfapp_t *app)
{
    fz_context *ctx = app->ctx;

    pdfapp_close(app);

    XDestroyWindow(xdpy, xwin);

    XFreePixmap(xdpy, xicon);

    XFreeCursor(xdpy, xccaret);
    XFreeCursor(xdpy, xcwait);
    XFreeCursor(xdpy, xchand);
    XFreeCursor(xdpy, xcarrow);

    XFreeGC(xdpy, xgc);

    XCloseDisplay(xdpy);

    fz_drop_context(ctx);
}

static int winresolution()
{
    return DisplayWidth(xdpy, xscr) * 25.4 /
        DisplayWidthMM(xdpy, xscr) + 0.5;
}

void wincursor(pdfapp_t *app, int curs)
{
    if (curs == ARROW)
        XDefineCursor(xdpy, xwin, xcarrow);
    if (curs == HAND)
        XDefineCursor(xdpy, xwin, xchand);
    if (curs == WAIT)
        XDefineCursor(xdpy, xwin, xcwait);
    if (curs == CARET)
        XDefineCursor(xdpy, xwin, xccaret);
    XFlush(xdpy);
}

void wintitle(pdfapp_t *app, char *s)
{
    XStoreName(xdpy, xwin, s);
#ifdef X_HAVE_UTF8_STRING
    Xutf8SetWMProperties(xdpy, xwin, s, s, NULL, 0, NULL, NULL, NULL);
#else
    XmbSetWMProperties(xdpy, xwin, s, s, NULL, 0, NULL, NULL, NULL);
#endif
    XChangeProperty(xdpy, xwin, NET_WM_NAME, XA_UTF8_STRING, 8,
            PropModeReplace, (unsigned char *)s, strlen(s));
}

void winhelp(pdfapp_t *app)
{
    fprintf(stderr, "%s\n%s", pdfapp_version(app), pdfapp_usage(app));
}

void winresize(pdfapp_t *app, int w, int h)
{
    int image_w = fz_pixmap_width(gapp.ctx, gapp.image);
    int image_h = fz_pixmap_height(gapp.ctx, gapp.image);
    XWindowChanges values;
    int mask, width, height;

    mask = CWWidth | CWHeight;
    values.width = w;
    values.height = h;
    XConfigureWindow(xdpy, xwin, mask, &values);

    reqw = w;
    reqh = h;

    if (!mapped)
    {
        gapp.winw = w;
        gapp.winh = h;
        width = -1;
        height = -1;

        XMapWindow(xdpy, xwin);
        XFlush(xdpy);

        while (1)
        {
            XNextEvent(xdpy, &xevt);
            if (xevt.type == ConfigureNotify)
            {
                width = xevt.xconfigure.width;
                height = xevt.xconfigure.height;
            }
            if (xevt.type == MapNotify)
                break;
        }

        XSetForeground(xdpy, xgc, WhitePixel(xdpy, xscr));
        XFillRectangle(xdpy, xwin, xgc, 0, 0, image_w, image_h);
        XFlush(xdpy);

        if (width != reqw || height != reqh)
        {
            gapp.shrinkwrap = 0;
            dirty = 1;
            pdfapp_onresize(&gapp, width, height);
        }

        mapped = 1;
    }
}

void winfullscreen(pdfapp_t *app, int state)
{
    XEvent xev;
    xev.xclient.type = ClientMessage;
    xev.xclient.serial = 0;
    xev.xclient.send_event = True;
    xev.xclient.window = xwin;
    xev.xclient.message_type = NET_WM_STATE;
    xev.xclient.format = 32;
    xev.xclient.data.l[0] = state;
    xev.xclient.data.l[1] = NET_WM_STATE_FULLSCREEN;
    xev.xclient.data.l[2] = 0;
    XSendEvent(xdpy, DefaultRootWindow(xdpy), False,
        SubstructureRedirectMask | SubstructureNotifyMask,
        &xev);
}

static void fillrect(int x, int y, int w, int h)
{
    if (w > 0 && h > 0)
        XFillRectangle(xdpy, xwin, xgc, x, y, w, h);
}

static void winblitstatusbar(pdfapp_t *app)
{
    if (gapp.issearching)
    {
        char buf[sizeof(gapp.search) + 50];
        sprintf(buf, "Search: %s", gapp.search);
        XSetForeground(xdpy, xgc, WhitePixel(xdpy, xscr));
        fillrect(0, 0, gapp.winw, 30);
        windrawstring(&gapp, 10, 20, buf);
    }
    else if (showingmessage)
    {
        XSetForeground(xdpy, xgc, WhitePixel(xdpy, xscr));
        fillrect(0, 0, gapp.winw, 30);
        windrawstring(&gapp, 10, 20, message);
    }
    else if (showingpage)
    {
        char buf[42];
        snprintf(buf, sizeof buf, "Page %d/%d", gapp.pageno, gapp.pagecount);
        windrawstringxor(&gapp, 10, 20, buf);
    }
}

static void winblit(pdfapp_t *app)
{
    int image_w = fz_pixmap_width(gapp.ctx, gapp.image);
    int image_h = fz_pixmap_height(gapp.ctx, gapp.image);
    int image_n = fz_pixmap_components(gapp.ctx, gapp.image);
    unsigned char *image_samples = fz_pixmap_samples(gapp.ctx, gapp.image);
    int x0 = gapp.panx;
    int y0 = gapp.pany;
    int x1 = gapp.panx + image_w;
    int y1 = gapp.pany + image_h;

    XSetForeground(xdpy, xgc, xbgcolor.pixel);
    fillrect(0, 0, x0, gapp.winh);
    fillrect(x1, 0, gapp.winw - x1, gapp.winh);
    fillrect(0, 0, gapp.winw, y0);
    fillrect(0, y1, gapp.winw, gapp.winh - y1);

    XSetForeground(xdpy, xgc, xshcolor.pixel);
    fillrect(x0+2, y1, image_w, 2);
    fillrect(x1, y0+2, 2, image_h);

    if (gapp.iscopying || justcopied)
    {
        pdfapp_invert(&gapp, &gapp.selr);
        justcopied = 1;
    }

    pdfapp_inverthit(&gapp);

    if (image_n == 4)
        ximage_blit(xwin, xgc,
            x0, y0,
            image_samples,
            0, 0,
            image_w,
            image_h,
            image_w * image_n);
    else if (image_n == 2)
    {
        int i = image_w*image_h;
        unsigned char *color = malloc(i*4);
        if (color)
        {
            unsigned char *s = image_samples;
            unsigned char *d = color;
            for (; i > 0 ; i--)
            {
                d[2] = d[1] = d[0] = *s++;
                d[3] = *s++;
                d += 4;
            }
            ximage_blit(xwin, xgc,
                x0, y0,
                color,
                0, 0,
                image_w,
                image_h,
                image_w * 4);
            free(color);
        }
    }

    pdfapp_inverthit(&gapp);

    if (gapp.iscopying || justcopied)
    {
        pdfapp_invert(&gapp, &gapp.selr);
        justcopied = 1;
    }

    winblitstatusbar(app);
}

void winrepaint(pdfapp_t *app)
{
    dirty = 1;
    if (app->in_transit)
        transition_dirty = 1;
}

void winrepaintsearch(pdfapp_t *app)
{
    dirtysearch = 1;
}

void winadvancetimer(pdfapp_t *app, float duration)
{
    struct timeval now;

    gettimeofday(&now, NULL);
    memset(&tmo_advance, 0, sizeof(tmo_advance));
    tmo_advance.tv_sec = (int)duration;
    tmo_advance.tv_usec = 1000000 * (duration - tmo_advance.tv_sec);
    timeradd(&tmo_advance, &now, &tmo_advance);
    advance_scheduled = 1;
}

void windrawstringxor(pdfapp_t *app, int x, int y, char *s)
{
    int prevfunction;
    XGCValues xgcv;

    XGetGCValues(xdpy, xgc, GCFunction, &xgcv);
    prevfunction = xgcv.function;
    xgcv.function = GXxor;
    XChangeGC(xdpy, xgc, GCFunction, &xgcv);

    XSetForeground(xdpy, xgc, WhitePixel(xdpy, DefaultScreen(xdpy)));

    XDrawString(xdpy, xwin, xgc, x, y, s, strlen(s));
    XFlush(xdpy);

    XGetGCValues(xdpy, xgc, GCFunction, &xgcv);
    xgcv.function = prevfunction;
    XChangeGC(xdpy, xgc, GCFunction, &xgcv);
}

void windrawstring(pdfapp_t *app, int x, int y, char *s)
{
    XSetForeground(xdpy, xgc, BlackPixel(xdpy, DefaultScreen(xdpy)));
    XDrawString(xdpy, xwin, xgc, x, y, s, strlen(s));
}

void docopy(pdfapp_t *app, Atom copy_target)
{
    unsigned short copyucs2[16 * 1024];
    char *latin1 = copylatin1;
    char *utf8 = copyutf8;
    unsigned short *ucs2;
    int ucs;

    pdfapp_oncopy(&gapp, copyucs2, 16 * 1024);

    for (ucs2 = copyucs2; ucs2[0] != 0; ucs2++)
    {
        ucs = ucs2[0];

        utf8 += fz_runetochar(utf8, ucs);

        if (ucs < 256)
            *latin1++ = ucs;
        else
            *latin1++ = '?';
    }

    *utf8 = 0;
    *latin1 = 0;

    XSetSelectionOwner(xdpy, copy_target, xwin, copytime);

    justcopied = 1;
}

void windocopy(pdfapp_t *app)
{
    docopy(app, XA_PRIMARY);
}

void onselreq(Window requestor, Atom selection, Atom target, Atom property, Time time)
{
    XEvent nevt;

    advance_scheduled = 0;

    if (property == None)
        property = target;

    nevt.xselection.type = SelectionNotify;
    nevt.xselection.send_event = True;
    nevt.xselection.display = xdpy;
    nevt.xselection.requestor = requestor;
    nevt.xselection.selection = selection;
    nevt.xselection.target = target;
    nevt.xselection.property = property;
    nevt.xselection.time = time;

    if (target == XA_TARGETS)
    {
        Atom atomlist[4];
        atomlist[0] = XA_TARGETS;
        atomlist[1] = XA_TIMESTAMP;
        atomlist[2] = XA_STRING;
        atomlist[3] = XA_UTF8_STRING;
        XChangeProperty(xdpy, requestor, property, target,
            32, PropModeReplace,
            (unsigned char *)atomlist, sizeof(atomlist)/sizeof(Atom));
    }

    else if (target == XA_STRING)
    {
        XChangeProperty(xdpy, requestor, property, target,
            8, PropModeReplace,
            (unsigned char *)copylatin1, strlen(copylatin1));
    }

    else if (target == XA_UTF8_STRING)
    {
        XChangeProperty(xdpy, requestor, property, target,
            8, PropModeReplace,
            (unsigned char *)copyutf8, strlen(copyutf8));
    }

    else
    {
        nevt.xselection.property = None;
    }

    XSendEvent(xdpy, requestor, False, 0, &nevt);
}

void winreloadpage(pdfapp_t *app)
{
    XEvent xev;
    Display *dpy = XOpenDisplay(NULL);

    xev.xclient.type = ClientMessage;
    xev.xclient.serial = 0;
    xev.xclient.send_event = True;
    xev.xclient.window = xwin;
    xev.xclient.message_type = WM_RELOAD_PAGE;
    xev.xclient.format = 32;
    xev.xclient.data.l[0] = 0;
    xev.xclient.data.l[1] = 0;
    xev.xclient.data.l[2] = 0;
    XSendEvent(dpy, xwin, 0, 0, &xev);
    XCloseDisplay(dpy);
}

void winopenuri(pdfapp_t *app, char *buf)
{
    char *browser = getenv("BROWSER");
    pid_t pid;
    if (!browser)
    {
#ifdef __APPLE__
        browser = "open";
#else
        browser = "xdg-open";
#endif
    }
    /* Fork once to start a child process that we wait on. This
     * child process forks again and immediately exits. The
     * grandchild process continues in the background. The purpose
     * of this strange two-step is to avoid zombie processes. See
     * bug 695701 for an explanation. */
    pid = fork();
    if (pid == 0)
    {
        if (fork() == 0)
        {
            execlp(browser, browser, buf, (char*)0);
            fprintf(stderr, "cannot exec '%s'\n", browser);
        }
        exit(0);
    }
    waitpid(pid, NULL, 0);
}

static void onkey(int c, int modifiers)
{
    advance_scheduled = 0;

    if (justcopied)
    {
        justcopied = 0;
        winrepaint(&gapp);
    }

    if (!gapp.issearching && c == 'P')
    {
        struct timeval now;
        struct timeval tmo;
        tmo.tv_sec = 2;
        tmo.tv_usec = 0;
        gettimeofday(&now, NULL);
        timeradd(&now, &tmo, &tmo_at);
        showingpage = 1;
        winrepaint(&gapp);
        return;
    }

    pdfapp_onkey(&gapp, c, modifiers);

    if (gapp.issearching)
    {
        showingpage = 0;
        showingmessage = 0;
    }
}

static void onmouse(int x, int y, int btn, int modifiers, int state)
{
    if (state != 0)
        advance_scheduled = 0;

    if (state != 0 && justcopied)
    {
        justcopied = 0;
        winrepaint(&gapp);
    }

    pdfapp_onmouse(&gapp, x, y, btn, modifiers, state);
}

static void signal_handler(int signal)
{
    if (signal == SIGHUP)
        reloading = 1;
}

static void usage(void)
{
    fprintf(stderr, "usage: mupdf [options] file.pdf [page]\n");
    fprintf(stderr, "\t-p -\tpassword\n");
    fprintf(stderr, "\t-r -\tresolution\n");
    fprintf(stderr, "\t-A -\tset anti-aliasing quality in bits (0=off, 8=best)\n");
    fprintf(stderr, "\t-C -\tRRGGBB (tint color in hexadecimal syntax)\n");
    fprintf(stderr, "\t-W -\tpage width for EPUB layout\n");
    fprintf(stderr, "\t-H -\tpage height for EPUB layout\n");
    fprintf(stderr, "\t-S -\tfont size for EPUB layout\n");
    fprintf(stderr, "\t-U -\tuser style sheet for EPUB layout\n");
    exit(1);
}

int main(int argc, char **argv)
{
    int c;
    int len;
    char buf[128];
    KeySym keysym;
    int oldx = 0;
    int oldy = 0;
    int resolution = -1;
    int pageno = 1;
    fd_set fds;
    int width = -1;
    int height = -1;
    fz_context *ctx;
    struct timeval now;
    struct timeval *timeout;
    struct timeval tmo_advance_delay;

    ctx = fz_new_context(NULL, NULL, FZ_STORE_DEFAULT);
    if (!ctx)
    {
        fprintf(stderr, "cannot initialise context\n");
        exit(1);
    }

    pdfapp_init(ctx, &gapp);

    while ((c = fz_getopt(argc, argv, "p:r:A:C:W:H:S:U:")) != -1)
    {
        switch (c)
        {
        case 'C':
            c = strtol(fz_optarg, NULL, 16);
            gapp.tint = 1;
            gapp.tint_r = (c >> 16) & 255;
            gapp.tint_g = (c >> 8) & 255;
            gapp.tint_b = (c) & 255;
            break;
        case 'p': password = fz_optarg; break;
        case 'r': resolution = atoi(fz_optarg); break;
        case 'A': fz_set_aa_level(ctx, atoi(fz_optarg)); break;
        case 'W': gapp.layout_w = fz_atof(fz_optarg); break;
        case 'H': gapp.layout_h = fz_atof(fz_optarg); break;
        case 'S': gapp.layout_em = fz_atof(fz_optarg); break;
        case 'U': gapp.layout_css = fz_optarg; break;
        default: usage();
        }
    }

    if (argc - fz_optind == 0)
        usage();

    filename = argv[fz_optind++];

    if (argc - fz_optind == 1)
        pageno = atoi(argv[fz_optind++]);

    winopen();

    if (resolution == -1)
        resolution = winresolution();
    if (resolution < MINRES)
        resolution = MINRES;
    if (resolution > MAXRES)
        resolution = MAXRES;

    gapp.transitions_enabled = 1;
    gapp.scrw = DisplayWidth(xdpy, xscr);
    gapp.scrh = DisplayHeight(xdpy, xscr);
    gapp.resolution = resolution;
    gapp.pageno = pageno;

    tmo_at.tv_sec = 0;
    tmo_at.tv_usec = 0;
    timeout = NULL;

    pdfapp_open(&gapp, filename, 0);

    FD_ZERO(&fds);

    signal(SIGHUP, signal_handler);

    while (!closing)
    {
        while (!closing && XPending(xdpy) && !transition_dirty)
        {
            XNextEvent(xdpy, &xevt);

            switch (xevt.type)
            {
            case Expose:
                dirty = 1;
                break;

            case ConfigureNotify:
                if (gapp.image)
                {
                    if (xevt.xconfigure.width != reqw ||
                        xevt.xconfigure.height != reqh)
                        gapp.shrinkwrap = 0;
                }
                width = xevt.xconfigure.width;
                height = xevt.xconfigure.height;

                break;

            case KeyPress:
                len = XLookupString(&xevt.xkey, buf, sizeof buf, &keysym, NULL);

                if (!gapp.issearching)
                    switch (keysym)
                    {
                    case XK_Escape:
                        len = 1; buf[0] = '\033';
                        break;

                    case XK_Up:
                        len = 1; buf[0] = 'k';
                        break;
                    case XK_Down:
                        len = 1; buf[0] = 'j';
                        break;

                    case XK_Left:
                        len = 1; buf[0] = 'b';
                        break;
                    case XK_Right:
                        len = 1; buf[0] = ' ';
                        break;

                    case XK_Page_Up:
                    case XF86XK_Back:
                        len = 1; buf[0] = ',';
                        break;
                    case XK_Page_Down:
                    case XF86XK_Forward:
                        len = 1; buf[0] = '.';
                        break;
                    }
                if (xevt.xkey.state & ControlMask && keysym == XK_c)
                    docopy(&gapp, XA_CLIPBOARD);
                else if (len)
                    onkey(buf[0], xevt.xkey.state);

                onmouse(oldx, oldy, 0, 0, 0);

                break;

            case MotionNotify:
                oldx = xevt.xmotion.x;
                oldy = xevt.xmotion.y;
                onmouse(xevt.xmotion.x, xevt.xmotion.y, 0, xevt.xmotion.state, 0);
                break;

            case ButtonPress:
                onmouse(xevt.xbutton.x, xevt.xbutton.y, xevt.xbutton.button, xevt.xbutton.state, 1);
                break;

            case ButtonRelease:
                copytime = xevt.xbutton.time;
                onmouse(xevt.xbutton.x, xevt.xbutton.y, xevt.xbutton.button, xevt.xbutton.state, -1);
                break;

            case SelectionRequest:
                onselreq(xevt.xselectionrequest.requestor,
                    xevt.xselectionrequest.selection,
                    xevt.xselectionrequest.target,
                    xevt.xselectionrequest.property,
                    xevt.xselectionrequest.time);
                break;

            case ClientMessage:
                if (xevt.xclient.message_type == WM_RELOAD_PAGE)
                    pdfapp_reloadpage(&gapp);
                else if (xevt.xclient.format == 32 && xevt.xclient.data.l[0] == WM_DELETE_WINDOW)
                    closing = 1;
                break;
            }
        }

        if (closing)
            continue;

        if (width != -1 || height != -1)
        {
            pdfapp_onresize(&gapp, width, height);
            width = -1;
            height = -1;
        }

        if (dirty || dirtysearch)
        {
            if (dirty)
                winblit(&gapp);
            else if (dirtysearch)
                winblitstatusbar(&gapp);
            dirty = 0;
            transition_dirty = 0;
            dirtysearch = 0;
            pdfapp_postblit(&gapp);
        }

        if (!showingpage && !showingmessage && (tmo_at.tv_sec || tmo_at.tv_usec))
        {
            tmo_at.tv_sec = 0;
            tmo_at.tv_usec = 0;
            timeout = NULL;
        }

        if (XPending(xdpy) || transition_dirty)
            continue;

        timeout = NULL;

        if (tmo_at.tv_sec || tmo_at.tv_usec)
        {
            gettimeofday(&now, NULL);
            timersub(&tmo_at, &now, &tmo);
            if (tmo.tv_sec <= 0)
            {
                tmo_at.tv_sec = 0;
                tmo_at.tv_usec = 0;
                timeout = NULL;
                showingpage = 0;
                showingmessage = 0;
                winrepaint(&gapp);
            }
            else
                timeout = &tmo;
        }

        if (advance_scheduled)
        {
            gettimeofday(&now, NULL);
            timersub(&tmo_advance, &now, &tmo_advance_delay);
            if (tmo_advance_delay.tv_sec <= 0)
            {
                /* Too late already */
                onkey(' ', 0);
                onmouse(oldx, oldy, 0, 0, 0);
                advance_scheduled = 0;
            }
            else if (timeout == NULL)
            {
                timeout = &tmo_advance_delay;
            }
            else
            {
                struct timeval tmp;
                timersub(&tmo_advance_delay, timeout, &tmp);
                if (tmp.tv_sec < 0)
                {
                    timeout = &tmo_advance_delay;
                }
            }
        }

        FD_SET(x11fd, &fds);
        if (select(x11fd + 1, &fds, NULL, NULL, timeout) < 0)
        {
            if (reloading)
            {
                pdfapp_reloadfile(&gapp);
                reloading = 0;
            }
        }
        if (!FD_ISSET(x11fd, &fds))
        {
            if (timeout == &tmo_advance_delay)
            {
                onkey(' ', 0);
                onmouse(oldx, oldy, 0, 0, 0);
                advance_scheduled = 0;
            }
            else
            {
                tmo_at.tv_sec = 0;
                tmo_at.tv_usec = 0;
                timeout = NULL;
                showingpage = 0;
                showingmessage = 0;
                winrepaint(&gapp);
            }
        }
    }

    cleanup(&gapp);

    return 0;
}