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

/* Some <math.h> files do not define M_PI... */
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

#define TRUE	1
#define FALSE	0

int whichSurface = 0;
int takeSnapshot = 0;


int 		winWidth, winHeight;
GLfloat 	*depthSave = NULL;
GLubyte 	*stencilSave = NULL;
GLubyte 	*colorSave = NULL;


void resizeBuffers(void)
{
    colorSave = realloc(colorSave, winWidth * winHeight * 4 * sizeof(GLubyte));
    depthSave = realloc(depthSave, winWidth * winHeight * 4 * sizeof(GLfloat));
    stencilSave = (GLubyte *)depthSave;
}


void pushOrthoView(float left, float right, float bottom, float top,
    float znear, float zfar)
{
    glPushMatrix();
    glLoadIdentity();
    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    glOrtho(left, right, bottom, top, znear, zfar);
}


void popView(void)
{
    glPopMatrix();
    glMatrixMode(GL_MODELVIEW);
    glPopMatrix();
}


void copyDepthToColor(GLenum whichColorBuffer)
{
    int		x, y;
    GLfloat	max, min;
    GLint	previousColorBuffer;

    glReadPixels(0, 0, winWidth, winHeight, GL_DEPTH_COMPONENT, GL_FLOAT,
        depthSave);

    /* I'm sure this could be done much better with OpenGL */
    max = 0;
    min = 1;
    for(y = 0; y < winHeight; y++)
	for(x = 0; x < winWidth; x++) {
	    if(depthSave[winWidth * y + x] < min)
		min = depthSave[winWidth * y + x];
	    if(depthSave[winWidth * y + x] > max && depthSave[winWidth * y + x] < .999)
		max = depthSave[winWidth * y + x];
	}

    for(y = 0; y < winHeight; y++)
	for(x = 0; x < winWidth; x++) {
	    if(depthSave[winWidth * y + x] <= max)
		depthSave[winWidth * y + x] = 1 -  (depthSave[winWidth * y + x] - min) / (max - min);
	    else
		depthSave[winWidth * y + x] = 0;
	}

    pushOrthoView(0, 1, 0, 1, 0, 1);
    glRasterPos3f(0, 0, -.5);
    glDisable(GL_DEPTH_TEST);
    glDisable(GL_STENCIL_TEST);
    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
    glGetIntegerv(GL_DRAW_BUFFER, &previousColorBuffer);
    glDrawBuffer(whichColorBuffer);
    glDrawPixels(winWidth, winHeight, GL_LUMINANCE , GL_FLOAT, depthSave);
    glDrawBuffer(previousColorBuffer);
    glEnable(GL_DEPTH_TEST);
    popView();
}


unsigned char colors[][3] =
{
    {255, 0, 0},	/* red */
    {255, 218, 0},	/* yellow */
    {72, 255, 0},	/* yellowish green */
    {0, 255, 145},	/* bluish cyan */
    {0, 145, 255},	/* cyanish blue */
    {72, 0, 255},	/* purplish blue */
    {255, 0, 218},	/* reddish purple */
};


void copyStencilToColor(GLenum whichColorBuffer)
{
    int		x, y;
    GLint	previousColorBuffer;

    glReadPixels(0, 0, winWidth, winHeight, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE,
        stencilSave);

    /* I'm sure this could be done much better with OpenGL */
    for(y = 0; y < winHeight; y++)
	for(x = 0; x < winWidth; x++) {
	    int stencilValue;
	    
	    stencilValue = stencilSave[winWidth * y + x];

	    colorSave[(winWidth * y + x) * 3 + 0] = colors[stencilValue % 7][0];
	    colorSave[(winWidth * y + x) * 3 + 1] = colors[stencilValue % 7][1];
	    colorSave[(winWidth * y + x) * 3 + 2] = colors[stencilValue % 7][2];
	}

    pushOrthoView(0, 1, 0, 1, 0, 1);
    glRasterPos3f(0, 0, -.5);
    glDisable(GL_DEPTH_TEST);
    glDisable(GL_STENCIL_TEST);
    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
    glGetIntegerv(GL_DRAW_BUFFER, &previousColorBuffer);
    glDrawBuffer(whichColorBuffer);
    glDrawPixels(winWidth, winHeight, GL_RGB, GL_UNSIGNED_BYTE, colorSave);
    glDrawBuffer(previousColorBuffer);
    glEnable(GL_DEPTH_TEST);
    popView();
}


#if 0

GLushort rrow[1280], grow[1280], brow[1280], arow[1280];


#define iopen junk
#include <gl/image.h>
#undef iopen


IMAGE *iopen(char *file, char *mode, ...);
int putrow(IMAGE *image, unsigned short *buff, int y, int z);
int iclose(IMAGE *);


void saveSGIImage(char *name, int doAlphaToo)
{
    IMAGE *img;
    FILE *fp;
    GLubyte *pixels; 
    int x, y;
    int numComp = doAlphaToo ? 4 : 3;

    pixels = malloc(winWidth * winHeight * numComp * sizeof(GLubyte));

    glReadPixels(0, 0, winWidth, winHeight, doAlphaToo ? GL_RGBA : GL_RGB,
        GL_UNSIGNED_BYTE, pixels);

    img = iopen(name, "w", RLE(1), numComp, winWidth, winHeight, numComp);

    for(y = 0; y < winHeight; y++) {
        for(x = 0; x < winWidth; x++) {
	    rrow[x] = pixels[(y * winWidth + x) * numComp + 0];
	    grow[x] = pixels[(y * winWidth + x) * numComp + 1];
	    brow[x] = pixels[(y * winWidth + x) * numComp + 2];
	    if(doAlphaToo)
		arow[x] = pixels[(y * winWidth + x) * numComp + 3];
	}
        putrow(img, rrow, y, 0);
        putrow(img, grow, y, 1);
        putrow(img, brow, y, 2);
	if(doAlphaToo)
	    putrow(img, arow, y, 3);
    }
    iclose(img);

    free(pixels);
}

#endif


struct transformation {
    float	translation[3];
    float	rotation[4];
    float	scale[3];
};


void drawXform(struct transformation *xform, int applyScale)
{
    glTranslatef(xform->translation[0], xform->translation[1], xform->translation[2]);
    glRotatef(xform->rotation[3] / M_PI * 180, xform->rotation[0], xform->rotation[1], xform->rotation[2]);
    if(applyScale)
	glScalef(xform->scale[0], xform->scale[1], xform->scale[2]);
}


enum trackballModeEnum {
    ROTATE,
    TRANSLATEXY,
    TRANSLATEZ,
    SCALEX,
    SCALEY,
    SCALEZ
} trackballMode = ROTATE;


struct transformation xform = 
{
    0, 0, 0,
    -0.65, -0.75, -0.04, 0.89,
    .7, .7, 2,
};


void axisamountToMat(float aa[], float mat[])
{
    float c, s, t;

    c = (float)cos(aa[3]);
    s = (float)sin(aa[3]);
    t = 1.0f - c;

    mat[0] = t * aa[0] * aa[0] + c;
    mat[1] = t * aa[0] * aa[1] + s * aa[2];
    mat[2] = t * aa[0] * aa[2] - s * aa[1];
    mat[3] = t * aa[0] * aa[1] - s * aa[2];
    mat[4] = t * aa[1] * aa[1] + c;
    mat[5] = t * aa[1] * aa[2] + s * aa[0];
    mat[6] = t * aa[0] * aa[2] + s * aa[1];
    mat[7] = t * aa[1] * aa[2] - s * aa[0];
    mat[8] = t * aa[2] * aa[2] + c;
}


void matToAxisamount(float mat[], float aa[])
{
    float c;
    float s;

    c = (mat[0] + mat[4] + mat[8] - 1.0f) / 2.0f;
    aa[3] = (float)acos(c);
    s = (float)sin(aa[3]);
    if(fabs(s / M_PI - (int)(s / M_PI)) < .0000001)
    {
        aa[0] = 0.0f;
        aa[1] = 1.0f;
        aa[2] = 0.0f;
    }
    else
    {
	aa[0] = (mat[5] - mat[7]) / (2.0f * s);
	aa[1] = (mat[6] - mat[2]) / (2.0f * s);
	aa[2] = (mat[1] - mat[3]) / (2.0f * s);
    }
}


void multMat(float m1[], float m2[], float r[])
{
    float t[9];
    int i;

    t[0] = m1[0] * m2[0] + m1[1] * m2[3] + m1[2] * m2[6];
    t[1] = m1[0] * m2[1] + m1[1] * m2[4] + m1[2] * m2[7];
    t[2] = m1[0] * m2[2] + m1[1] * m2[5] + m1[2] * m2[8];
    t[3] = m1[3] * m2[0] + m1[4] * m2[3] + m1[5] * m2[6];
    t[4] = m1[3] * m2[1] + m1[4] * m2[4] + m1[5] * m2[7];
    t[5] = m1[3] * m2[2] + m1[4] * m2[5] + m1[5] * m2[8];
    t[6] = m1[6] * m2[0] + m1[7] * m2[3] + m1[8] * m2[6];
    t[7] = m1[6] * m2[1] + m1[7] * m2[4] + m1[8] * m2[7];
    t[8] = m1[6] * m2[2] + m1[7] * m2[5] + m1[8] * m2[8];
    for(i = 0; i < 9; i++)
    {
        r[i] = t[i];
    }
}


void rotateTrackball(int dx, int dy, float rotation[4])
{
    float dist;
    float oldMat[9];
    float rotMat[9];
    float newRot[4];

    dist = (float)sqrt((double)(dx * dx + dy * dy));
    if(fabs(dist) < 0.99)
        return;

    newRot[0] = (float) dy / dist;
    newRot[1] = (float) dx / dist;
    newRot[2] = 0.0f;
    newRot[3] = (float)M_PI * dist / winWidth;

    axisamountToMat(rotation, oldMat);
    axisamountToMat(newRot, rotMat);
    multMat(oldMat, rotMat, oldMat);
    matToAxisamount(oldMat, rotation);

    dist = (float)sqrt(rotation[0] * rotation[0] + rotation[1] * rotation[1] +
        rotation[2] * rotation[2]);

    rotation[0] /= dist;
    rotation[1] /= dist;
    rotation[2] /= dist;
}


int stage = 6;
typedef enum {COLOR, DEPTH, STENCIL} BufferInterest;

BufferInterest bufferInterest = COLOR;


void init(void)
{
    GLfloat defaultMat[] = {.75, .25, .25, 1};

    glEnable(GL_DEPTH_TEST);
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);

    glMatrixMode(GL_PROJECTION);
    glFrustum(-.33, .33, -.33, .33, .5, 40);

    glMatrixMode(GL_MODELVIEW);
    gluLookAt(0, 0, 10, 0, 0, 0, 0, 1, 0);

    glEnable(GL_LIGHTING);
    glEnable(GL_LIGHT0);

    /* glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); */

    glEnable(GL_NORMALIZE);

    glEnable(GL_STENCIL_TEST);
    glStencilOp(GL_INCR, GL_INCR, GL_INCR);

    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, defaultMat);

    glLineWidth(3);
    glShadeModel(GL_FLAT);
}


void redraw(void)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    
    glPushMatrix();

    drawXform(&xform, TRUE);

    glEnable(GL_STENCIL_TEST);
    if(whichSurface < 2)
	glStencilFunc(GL_EQUAL, whichSurface, 0xff);
    else
	glStencilFunc(GL_ALWAYS, 0, 0);

    glutSolidTorus(2, 4, 20, 20);

    /* glDisable(GL_STENCIL_TEST); */
    /* glDisable(GL_LIGHTING); */
    /* glColor3f(1, 1, 1); */
    /* glutWireTorus(2, 4, 20, 20); */
    /* glEnable(GL_LIGHTING); */

    glPopMatrix();

    switch(bufferInterest) {
        case COLOR:
	    break; /* color already in back buffer */

	case STENCIL:
	    copyStencilToColor(GL_BACK);
	    break;

	case DEPTH:
	    copyDepthToColor(GL_BACK);
	    break;
    }

    if(takeSnapshot) {
        takeSnapshot = 0;
	/* saveSGIImage("snap.rgb", FALSE); */
	/* printf("Saved RGBA image in snap.rgba\n"); */
    }

    glutSwapBuffers();
}


void reshape(int width, int height)
{
    glViewport(0, 0, width, height);
    winWidth = width;
    winHeight = height;
    resizeBuffers();
    glutPostRedisplay();
}


void changeData(int data)
{
    char *s;

    bufferInterest = (BufferInterest) data;
    glutPostRedisplay();

    switch(data) {
        case COLOR:
	    s = "color";
	    break;

        case STENCIL:
	    s = "stencil";
	    break;

        case DEPTH:
	    s = "depth";
	    break;
    }
    printf("Now displaying %s data\n", s);
}


int mainMenu;


/* ARGSUSED1 */
void keyboard(unsigned char key, int x, int y)
{
    switch(key)
    {
        case '1':
        case '2':
        case '3':
	    whichSurface = key - '1';
	    glutPostRedisplay();
	    break;

        case 'r':
	    trackballMode = ROTATE;
	    break;

	case 't':
	    trackballMode = TRANSLATEXY;
	    break;

	case 'T':
	    trackballMode = TRANSLATEZ;
	    break;

	case 'x':
	    trackballMode = SCALEX;
	    break;

	case 'y':
	    trackballMode = SCALEY;
	    break;

	case 'z':
	    trackballMode = SCALEZ;
	    break;

	case 'q': case 'Q': case '\033':
	    exit(0);
	    break;

	case '+': case '=':
	    stage++;
	    glutPostRedisplay();
	    break;

	case '-': case '_':
	    stage--;
	    glutPostRedisplay();
	    break;

	case 's':
	    printf("%f %f %f %f\n", xform.rotation[0], xform.rotation[1],
	        xform.rotation[2], xform.rotation[3]);
	    glutPostRedisplay();
	    break;
    }
}


static int ox, oy;


/* ARGSUSED */
void button(int b, int state, int x, int y)
{
    ox = x;
    oy = y;
}


void motion(int x, int y)
{
    int dx, dy;

    dx = x - ox;
    dy = y - oy;

    ox = x;
    oy = y;

    switch(trackballMode) {
	case ROTATE:
	    rotateTrackball(dx, dy, xform.rotation);
	    break;

	case SCALEX:
	    xform.scale[0] += (dx + dy) / 20.0f;
	    break;

	case SCALEY:
	    xform.scale[1] += (dx + dy) / 20.0f;
	    break;

	case SCALEZ:
	    xform.scale[2] += (dx + dy) / 20.0f;
	    break;

	case TRANSLATEXY:
	    xform.translation[0] += dx / 20.0f;
	    xform.translation[1] -= dy / 20.0f;
	    break;

	case TRANSLATEZ:
	    xform.translation[2] += (dx + dy) / 20.0f;
	    break;
    }
    glutPostRedisplay();
}

/* ARGSUSED */
void mainMenuFunc(int menu)
{
    /* */
}


int main(int argc, char **argv)
{
    int bufferMenu;
    int stenSize;

    glutInit(&argc, argv);
    glutInitWindowSize(winWidth = 256, winHeight = 256);
    glutInitDisplayMode(GLUT_DOUBLE|GLUT_STENCIL|GLUT_DEPTH|GLUT_ALPHA);
    (void)glutCreateWindow("torus depth");
    glutDisplayFunc(redraw);
    glutKeyboardFunc(keyboard);
    glutMotionFunc(motion);
    glutMouseFunc(button);
    glutReshapeFunc(reshape);

    resizeBuffers();
    glGetIntegerv(GL_STENCIL_BITS, &stenSize);
    fprintf(stderr, "(%d bits of stencil available in this visual)\n", stenSize);

    fprintf(stderr, "Hit 'h' for help message\n");

    bufferMenu = glutCreateMenu(changeData);
    glutAddMenuEntry("Color data", COLOR);
    glutAddMenuEntry("Stencil data", STENCIL);
    glutAddMenuEntry("Depth data", DEPTH);

    mainMenu = glutCreateMenu(mainMenuFunc);
    glutAddSubMenu("Visible buffer", bufferMenu);
    glutAttachMenu(GLUT_RIGHT_BUTTON);
    init();
    glutMainLoop();

    return 0;
}
