/*
 * $Id: x11.c,v 1.32 2005/01/14 18:58:03 phil Exp $
 * X11 support for XY display simulator
 * Phil Budne <phil@ultimate.com>
 * September 2003
 *
 * Changes from Douglas A. Gwyn, Jan 8, 2004
 *
 * started from PDP-8/E simulator (vc8e.c & kc8e.c);
 *      This PDP8 Emulator was written by Douglas W. Jones at the
 *      University of Iowa.  It is distributed as freeware, of
 *      uncertain function and uncertain utility.
 */

/*
 * Copyright (c) 2003-2018, Philip L. Budne
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * Except as contained in this notice, the names of the authors shall
 * not be used in advertising or otherwise to promote the sale, use or
 * other dealings in this Software without prior written authorization
 * from the authors.
 */

#ifndef USE_XKB
#define USE_XKB 1
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ws.h"
#include "display.h"

#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Intrinsic.h>
#include <X11/StringDefs.h>
#include <X11/Core.h>
#include <X11/Shell.h>
#include <X11/cursorfont.h>
#ifdef USE_XKB
#include <X11/XKBlib.h>
#endif

#include <sys/types.h>
#include <sys/time.h>

#ifndef PIX_SIZE
#define PIX_SIZE 1
#endif

/*
 * light pen location
 * see ws.h for full description
 */
int ws_lp_x = -1;
int ws_lp_y = -1;

static XtAppContext app_context; /* the topmost context for everything */
static Display* dpy;            /* its display */
static int      scr;            /* its screen */
static Colormap cmap;           /* its colormap */
static Widget crtshell;         /* the X window shell */
static Widget crt;              /* the X window in which output will plot */
static int xpixels, ypixels;
#ifdef FULL_SCREEN
/* occupy entire screen for vintage computer fan Sellam Ismail */
static int xoffset, yoffset;
#endif

static GC whiteGC;              /* gc with white foreground */
static GC blackGC;              /* gc with black foreground */
static int buttons = 0;         /* tracks state of all buttons */

static int os_pollfd(int, int); /* forward */

/* here on any mouse button down, AND movement when any button down */
static void
handle_button_press(w, d, e, b)
    Widget w;
    XtPointer d;
    XEvent *e;
    Boolean *b;
{
    int x, y;

    x = e->xbutton.x;
    y = e->xbutton.y;
#ifdef FULL_SCREEN
    /* untested! */
    x -= xoffset;
    y -= yoffset;
#endif
#if PIX_SIZE > 1
    x *= PIX_SIZE;
    y *= PIX_SIZE;
#endif

    if (!display_tablet)
        /* crosshair cursor to indicate tip of active pen */
        XDefineCursor(dpy, XtWindow(crt),
                        (Cursor) XCreateFontCursor(dpy, XC_crosshair));

    y = ypixels - y - 1;
    /*printf("lightpen at %d,%d\n", x, y); fflush(stdout);*/
    ws_lp_x = x;
    ws_lp_y = y;

    if (e->type == ButtonPress) {
        buttons |= e->xbutton.button;

        if (e->xbutton.button == 1) {
            display_lp_sw = 1;
            /*printf("tip switch activated\n"); fflush(stdout);*/
        }
    }

    if (b)
        *b = TRUE;
}

static void
handle_button_release(w, d, e, b)
    Widget w;
    XtPointer d;
    XEvent *e;
    Boolean *b;
{
    if ((buttons &= ~e->xbutton.button) == 0) { /* all buttons released */
        if (!display_tablet)
            /* pencil cursor (close to a pen!) to indicate inactive pen posn */
            XDefineCursor(dpy, XtWindow(crt),
                                (Cursor) XCreateFontCursor(dpy, XC_pencil));

        /* XXX change cursor back?? */
        ws_lp_x = ws_lp_y = -1;
    }

    if (e->xbutton.button == 1) {
        display_lp_sw = 0;
        /*printf("tip switch deactivated\n"); fflush(stdout);*/
    }

    if (b)
        *b = TRUE;
}

/*
 * map keyboard XEvent to 8 bit char or -1
 */
static int
mapkey(e)
    XEvent *e;
{
    int shift = (ShiftMask & e->xkey.state) != 0;
    KeySym key;

#ifdef USE_XKB
    /*
     * X Keyboard Extension
     * described in
     * https://www.x.org/releases/X11R7.7/doc/libX11/XKB/xkblib.html#Xkb_Keyboard_Extension_Support_for_Keyboards
     * copyright 1995, 1996
     *
     * use XkbLibraryVersion and/or XkbQueryExtension
     * to determine if available???
     */
    key = XkbKeycodeToKeysym(dpy, e->xkey.keycode, 0, shift);
#elif 1
    /*
     * documented in
     * Xlib: C Language X Interface (X Version 11, Release 4)
     * copyright 1989
     */
    int keysyms_per_keycode_return;
    KeySym *keysyms = XGetKeyboardMapping(dpy,
                                          e->xkey.keycode,
                                          1,
                                          &keysyms_per_keycode_return);
    key = keysyms[0];
    XFree(keysyms);
#else  /* just in case... */
    /* XKeycodeToKeysym deprecated */
    key = XKeycodeToKeysym(dpy, e->xkey.keycode, shift);
#endif

    if ((key & 0xff00) == 0)
        return key & 0xff;

    switch (key) {
    case XK_Return: return '\r';
    }
    /* printf("ignoring keycode %#x\r\n", key); /**/
    return -1;
}

static void
handle_key_press(w, d, e, b)
    Widget w;
    XtPointer d;
    XEvent *e;
    Boolean *b;
{
    int k = mapkey(e);
    if (k >= 0)
        display_keydown(k);

    if (b)
        *b = TRUE;
}

static void
handle_key_release(w, d, e, b)
    Widget w;
    XtPointer d;
    XEvent *e;
    Boolean *b;
{
    int k = mapkey(e);
    if (k >= 0)
        display_keyup(k);

    if (b)
        *b = TRUE;
}

static void
handle_exposure(w, d, e, b)
    Widget w;
    XtPointer d;
    XEvent *e;
    Boolean *b;
{
    display_repaint();

    if (b)
        *b = TRUE;
}

int
ws_init(const char *crtname,    /* crt type name */
    int xp, int yp,             /* screen size in pixels */
    int colors,                 /* colors to support (not used) */
    void *dptr)
{
    Arg arg[25];
    XGCValues gcvalues;
    unsigned int n;
    int argc;
    char *argv[1];
    int height, width;
#ifdef USE_XKB
    Bool supported;
#endif
    xpixels = xp;               /* save screen size */
    ypixels = yp;

    if (getenv ("DISPLAY") == NULL) {
        return 0;
    }

    XtToolkitInitialize();
    app_context = XtCreateApplicationContext();
    argc = 0;
    argv[0] = NULL;
    dpy = XtOpenDisplay( app_context, NULL, NULL, crtname, NULL, 0,
                        &argc, argv);

#ifdef USE_XKB
    /*
     * suppress synthetic key up events from autorepeat
     * (will still see repeated down events)
     * see keymap function for XKb history
     */
    supported = False;
    (void) XkbSetDetectableAutoRepeat(dpy, True, &supported);
#endif

    scr = DefaultScreen(dpy);

    crtshell = XtAppCreateShell( crtname, /* app name */
                                crtname, /* app class */
                                applicationShellWidgetClass, /* wclass */
                                dpy, /* display */
                                NULL, /* arglist */
                                0);     /* nargs */

    cmap = DefaultColormap(dpy, scr);

    /*
     * Create a drawing area
     */

    n = 0;
#ifdef FULL_SCREEN
    /* center raster in full-screen black window */
    width = DisplayWidth(dpy,scr);
    height = DisplayHeight(dpy,scr);

    xoffset = (width - xpixels*PIX_SIZE)/2;
    yoffset = (height - ypixels*PIX_SIZE)/2;
#else
    width = xpixels*PIX_SIZE;
    height = ypixels*PIX_SIZE;
#endif
    XtSetArg(arg[n], XtNwidth, width);                          n++;
    XtSetArg(arg[n], XtNheight, height);                        n++;
    XtSetArg(arg[n], XtNbackground, BlackPixel( dpy, scr ));    n++;

    crt = XtCreateWidget( crtname, widgetClass, crtshell, arg, n);
    XtManageChild(crt);
    XtPopup(crtshell, XtGrabNonexclusive);
    XtSetKeyboardFocus(crtshell, crt);  /* experimental? */

    /*
     * Create black and white Graphics Contexts
     */

    gcvalues.foreground = BlackPixel( dpy, scr );
    gcvalues.background = BlackPixel( dpy, scr );
    blackGC = XCreateGC(dpy, XtWindow(crt),
                        GCForeground | GCBackground, &gcvalues);

    gcvalues.foreground = WhitePixel( dpy, scr );
    whiteGC = XCreateGC(dpy, XtWindow(crt),
                        GCForeground | GCBackground, &gcvalues);

    if (!display_tablet) {
        /* pencil cursor */
        XDefineCursor(dpy, XtWindow(crt),
                      (Cursor) XCreateFontCursor(dpy, XC_pencil));
    }

    /*
     * Setup to handle events
     */

    XtAddEventHandler(crt, ButtonPressMask|ButtonMotionMask, FALSE,
                      handle_button_press, NULL);
    XtAddEventHandler(crt, ButtonReleaseMask, FALSE,
                      handle_button_release, NULL);
    XtAddEventHandler(crt, KeyPressMask, FALSE,
                      handle_key_press, NULL);
    XtAddEventHandler(crt, KeyReleaseMask, FALSE,
                      handle_key_release, NULL);
    XtAddEventHandler(crt, ExposureMask, FALSE,
                      handle_exposure, NULL);
    return 1;
} /* ws_init */

void ws_shutdown (void)
{
}

void *
ws_color_black(void)
{
    return blackGC;
}

void *
ws_color_white(void)
{
    return whiteGC;
}

void *
ws_color_rgb(int r, int g, int b)
{
    XColor color;

    color.red = r;
    color.green = g;
    color.blue = b;
    /* ignores flags */

    if (XAllocColor(dpy, cmap, &color)) {
        XGCValues gcvalues;
        memset(&gcvalues, 0, sizeof(gcvalues));
        gcvalues.foreground = gcvalues.background = color.pixel;
        return XCreateGC(dpy, XtWindow(crt),
                         GCForeground | GCBackground,
                         &gcvalues);
    }
    /* allocation failed */
    return NULL;
}

/* put a point on the screen */
void
ws_display_point(int x, int y, void *color)
{
    GC gc = (GC) color;

    if (x > xpixels || y > ypixels)
        return;

    y = ypixels - y - 1;                /* X11 coordinate system */

#ifdef FULL_SCREEN
    x += xoffset;
    y += yoffset;
#endif
    if (gc == NULL)
        gc = blackGC;                   /* default to off */
#if PIX_SIZE == 1
    XDrawPoint(dpy, XtWindow(crt), gc, x, y);
#else
    XFillRectangle(dpy, XtWindow(crt), gc,
                   x*PIX_SIZE, y*PIX_SIZE, PIX_SIZE, PIX_SIZE);
#endif
}

void
ws_sync(void)
{
    XFlush(dpy);
}

/*
 * elapsed wall clock time since last call
 * +INF on first call
 */

struct elapsed_state {
    struct timeval tvs[2];
    int new;
};

static unsigned long
elapsed(struct elapsed_state *ep)
{
    unsigned long val;

    gettimeofday(&ep->tvs[ep->new], NULL);
    if (ep->tvs[!ep->new].tv_sec == 0)
        val = ~0L;
    else
        val = ((ep->tvs[ep->new].tv_sec - ep->tvs[!ep->new].tv_sec) * 1000000 +
               (ep->tvs[ep->new].tv_usec - ep->tvs[!ep->new].tv_usec));
    ep->new = !ep->new;
    return val;
}

/* called periodically */
int
ws_poll(int *valp, int maxusec)
{
    static struct elapsed_state es;     /* static to avoid clearing! */

#ifdef WS_POLL_DEBUG
    printf("ws_poll %d\n", maxusec);
    fflush(stdout);
#endif
    elapsed(&es);                       /* start clock */
    do {
        unsigned long e;

        /* tried checking return, but lost on TCP connections? */
        os_pollfd(ConnectionNumber(dpy), maxusec);

        while (XtAppPending(app_context)) {
            XEvent event;

            /* XXX check for connection loss; set *valp? return 0 */
            XtAppNextEvent(app_context, &event );
            XtDispatchEvent( &event );
        }
        e = elapsed(&es);
#ifdef WS_POLL_DEBUG
        printf(" maxusec %d e %d\r\n", maxusec, e);
        fflush(stdout);
#endif
        maxusec -= e;
    } while (maxusec > 10000);  /* 10ms */
    return 1;
}

/* utility: can be called from main program
 * which is willing to cede control
 */
int
ws_loop(void (*func)(void *), void *arg)
{
    int val;

    /* XXX use XtAppAddWorkProc & XtAppMainLoop? */
    while (ws_poll(&val,0))
        (*func)(arg);
    return val;
}

void
ws_beep(void)
{
    XBell(dpy, 0);                      /* ring at base volume */
    XFlush(dpy);
}

/****************
 * could move these to unix.c, if VMS versions needed
 * (or just (GASP!) ifdef)
 */

/* public version, used by delay code */
unsigned long
os_elapsed(void)
{
    static struct elapsed_state es;
    return elapsed(&es);
}

/*
 * select/DisplayNumber works on VMS 7.0+?
 * could move to "unix.c"
 * (I have some nasty VMS code that's supposed to to the job
 * for older systems)
 */

/*
 * sleep for maxus microseconds, returning TRUE sooner if fd is readable
 * used by X11 driver
 */
static int
os_pollfd(int fd, int maxus)
{

    /* use trusty old select (most portable) */
    fd_set rfds;
    struct timeval tv;

    if (maxus >= 1000000) {             /* not bloody likely, avoid divide */
        tv.tv_sec = maxus / 1000000;
        tv.tv_usec = maxus % 1000000;
    }
    else {
        tv.tv_sec = 0;
        tv.tv_usec = maxus;
    }
    FD_ZERO(&rfds);
    FD_SET(fd, &rfds);
    return select(fd+1, &rfds, NULL, NULL, &tv) > 0;
}