
/* Copyright (c) Mark J. Kilgard, 1996. */

/* This program is freely distributable without licensing fees
	 and is provided without guarantee or warrantee expressed or
	 implied. This program is -not- in the public domain. */

/*
 * paperplane can be compiled to use a "single visual" for the entire window
 * hierarchy and render OpenGL into a standard Motif drawing area widget:
 *
 *  cc -o sv_paperplane paperplane.c -DnoGLwidget -lGL -lXm -lXt -lX11 -lm
 *
 * Or paperplane can be compiled to use the default visual for most of
 * the window hierarchy but render OpenGL into a special "OpenGL widget":
 *
 *  cc -o glw_paperplane paperplane.c -lGLw -lGL -lXm -lXt -lX11 -lm
 */

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <math.h>
#include <Xm/MainW.h>
#include <Xm/RowColumn.h>
#include <Xm/PushB.h>
#include <Xm/ToggleB.h>
#include <Xm/CascadeB.h>
#include <Xm/Frame.h>

#ifdef noGLwidget
#include <Xm/DrawingA.h>  /* Motif drawing area widget */
#elif defined(noMotifGLwidget)
#include <GLw/GLwDrawA.h> /* Pure Xt OpenGL drawing area widget. */
#else
#include <GLw/GLwMDrawA.h>  /* Motif OpenGL drawing area widget. */
#endif

#include <X11/keysym.h>
#include <GL/gl.h>
#include <GL/glx.h>

static int dblBuf[] = {
	GLX_DOUBLEBUFFER, GLX_RGBA, GLX_DEPTH_SIZE, 16,
	GLX_RED_SIZE, 1, GLX_GREEN_SIZE, 1, GLX_BLUE_SIZE, 1,
	None
};
static int *snglBuf = &dblBuf[1];
static const char *fallbackResources[] = {
#ifdef IRIX_5_2_or_higher
	"*sgiMode: true",           /* try to enable IRIX 5.2+ look & feel */
	"*useSchemes: all",         /* and SGI schemes */
#endif
	"*title: OpenGL paper plane demo",
	"*glxarea*width: 420", "*glxarea*height: 420", NULL
};

Display *dpy;
Bool doubleBuffer = True, moving = False, made_current = False;
XtAppContext app;
XtWorkProcId workId = 0;
Widget toplevel, mainw, menubar, menupane, btn, cascade, frame, glxarea;
GLXContext cx;
XVisualInfo *vi;
#ifdef noGLwidget
Colormap cmap;
#endif
Arg menuPaneArgs[1], args[1];

#define MAX_PLANES 15

struct {
	float speed;          /* Zero speed means not flying. */
	GLfloat red, green, blue;
	float theta;
	float x, y, z, angle;
} planes[MAX_PLANES];

void draw(Widget w, XtPointer data, XtPointer callData)
{
	int i;
	GLfloat red, green, blue;

#ifdef DEBUG
	GLenum error;
#endif

	(void)data;
	(void)callData;

	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	/* Paint black to blue smooth-shaded polygon for background. */
	glDisable(GL_DEPTH_TEST);
	glShadeModel(GL_SMOOTH);
	glBegin(GL_POLYGON);
	glColor3f(0.0, 0.0, 0.0);
	glVertex3f(-20, 20, -19);
	glVertex3f(20, 20, -19);
	glColor3f(0.0, 0.0, 1.0);
	glVertex3f(20, -20, -19);
	glVertex3f(-20, -20, -19);
	glEnd();

	/* Render planes. */
	glEnable(GL_DEPTH_TEST);
	glShadeModel(GL_FLAT);
	for (i = 0; i < MAX_PLANES; i++) {
		if (!planes[i].speed)
				continue;

		glPushMatrix();
		glTranslatef(planes[i].x, planes[i].y, planes[i].z);
		glRotatef(290.0, 1.0, 0.0, 0.0);
		glRotatef(planes[i].angle, 0.0, 0.0, 1.0);
		glScalef(1.0 / 3.0, 1.0 / 4.0, 1.0 / 4.0);
		glTranslatef(0.0, -4.0, -1.5);
		glBegin(GL_TRIANGLE_STRIP);

		/* Left wing. */
		glVertex3f(-7.0, 0.0, 2.0);
		glVertex3f(-1.0, 0.0, 3.0);
		glColor3f(red = planes[i].red,
			      green = planes[i].green,
			      blue = planes[i].blue);
		glVertex3f(-1.0, 7.0, 3.0);

		/* Left side. */
		glColor3f(0.6 * red, 0.6 * green, 0.6 * blue);
		glVertex3f(0.0, 0.0, 0.0);
		glVertex3f(0.0, 8.0, 0.0);

		/* Right side. */
		glVertex3f(1.0, 0.0, 3.0);
		glVertex3f(1.0, 7.0, 3.0);

		/* Final tip of right wing. */
		glColor3f(red, green, blue);
		glVertex3f(7.0, 0.0, 2.0);
		glEnd();
		glPopMatrix();
	}

	if (doubleBuffer)
#ifndef noGLwidget
		GLwDrawingAreaSwapBuffers(w);
#else
		glXSwapBuffers(dpy, XtWindow(w));
#endif

	if (!glXIsDirect(dpy, cx))
		glFinish(); /* Avoid indirect rendering latency from queuing. */

#ifdef DEBUG
	/* For help debugging, report any OpenGL errors that occur per frame. */
	while ((error = glGetError()) != GL_NO_ERROR)
		fprintf(stderr, "GL error %d\n", error);
#endif
}

void tick_per_plane(int i)
{
	float theta = planes[i].theta += planes[i].speed;
	planes[i].z = -9 + 4 * cos(theta);
	planes[i].x = 4 * sin(2 * theta);
	planes[i].y = sin(theta / 3.4) * 3;
	planes[i].angle = ((atan(2.0) + M_PI_2) * sin(theta) - M_PI_2) * 180 / M_PI;
	if (planes[i].speed < 0.0)
		planes[i].angle += 180;
}

#define SET_COLOR(r,g,b) \
	      planes[i].red=r; planes[i].green=g; planes[i].blue=b;

void add_plane(void)
{
	int i;

	for (i = 0; i < MAX_PLANES; i++) {
		if (planes[i].speed)
			continue;

		switch (rand() % 6) {
		case 0:  SET_COLOR(1.0, 0.0, 0.0); break; /* red     */
		case 1:  SET_COLOR(1.0, 1.0, 1.0); break; /* white   */
		case 2:  SET_COLOR(0.0, 1.0, 0.0); break; /* green   */
		case 3:  SET_COLOR(1.0, 0.0, 1.0); break; /* magenta */
		case 4:  SET_COLOR(1.0, 1.0, 0.0); break; /* yellow  */
		default: SET_COLOR(0.0, 1.0, 1.0);        /* cyan    */
		}

		planes[i].speed = (rand() % 20) * 0.001 + 0.02;
		if (rand() & 0x1)
			planes[i].speed *= -1;
		planes[i].theta = ((float) (rand() % 257)) * 0.1111;
		tick_per_plane(i);
		if (!moving)
			draw(glxarea, NULL, NULL);
		return;
	}

	XBell(dpy, 100); /* Cannot add any more planes. */
}

void remove_plane(void)
{
	int i;

	for (i = MAX_PLANES - 1; i >= 0; i--) {
		if (!planes[i].speed)
			continue;

		planes[i].speed = 0;
		if (!moving)
			draw(glxarea, NULL, NULL);
		return;
	}

	XBell(dpy, 100); /* No more planes to remove. */
}

void resize(Widget w, XtPointer data, XtPointer callData)
{
	Dimension width = 0, height = 0;

#ifndef noGLwidget
	GLwDrawingAreaCallbackStruct *resize;
#endif

#ifdef noGLwidget
	/**
	 * Unfortunately, drawing area resize callback does not give
	 * height and width via its parameters.
	 */
	XtVaGetValues(w, XmNwidth, &width, XmNheight, &height, NULL);
#else
	resize = (GLwDrawingAreaCallbackStruct *)callData;
	width  = resize->width;
	height = resize->height;
#endif

	if (!made_current)
		return;

	glXWaitX();
	glViewport(0, 0, (GLint)width, (GLint)height);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glFrustum(-1.0, 1.0, -1.0, 1.0, 1.0, 20);
	glMatrixMode(GL_MODELVIEW);
}

void tick(void)
{
	int i;

	for (i = 0; i < MAX_PLANES; i++) {
		if (planes[i].speed != 0.0)
			tick_per_plane(i);
	}
}

Boolean animate(XtPointer data)
{
	tick();
	draw(glxarea, NULL, NULL);
	return False; /* Leave Xt work proc active. */
}

void toggle(void)
{
	if ((moving = !moving))
		workId = XtAppAddWorkProc(app, animate, NULL);
	else XtRemoveWorkProc(workId);
}

void quit(Widget w, XtPointer data, XtPointer callData)
{
	XtAppSetExitFlag(app);
}

void input(Widget w, XtPointer data, XtPointer callData)
{
	KeySym key;

#ifdef noGLwidget
	XmDrawingAreaCallbackStruct *cd = (XmDrawingAreaCallbackStruct *)callData;
#else
	GLwDrawingAreaCallbackStruct *cd = (GLwDrawingAreaCallbackStruct *)callData;
#endif

	if (cd->event->type != KeyPress)
		return;

	if ((key = XLookupKeysym((XKeyEvent *)cd->event, 0)) == NoSymbol)
		return;

	switch (key) {
	case XK_space:
		if (!moving) {
			tick();
			draw(w, NULL, NULL);
		}
		break;
	case XK_Escape:
		XtAppSetExitFlag(app);
		break;
	}
}

void map_state_changed(Widget w, XtPointer data, XEvent *event, Boolean *cont)
{
	switch (event->type) {
	case MapNotify:
		if (moving && workId)
			workId = XtAppAddWorkProc(app, animate, NULL);
		break;
	case UnmapNotify:
		if (moving)
			XtRemoveWorkProc(workId);
		break;
	}
}

int main(int argc, char *argv[])
{
	int i,j;

	XtSetLanguageProc(NULL, NULL, NULL);
	toplevel = XtAppInitialize(&app, "Paperplane", NULL, 0, &argc, argv,
	                           (String *)fallbackResources, NULL, 0);
	dpy = XtDisplay(toplevel);

	if (!glXQueryExtension(dpy, &i, &j))
		XtAppError(app, "GLX extension not available");

	/* Find an OpenGL-capable RGB visual with depth buffer. */
	if (!(vi = glXChooseVisual(dpy, DefaultScreen(dpy), dblBuf))) {
		if (!(vi = glXChooseVisual(dpy, DefaultScreen(dpy), snglBuf)))
			XtAppError(app, "no RGB visual with depth buffer");
		doubleBuffer = False;
	}

	/**
	 * Create an OpenGL rendering context without display list sharing,
	 * and favoring direct rendering.
	 */
	if (!(cx = glXCreateContext(dpy, vi, None, True)))
	  XtAppError(app, "could not create rendering context");

#ifdef noGLwidget
	/**
	 * Create an X colormap since probably not using default visual.
	 */
	cmap = XCreateColormap(dpy, RootWindow(dpy, vi->screen),
	                       vi->visual, AllocNone);

	/**
	 * Must establish the visual, depth, and colormap of the toplevel
	 * widget before the widget is realized.
	 */
	XtVaSetValues(toplevel, XtNvisual, vi->visual, XtNdepth, vi->depth,
	              XtNcolormap, cmap, NULL);
#endif

	XtAddEventHandler(toplevel, StructureNotifyMask, False, map_state_changed, NULL);
	mainw = XmCreateMainWindow(toplevel, "mainw", NULL, 0);
	XtManageChild(mainw);

	/* create menu bar */
	menubar = XmCreateMenuBar(mainw, "menubar", NULL, 0);
	XtManageChild(menubar);

#ifdef noGLwidget
	/* Hack around Xt's unfortunate default visual inheritance. */
	XtSetArg(menuPaneArgs[0], XmNvisual, vi->visual);
	menupane = XmCreatePulldownMenu(menubar, "menupane", menuPaneArgs, 1);
#else
	menupane = XmCreatePulldownMenu(menubar, "menupane", NULL, 0);
#endif

	btn = XmCreatePushButton(menupane, "Quit", NULL, 0);
	XtAddCallback(btn, XmNactivateCallback, quit, NULL);
	XtManageChild(btn);

	XtSetArg(args[0], XmNsubMenuId, menupane);
	cascade = XmCreateCascadeButton(menubar, "File", args, 1);
	XtManageChild(cascade);

#ifdef noGLwidget
	menupane = XmCreatePulldownMenu(menubar, "menupane", menuPaneArgs, 1);
#else
	menupane = XmCreatePulldownMenu(menubar, "menupane", NULL, 0);
#endif
	btn = XmCreateToggleButton(menupane, "Motion", NULL, 0);
	XtAddCallback(btn, XmNvalueChangedCallback, (XtCallbackProc)toggle, NULL);
	XtManageChild(btn);

	btn = XmCreatePushButton(menupane, "Add plane", NULL, 0);
	XtAddCallback(btn, XmNactivateCallback, (XtCallbackProc)add_plane, NULL);
	XtManageChild(btn);

	btn = XmCreatePushButton(menupane, "Remove plane", NULL, 0);
	XtAddCallback(btn, XmNactivateCallback, (XtCallbackProc)remove_plane, NULL);
	XtManageChild(btn);

	XtSetArg(args[0], XmNsubMenuId, menupane);
	cascade = XmCreateCascadeButton(menubar, "Planes", args, 1);
	XtManageChild(cascade);

	/* create framed drawing area for OpenGL rendering */
	frame = XmCreateFrame(mainw, "frame", NULL, 0);
	XtManageChild(frame);

#ifdef noGLwidget
	glxarea = XtVaCreateManagedWidget("glxarea", xmDrawingAreaWidgetClass, frame, NULL);
#elif defined(noMotifGLwidget)
	/* notice glwDrawingAreaWidgetClass lacks an 'M' */
	glxarea = XtVaCreateManagedWidget("glxarea", glwDrawingAreaWidgetClass,
	                                  frame, GLwNvisualInfo, vi, NULL);
#else
	glxarea = XtVaCreateManagedWidget("glxarea", glwMDrawingAreaWidgetClass,
	                                  frame, GLwNvisualInfo, vi, NULL);
#endif

	XtAddCallback(glxarea, XmNexposeCallback, draw, NULL);
	XtAddCallback(glxarea, XmNresizeCallback, resize, NULL);
	XtAddCallback(glxarea, XmNinputCallback, input, NULL);

	/* Set up application's window layout. */
	XmMainWindowSetAreas(mainw, menubar, NULL, NULL, NULL, frame);
	XtRealizeWidget(toplevel);

	/**
	 * Once widget is realized (ie, associated with a created X window),
	 * we can bind the OpenGL rendering context to the window.
	 */
#ifndef noGLwidget
	GLwDrawingAreaMakeCurrent(glxarea, cx);
#else
	glXMakeCurrent(dpy, XtWindow(glxarea), cx);
#endif
	made_current = True;

	/* Add three initial random planes. */
	srand(getpid());
	add_plane();
	add_plane();
	add_plane();

	XtAppMainLoop(app);
	glXDestroyContext(dpy, cx);
	return 0;
}

