#include "ibm1130_defs.h"

/* ibm1130_gdu.c: IBM 1130 2250 Graphical Display Unit

   (Under construction)
   stuff to fix:
   "store revert" might be backwards?
   alpha keyboard is not implemented
   pushbuttons are not implemented
   there is something about interrupts being deferred during a subroutine transition?

   Based on the SIMH package written by Robert M Supnik

 * (C) Copyright 2002, Brian Knittel.
 * You may freely use this program, but: it offered strictly on an AS-IS, AT YOUR OWN
 * RISK basis, there is no warranty of fitness for any purpose, and the rest of the
 * usual yada-yada. Please keep this notice and the copyright in any distributions
 * or modifications.
 *
 * This is not a supported product, but I welcome bug reports and fixes.
 * Mail to simh@ibm1130.org
 */

#define BLIT_MODE                   /* define for better performance, undefine when debugging generate_image() */
/* #define DEBUG_LIGHTPEN */        /* define to debug light-pen sensing */

#define DEFAULT_GDU_RATE      20    /* default frame rate */
#define DEFAULT_PEN_THRESHOLD  3    /* default looseness of light-pen hit */
#define INDWIDTH              32    /* width of an indicator (there are two columns of these) */
#define INITSIZE             512    /* initial window size */

#define GDU_DSW_ORDER_CONTROLLED_INTERRUPT  0x8000
#define GDU_DSW_KEYBOARD_INTERUPT           0x4000
#define GDU_DSW_DETECT_INTERRUPT            0x2000
#define GDU_DSW_CYCLE_STEAL_CHECK           0x1000
#define GDU_DSW_DETECT_STATUS               0x0800
#define GDU_DSW_LIGHT_PEN_SWITCH            0x0100
#define GDU_DSW_BUSY                        0x0080
#define GDU_DSW_CHARACTER_MODE              0x0040
#define GDU_DSW_POINT_MODE                  0x0020
#define GDU_DSW_ADDR_DISP                   0x0003

#define GDU_FKEY_DATA_AVAILABLE             0x8000
#define GDU_FKEY_KEY_CODE                   0x1F00
#define GDU_FKEY_OVERLAY_CODE               0x00FF

#define GDU_AKEY_DATA_AVAILABLE             0x8000
#define GDU_AKEY_END                        0x1000
#define GDU_AKEY_CANCEL                     0x0800
#define GDU_AKEY_ADVANCE                    0x0400
#define GDU_AKEY_BACKSPACE                  0x0200
#define GDU_AKEY_JUMP                       0x0100
#define GDU_AKEY_KEY_CODE                   0x00FF

/* -------------------------------------------------------------------------------------- */

#define UNIT_V_DISPLAYED            (UNIT_V_UF + 0)
#define UNIT_V_DETECTS_ENABLED      (UNIT_V_UF + 1)
#define UNIT_V_INTERRUPTS_DEFERRED  (UNIT_V_UF + 2)
#define UNIT_V_LARGE_CHARS          (UNIT_V_UF + 3)

#define UNIT_DISPLAYED              (1u << UNIT_V_DISPLAYED)            /* display windows is up */
#define UNIT_DETECTS_ENABLED        (1u << UNIT_V_DETECTS_ENABLED)      /* light pen detects are enabled */
#define UNIT_INTERRUPTS_DEFERRED    (1u << UNIT_V_INTERRUPTS_DEFERRED)  /* light pen interrupts are deferred */
#define UNIT_LARGE_CHARS            (1u << UNIT_V_LARGE_CHARS)          /* large character mode */

static t_stat gdu_reset (DEVICE *dptr);

static int16 gdu_dsw  = 1;                                  /* device status word */
static int16 gdu_ar   = 0;                                  /* address register */
static int16 gdu_x    = 0;                                  /* X deflection */
static int16 gdu_y    = 0;                                  /* Y deflection */
static int16 gdu_fkey = 0;                                  /* function keyboard register */
static int16 gdu_akey = 0;                                  /* alphanumeric keyboard register */
static int16 gdu_revert = 0;                                /* revert address register */
static int32 gdu_indicators = 0;                            /* programmed indicator lamps */
static int32 gdu_threshold = DEFAULT_PEN_THRESHOLD;         /* mouse must be within 3/1024 of line to be a hit */
static int32 gdu_rate = DEFAULT_GDU_RATE;                   /* refresh rate. 0 = default */

UNIT gdu_unit = { UDATA (NULL, 0, 0) };

REG gdu_reg[] = {
    { HRDATA (GDUDSW,   gdu_dsw,        16) },              /* device status word */
    { HRDATA (GDUAR,    gdu_ar,         16) },              /* address register */
    { HRDATA (GDUXREG,  gdu_x,          16) },              /* X deflection register */
    { HRDATA (GDUYREG,  gdu_y,          16) },              /* Y deflection register */
    { HRDATA (GDUFKEY,  gdu_fkey,       16) },              /* function keyboard register */
    { HRDATA (GDUAKEY,  gdu_akey,       16) },              /* alphanumeric keyboard register */
    { HRDATA (GDUREVERT,gdu_revert,     16) },              /* revert address register */
    { HRDATA (GDUAKEY,  gdu_indicators, 32) },              /* programmed indicators */
    { DRDATA (GDUTHRESH,gdu_threshold,  32) },              /* mouse closeness threshhold */
    { DRDATA (GDURATE,  gdu_rate,       32) },              /* refresh rate in frames/sec */
    { NULL }  };

DEVICE gdu_dev = {
    "GDU", &gdu_unit, gdu_reg, NULL,
    1, 16, 16, 1, 16, 16,
    NULL, NULL, gdu_reset,
    NULL, NULL, NULL};

/* -------------------------------------------------------------------------------------- */

#ifndef GUI_SUPPORT

static t_stat gdu_reset (DEVICE *dptr)
{
    return SCPE_OK;
}

void xio_2250_display (int32 addr, int32 func, int32 modify)
{
    /* ignore commands if device is nonexistent */
}

t_bool gdu_active (void)
{
    return 0;
}

/* -------------------------------------------------------------------------------------- */
#else   /* GUI_SUPPORT defined */

/******* PLATFORM INDEPENDENT CODE ********************************************************/

static int32 gdu_instaddr;                          // address of first word of instruction
static int xmouse, ymouse, lpen_dist, lpen_dist2;   // current mouse pointer, scaled closeness threshhold, same squared
static double sfactor;                              // current scaling factor
static t_bool last_abs = TRUE;                      // last positioning instruction was absolute
static t_bool mouse_present = FALSE;                // mouse is/is not in the window
static void clear_interrupts (void);
static void set_indicators (int32 new_inds);
static void start_regeneration (void);
static void halt_regeneration (void);
static void draw_characters (void);
static void notify_window_closed (void);

// routines that must be implemented per-platform

static void   DrawLine(int x0, int y0, int x1, int y1);
static void   DrawPoint(int x, int y);
static void   CheckGDUKeyboard(void);
static t_bool CreateGDUWindow(void);
static void   StartGDUUpdates(void);
static void   StopGDUUpdates(void);
static void   GetMouseCoordinates(void);
static void   UpdateGDUIndicators(void);
static void   ShowPenHit (int x, int y);
static void   EraseGDUScreen (void);

/* -------------------------------------------------------------------------------------- */

void xio_2250_display (int32 addr, int32 func, int32 modify)
{
    if (cgi) return;                                /* ignore this device in CGI mode */

    switch (func) {
        case XIO_SENSE_DEV:
            ACC = (gdu_dsw & GDU_DSW_BUSY) ? GDU_DSW_BUSY : gdu_dsw;
            if (modify & 1)
                clear_interrupts();
            break;

        case XIO_READ:                              /* store status data into word pointed to by IOCC packet */
            if (gdu_dsw & GDU_DSW_BUSY)             /* not permitted while device is busy */
                break;

            WriteW(addr,   gdu_ar);                 /* save status information */
            WriteW(addr+1, gdu_dsw);
            WriteW(addr+2, gdu_x & 0x7FF);
            WriteW(addr+3, gdu_y & 0x7FF);
            WriteW(addr+4, gdu_fkey);
            WriteW(addr+5, gdu_akey);
            gdu_ar = (int16) (addr+6);              /* this alters the channel address register? */

            clear_interrupts();                     /* read status clears the interrupts */
            break;

        case XIO_WRITE:
            if (gdu_dsw & GDU_DSW_BUSY)             /* treated as no-op if display is busy */
                break;

            if (modify & 0x80) {                    /* bit 8 on means set indicators, 0 means start regeneration */
                set_indicators((ReadW(addr) << 16) | ReadW(addr+1));
            }
            else {
                gdu_ar   = (int16) addr;
                gdu_fkey = 0;
                gdu_akey = 0;
                clear_interrupts();
                start_regeneration();
            }
            break;

        case XIO_CONTROL:
            if (modify & 0x80) {                    /* bit 8 on means reset, off is no-op */
                gdu_reset(&gdu_dev);
                set_indicators((addr << 16) | addr);
            }
            break;

        default:                                    /* all other commands are no-ops */
            break;
    }
}

static t_stat gdu_reset (DEVICE *dptr)
{
    if (cgi) return SCPE_OK;                            /* ignore this device in CGI mode */

    halt_regeneration();
    clear_interrupts();
    set_indicators(0);
    gdu_x = gdu_y = 512;
    CLRBIT(gdu_unit.flags, UNIT_INTERRUPTS_DEFERRED | UNIT_DETECTS_ENABLED | UNIT_LARGE_CHARS);
    gdu_dsw = 1;
    return SCPE_OK;
}

static void clear_interrupts (void)
{
    CLRBIT(gdu_dsw, GDU_DSW_ORDER_CONTROLLED_INTERRUPT | GDU_DSW_KEYBOARD_INTERUPT | GDU_DSW_DETECT_INTERRUPT);
    CLRBIT(ILSW[3], ILSW_3_2250_DISPLAY);
    calc_ints();
}

static void gdu_interrupt (int32 dswbit)
{
    SETBIT(gdu_dsw, dswbit);
    SETBIT(ILSW[3], ILSW_3_2250_DISPLAY);
    calc_ints();
    halt_regeneration();
}

static void set_indicators (int32 new_inds)
{
    gdu_indicators = new_inds;
    if (gdu_unit.flags & UNIT_DISPLAYED)
        UpdateGDUIndicators();
}

static void start_regeneration (void)
{
    SETBIT(gdu_dsw, GDU_DSW_BUSY);

    if ((gdu_unit.flags & UNIT_DISPLAYED) == 0) {
        if (! CreateGDUWindow())
            return;

        SETBIT(gdu_unit.flags, UNIT_DISPLAYED);
    }

    StartGDUUpdates();
}

static void halt_regeneration (void)
{
    // halt_regeneration gets called at end of every refresh interation, so it should NOT black out the
    // screen -- this is why it was flickering so badly. The lower level code (called on a timer)
    // should check to see if GDU_DSW_BUSY is clear, and if it it still zero after several msec,
    // only then should it black out the screen and call StopGDUUpdates.
    if (gdu_dsw & GDU_DSW_BUSY) {
        CLRBIT(gdu_dsw, GDU_DSW_BUSY);
    }
}

static void notify_window_closed (void)
{
    if (gdu_dsw & GDU_DSW_BUSY) {
        StopGDUUpdates();
        CLRBIT(gdu_dsw, GDU_DSW_BUSY);
    }

    CLRBIT(gdu_unit.flags, UNIT_DISPLAYED);

    gdu_reset(&gdu_dev);
}

static int32 read_gduword (void)
{
    int32 w;

    w = M[gdu_ar++ & mem_mask];
    gdu_dsw = (int16) ((gdu_dsw & ~GDU_DSW_ADDR_DISP) | ((gdu_ar - gdu_instaddr) & GDU_DSW_ADDR_DISP));

    return w;
}

#define DIST2(x0,y0,x1,y1) (((x1)-(x0))*((x1)-(x0))+((y1)-(y0))*((y1)-(y0)))

static void draw (int32 newx, int32 newy, t_bool beam)
{
    int xmin, xmax, ymin, ymax, xd, yd;
    double s;
    int hit = FALSE;

    if (beam) {
        if (gdu_dsw & GDU_DSW_POINT_MODE) {
            DrawPoint(newx, newy);

#ifdef DEBUG_LIGHTPEN
            if (DIST2(newx, newy, xmouse, ymouse) <= lpen_dist2)
                hit = TRUE;
#else
            if (gdu_unit.flags & UNIT_DETECTS_ENABLED && mouse_present)
                if (DIST2(newx, newy, xmouse, ymouse) <= lpen_dist2)
                    hit = TRUE;
#endif
        }
        else {
            DrawLine(gdu_x, gdu_y, newx, newy);

            // calculate proximity of light pen to the line
#ifndef DEBUG_LIGHTPEN
            if (gdu_unit.flags & UNIT_DETECTS_ENABLED && mouse_present) {
#endif
                if (gdu_x <= newx)
                    xmin = gdu_x, xmax = newx;
                else
                    xmin = newx,  xmax = gdu_x;

                if (gdu_y <= newy)
                    ymin = gdu_y, ymax = newy;
                else
                    ymin = newy,  ymax = gdu_y;

                if (newx == gdu_x) {
                    // line is vertical. Nearest point is an endpoint if the mouse is above or
                    // below the line segment, otherwise the segment point at the same y as the mouse
                    xd = gdu_x;
                    yd = (ymouse <= ymin) ? ymin : (ymouse >= ymax) ? ymax : ymouse;

                    if (DIST2(xd, yd, xmouse, ymouse) <= lpen_dist2)
                        hit = TRUE;
                }
                else if (newy == gdu_y) {
                    // line is horizontal. Nearest point is an endpoint if the mouse is to the left or
                    // the right of the line segment, otherwise the segment point at the same x as the mouse
                    xd = (xmouse <= xmin) ? xmin : (xmouse >= xmax) ? xmax : xmouse;
                    yd = gdu_y;

                    if (DIST2(xd, yd, xmouse, ymouse) <= lpen_dist2)
                        hit = TRUE;
                }
                else {
                    // line is diagonal. See if the mouse is inside the box lpen_dist wider than the line segment's bounding rectangle
                    if (xmouse >= (xmin-lpen_dist) && xmouse <= (xmax+lpen_dist) && ymouse >= (ymin-lpen_dist) || ymouse <= (ymax+lpen_dist)) {
                        // compute the point at the intersection of the line through the line segment and the normal
                        // to that line through the mouse. This is the point on the line through the line segment
                        // nearest the mouse

                        s  = (double)(newy - gdu_y) / (double)(newx - gdu_x);       // slope of line segment
                        xd = (int) ((ymouse + xmouse/s - gdu_y + s*gdu_x) / (s + 1./s) + 0.5);

                        // if intersection is beyond either end of the line segment, the nearest point to the
                        // mouse is nearest segment end, otherwise it's the computed intersection point
                        if (xd < xmin || xd > xmax) {
#ifdef DEBUG_LIGHTPEN
                            // if it's a hit, set xd and yd so we can display the hit
                            if (DIST2(gdu_x, gdu_y, xmouse, ymouse) <= lpen_dist2) {
                                hit = TRUE;
                                xd = gdu_x;
                                yd = gdu_y;
                            }
                            else if (DIST2(newx, newy, xmouse, ymouse) <= lpen_dist2) {
                                hit = TRUE;
                                xd = newx;
                                yd = newy;
                            }
#else
                            if (DIST2(gdu_x, gdu_y, xmouse, ymouse) <= lpen_dist2 || DIST2(newx, newy, xmouse, ymouse) <= lpen_dist2)
                                hit = TRUE;
#endif
                        }
                        else {
                            yd = (int) (gdu_y + s*(xd - gdu_x) + 0.5);
                            if (DIST2(xd, yd, xmouse, ymouse) <= lpen_dist2)
                                hit = TRUE;
                        }
                    }
                }
#ifndef DEBUG_LIGHTPEN
            }
#endif
        }
    }

    if (hit) {
#ifdef DEBUG_LIGHTPEN
        ShowPenHit(xd, yd);
        if (gdu_unit.flags & UNIT_DETECTS_ENABLED && mouse_present)
            SETBIT(gdu_dsw, GDU_DSW_DETECT_STATUS);
#else
        SETBIT(gdu_dsw, GDU_DSW_DETECT_STATUS);
#endif
    }

    gdu_x = (int16) newx;
    gdu_y = (int16) newy;
}

static void generate_image (void)
{
    int32 instr, new_addr, newx, newy;
    t_bool run = TRUE, accept;

    if (! (gdu_dsw & GDU_DSW_BUSY))
        return;

    GetMouseCoordinates();

    lpen_dist  = (int) (gdu_threshold/sfactor + 0.5);   // mouse-to-line threshhold at current scaling factor
    lpen_dist2 = lpen_dist * lpen_dist;

    while (run) {
        if ((gdu_dsw & GDU_DSW_DETECT_STATUS) && ! (gdu_unit.flags & UNIT_INTERRUPTS_DEFERRED)) {
            CLRBIT(gdu_dsw, GDU_DSW_DETECT_STATUS); // clear when interrupt is activated
            gdu_interrupt(GDU_DSW_DETECT_INTERRUPT);
            run = FALSE;
            break;
        }

        gdu_instaddr = gdu_ar;                      // remember address of GDU instruction
        instr = read_gduword();                     // fetch instruction (and we really are cycle stealing here!)

        switch ((instr >> 12) & 0xF) {              // decode instruction
            case 0:                                 // short branch
            case 1:
                gdu_revert = gdu_ar;                // save revert address & get new address
                gdu_ar = (int16) (read_gduword() & 0x1FFF);
                if (gdu_dsw & GDU_DSW_CHARACTER_MODE) {
                    draw_characters();              // in character mode this means we are at character data
                    gdu_ar = gdu_revert;
                }
                break;

            case 2:                                 // long branch/interrupt
                new_addr = read_gduword();          // get next word
                accept = ((instr & 1) ? (gdu_dsw & GDU_DSW_LIGHT_PEN_SWITCH) : TRUE) && ((instr & 2) ? (gdu_dsw & GDU_DSW_DETECT_STATUS) : TRUE);

                if (instr & 2)                      // clear after testing
                    CLRBIT(gdu_dsw, GDU_DSW_DETECT_STATUS);

                if (instr & 0x0400)                 // NOP
                    accept = FALSE;

                if (accept) {
                    if (instr & 0x0800) {           // branch
                        gdu_revert = gdu_ar;

                        if (instr & 0x0080)         // indirect
                            new_addr = M[new_addr & mem_mask];

                        gdu_ar = (int16) new_addr;

                        if (gdu_dsw & GDU_DSW_CHARACTER_MODE) {
                            draw_characters();
                            gdu_ar = gdu_revert;
                        }
                    }
                    else {                          // interrupt
                        gdu_interrupt(GDU_DSW_ORDER_CONTROLLED_INTERRUPT);
                        run = FALSE;
                    }
                }
                break;

            case 3:                                 // control instructions
                CLRBIT(gdu_dsw, GDU_DSW_CHARACTER_MODE);

                switch ((instr >> 8) & 0xF) {
                    case 1:                         // set pen mode
                        if ((instr & 0xC) == 8)
                            SETBIT(gdu_unit.flags, UNIT_DETECTS_ENABLED);
                        else if ((instr & 0xC) == 4)
                            CLRBIT(gdu_unit.flags, UNIT_DETECTS_ENABLED);

                        if ((instr & 0x3) == 2)
                            SETBIT(gdu_unit.flags, UNIT_INTERRUPTS_DEFERRED);
                        else if ((instr & 0x3) == 1)
                            CLRBIT(gdu_unit.flags, UNIT_INTERRUPTS_DEFERRED);
                        break;

                    case 2:                         // set graphic mode
                        if (instr & 1)
                            SETBIT(gdu_dsw, GDU_DSW_POINT_MODE);
                        else
                            CLRBIT(gdu_dsw, GDU_DSW_POINT_MODE);
                        break;

                    case 3:                         // set character mode
                        SETBIT(gdu_dsw, GDU_DSW_CHARACTER_MODE);
                        if (instr & 1)
                            SETBIT(gdu_unit.flags, UNIT_LARGE_CHARS);
                        else
                            CLRBIT(gdu_unit.flags, UNIT_LARGE_CHARS);
                        break;

                    case 4:                         // start timer
                        run = FALSE;                // (which, for us, means stop processing until next timer message)
                        CheckGDUKeyboard();
                        break;

                    case 5:                         // store revert
                        M[gdu_ar & mem_mask] = gdu_revert;
                        read_gduword();             // skip to next address
                        break;

                    case 6:                         // revert
                        gdu_ar = gdu_revert;
                        break;

                    default:                        // all others treated as no-ops
                        break;
                }
                break;

            case 4:                                 // long absolute
            case 5:
                CLRBIT(gdu_dsw, GDU_DSW_CHARACTER_MODE);
                newx = instr & 0x3FF;
                newy = read_gduword() & 0x3FF;
                draw(newx, newy, instr & 0x1000);
                last_abs = TRUE;
                break;

            case 6:                                 // short absolute
            case 7:
                CLRBIT(gdu_dsw, GDU_DSW_CHARACTER_MODE);
                newx = gdu_x;
                newy = gdu_y;
                if (instr & 0x0800)
                    newy = instr & 0x3FF;
                else
                    newx = instr & 0x3FF;
                draw(newx, newy, instr & 0x1000);
                last_abs = TRUE;
                break;

            default:                                // high bit set - it's a relative instruction
                CLRBIT(gdu_dsw, GDU_DSW_CHARACTER_MODE);
                newx = (instr >> 8) & 0x3F;
                newy = instr & 0x3F;

                if (instr & 0x4000)                 // sign extend x - values are in 2's complement
                    newx |= -1 & ~0x3F;             // although documentation doesn't make that clear

                if (instr & 0x0040)                 // sign extend y
                    newy |= -1 & ~0x3F;

                newx = gdu_x + newx;
                newy = gdu_y + newy;
                draw(newx, newy, instr & 0x0080);
                last_abs = FALSE;
                break;
        }
    }
}

static struct charinfo {        // character mode scaling info:
    int dx, dy;                     // character and line spacing
    double sx, sy;                  // scaling factors: character units to screen units
    int xoff, yoff;                 // x and y offset to lower left corner of character
    int suby;                       // subscript/superscript offset
} cx[2] = {
    {14, 20, 1.7, 2.0, -6, -7,  6}, // regular
    {21, 30, 2.5, 3.0, -9, -11, 9}  // large
};

static void draw_characters (void)
{
    int32 w, x0, y0, x1, y1, yoff = 0, ninstr = 0;
    t_bool dospace, didstroke = FALSE;
    struct charinfo *ci;

    ci = &cx[(gdu_unit.flags & UNIT_LARGE_CHARS) ? 1 : 0];
    x0 = gdu_x + ci->xoff;                      // starting position
    y0 = gdu_y + ci->yoff;

    do {
        if (++ninstr > 29) {                    // too many control words
            gdu_interrupt(GDU_DSW_CYCLE_STEAL_CHECK);
            return;
        }

        dospace = TRUE;
        w = M[gdu_ar++ & mem_mask];             // get next stroke or control word

        x1 = (w >> 12) & 7;
        y1 = (w >>  8) & 7;

        if (x1 == 7) {                          // this is a character control word
            dospace = FALSE;                    // inhibit character spacing

            switch (y1) {
                case 1:                         // subscript
                    if (yoff == 0)              // (ignored if superscript is in effect)
                        yoff = -ci->suby;
                    break;

//              case 2:                         // no-op or null (nothing to do)
//              default:                        // all unknowns are no-ops
//                  break;

                case 4:                         // superscript
                    yoff = ci->suby;
                    break;

                case 7:                         // new line
                    gdu_x = 0;
                    gdu_y -= (int16) ci->dy;
                    if (gdu_y < 0 && last_abs)
                        gdu_y = (int16) (1024 - ci->dy);    // this is a guess
                    break;
            }
        }
        else {                                  // this is stroke data -- extract two strokes
            x1 = gdu_x + (int) (x1*ci->sx + 0.5);
            y1 = gdu_y + (int) ((y1+yoff)*ci->sy + 0.5);

            if (w & 0x0800) {
                didstroke = TRUE;
                DrawLine(x0, y0, x1, y1);
            }

            x0 = (w >> 4) & 7;
            y0 =  w       & 7;

            x0 = gdu_x + (int) (x0*ci->sx + 0.5);
            y0 = gdu_y + (int) ((y0+yoff)*ci->sy + 0.5);

            if (w & 0x0008) {
                didstroke = TRUE;
                DrawLine(x1, y1, x0, y0);
            } 
        }

        if (dospace) {
            gdu_x += ci->dx;
            if (gdu_x > 1023 && last_abs) {     // line wrap
                gdu_x = 0;
                gdu_y -= (int16) ci->dy;
            }
        }
    } while ((w & 0x0080) == 0);                // repeat until we hit revert bit

    if (didstroke && mouse_present && (gdu_unit.flags & UNIT_DETECTS_ENABLED)) {
        if (xmouse >= (gdu_x - ci->xoff/2) && xmouse <= (gdu_x + ci->xoff/2) &&
                ymouse >= (gdu_y - ci->yoff/2) && ymouse <= (gdu_y + ci->yoff/2))
            SETBIT(gdu_dsw, GDU_DSW_DETECT_STATUS);
    }
}

/******* PLATFORM SPECIFIC CODE ***********************************************************/

#ifdef _WIN32

#include <windows.h>
#include <windowsx.h>

#define APPCLASS "IBM2250GDU"                   // window class name

#define RGB_GREEN RGB(0,255,0)                  // handy colors
#define RGB_RED   RGB(255,0,0)

static HINSTANCE hInstance;
static HWND hwGDU       = NULL;
static HDC  hdcGDU      = NULL;
static HBITMAP hBmp     = NULL;
static int  curwid      = 0;
static int  curht       = 0;
static BOOL wcInited    = FALSE;
static DWORD GDUPumpID   = 0;
static HANDLE hGDUPump  = INVALID_HANDLE_VALUE;
static HPEN hGreenPen   = NULL;
static HBRUSH hRedBrush = NULL;
#ifdef DEBUG_LIGHTPEN
static HPEN hRedPen     = NULL;
#endif
static HBRUSH hGrayBrush, hDarkBrush;
static HPEN hBlackPen;
static int halted = 0;                              // number of time intervals that GDU has been halted w/o a regeneration
static UINT idTimer = 0;
static t_bool painting = FALSE;
static LRESULT APIENTRY GDUWndProc (HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam);
static DWORD   WINAPI   GDUPump (LPVOID arg);

static void destroy_GDU_window (void)
{
    if (hwGDU != NULL)
        SendMessage(hwGDU, WM_CLOSE, 0, 0);         // cross thread call is OK

#ifdef XXXX
    // let window closure do this
    if (hGDUPump != INVALID_HANDLE_VALUE) {         // this is not the most graceful way to do it
        TerminateThread(hGDUPump, 0);
        hGDUPump  = INVALID_HANDLE_VALUE;
        GDUPumpID = 0;
        hwGDU     = NULL;
    }

    if (hdcGDU != NULL) {
        DeleteDC(hdcGDU);
        hdcGDU = NULL;
    }

    if (hBmp != NULL) {
        DeleteObject(hBmp);
        hBmp = NULL;
    }

    if (hGreenPen != NULL) {
        DeleteObject(hGreenPen);
        hGreenPen = NULL;
    }

    if (hRedBrush != NULL) {
        DeleteObject(hRedBrush);
        hRedBrush = NULL;
    }
#endif

#ifdef DEBUG_LIGHTPEN
    if (hRedPen != NULL) {
        DeleteObject(hRedPen);
        hRedPen = NULL;
    }
#endif
}

static t_bool CreateGDUWindow (void)
{
    static BOOL did_atexit = FALSE;

    hInstance = GetModuleHandle(NULL);

    if (hGDUPump == INVALID_HANDLE_VALUE)
        hGDUPump = CreateThread(NULL, 0, GDUPump, 0, 0, &GDUPumpID);

    if (! did_atexit) {
        atexit(destroy_GDU_window);
        did_atexit = TRUE;
    }

    return TRUE;
}

// windows message handlers ----------------------------------------------------

// close the window

static void gdu_WM_CLOSE (HWND hWnd)
{
    DestroyWindow(hWnd);
}

// the window is being destroyed

static void gdu_WM_DESTROY (HWND hWnd)
{
    PostMessage(hWnd, WM_QUIT, 0, 0);
    if (idTimer != 0) {
        KillTimer(hwGDU, 1);
        idTimer = 0;
        halted  = 10000;
        painting = FALSE;
    }
    notify_window_closed();
    hwGDU = NULL;
}

// adjust the min and max resizing boundaries

static void gdu_WM_GETMINMAXINFO (HWND hWnd, LPMINMAXINFO mm)
{
    mm->ptMinTrackSize.x = 100 + 2*INDWIDTH;
    mm->ptMinTrackSize.y = 100;
}

static void PaintImage (HDC hDC, BOOL draw_indicators)
{
    HPEN hOldPen;
    RECT r;
    int wid, ht, x, y, dy, i, j, ycirc;
    unsigned long bit;

    GetClientRect(hwGDU, &r);
    wid = r.right+1 - 2*INDWIDTH;
    ht  = r.bottom+1;
    sfactor = (double) MIN(wid,ht) / 1024.;

    if (gdu_dsw & GDU_DSW_BUSY) {
#ifdef BLIT_MODE
        // if compiled for BLIT_MODE, draw the image into a memory display context, then
        // blit the new image over window. This eliminates the flicker that a normal erase-and-
        // repaint would cause.

        if (wid != curwid || ht != curht) {     // dimensions have changed, discard old memory display context
            if (hdcGDU != NULL) {
                DeleteDC(hdcGDU);
                hdcGDU = NULL;
            }
            curwid = wid;
            curht  = ht;
        }

        if (hdcGDU == NULL) {                   // allocate memory display context & select a bitmap into it
            hdcGDU = CreateCompatibleDC(hDC);
            if (hBmp != NULL)
                DeleteObject(hBmp);
            hBmp = CreateCompatibleBitmap(hDC, wid, ht);
            SelectObject(hdcGDU, hBmp);
        }

        PatBlt(hdcGDU, 0, 0, wid, ht, BLACKNESS);           // start with a black screen

        hOldPen = SelectObject(hdcGDU, hGreenPen);

        SetMapMode(hdcGDU, MM_ANISOTROPIC);
        SetWindowExtEx(hdcGDU, 1024, -1024, NULL);
        SetViewportExtEx(hdcGDU, wid, ht, NULL);
        SetWindowOrgEx(hdcGDU, 0, 1023, NULL);

        generate_image();                                   // run the display program to paint the image into the memory context

        SetWindowExtEx(hdcGDU, wid, ht, NULL);              // undo the scaling so the blit isn't distorted
        SetViewportExtEx(hdcGDU, wid, ht, NULL);
        SetWindowOrgEx(hdcGDU, 0, 0, NULL);
        BitBlt(hDC, 0, 0, wid, ht, hdcGDU, 0, 0, SRCCOPY);  // blit the new image over the old

        SelectObject(hdcGDU, hOldPen);
#else
        // for testing purposes -- draw the image directly onto the screen.
        // Compile this way when you want to single-step through the image drawing routine,
        // so you can see the draws occur.
        hdcGDU = hDC;
        hOldPen = SelectObject(hdcGDU, hGreenPen);

        SetMapMode(hdcGDU, MM_ANISOTROPIC);
        SetWindowExtEx(hdcGDU, 1024, -1024, NULL);
        SetViewportExtEx(hdcGDU, wid, ht, NULL);
        SetWindowOrgEx(hdcGDU, 0, 1023, NULL);

        generate_image();

        SelectObject(hdcGDU, hOldPen);
        hdcGDU = NULL;
#endif
    }

    if (draw_indicators) {
        x  = r.right-2*INDWIDTH+1;
        dy = ht / 16;
        ycirc = MIN(dy-2, 8);

        r.left = x;
        FillRect(hDC, &r, hGrayBrush);
        SelectObject(hDC, hBlackPen);

        bit = 0x80000000L;
        for (i = 0; i < 2; i++) {
            MoveToEx(hDC, x, 0, NULL);
            LineTo(hDC, x, r.bottom);
            y = 0;
            for (j = 0; j < 16; j++) {
                MoveToEx(hDC, x, y, NULL);
                LineTo(hDC, x+INDWIDTH, y);

                SelectObject(hDC, (gdu_indicators & bit) ? hRedBrush : hDarkBrush);
                Pie(hDC, x+1, y+1, x+1+ycirc, y+1+ycirc, x+1, y+1, x+1, y+1);

                y += dy;
                bit >>= 1;
            }
            x += INDWIDTH;
        }
    }
}

// repaint the window

static void gdu_WM_PAINT (HWND hWnd)
{
    PAINTSTRUCT ps;
    HDC hDC;
    int msec;

                // code for display
    hDC = BeginPaint(hWnd, &ps);
    PaintImage(hDC, TRUE);
    EndPaint(hWnd, &ps);

                // set a timer so we keep doing it!
    if (idTimer == 0) {
        msec = (gdu_rate == 0) ? (1000 / DEFAULT_GDU_RATE) : 1000/gdu_rate;
        idTimer = SetTimer(hwGDU, 1, msec, NULL);
    }
}

// the window has been resized

static void gdu_WM_SIZE (HWND hWnd, UINT state, int cx, int cy)
{
#ifdef BLIT_MODE
    InvalidateRect(hWnd, NULL, FALSE);      // in blt mode, we'll paint a full black bitmap over the new screen size
#else
    InvalidateRect(hWnd, NULL, TRUE);
#endif
}

// tweak the sizing rectangle during a resize to guarantee a square window

static void gdu_WM_SIZING (HWND hWnd, WPARAM fwSide, LPRECT r)
{
    switch (fwSide) {
        case WMSZ_LEFT:
        case WMSZ_RIGHT:
        case WMSZ_BOTTOMLEFT:
        case WMSZ_BOTTOMRIGHT:
            r->bottom = r->right - r->left - 2*INDWIDTH + r->top;
            break;

        case WMSZ_TOP:
        case WMSZ_BOTTOM:
        case WMSZ_TOPRIGHT:
            r->right  = r->bottom - r->top + r->left + 2*INDWIDTH;
            break;

        case WMSZ_TOPLEFT:
            r->left   = r->top - r->bottom + r->right - 2*INDWIDTH;
            break;
    }
}

// the refresh timer has gone off

static void gdu_WM_TIMER (HWND hWnd, UINT id)
{
    HDC hDC;

    if (painting)       {                       // if GDU is running, update picture
        if ((gdu_dsw & GDU_DSW_BUSY) == 0) {    // regeneration is not to occur
            if (++halted >= 4) {                // stop the timer if four timer intervals go by with the display halted
                EraseGDUScreen();               // screen goes black due to cessation of refreshing
                StopGDUUpdates();               // might as well kill the timer
                return;
            }
        }
        else
            halted = 0;

#ifdef BLIT_MODE
        hDC = GetDC(hWnd);                      // blit the new image right over the old
        PaintImage(hDC, FALSE);
        ReleaseDC(hWnd, hDC);
#else
        InvalidateRect(hWnd, NULL, TRUE);       // repaint
#endif
    }
}

// window procedure ------------------------------------------------------------

#define HANDLE(msg) case msg: return HANDLE_##msg(hWnd, wParam, lParam, gdu_##msg);

#ifndef HANDLE_WM_SIZING
// void Cls_OnSizing(HWND hwnd, UINT fwSide, LPRECT r)
#  define HANDLE_WM_SIZING(hwnd, wParam, lParam, fn) \
    ((fn)((hwnd), (UINT)(wParam), (LPRECT)(lParam)), 0L)
#endif

static LRESULT APIENTRY GDUWndProc (HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
    switch (iMessage) {
        HANDLE(WM_CLOSE);
        HANDLE(WM_GETMINMAXINFO);
        HANDLE(WM_DESTROY);
        HANDLE(WM_PAINT);
        HANDLE(WM_SIZE);
        HANDLE(WM_SIZING);
        HANDLE(WM_TIMER);
        default:                    // any message we don't process
            return DefWindowProc(hWnd, iMessage, wParam, lParam);
    }
    return 0L;
}

// graphic calls ----------------------------------------------------------------

static void DrawLine (int x0, int y0, int x1, int y1)
{
    MoveToEx(hdcGDU, x0, y0, NULL);
    LineTo(hdcGDU, x1, y1);
}

static void DrawPoint (int x, int y)
{
    SetPixel(hdcGDU, x, y, RGB_GREEN);
}

static void UpdateGDUIndicators(void)
{
    if (hwGDU != NULL)
        InvalidateRect(hwGDU, NULL, FALSE);         // no need to erase the background -- the draw routine fully paints the indicator
}

static void CheckGDUKeyboard (void)
{
}

static void StartGDUUpdates (void)
{
    halted = 0;
    painting = TRUE;
}

static void StopGDUUpdates (void)
{
    painting = FALSE;
}

static void GetMouseCoordinates()
{
    POINT p;
    RECT r;

    GetCursorPos(&p);
    GetClientRect(hwGDU, &r);
    if (! ScreenToClient(hwGDU, &p)) {
        xmouse = ymouse = -2000;
        mouse_present = FALSE;
        return;
    }

    if (p.x < r.left || p.x >= r.right || p.y < r.top || p.y > r.bottom) {
        mouse_present = FALSE;
        return;
    }

    // convert mouse coordinates to scaled coordinates

    xmouse =        (int) (1024./(r.right+1.-2*INDWIDTH)*p.x + 0.5);
    ymouse = 1023 - (int) (1024./(r.bottom+1.)*p.y + 0.5);
    mouse_present = TRUE;
}

t_bool gdu_active (void)
{
    return cgi ? 0 : (gdu_dsw & GDU_DSW_BUSY);
}

static void EraseGDUScreen (void)
{
    if (hwGDU != NULL)                              /* redraw screen. it will be blank if GDU is not running */
        InvalidateRect(hwGDU, NULL, TRUE);
}

/* GDUPump - thread responsible for creating and displaying the graphics window */

static DWORD WINAPI GDUPump (LPVOID arg)
{
    MSG msg;
    WNDCLASS wc;

    if (! wcInited) {                               /* register Window class */
        memset(&wc, 0, sizeof(wc));
        wc.style         = CS_NOCLOSE;
        wc.lpfnWndProc   = GDUWndProc;
        wc.hInstance     = hInstance;
        wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
        wc.hbrBackground = GetStockObject(BLACK_BRUSH);
        wc.lpszClassName = APPCLASS;

        if (! RegisterClass(&wc)) {
            GDUPumpID = 0;
            return 0;
        }

        wcInited = TRUE;
    }

    if (hGreenPen == NULL)
        hGreenPen = CreatePen(PS_SOLID, 1, RGB_GREEN);

#ifdef DEBUG_LIGHTPEN
    if (hRedPen == NULL)
        hRedPen = CreatePen(PS_SOLID, 1, RGB_RED);
#endif

    if (hRedBrush == NULL)
        hRedBrush = CreateSolidBrush(RGB_RED);

    hGrayBrush = GetStockObject(GRAY_BRUSH);
    hDarkBrush = GetStockObject(DKGRAY_BRUSH);
    hBlackPen  = GetStockObject(BLACK_PEN);

    if (hwGDU == NULL) {                            /* create window */
        hwGDU = CreateWindow(APPCLASS,
            "2250 Display",
            WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
            CW_USEDEFAULT, CW_USEDEFAULT,           // initial x, y position
            INITSIZE+2*INDWIDTH, INITSIZE,          // initial width and height
            NULL,                                   // parent window handle
            NULL,                                   // use class menu
            hInstance,                              // program instance handle
            NULL);                                  // create parameters

        if (hwGDU == NULL) {
            GDUPumpID = 0;
            return 0;
        }
    }

    ShowWindow(hwGDU, SW_SHOWNOACTIVATE);           /* display it */
    UpdateWindow(hwGDU);
    
    while (GetMessage(&msg, hwGDU, 0, 0)) {         /* message pump - this basically loops forevermore */
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    painting = FALSE;

    if (hwGDU != NULL) {
        DestroyWindow(hwGDU);                       /* but if a quit message got posted, clean up */
        hwGDU = NULL;
    }

    GDUPumpID = 0;
    return 0;
}

#ifdef DEBUG_LIGHTPEN
static void ShowPenHit (int x, int y)
{
    HPEN hOldPen;

    hOldPen = SelectObject(hdcGDU, hRedPen);
    DrawLine(x-10, y-10, x+10, y+10);
    DrawLine(x-10, y+10, x+10, y-10);
    SelectObject(hdcGDU, hOldPen);
}
#endif

#endif      // _WIN32 defined
#endif      // GUI_SUPPORT defined
