#ifndef lint
static char sccsid[] = "@(#)dispX11.c	1.9 (mg@ukc.ac.uk) 5/24/90";
#endif
/*
 * X window display function for CSOUND
 * written by Al Tervalon, Fall 1986
 * Converted to X11 by Martin Guy <mg@ukc.ac.uk>
 *
 * Environment variables control
 * DISPLAY_STYLE
 *	0 - clear and redraw every frame
 *	1 - redraw the graph in white & renew the axes (default)
 * FONT
 *	A string name for XLoadQueryFont.
 */
#include <X11/Xlib.h>
#include <stdio.h>
#include "sysdep.h"

extern char *strdup();

#define XINIT    0       /* set default window location */
#define YINIT    0

extern int  dsploption;

/* Do we want to have to click on each display before the next frame
 * will be displayed? (set by -e option) */
int x_single_step = 0;

/* forward declarations */
static findminmax();
static calc_geom(), calc_x_geom(), calc_y_geom();
static p_to_xp();
static dw_refresh();
static draw_axes(), label_axes(), draw_label(), draw_graph(), redraw_graph();
static x_create_window(), x_configure(), x_expose();
static x_do_next_event();
static x_do_event();

static Display *dpy;
static GC bgc, wgc;	/* black- and white-drawing graphic contexts */
static char *fontname = "6x10";
static XFontStruct *fontstruct;	/* current font */
static GC tgc;		/* text graphic context */
static int textheight;	/* space to allow at bottom of window for text */

/*
 * A call of this function announces a new set of data to display.
 * It may be the first call for this display, or a new set of points
 * for an existing display - we are not told which.
 *
 * If it's a new one, we create windows etc and set the data for the points.
 * We don't draw them - this is done when the configure and expose events
 * arrive.
 *
 * If it's new data for an existing display, we just need to redraw with it.
 */
dsplXgraph(array,npts,label, dwidp)
 float *array;
 int npts;
 char *label;
 DWID *dwidp;
{
	XDWID *dwid = *dwidp;
	Window window;

	float min, max;
	static initialised = 0;
	int first_call_on_window;
	int size_has_changed = 0;	/* Do we need to recalculate sizes? */

	/* find least and most samples of this frame */
	findminmax(array, npts, &min, &max);
  
	/*
	 *	If this is the first call for this window, create it.
	 *	The storage for displays is obtained from calloc, hence
	 *	is zeroed.
	 */
	if (dwid == NULL) /* there is no window for this signal yet */
	{                 
		dwid = (XDWID *) mcalloc((long) sizeof(XDWID));
		*dwidp = dwid;

		/* copy stuff from args into window structure */
		dwid->points = array;
		dwid->xp_up_to_date = 0; /* register arrival of new points */
		dwid->npoints = npts;
		dwid->label = strdup(label);
		dwid->min = min;
		dwid->max = max;
		if (x_single_step) dwid->clicked_on = 0;

		x_create_window(dwid);

		/* calculate sizes */
		calc_geom(dwid);
		p_to_xp(dwid);

		/* and draw on it */
		dw_refresh(dwid);
	}
	else	/* this signal already has a window to draw to, */
	{	/* so just re-use the window. */

		if (x_single_step) {
			while (!dwid->clicked_on) {
				x_do_next_event();
			}
			dwid->clicked_on--;
		}
		dwid->xp_up_to_date = 0; /* register arrival of new points */

		/* copy stuff from args into window structure */
		dwid->points = array;

		/* If npoints has changed, reallocate dependent storage */
		if (dwid->npoints != npts) {
			/* Reallocate space for vertices */
			free((char *)dwid->xpoints);
			dwid->xpoints = (XPoint *)mmalloc((long)npts * sizeof(XPoint));
			dwid->npoints = npts;
			size_has_changed = 1;
		}

		/* Set all-time idea of max and min */
		if (min < dwid->min) {
			size_has_changed = 1;
			dwid->min = min;
		}
		if (max > dwid->max) {
			size_has_changed = 1;
			dwid->max = max;
		}

		/* clear old graph line and redraw line only */
		if (size_has_changed) {
			/* redraw window from scratch */
			calc_geom(dwid);
			dw_refresh(dwid);
		} else {
			/* Redraw of an old display */
			redraw_graph(dwid);
		}
	}

	XFlush(dpy);      /* flush output to the screen */
}

static
findminmax(array, npts, minp, maxp)
float *array;
int npts;
float *minp, *maxp;
{
	register float min, max;
	register float *ptr, *maxptr;

	min = 0.;
	max = 0.;
    
	/* set maxptr to end of array */
	maxptr = array + npts;

	/* find max and min of array */
	for ( ptr = array ; ptr < maxptr ; ptr++)
	{
		if (*ptr > max)
			max = *ptr;
		else if (*ptr < min)
			min = *ptr;
	}

	*minp = min;
	*maxp = max;
}

static
calc_geom(dwid)
XDWID *dwid;
{
	/* If we ever put y-axis values on the graph, the length of these may
	 * affect the position of the y axis, so do y geom first. */

	calc_y_geom(dwid);
	calc_x_geom(dwid);
	dwid->xp_up_to_date = 0;
}

/* margin round graph area */
static int left_border = 1;
static int right_border = 1;
static int bottom_border = 1;
static int top_border = 1;

static int label_x;	/* where to set the label's reference point */
static int label_y;	/* where to set the label's reference point */

/* calculate parameters for mapping [0..npts-1] to window coords */
static
calc_x_geom(dwid)
XDWID *dwid;
{
	/* find size of drawable area on display */
	dwid->xmin = left_border;
	dwid->grwidth = dwid->width - left_border - right_border;

	dwid->xzero = left_border;

	label_x = left_border;

	/* Decide how many output points we need, max is one per pixel */
	if (dwid->npoints >= dwid->grwidth) {
		/* we have more points than pixels */
		dwid->nxpoints = dwid->grwidth;
		dwid->xscale = 1.0;
	} else {
		/* less points than pixels: spread them out a little */
		dwid->nxpoints = dwid->npoints;
		dwid->xscale = (float) (dwid->grwidth - 1) / (dwid->nxpoints - 1);
	}
}

/* calculate parameters for mapping sample values to window coords */
static
calc_y_geom(dwid)
XDWID *dwid;
{
	label_y = (dwid->height-1) - 1;
	/*
	 * and between top of text and graph
	 */
	bottom_border = textheight + 2;

	/* find size of drawable area on display */
	dwid->ymin = (dwid->height - 1) - bottom_border;
	dwid->grheight = dwid->height - bottom_border - top_border;

	/* decide on axis positioning and calculate scale factors */

	/* how to draw the x-axis and subsequently, how to scale the y-values. 
	 * Case 1 - all y-values positive, axis at bottom of window 
	 * Case 2 - all y-values negative, axis at top of window 
	 * Case 3 - y-values positive AND negative, axis in center of window 
	 * The appropriate 'yfactor' will be added to the y-values to allow all 
	 * plotting to be done by the same function call in the for-loop, which
	 * assumes that all y-values are positive
	 *
	 * Account is taken here for X's (0,0) being in the top left corner
	 * so yscale will be negative.
	 */
    
	if (dwid->min >= 0) 
	{    /* CASE 1 */
		dwid->yzero = dwid->ymin;
		dwid->yscale = - (dwid->grheight - 1) / dwid->max;
	}
	else if (dwid->max <= 0) 
	{    /* CASE 2 */
		dwid->yzero = dwid->ymin - (dwid->grheight - 1);
		dwid->yscale = (dwid->grheight - 1) / dwid->min;
	}
	else 
	{    /* CASE 3 */
#if 0
		dwid->yzero = dwid->ymin - dwid->grheight/2;
		if (dwid->max >= -dwid->min) {
			dwid->yscale = - (dwid->grheight/2.0) / dwid->max;
		} else {
			dwid->yscale = - (dwid->grheight/2.0) / -dwid->min;
		}
#else
		dwid->yscale = - (dwid->grheight - 1) / (dwid->max - dwid->min);
		dwid->yzero  = dwid->ymin - dwid->min * dwid->yscale;
#endif
	}
}

/* Convert floating points to physical coordinates */
static
p_to_xp(dwid)
XDWID *dwid;
{
	register int i;

	/* grub out parameters into convenient locals */
	register int xzero = dwid->xzero;
	register int yzero = dwid->yzero;
	register float xscale = dwid->xscale;
	register float yscale = dwid->yscale;
	register int nxpoints = dwid->nxpoints;
	register float *points = dwid->points;

	register XPoint *xpoint;	/* runs along dwid->xpoints */
	register float piscale;		/* extract loop constant */

	if (dwid->xp_up_to_date) return;

	piscale = (float) (dwid->npoints-1) / (float) (nxpoints-1);

	if (xscale == 1.0) {
		for (i=0, xpoint=dwid->xpoints; i<nxpoints; i++, xpoint++) {
			xpoint->x = xzero + i;
			xpoint->y = yzero + points[(int)(i * piscale)] * yscale;
		}
	} else {
		for (i=0, xpoint=dwid->xpoints; i<nxpoints; i++, xpoint++) {
			xpoint->x = xzero + i * xscale;
			xpoint->y = yzero + points[(int)(i * piscale)] * yscale;
		}
	}

	dwid->xp_up_to_date = 1;
}

/*
 *	Functions to draw parts of the display.
 *	These rely on calc_geom() having been called since the window
 *	geometry or the maximum sample value have changed.
 */

/* Clear and redraw the whole window */
static
dw_refresh(dwid)
XDWID *dwid;
{
	XClearWindow(dpy, dwid->window);

	draw_axes(dwid);

	label_axes(dwid);

	draw_label(dwid);

	draw_graph(dwid);
}

static
draw_axes(dwid)
XDWID *dwid;
{
	/* ...draw axes... 
	 * y-axis is always on the left edge, and the x-axis height 
	 * will be in the position determined by the case we're in
	 */

	/* X axis */
	XDrawLine(dpy, dwid->window, bgc,
		dwid->xmin, dwid->yzero,
		dwid->xmin + dwid->grwidth - 1, dwid->yzero);

	/* Y axis */
	XDrawLine(dpy, dwid->window, bgc,
		dwid->xzero, dwid->ymin,
		dwid->xzero, dwid->ymin - (dwid->grheight - 1));
}

/* Put labels against top and bottom of y-axis */
/* Maximum label is 6 chars (for -32767) */
static
label_axes(dwid)
XDWID *dwid;
{
	char buf[256];	/* %f does not guarantee to produce <=5 chars */
	int dir, asc, desc;
	XCharStruct cs;
	int nchars;

	sprintf(buf, "%f", dwid->max);
	nchars = strlen(buf);
	if (nchars > 6) nchars = 6;
	XTextExtents(fontstruct, buf, nchars, &dir, &asc, &desc, &cs);
	XDrawString(dpy, dwid->window, tgc,
		/* x */ left_border -2 -cs.rbearing,	/* right-adjusted */
		/* y */ cs.ascent + 1,
		buf, nchars);

	sprintf(buf, "%f", dwid->min);
	nchars = strlen(buf);
	if (nchars > 6) nchars = 6;
	XTextExtents(fontstruct, buf, nchars, &dir, &asc, &desc, &cs);
	XDrawString(dpy, dwid->window, tgc,
		/* x */ left_border-2 - cs.rbearing,	/* right-adjusted */
		/* y */ dwid->height-1 - bottom_border - (cs.descent - 1),
		buf, nchars);
}

/* draw the label under the curve */
/* must have called calc_geom to set position of label */
static
draw_label(dwid)
XDWID *dwid;
{
	char string[65];
	int length;
	int direction, asc, desc;	/* dummies */
	XCharStruct ext;		/* size of string */

	sprintf(string, "%s  %d points", dwid->label, dwid->npoints);
	length = strlen(string);

	XTextExtents(fontstruct, string, length, &direction, &asc, &desc, &ext);

	/* set string left-adjusted with a margin of one pixel all round */
	XDrawString(dpy, dwid->window, tgc, 
		1 - ext.lbearing, (dwid->height - 1) - desc,
		string, length);
}

/*
 * Draw the line of the graph itself.
 */
static
draw_graph(dwid)
XDWID *dwid;
{
	p_to_xp(dwid);

	XDrawLines(dpy, dwid->window, bgc, dwid->xpoints, dwid->nxpoints,
		CoordModeOrigin);
}

/*
 * Draw a new graph line in an old frame: erase the old line and draw the new
 */
static
redraw_graph(dwid)
XDWID *dwid;
{
	XDrawLines(dpy, dwid->window, wgc, dwid->xpoints, dwid->nxpoints,
		CoordModeOrigin);

	p_to_xp(dwid);

	XDrawLines(dpy, dwid->window, bgc, dwid->xpoints, dwid->nxpoints,
		CoordModeOrigin);
	
	draw_axes(dwid);	/* clearing wipes the axes */
}


/*
 *	X Functions start here, mostly wrappers for true Xlib functions.
 */

/* Once-only X initialisation, called from main(). */
x_init()
{
	char *cp;
	extern char *getenv();
	int dir, asc, desc;
	XCharStruct cs;

	/* Initialise the X world */
	dpy = XOpenDisplay(NULL);
	if (dpy == NULL) {
		fprintf(stderr, "csound: Cannot open X display.\n");
		exit(1);
	}

	/* Load in font for labels */
	if ((cp = getenv("FONT")) != NULL) fontname = cp;

	fontstruct = XLoadQueryFont(dpy, fontname);
	if (fontstruct == NULL) die("No such font");
	textheight = fontstruct->max_bounds.ascent
		   + fontstruct->max_bounds.descent;
	
	/* find the width of 5 numerals and a minus sign, for y-axis labels */
	XTextExtents(fontstruct, "-00000", 6, &dir, &asc, &desc, &cs);
	left_border = 1 + cs.rbearing - cs.lbearing + 1;
}

/* pointer to first cell of linked list of XDWIDs, used to convert Window IDs
 * from X events into XDWIDs */
static XDWID *dwid_head = NULL;

/*
 *	Create a window for displaying a graph
 */
static
x_create_window(dwid)
XDWID *dwid;
{
	Window window;
	/* preferred width: one pixel per point */
	int width = 200;
	/* Golden ratio would be best, A4 will do */
	int height = width / 1.414;

	static int firstwindow = 1;

	window = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy),
		XINIT, YINIT,
		width, height,
		0,	/* border width */
		0,	/* border colour */
		WhitePixel(dpy, 0));    /* background */
	dwid->window = window;

	/* Link the display into our linked list, at the head 'cos it's easy */
	dwid->link = dwid_head;
	dwid_head = dwid;

	if (firstwindow) {
		/* Special initialisation for the first window ever created. */
		/* You need a window to be able to create graphic contexts. */
		XGCValues xgcvalues;

		/* For drawing black and for text */
		xgcvalues.foreground = BlackPixel(dpy, 0);
		xgcvalues.font = fontstruct->fid;
		bgc = XCreateGC(dpy, window,
			GCForeground|GCFont, &xgcvalues);
		tgc = bgc;

		/* draw white */
		xgcvalues.foreground = WhitePixel(dpy, 0);
		wgc = XCreateGC(dpy, window, GCForeground, &xgcvalues);

		firstwindow = 0;
	}

	/* Allocate space for vertices - can't be more than npts */
	dwid->xpoints = (XPoint *)mmalloc((long)dwid->npoints * sizeof(XPoint));

	/*
	 * Register an interest in events that make us redraw.
	 */
	XSelectInput(dpy, window,
			(x_single_step ? ButtonPressMask : 0) |
			StructureNotifyMask | ExposureMask);

	/*
	 * Map it.  When this is flushed by XNextEvent,
	 * it causes a ConfigureNotify event, which causes us to redraw.
	 */
	XMapWindow(dpy, window);

	/*
	 * Can't draw on a window until it has been configured.
	 * Wait until it has been.
	 */
	dwid->configured = 0;
	do {
		x_do_next_event();
	} while (!dwid->configured);
}

static XDWID *
wid_to_dwid(wid)
Window wid;
{
	XDWID *p;

	for (p=dwid_head; p != NULL; p = p->link) {
		if (p->window == wid) return(p);
	}

	die("Received event from non-existent window!!");
}

/*
 *	Handle X events, called once every control period.
 */
x_process()
{
	int nevents;

	while ((nevents = XEventsQueued(dpy, QueuedAfterFlush)) > 0)
	while (nevents--) {
		x_do_next_event();
	}
}

/*
 * Wait for, and process the next X event.
 * Used, eg, wihle waiting for a window to be created.
 */
static
x_do_next_event()
{
	XEvent event;

	XNextEvent(dpy, &event);
	x_do_event(&event);
}

/* Take action on an X event */
static
x_do_event(e)
XEvent *e;
{
	switch (e->type) {
	/* Ignore the other events you get for free
	 * with ConfigureNotify */
	case CirculateNotify:
	case DestroyNotify:
	case GravityNotify:
	case MapNotify:
	case ReparentNotify:
		break;
	case ConfigureNotify:
#ifdef DEBUG
		fputs("ConfigureNotify\n", stderr);
#endif
		/* stash window size */
		x_configure(e->xconfigure.width,
			    e->xconfigure.height,
			    e->xconfigure.window);
		break;
	case Expose:
#ifdef DEBUG
		fputs("Expose\n", stderr);
#endif
		if (e->xexpose.count == 0) {
			x_expose(e->xexpose.window);
		}
		break;
	case ButtonPress:
		x_button_press(e->xbutton.window);
		break;
	default:
		fprintf(stderr, "Strange event %d received.\n", e->type);
		break;
	}
}

/*
 *	Window's size has changed.
 */
static
x_configure(width, height, wid)
int width, height;
Window wid;
{
	XDWID *dwid = wid_to_dwid(wid);

	if (dwid->configured && dwid->width == width && dwid->height == height)
	{
		/* Nothing that affects us (eg they moved a window) */
		return;
	}

	dwid->width = width;
	dwid->height = height;

	if (!dwid->configured) {
		dwid->configured = 1;	/* mark the window as drawable */
	} else {
		/* reshape of an extant window */
		calc_geom(dwid);
		dw_refresh(dwid);
	}
}

/*
 *	Window has been exposed - redraw it completely
 */
static
x_expose(wid)
Window wid;
{
	XDWID *dwid = wid_to_dwid(wid);

	dw_refresh(dwid);
}

static
x_button_press(wid)
Window wid;
{
	XDWID *dwid = wid_to_dwid(wid);

	dwid->clicked_on++;
}

/* wait for a button press */
x_wait_for_button()
{
#	define BORDER 10
	XEvent event;
	int button_pressed = 0;
	Window window;
	static char message[] = "Prod me";
	int len = strlen(message);
	int dir, asc, desc;
	XCharStruct cs;
	XSetWindowAttributes swa;

	/* Create a pop-up window for them to prod */
	XTextExtents(fontstruct, message, len,
			&dir, &asc, &desc, &cs);

	/* don't let the window manager mess about - just pop up */
	swa.override_redirect = True;
	swa.background_pixel = WhitePixel(dpy, 0);

	window = XCreateWindow(dpy, DefaultRootWindow(dpy),
			0, 0,		/* x, y */
			cs.rbearing - cs.lbearing + 2*BORDER,	/* width */
			cs.ascent + cs.descent + 2*BORDER,	/* height */
			1,		/* border width */
			CopyFromParent,	/* depth */
			InputOutput,	/* class */
			CopyFromParent,	/* visual type */
			CWOverrideRedirect|CWBackPixel,	/* valuemask */
			&swa);		/* attributes */
	
	/* Register an interest on button presses */
	XSelectInput(dpy, window, ButtonPressMask | ExposureMask);

	XMapWindow(dpy, window);

	do {
		XNextEvent(dpy, &event);

		if (event.xany.window != window) {
			/* It's for someone else */
			x_do_event(&event);
		} else {
			switch (event.type) {
			case ButtonPress:
				button_pressed = 1;
				break;
			case Expose:
				XClearWindow(dpy, window);
				XDrawString(dpy, window, tgc,
					BORDER-cs.lbearing,
					BORDER+cs.ascent,
					message, len);
				break;
			default:
				fprintf(stderr, "Strange event %d received in prodding window.\n", event.type);
				break;
			}
		}
	} while (!button_pressed);
}
