/*	Public domain	*/
/*
 * This program demonstrates the use of the (now deprecated) AG_GLView(3)
 * widget, which provides a low-level OpenGL context.
 * 
 * Under OpenGL-based Agar display drivers, all the AG_GLView(3) widget
 * does is to save and restore the relevant GL matrices and states around
 * calls to Draw().
 *
 * Since Agar 1.5, this functionality is now implemented in base AG_Widget(3)
 * class, so any widget (which sets AG_WIDGET_USE_OPENGL) may use OpenGL
 * commands in draw() and the relevant GL states will be saved and restored
 * implicitely.
 */

#include "agartest.h"

#ifdef HAVE_OPENGL

#include <agar/gui/opengl.h>

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>

typedef struct {
	AG_TestInstance _inherit;
	enum { CUBE, SPHERE } primitive;
	enum { FLATSHADING, SMOOTHSHADING } shading;
	GLfloat spin;
	GLdouble vz;
	GLfloat ambient[4];
	GLfloat diffuse[4];
	GLfloat specular[4];
	GLfloat L1pos[4];
	GLfloat L2pos[4];
	int wireframe;
	int subdiv;
	int overlay;
	int moveLight;
} MyTestInstance;

static GLdouble isoVtx[12][3] = {    
#define X .525731112119133606 
#define Z .850650808352039932
    {-X, 0.0, Z}, {X, 0.0, Z}, {-X, 0.0, -Z}, {X, 0.0, -Z},    
    {0.0, Z, X}, {0.0, Z, -X}, {0.0, -Z, X}, {0.0, -Z, -X},    
    {Z, X, 0.0}, {-Z, X, 0.0}, {Z, -X, 0.0}, {-Z, -X, 0.0} 
};
static GLuint isoInd[20][3] = { 
    {0,4,1}, {0,9,4}, {9,5,4}, {4,5,8}, {4,8,1},    
    {8,10,1}, {8,3,10}, {5,3,8}, {5,2,3}, {2,7,3},    
    {7,10,3}, {7,6,10}, {7,11,6}, {11,0,6}, {0,1,6}, 
    {6,1,10}, {9,0,11}, {9,11,2}, {9,2,5}, {7,2,11} };


/* Widget resize callback function. */
static void
MyScaleFunction(AG_Event *event)
{
	GLdouble xMin, xMax, yMin, yMax;
	
	glLoadIdentity();

	/* Set a 60 degrees field of view with 1.0 aspect ratio. */
	yMax = 0.01*tan(0.523598f);
	yMin = -yMax;
	xMin = yMin;
	xMax = yMax;
	glFrustum(xMin, xMax, yMin, yMax, 0.01, 100.0);
}

static __inline__ void
Norm(GLdouble *a)
{
	GLdouble d = sqrt(a[0]*a[0] + a[1]*a[1] + a[2]*a[2]);

	a[0] /= d;
	a[1] /= d;
	a[2] /= d;
}

static void
DrawTriangle(GLdouble *a, GLdouble *b, GLdouble *c, int div, float r)
{
	if (div <= 0) {
		glNormal3dv(a); glVertex3d(a[0]*r, a[1]*r, a[2]*r);
		glNormal3dv(b); glVertex3d(b[0]*r, b[1]*r, b[2]*r);
		glNormal3dv(c); glVertex3d(c[0]*r, c[1]*r, c[2]*r);
	} else {
		GLdouble ab[3], ac[3], bc[3];
		int i;

		for (i = 0; i < 3; i++) {
			ab[i] = (a[i]+b[i])/2.0;
			ac[i] = (a[i]+c[i])/2.0;
			bc[i] = (b[i]+c[i])/2.0;
		}
		Norm(ab);
		Norm(ac);
		Norm(bc);
		DrawTriangle(a, ab, ac, div-1, r);
		DrawTriangle(b, bc, ab, div-1, r);
		DrawTriangle(c, ac, bc, div-1, r);
		DrawTriangle(ab, bc, ac, div-1, r);
	}
}

/* Render the test object. */
static void
MyDrawFunction(AG_Event *event)
{
	MyTestInstance *ti = AG_PTR(1);
	const int subdiv = ti->subdiv;
	int i;

	if (ti->moveLight) {
		if (ti->L2pos[2] == 1.1f) {
			ti->L2pos[0] -= 1.0f;
			if (ti->L2pos[0] <= -100.0f)
				ti->L2pos[2] = 1.0f;
		} else {
			ti->L2pos[0] += 1.0f;
			if (ti->L2pos[0] > 100.0f)
				ti->L2pos[2] = 1.1f;
		}
	}

	glLoadIdentity();
	glPushAttrib(GL_POLYGON_BIT | GL_LIGHTING_BIT | GL_DEPTH_BUFFER_BIT);

	glEnable(GL_LIGHTING);
	glEnable(GL_LIGHT0);
	glEnable(GL_LIGHT1);
	glEnable(GL_DEPTH_TEST);

	glPolygonMode(GL_FRONT_AND_BACK, ti->wireframe ? GL_LINE : GL_FILL);
	glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, ti->ambient);
	glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, ti->diffuse);
	glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, ti->specular);
	glShadeModel(ti->shading == FLATSHADING ? GL_FLAT : GL_SMOOTH);

	glLightfv(GL_LIGHT1, GL_POSITION, ti->L1pos);
	glLightf(GL_LIGHT1, GL_CONSTANT_ATTENUATION, 10.0f);

	glLightfv(GL_LIGHT0, GL_POSITION, ti->L2pos);
	glLightf(GL_LIGHT0, GL_CONSTANT_ATTENUATION, 10.0f);

	glLightfv(GL_LIGHT1, GL_AMBIENT, ti->ambient);
	glLightfv(GL_LIGHT1, GL_DIFFUSE, ti->diffuse);
	glLightfv(GL_LIGHT1, GL_SPECULAR, ti->specular);

	glPushMatrix();
	glTranslated(0.0, 0.0, ti->vz);
	glRotatef(ti->spin,0.0f,1.0f,0.0f);
	glRotatef(ti->spin,1.0f,1.0f,1.0f);

	switch (ti->primitive) {
	case CUBE:
		glBegin(GL_QUADS);
		glColor3f(1.0f, 0.0f, 0.0f);
		glVertex3f( 1.0f, 1.0f,-1.0f);
		glVertex3f(-1.0f, 1.0f,-1.0f);
		glVertex3f(-1.0f, 1.0f, 1.0f);
		glVertex3f( 1.0f, 1.0f, 1.0f);
		glVertex3f( 1.0f,-1.0f, 1.0f);
		glVertex3f(-1.0f,-1.0f, 1.0f);
		glVertex3f(-1.0f,-1.0f,-1.0f);
		glVertex3f( 1.0f,-1.0f,-1.0f);
		glVertex3f( 1.0f, 1.0f, 1.0f);
		glVertex3f(-1.0f, 1.0f, 1.0f);
		glVertex3f(-1.0f,-1.0f, 1.0f);
		glVertex3f( 1.0f,-1.0f, 1.0f);
		glVertex3f( 1.0f,-1.0f,-1.0f);
		glVertex3f(-1.0f,-1.0f,-1.0f);
		glVertex3f(-1.0f, 1.0f,-1.0f);
		glVertex3f( 1.0f, 1.0f,-1.0f);
		glVertex3f(-1.0f, 1.0f, 1.0f);
		glVertex3f(-1.0f, 1.0f,-1.0f);
		glVertex3f(-1.0f,-1.0f,-1.0f);
		glVertex3f(-1.0f,-1.0f, 1.0f);
		glVertex3f( 1.0f, 1.0f,-1.0f);
		glVertex3f( 1.0f, 1.0f, 1.0f);
		glVertex3f( 1.0f,-1.0f, 1.0f);
		glVertex3f( 1.0f,-1.0f,-1.0f);
		glEnd();
		break;
	case SPHERE:
		glBegin(GL_TRIANGLES);
		for (i = 0; i < 20; i++) {
			DrawTriangle(isoVtx[isoInd[i][0]],
			             isoVtx[isoInd[i][1]],
				     isoVtx[isoInd[i][2]],
				     subdiv, 1.0f);
		}
    		glEnd();
	}

	glPopMatrix();
	glPopAttrib();
}

#ifdef AG_TIMERS
static Uint32
UpdateRotation(AG_Timer *to, AG_Event *event)
{
	MyTestInstance *ti = AG_PTR(1);
	
	if (++ti->spin > 360.0f) { ti->spin -= 360.0f; }
	return (to->ival);
}
#endif /* AG_TIMERS */

/*
 * Overlay callback function. This type of callback is useful for rendering
 * things such as status text on top of the OpenGL context.
 */
static void
MyOverlayFunction(AG_Event *event)
{
	AG_GLView *glv = AG_GLVIEW_SELF();
	MyTestInstance *ti = AG_PTR(1);
	AG_Surface *myText;

	if (!ti->overlay)
		return;

	/* Render a text string using the font engine. */
	AG_PushTextState();
	AG_TextColorRGB(255, 255, 125);
	AG_TextFontLookup("league-gothic",
			  agZoomValues[AG_ParentWindow(glv)->zoom]*20.0f/100.0f, 0);
	myText = AG_TextRenderF("Rotation: %.0f degrees.\nZ = %.02f",
				ti->spin, ti->vz);

	AG_PopTextState();

	/*
	 * Blit the text at the lower left of the widget. Note that the
	 * AG_WidgetMapSurface() interface is much more efficient at this
	 * (hardware->hardware copy), unless the text changes frequently.
	 */
	if (myText != NULL) {
		AG_PushBlendingMode(glv, AG_ALPHA_SRC, AG_ALPHA_ONE_MINUS_SRC);

		AG_WidgetBlit(glv, myText,
		    0,
		    AGWIDGET(glv)->h - AGWIDGET_FONT(glv)->height*2.66f);

		AG_PopBlendingMode(glv);

		AG_SurfaceFree(myText);
	}
}

/* Control the Z using the mouse wheel. */
static void
ButtonDown(AG_Event *event)
{
	AG_GLView *glv = AG_GLVIEW_SELF();
	MyTestInstance *ti = AG_PTR(1);
	int button = AG_INT(2);

	AG_WidgetFocus(glv);

	switch (button) {
	case AG_MOUSE_WHEELDOWN:
		ti->vz -= 0.2;
		break;
	case AG_MOUSE_WHEELUP:
		ti->vz += 0.2;
		break;
	}
}

static int
Init(void *obj)
{
	MyTestInstance *ti = obj;
	GLfloat amb[4] = { 0.5f, 0.5f, 0.5f, 1.0f };
	GLfloat dif[4] = { 0.0f, 1.0f, 0.5f, 1.0f };
	GLfloat spe[4] = { 1.0f, 1.0f, 0.0f, 1.0f };
	int i;

	ti->primitive = SPHERE;
	ti->shading = FLATSHADING;
	ti->spin = 0.0f;
	ti->vz = -5.0;
	for (i = 0; i < 4; i++) {
		ti->ambient[i] = amb[i];
		ti->diffuse[i] = dif[i];
		ti->specular[i] = spe[i];
	}
	ti->wireframe = 0;
	ti->overlay = 1;
	ti->moveLight = 1;
	ti->subdiv = 4;

	ti->L1pos[0] = 10.0f;
	ti->L1pos[1] = 10.0f;
	ti->L1pos[2] = 0.0f;
	ti->L1pos[3] = 1.0f;

	ti->L2pos[0] = -100.0f;
	ti->L2pos[1] = 10.0f;
	ti->L2pos[2] = 10.0f;
	ti->L2pos[3] = 1.0f;

	return (0);
}

static int
TestGUI(void *obj, AG_Window *win)
{
	MyTestInstance *ti = obj;
	AG_GLView *glv;
	AG_Box *hb;
	AG_HSVPal *pal;

	hb = AG_BoxNewHoriz(win, AG_BOX_EXPAND);
	{
		AG_Pane *pa;
		AG_Notebook *nb, *nbColor;
		AG_NotebookTab *ntab;
		
		pa = AG_PaneNewHoriz(hb, AG_PANE_EXPAND|AG_PANE_DIV1FILL);
		AG_PaneResizeAction(pa, AG_PANE_EXPAND_DIV1);
		nb = AG_NotebookNew(pa->div[0], AG_NOTEBOOK_EXPAND);

		/* Test at 30 fps */
		ntab = AG_NotebookAdd(nb, "GLView", AG_BOX_VERT);
		{
			/*
			 * Create a GLView and size it 320x320. Use USE_TEXT
			 * because our overlay function needs to render text.
			 */
			glv = AG_GLViewNew(ntab, AG_GLVIEW_EXPAND);
			AGWIDGET(glv)->flags |= AG_WIDGET_USE_TEXT;
			AG_GLViewSizeHint(glv, 320,320);
			
			AG_GLViewScaleFn(glv, MyScaleFunction, NULL);
			AG_GLViewDrawFn(glv, MyDrawFunction, "%p", ti);
			AG_GLViewOverlayFn(glv, MyOverlayFunction, "%p", ti);
			AG_GLViewButtondownFn(glv, ButtonDown, "%p", ti);
#ifdef AG_TIMERS
			AG_AddTimerAuto(win, 1000/30,  UpdateRotation, "%p", ti);
#endif
			AG_RedrawOnTick(glv, 1000/30);             /* 30fps */
		}

		/*
		 * Edit Ambient, Diffuse and Specular color components.
		 */
		nbColor = AG_NotebookNew(pa->div[1], AG_NOTEBOOK_EXPAND);
		{
			const Uint flags = AG_HSVPAL_NOALPHA | AG_HSVPAL_EXPAND;
			AG_Label *lbl;

			/* Ambient */
			ntab = AG_NotebookAdd(nbColor, "A", AG_BOX_VERT);
			AG_BoxSetHorizAlign(AGBOX(ntab), AG_BOX_CENTER); 
			lbl = AG_LabelNewS(ntab, 0, "Ambient");
			AG_SetFontFamily(lbl, "league-spartan");

			pal = AG_HSVPalNew(ntab, flags);
			AG_BindFloat(pal, "RGBAv", ti->ambient);

			/* Diffuse */
			ntab = AG_NotebookAdd(nbColor, "D", AG_BOX_VERT);
			AG_BoxSetHorizAlign(AGBOX(ntab), AG_BOX_CENTER); 
			lbl = AG_LabelNewS(ntab, 0, "Diffuse");
			AG_SetFontFamily(lbl, "league-spartan");

			pal = AG_HSVPalNew(ntab, flags);
			AG_BindFloat(pal, "RGBAv", ti->diffuse);

			/* Specular */
			ntab = AG_NotebookAdd(nbColor, "S", AG_BOX_VERT);
			AG_BoxSetHorizAlign(AGBOX(ntab), AG_BOX_CENTER); 
			lbl = AG_LabelNewS(ntab, 0, "Specular");
			AG_SetFontFamily(lbl, "league-spartan");

			pal = AG_HSVPalNew(ntab, flags);
			AG_BindFloat(pal, "RGBAv", ti->specular);
		}
	}
	hb = AG_BoxNewHoriz(win, AG_BOX_HFILL);
	{
		const char *primitiveNames[] = { "Cube", "Sphere", NULL };
		const char *shadingNames[] = { "Flat Shading", "Smooth Shading", NULL };
		AG_Box *vb;
		AG_Numerical *num;
		AG_Editable *edNum;
		AG_Radio *rad;

		rad = AG_RadioNewInt(hb, 0, primitiveNames, (void *)&ti->primitive);
		AG_SetFontSize(rad, "120%");

		num = AG_NumericalNewIntR(hb, 0, NULL, "Sphere\nsubdiv: ",
		    &ti->subdiv, 0,8);
		edNum = num->input->ed;
		AG_SetFontFamily(edNum, "charter");
		AG_SetFontSize(edNum, "160%");
		AG_SetFontWeight(edNum, "bold");
		AG_SetMargin(num, "0 10 0 10");

		rad = AG_RadioNewInt(hb, 0, shadingNames, (void *)&ti->shading);
		AG_SetFontSize(rad, "80%");

		vb = AG_BoxNewVert(hb, AG_BOX_EXPAND);
		AG_SetFontSize(vb, "80%");
		AG_SetSpacing(vb, "0");
		AG_SetMargin(vb, "0 0 0 5");
		{
			AG_CheckboxNewInt(vb, 0, "Moving Light", &ti->moveLight);
			AG_CheckboxNewInt(vb, 0, "Text Overlay", &ti->overlay);
			AG_CheckboxNewInt(vb, 0, "Wireframe", &ti->wireframe);
		}
	}
	return (0);
}

#endif /* HAVE_OPENGL */

const AG_TestCase glviewTest = {
	AGSI_IDEOGRAM AGSI_WIREFRAME_CUBE AGSI_RST,
	"glview",
	"Test the AG_GLView(3) widget (OpenGL required)",
	"1.6.0",
	AG_TEST_OPENGL,
#ifdef HAVE_OPENGL
	sizeof(MyTestInstance),
	Init,
	NULL,
	NULL,		/* test */
	TestGUI,
#else
	sizeof(AG_TestInstance),
	NULL,		/* init */
	NULL,		/* destroy */
	NULL,		/* test */
	NULL,		/* testGUI */
#endif
	NULL		/* bench */
};
