#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)
{
	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)
{
	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) {
//		StopGDUUpdates();							// let lower level code discover this during next refresh
		CLRBIT(gdu_dsw, GDU_DSW_BUSY);
	}
//	EraseGDUScreen();								// let cessation of regeneration erase it (eventually)
}

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 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

	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;
	}

#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)
{
	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;
				// code for display
	hDC = BeginPaint(hWnd, &ps);
	PaintImage(hDC, TRUE);
	EndPaint(hWnd, &ps);
}

// the window has been resized

static void gdu_WM_SIZE (HWND hWnd, UINT state, int cx, int cy)
{
	InvalidateRect(hWnd, NULL, TRUE);
}

// 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 (running) {			// if CPU 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 UINT idTimer = 0;

static void	StartGDUUpdates (void)
{
	int msec;

	if (idTimer == 0) {
		msec = (gdu_rate == 0) ? (1000 / DEFAULT_GDU_RATE) : 1000/gdu_rate;
		idTimer = SetTimer(hwGDU, 1, msec, NULL);
	}
	halted = 0;
}

static void	StopGDUUpdates (void)
{
	if (idTimer != 0) {
		KillTimer(hwGDU, 1);
		idTimer = 0;
		halted  = 10000;
	}
}

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 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);
	}

	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