#include <GL/glut.h>
#include <math.h>
#include "texture.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

/* nonzero if not power of 2 */ 
#define NOTPOW2(num) ((num) & (num - 1))

int
makepow2(int val)
{
    int power = 0;
    if(!val)
	return 0;

    while(val >>= 1)
	power++;

    return(1 << power);
}

#define CHECK_ERROR(str)                                           \
{                                                                  \
    GLenum error;                                                  \
    if(error = glGetError())                                       \
       printf("GL Error: %s (%s)\n", gluErrorString(error), str);  \
}

enum {X, Y, Z, W};
enum {R, G, B, A};
enum {OVER, ATTENUATE, NONE, LASTOP}; /* blend modes */
/* mouse modes */
enum {OBJ_ANGLE, SLICES, CUTTING, GEOMXY, GEOMZ, MINBOOST, BOOSTWID, BOOST}; 
enum {NOLIST, SPHERE}; /* display list */

/* window dimensions */
int winWidth = 512;
int winHeight = 512;
int active;
int operator = OVER;
GLboolean texture = GL_TRUE;
GLboolean dblbuf = GL_TRUE;
GLboolean cut = GL_FALSE;
GLboolean geom = GL_FALSE;
GLboolean map = GL_FALSE;
GLint cutbias = 50;
int hasBlendColor = 0;

GLfloat objangle[2] = {0.f, 0.f};
GLfloat objpos[3] = {0.f, 0.f, 0.f};


GLfloat minboost = 0.f, boostwid = .03f, boost = 3.f; /* transfer function */

/* 3d texture data that's read in */
/* XXX TODO; make command line arguments */
int Texwid = 128; /* dimensions of each 2D texture */
int Texht = 128;
int Texdepth = 69; /* number of 2D textures */

/* Actual dimensions of the texture (restricted to max 3d texture size) */
int texwid, texht, texdepth;
int slices;
GLubyte *tex3ddata; /* pointer to 3D texture data */


GLfloat *lighttex = 0;
GLfloat lightpos[4] = {0.f, 0.f, 1.f, 0.f};
GLboolean lightchanged[2] = {GL_TRUE, GL_TRUE};


void
reshape(int wid, int ht)
{
    winWidth = wid;
    winHeight = ht;
    glViewport(0, 0, wid, ht);
}


void
motion(int x, int y)
{
    switch(active)
    {
    case OBJ_ANGLE:
	objangle[X] = (x - winWidth/2) * 360./winWidth;
	objangle[Y] = (y - winHeight/2) * 360./winHeight;
	glutPostRedisplay();
	break;
    case SLICES:
	slices = x * texwid/winWidth;
	glutPostRedisplay();
	break;
    case CUTTING:
	cutbias = (x - winWidth/2) * 300/winWidth;
	glutPostRedisplay();
	break;
    case GEOMXY:
	objpos[X] = (x - winWidth/2) * 300/winWidth;
	objpos[Y] = (winHeight/2 - y) * 300/winHeight;
	glutPostRedisplay();
	break;
    case GEOMZ:
	objpos[Z] = (x - winWidth/2) * 300/winWidth;
	glutPostRedisplay();
	break;
    case MINBOOST:
	minboost = x * .25f/winWidth;
	glutPostRedisplay();
	break;
    case BOOSTWID:
	boostwid = x * .5f/winWidth;
	glutPostRedisplay();
	break;
    case BOOST:
	boost = x * 20.f/winWidth;
	glutPostRedisplay();
	break;
    }
}

void
mouse(int button, int state, int x, int y)
{
    if(state == GLUT_DOWN)
	switch(button)
	{
	case GLUT_LEFT_BUTTON: /* rotate the data volume */
	    if(map)
		active = MINBOOST;
	    else
		active = OBJ_ANGLE;
	    motion(x, y);
	    break;
	case GLUT_MIDDLE_BUTTON:
	    if(map)
		active = BOOSTWID;
	    else
		if(cut)
		    active = CUTTING; /* move cutting plane */
		else
		    active = GEOMXY; /* move geometry */
	    motion(x, y);
	    break;
	case GLUT_RIGHT_BUTTON: /* move the polygon */
	    if(map)
		active = BOOST;
	    else
		if(geom)
		    active = GEOMZ;
		else
		    active = SLICES;
	    motion(x, y);
	    break;
	}
}

/* use pixel path to remap 3D texture data */
void
remaptex(void)
{
    int i, size;
    GLfloat *map;

    glPixelTransferi(GL_MAP_COLOR, GL_TRUE);

    glGetIntegerv(GL_MAX_PIXEL_MAP_TABLE, &size);

    map = (GLfloat *)malloc(sizeof(GLfloat) * size);
    for(i = 0; i < size;i++)
    {
	map[i] = (GLfloat)i/(size - 1);
	if(((GLfloat)i/size > minboost) &&
	   ((GLfloat)i/size < minboost + boostwid))
	{
	    map[i] *= boost;
	}
	else
	    map[i] /= boost;
    }

    glPixelMapfv(GL_PIXEL_MAP_R_TO_R, size, map);
    glPixelMapfv(GL_PIXEL_MAP_G_TO_G, size, map);
    glPixelMapfv(GL_PIXEL_MAP_B_TO_B, size, map);
    glPixelMapfv(GL_PIXEL_MAP_A_TO_A, size, map);

#ifdef GL_EXT_texture3D
    glTexImage3DEXT(GL_TEXTURE_3D_EXT, 0, GL_LUMINANCE_ALPHA,
		    texwid, texht, texdepth,
		    0,
		    GL_RGBA, GL_UNSIGNED_BYTE, tex3ddata);
#endif

    glPixelTransferi(GL_MAP_COLOR, GL_FALSE);
    free(map);

    CHECK_ERROR("OpenGL Error in remaptex()");
}


GLdouble clipplane0[] = {-1.,  0.,  0., 100.}; /* x < 100 out */
GLdouble clipplane1[] = { 1.,  0.,  0., 100.}; /* x > 100 out */
GLdouble clipplane2[] = { 0., -1.,  0., 100.}; /* y < 100 out */
GLdouble clipplane3[] = { 0.,  1.,  0., 100.}; /* y > 100 out */
GLdouble clipplane4[] = { 0.,  0., -1., 100.}; /* z < 100 out */
GLdouble clipplane5[] = { 0.,  0.,  1., 100.}; /* z > 100 out */

/* define a cutting plane */
GLdouble cutplane[] = {0.f, -.5f, -2.f, 50.f};

/* draw the object unlit without surface texture */
void redraw(void)
{
    int i;
    GLfloat offS, offT, offR; /* mapping texture to planes */

    offS = 200.f/texwid;
    offT = 200.f/texht;
    offR = 200.f/texdepth;
    
    clipplane0[W] = 100.f - offS;
    clipplane1[W] = 100.f - offS;
    clipplane2[W] = 100.f - offT;
    clipplane3[W] = 100.f - offT;
    clipplane4[W] = 100.f - offR;
    clipplane5[W] = 100.f - offR;

    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);

    if(map)
	remaptex();

    /* GL_MODELVIEW */
    if(cut)
    {
	cutplane[W] = cutbias;
	glClipPlane(GL_CLIP_PLANE5, cutplane);
    }

    glPushMatrix(); /* identity */
    glRotatef(objangle[X], 0.f, 1.f, 0.f);
    glRotatef(objangle[Y], 1.f, 0.f, 0.f);
    glClipPlane(GL_CLIP_PLANE0, clipplane0);
    glClipPlane(GL_CLIP_PLANE1, clipplane1);
    glClipPlane(GL_CLIP_PLANE2, clipplane2);
    glClipPlane(GL_CLIP_PLANE3, clipplane3);
    glClipPlane(GL_CLIP_PLANE4, clipplane4);
    if(!cut)
	glClipPlane(GL_CLIP_PLANE5, clipplane5);
    glPopMatrix(); /* back to identity */

    /* draw opaque geometry here */
    glDisable(GL_CLIP_PLANE0);
    glDisable(GL_CLIP_PLANE1);
    glDisable(GL_CLIP_PLANE2);
    glDisable(GL_CLIP_PLANE3);
    glDisable(GL_CLIP_PLANE4);
    if(geom)
    {
	if(!cut)
	    glDisable(GL_CLIP_PLANE5);
	glPushMatrix();
	glTranslatef(objpos[X], objpos[Y], objpos[Z]);
	glCallList(SPHERE);
	glPopMatrix();
    }
    glMatrixMode(GL_TEXTURE);
    glEnable(GL_CLIP_PLANE0);
    glEnable(GL_CLIP_PLANE1);
    glEnable(GL_CLIP_PLANE2);
    glEnable(GL_CLIP_PLANE3);
    glEnable(GL_CLIP_PLANE4);
    glEnable(GL_CLIP_PLANE5);

    glMatrixMode(GL_TEXTURE);
    glPushMatrix(); /* identity */
    glTranslatef( .5f,  .5f, .5f);
    glRotatef(objangle[Y], 1.f, 0.f, 0.f);
    glRotatef(objangle[X], 0.f, 0.f, 1.f);
    glTranslatef( -.5f,  -.5f, -.5f);

    switch(operator)
    {
    case OVER:
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	break;
    case ATTENUATE:
#ifdef GL_EXT_blend_color
        if (hasBlendColor){
	    glEnable(GL_BLEND);
	    glBlendFunc(GL_CONSTANT_ALPHA_EXT, GL_ONE);
	    glBlendColorEXT(1.f, 1.f, 1.f, 1.f/slices);
        } else
#endif
        {
            fprintf(stderr, "volume: attenuate not supported!\n");
        }
        break;
    case NONE:
	/* don't blend */
	break;
    }

    if(texture) {
#ifdef GL_EXT_texture3D
       glEnable(GL_TEXTURE_3D_EXT);
#endif
    } else {
#ifdef GL_EXT_texture3D
       glDisable(GL_TEXTURE_3D_EXT);
#endif
       glEnable(GL_LIGHTING);
       glEnable(GL_LIGHT0);
    }

    

    for(i = 0; i < slices; i++)
    {
	glBegin(GL_QUADS);
	glVertex3f(-100.f, -100.f, 
		   -100.f + offR + i * (200.f - 2 * offR)/(slices - 1));
	glVertex3f( 100.f, -100.f,
		   -100.f + offR + i * (200.f - 2 * offR)/(slices - 1));
	glVertex3f( 100.f,  100.f,
		   -100.f + offR + i * (200.f - 2 * offR)/(slices - 1));
	glVertex3f(-100.f,  100.f,
		   -100.f + offR + i * (200.f - 2 * offR)/(slices - 1));
	glEnd();
    }
#ifdef GL_EXT_texture3D
    glDisable(GL_TEXTURE_3D_EXT);
#endif
    if(!texture)
    {
       glDisable(GL_LIGHTING);
    }
    glDisable(GL_BLEND);

    glPopMatrix(); /* back to identity */
    glMatrixMode(GL_MODELVIEW);

    if(operator == ATTENUATE)
    {
	glPixelTransferf(GL_RED_SCALE, 3.f); /* brighten image */
	glPixelTransferf(GL_GREEN_SCALE, 3.f);
	glPixelTransferf(GL_BLUE_SCALE, 3.f);
	glCopyPixels(0, 0, winWidth, winHeight, GL_COLOR);
    }
    if(dblbuf)
	glutSwapBuffers(); 
    else
	glFlush(); 

    CHECK_ERROR("OpenGL Error in redraw()");
}

/* ARGSUSED1 */
void key(unsigned char key, int x, int y)
{
    switch(key)
    {
    case 'm': /* remap texture values */
	if(map)
	{
	    fprintf(stderr, "remapping off\n");
	    map = GL_FALSE;
	}
	else
	{
	    fprintf(stderr, "remapping on:\n"
		            "left mouse moves emphasize value\n"
		            "middle mouse moves emphasize width\n"
		            "right mouse adjusts gain\n");
	    map = GL_TRUE;
	}

	remaptex();
	glutPostRedisplay();
	break;
    case 'o':
	operator++;
	if(operator == LASTOP)
	    operator = OVER;
	glutPostRedisplay();
	break;
    case 't':
	if(texture)
	    texture = GL_FALSE;
	else
	    texture = GL_TRUE;
	glutPostRedisplay();
	break;
    case 'c':
	if(cut)
	{
	    fprintf(stderr, "cutting plane off\n");
	    cut = GL_FALSE;
	}
	else
	{
	    fprintf(stderr, 
		    "Cutting plane on: "
		    "middle mouse (horizontal) moves cutting plane\n");
	    cut = GL_TRUE;
	}
	glutPostRedisplay();
	break;
    case 'g': /* toggle geometry */
	if(geom)
	    geom = GL_FALSE;
	else
	    geom = GL_TRUE;
	glutPostRedisplay();
	break;
    case '\033':
	exit(0);
	break;
    case '?':
    case 'h':
    default:
	fprintf(stderr, 
		"Keyboard Commands\n"
		"m - toggle transfer function (remapping)\n"
		"o - toggle operator\n"
		"t - toggle 3D texturing\n"
		"c - toggle cutting plane\n"
		"g - toggle geometry\n");
	break;
    }
}

GLubyte *
loadtex3d(int *texwid, int *texht, int *texdepth, int *texcomps)
{
    char *filename;
    GLubyte *tex3ddata;
    GLuint *texslice; /* 2D slice of 3D texture */
    GLint max3dtexdims; /* maximum allowed 3d texture dimension */
    GLint newval;
    int i;

    /* load 3D texture data */
    filename = (char*)malloc(sizeof(char) * strlen("data/skull/skullXX.la"));

    tex3ddata = (GLubyte *)malloc(Texwid * Texht * Texdepth * 
				  4 * sizeof(GLubyte));
    for(i = 0; i < Texdepth; i++)
    {
	sprintf(filename, "data/skull/skull%d.la", i);
	/* read_texture reads as RGBA */
	texslice = read_texture(filename, texwid, texht, texcomps);
	memcpy(&tex3ddata[i * Texwid * Texht * 4],  /* copy in a slice */
	       texslice, 
	       Texwid * Texht * 4 * sizeof(GLubyte));
	free(texslice);
    }
    free(filename);

    *texdepth = Texdepth;

    max3dtexdims = 0;
#ifdef GL_EXT_texture3D
    glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE_EXT, &max3dtexdims);
#endif

    /* adjust width */
    newval = *texwid;
    if(*texwid > max3dtexdims)
	newval = max3dtexdims;
    if(NOTPOW2(*texwid))
        newval = makepow2(*texwid);
    if(newval != *texwid)
    {
	glPixelStorei(GL_UNPACK_ROW_LENGTH, *texwid);
	glPixelStorei(GL_UNPACK_SKIP_PIXELS, (*texwid - newval)/2);
	*texwid = newval;
    }

    /* adjust height */
    newval = *texht;
    if(*texht > max3dtexdims)
	newval = max3dtexdims;
    if(NOTPOW2(*texht))
        newval = makepow2(*texht);
    if(*texht > newval)
    {
#ifdef GL_EXT_texture3D
	glPixelStorei(GL_UNPACK_IMAGE_HEIGHT_EXT, *texht);
#endif
	glPixelStorei(GL_UNPACK_SKIP_ROWS, (*texht - newval)/2);
	*texht = newval;
    }

    /* adjust depth */
    newval = *texdepth;
    if(*texdepth > max3dtexdims)
	newval = max3dtexdims;
    if(NOTPOW2(*texdepth))
        newval = makepow2(*texdepth);
    if(*texdepth > newval)
    {
	*texdepth = newval;
    }
    return tex3ddata;
}



main(int argc, char *argv[])
{
    int texcomps;
    static GLfloat splane[4] = {1.f/200.f, 0.f, 0.f, .5f};
    static GLfloat rplane[4] = {0, 1.f/200.f, 0, .5f};
    static GLfloat tplane[4] = {0, 0, 1.f/200.f, .5f};
    static GLfloat lightpos[4] = {150., 150., 150., 1.f};


    glutInit(&argc, argv);
    glutInitWindowSize(winWidth, winHeight);
    if(argc > 1)
    {
	char *args = argv[1];
	GLboolean done = GL_FALSE;
	while(!done)
	{
	    switch(*args)
	    {
	    case 's': /* single buffer */
		printf("Single Buffered\n");
		dblbuf = GL_FALSE;
		break;
	    case '-': /* do nothing */
		break;
	    case 0:
		done = GL_TRUE;
		break;
	    }
	    args++;
	}
    }
    if(dblbuf)
	glutInitDisplayMode(GLUT_RGBA|GLUT_DEPTH|GLUT_DOUBLE);
    else
	glutInitDisplayMode(GLUT_RGBA|GLUT_DEPTH);

    (void)glutCreateWindow("volume rendering demo");
    glutDisplayFunc(redraw);
    glutReshapeFunc(reshape);
    glutMouseFunc(mouse);
    glutMotionFunc(motion);
    glutKeyboardFunc(key);

    /* Initialize OpenGL State */

    /* draw a perspective scene */
#if 0
    glMatrixMode(GL_PROJECTION);
    /* cube, 300 on a side */
    glFrustum(-150., 150., -150., 150., 300., 600.);
    glMatrixMode(GL_MODELVIEW);
    /* look at scene from (0, 0, 450) */
    gluLookAt(0., 0., 450., 0., 0., 0., 0., 1., 0.);
#else
    glMatrixMode(GL_PROJECTION);
    /* cube, 300 on a side */
    glOrtho(-150., 150., -150., 150., -150., 150.);
    glMatrixMode(GL_MODELVIEW);
#endif

    glEnable(GL_DEPTH_TEST);
#ifdef GL_EXT_texture3D
    glEnable(GL_TEXTURE_3D_EXT);
#endif

    glEnable(GL_TEXTURE_GEN_S);
    glEnable(GL_TEXTURE_GEN_T);
    glEnable(GL_TEXTURE_GEN_R);

    glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
    glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
    glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);

    glTexGenfv(GL_S, GL_OBJECT_PLANE, splane);
    glTexGenfv(GL_T, GL_OBJECT_PLANE, tplane);
    glTexGenfv(GL_R, GL_OBJECT_PLANE, rplane);

#ifdef GL_EXT_texture3D
    /* to avoid boundary problems */
    glTexParameteri(GL_TEXTURE_3D_EXT, GL_TEXTURE_WRAP_S, GL_CLAMP);
    glTexParameteri(GL_TEXTURE_3D_EXT, GL_TEXTURE_WRAP_T, GL_CLAMP);
    glTexParameteri(GL_TEXTURE_3D_EXT, GL_TEXTURE_WRAP_R_EXT, GL_CLAMP);
#endif

    glEnable(GL_CLIP_PLANE0);
    glEnable(GL_CLIP_PLANE1);
    glEnable(GL_CLIP_PLANE2);
    glEnable(GL_CLIP_PLANE3);
    glEnable(GL_CLIP_PLANE4);
    glEnable(GL_CLIP_PLANE5);

    glDisable(GL_LIGHT0);
    glLightfv(GL_LIGHT0, GL_POSITION, lightpos);



    tex3ddata = loadtex3d(&texwid, &texht, &texdepth, &texcomps);

    slices = texht;

#ifdef GL_EXT_texture3D
    glTexParameteri(GL_TEXTURE_3D_EXT, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexImage3DEXT(GL_TEXTURE_3D_EXT, 0, GL_LUMINANCE_ALPHA,
		    texwid, texht, texdepth,
		    0,
		    GL_RGBA, GL_UNSIGNED_BYTE, tex3ddata);
#endif

    /* make a display list containing a sphere */
    glNewList(SPHERE, GL_COMPILE);
    {
	static GLfloat lightpos[] = {150.f, 150.f, 150.f, 1.f};
	static GLfloat material[] = {1.f, .5f, 1.f, 1.f};
	GLUquadricObj *qobj = gluNewQuadric();
	glPushAttrib(GL_LIGHTING_BIT);
	glEnable(GL_LIGHTING);
	glEnable(GL_LIGHT0);
	glLightfv(GL_LIGHT0, GL_POSITION, lightpos);
	glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, material);
	gluSphere(qobj, 20.f, 20, 20);
	gluDeleteQuadric(qobj);
	glPopAttrib();
    }
    glEndList();

    key('?', 0, 0); /* print usage message */

    CHECK_ERROR("end of main");

    if(!glutExtensionSupported("GL_EXT_texture3d")) {
      fprintf(stderr,
        "volume: requires OpenGL texture 3D extension to operate correctly.\n");
    }
    hasBlendColor = glutExtensionSupported("GL_EXT_blend_color");
    if(!hasBlendColor) {
      fprintf(stderr,
        "volume: needs OpenGL blend color extension to attenuate.\n");
    }

    glutMainLoop();
    return 0;             /* ANSI C requires main to return int. */
}
