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

/*
** Create a single component texture map
*/
GLfloat *make_texture(int maxs, int maxt)
{
    int s, t;
    static GLfloat *texture;

    texture = (GLfloat *)malloc(maxs * maxt * sizeof(GLfloat));
    for(t = 0; t < maxt; t++) {
	for(s = 0; s < maxs; s++) {
	    texture[s + maxs * t] = ((s >> 4) & 0x1) ^ ((t >> 4) & 0x1);
	}
    }
    return texture;
}

GLboolean stencil = GL_TRUE;

/* ARGSUSED1 */
void key(unsigned char key, int x, int y)
{
    switch(key) {
    case 't': /* toggle using stencil */
      if(stencil == GL_TRUE)
	stencil = GL_FALSE;
      else
	stencil = GL_TRUE;
      glutPostRedisplay();
      break;
    case '\033':
      exit(0);
      break;
    }
}

enum {SPHERE = 1, CONE};
enum {X, Y, Z};

int startx, starty;
int wid, ht;
int oldwid = 0, oldht = 0;

const int WINDIM = 512;
const GLfloat FRUSTDIM = 110.f;
const GLfloat FRUSTNEAR = 320.f;
const GLfloat FRUSTFAR = 540.f;
const GLfloat FRUSTDIFF = 540.f - 320.f;

GLboolean drawmode = GL_FALSE;
GLboolean depthmode = GL_FALSE;
GLboolean rubberbandmode = GL_FALSE;
GLfloat *color;
GLfloat *depth;
GLfloat depthbias = 0.f;
GLfloat raspos[] = {0.f, 0.f, -430.f};


int winWidth = 512;
int winHeight = 512;

GLfloat sx = 0;
GLfloat sy = 0;


/* Overlay Stuff */
int transparent;
int red;

void
setRasterPosXY(int x, int y)
{
      raspos[X] = (x - winWidth/2) * sx;
      raspos[Y] = (y - winHeight/2) * sy;

      glRasterPos3fv(raspos);

      glutPostRedisplay();
}

void
setRasterPosZ(int y)
{
      raspos[Z] = -(FRUSTNEAR + y * FRUSTDIFF/winHeight);

      depthbias = (y - winHeight/2.f)/winHeight;

      glRasterPos3fv(raspos);

      glutPostRedisplay();
}



void
motion(int x, int y)
{
  y = winHeight - y;
  if(drawmode)
    setRasterPosXY(x, y);

  if(rubberbandmode) {
    wid = x - startx;
    ht = y - starty;
    glutPostOverlayRedisplay();
  }

  if(depthmode)
     setRasterPosZ(y);
}

/* redraw function for overlay: used to show selected region */
void
overlay(void)
{
  if(glutLayerGet(GLUT_OVERLAY_DAMAGED)) {
    glClear(GL_COLOR_BUFFER_BIT);
  } else {
    glIndexi(transparent);
    glBegin(GL_LINE_LOOP);
    glVertex2i(startx, starty);
    glVertex2i(startx + oldwid, starty);
    glVertex2i(startx + oldwid, starty + oldht);
    glVertex2i(startx, starty + oldht);
    glEnd();
  }

  glIndexi(red);
  glBegin(GL_LINE_LOOP);
  glVertex2i(startx, starty);
  glVertex2i(startx + wid, starty);
  glVertex2i(startx + wid, starty + ht);
  glVertex2i(startx, starty + ht);
  glEnd();

  oldwid = wid;
  oldht = ht;

  glFlush();
}


/* used to get current width and height of viewport */
void
reshape(int wid, int ht)
{
  glutUseLayer(GLUT_OVERLAY);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluOrtho2D(0, wid, 0, ht); /* 1 to 1 with window */
  glMatrixMode(GL_MODELVIEW);
  glViewport(0, 0, wid, ht);

  glutUseLayer(GLUT_NORMAL);
  glViewport((GLint) (-wid * .1), (GLint) (-ht * .1),
    (GLsizei) (wid * 1.2), (GLsizei) (ht * 1.2));

  winWidth = wid;
  winHeight = ht;

  sx = 2 * FRUSTDIM/(winWidth * 1.2);
  sy = 2 * FRUSTDIM/(winHeight * 1.2);
}


void
mouse(int button, int state, int x, int y)
{
  y = winHeight - y; /* flip y orientation */
  if(state == GLUT_DOWN)
    switch(button) {
    case GLUT_LEFT_BUTTON: /* select an image */
      startx = x;
      starty = y;
      wid = 0; ht = 0;
      rubberbandmode = GL_TRUE;
      glutShowOverlay();
      break;
    case GLUT_MIDDLE_BUTTON:
      glutUseLayer(GLUT_NORMAL);
      if(color && depth) {
	  drawmode = GL_TRUE;
	  setRasterPosXY(x, y);
      }
      break;
    case GLUT_RIGHT_BUTTON: /* change depth */
      glutUseLayer(GLUT_NORMAL);
      if(color && depth) {
	depthmode = GL_TRUE;
	setRasterPosZ(y);
      }
      break;
    }
  else /* GLUT_UP */
    switch(button) {
    case GLUT_LEFT_BUTTON:
      rubberbandmode = GL_FALSE;
      glutHideOverlay();
      wid = x - startx;
      ht = y - starty;
      if(wid < 0) {
	wid = -wid;
	startx = x;
      }
      if(ht < 0) {
	ht = -ht;
	starty = y;
      }
      color = (GLfloat *)realloc(color, wid * ht * 3 * sizeof(GLfloat));
      depth = (GLfloat *)realloc(depth, wid * ht * sizeof(GLfloat));

      glutUseLayer(GLUT_NORMAL);
      glReadPixels(startx, starty, wid, ht, GL_RGB, GL_FLOAT, color);
      glReadPixels(startx, starty, wid, ht, GL_DEPTH_COMPONENT, GL_FLOAT,
		   depth);
      break;
    case GLUT_MIDDLE_BUTTON:
      drawmode = GL_FALSE;
      break;
    case GLUT_RIGHT_BUTTON: /* change depth */
      depthmode = GL_FALSE;
      break;
    }
}


/* Called when window needs to be redrawn */
void 
redraw(void)
{
    /* material properties for objects in scene */
    static GLfloat wall_mat[] = {1.f, 1.f, 1.f, 1.f};

    glutUseLayer(GLUT_NORMAL);	
    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT);

    /*
    ** Note: wall verticies are ordered so they are all front facing
    ** this lets me do back face culling to speed things up.
    */
 
    glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, wall_mat);

    /* floor */
    /* make the floor textured */
    glEnable(GL_TEXTURE_2D);

    /*
    ** Since we want to turn texturing on for floor only, we have to
    ** make floor a separate glBegin()/glEnd() sequence. You can't
    ** turn texturing on and off between begin and end calls
    */
    glBegin(GL_QUADS);
    glNormal3f(0.f, 1.f, 0.f);
    glTexCoord2i(0, 0);
    glVertex3f(-100.f, -100.f, -320.f);
    glTexCoord2i(1, 0);
    glVertex3f( 100.f, -100.f, -320.f);
    glTexCoord2i(1, 1);
    glVertex3f( 100.f, -100.f, -520.f);
    glTexCoord2i(0, 1);
    glVertex3f(-100.f, -100.f, -520.f);
    glEnd();

    glDisable(GL_TEXTURE_2D);

    /* walls */

    glBegin(GL_QUADS);
    /* left wall */
    glNormal3f(1.f, 0.f, 0.f);
    glVertex3f(-100.f, -100.f, -320.f);
    glVertex3f(-100.f, -100.f, -520.f);
    glVertex3f(-100.f,  100.f, -520.f);
    glVertex3f(-100.f,  100.f, -320.f);

    /* right wall */
    glNormal3f(-1.f, 0.f, 0.f);
    glVertex3f( 100.f, -100.f, -320.f);
    glVertex3f( 100.f,  100.f, -320.f);
    glVertex3f( 100.f,  100.f, -520.f);
    glVertex3f( 100.f, -100.f, -520.f);

    /* ceiling */
    glNormal3f(0.f, -1.f, 0.f);
    glVertex3f(-100.f,  100.f, -320.f);
    glVertex3f(-100.f,  100.f, -520.f);
    glVertex3f( 100.f,  100.f, -520.f);
    glVertex3f( 100.f,  100.f, -320.f);

    /* back wall */
    glNormal3f(0.f, 0.f, 1.f);
    glVertex3f(-100.f, -100.f, -520.f);
    glVertex3f( 100.f, -100.f, -520.f);
    glVertex3f( 100.f,  100.f, -520.f);
    glVertex3f(-100.f,  100.f, -520.f);
    glEnd();

    glPushMatrix();
    glTranslatef(-40.f, -60.f, -400.f);
    glScalef(2, 2, 2);
    glCallList(SPHERE);
    glPopMatrix();

    glPushMatrix();
    glTranslatef(50.f, -120.f, -400.f);
    glScalef(2, 2, 2);
    glCallList(CONE);
    glPopMatrix();

    if(stencil) {
      glEnable(GL_STENCIL_TEST);
      glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
      glStencilFunc(GL_ALWAYS, 1, 1);
      glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
      glPixelTransferf(GL_DEPTH_BIAS, depthbias);

      glDrawPixels(wid, ht, GL_DEPTH_COMPONENT, GL_FLOAT, depth);

      glPixelTransferf(GL_DEPTH_BIAS, 0.f);
      glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
      glStencilFunc(GL_EQUAL, 1, 1);
      glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
      glDisable(GL_DEPTH_TEST);

      glDrawPixels(wid, ht, GL_RGB, GL_FLOAT, color);

      glEnable(GL_DEPTH_TEST);
      glDisable(GL_STENCIL_TEST);
    } else
      glDrawPixels(wid, ht, GL_RGB, GL_FLOAT, color);

    glutSwapBuffers();
}


const int TEXDIM = 256;
/* Parse arguments, and set up interface between OpenGL and window system */
main(int argc, char *argv[])
{
    GLfloat *tex;
    static GLfloat lightpos[] = {50.f, 50.f, -320.f, 1.f};
    static GLfloat sphere_mat[] = {1.f, .5f, 0.f, 1.f};
    static GLfloat cone_mat[] = {0.f, .5f, 1.f, 1.f};
    GLUquadricObj *sphere, *cone, *base;

    glutInit(&argc, argv);
    glutInitWindowSize(WINDIM, WINDIM);

    glutInitDisplayMode(GLUT_RGBA|GLUT_DEPTH|GLUT_STENCIL|GLUT_DOUBLE);
    (void)glutCreateWindow("compositing images with depth");
    glutDisplayFunc(redraw);
    glutKeyboardFunc(key);
    glutMouseFunc(mouse);
    glutMotionFunc(motion);
    glutReshapeFunc(reshape);

    /* draw a perspective scene */
    glMatrixMode(GL_PROJECTION);
    glFrustum(-FRUSTDIM, FRUSTDIM, -FRUSTDIM, FRUSTDIM, FRUSTNEAR, FRUSTFAR); 
    glMatrixMode(GL_MODELVIEW);

    /* turn on features */
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_LIGHTING);
    glEnable(GL_LIGHT0);

    /* place light 0 in the right place */
    glLightfv(GL_LIGHT0, GL_POSITION, lightpos);

    /* remove back faces to speed things up */
    glCullFace(GL_BACK);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

    glNewList(SPHERE, GL_COMPILE);
    /* make display lists for sphere and cone; for efficiency */
    sphere = gluNewQuadric();
    glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, sphere_mat);
    gluSphere(sphere, 20.f, 20, 20);
    gluDeleteQuadric(sphere);
    glEndList();

    glNewList(CONE, GL_COMPILE);
    cone = gluNewQuadric();
    base = gluNewQuadric();
    glRotatef(-90.f, 1.f, 0.f, 0.f);
    glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, cone_mat);
    gluQuadricOrientation(base, GLU_INSIDE);
    gluDisk(base, 0., 20., 20, 1);
    gluCylinder(cone, 20., 0., 60., 20, 20);
    gluDeleteQuadric(cone);
    gluDeleteQuadric(base);
    glEndList();

    /* load pattern for current 2d texture */
    tex = make_texture(TEXDIM, TEXDIM);
    glTexImage2D(GL_TEXTURE_2D, 0, 1, TEXDIM, TEXDIM, 0, GL_RED, GL_FLOAT, tex);
    free(tex);

    /* storage for saved image */
    color = 0;
    depth = 0;
    
    glReadBuffer(GL_FRONT);/* so glReadPixel() always get the right image */

    glutInitDisplayMode(GLUT_SINGLE|GLUT_INDEX);
    if(glutLayerGet(GLUT_OVERLAY_POSSIBLE)) {
      glutEstablishOverlay();
      glutHideOverlay();
      transparent = glutLayerGet(GLUT_TRANSPARENT_INDEX);
      glClearIndex(transparent);
      red = (transparent + 1) % glutGet(GLUT_WINDOW_COLORMAP_SIZE);
      glutSetColor(red, 1.0, 0.0, 0.0);  /* Red. */
      glutOverlayDisplayFunc(overlay);
    }

    glutMainLoop();

    return 0;
}
