/*
 * Demo of a reflective, texture-mapped surface with OpenGL.
 * Brian Paul   August 14, 1995   This file is in the public domain.
 *
 * Hardware texture mapping is highly recommended!
 *
 * The basic steps are:
 *    1. Render the reflective object (a polygon) from the normal viewpoint,
 *       setting the stencil planes = 1.
 *    2. Render the scene from a special viewpoint:  the viewpoint which
 *       is on the opposite side of the reflective plane.  Only draw where
 *       stencil = 1.  This draws the objects in the reflective surface.
 *    3. Render the scene from the original viewpoint.  This draws the
 *       objects in the normal fashion.  Use blending when drawing
 *       the reflective, textured surface.
 *
 * This is a very crude demo.  It could be much better.
 */
 
/*
 * Authors:
 *   Brian Paul
 *   Dirk Reiners (reiners@igd.fhg.de) made some modifications to this code.
 *   Mark Kilgard (April 1997)
 *   Brian Paul (April 2000 - added keyboard d/s options)
 *   Brian Paul (August 2005 - added multi window feature)
 */


#include <assert.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include "GL/glut.h"
#include "showbuffer.h"
#include "readtex.h"


#define DEG2RAD (3.14159/180.0)
#define TABLE_TEXTURE "../images/tile.rgb"
#define MAX_OBJECTS 2
#define INIT_WIDTH 400
#define INIT_HEIGHT 300

#ifdef _WIN32
#undef CreateWindowA
#endif

struct window {
   int id;               /* returned by glutCreateWindow() */
   int width, height;
   GLboolean anim;
   GLfloat xrot, yrot;
   GLfloat spin;
   GLenum showBuffer;
   GLenum drawBuffer;
   GLuint table_list;
   GLuint objects_list[MAX_OBJECTS];
   double t0;
   struct window *next;
};


static struct window *FirstWindow = NULL;


static void
CreateWindow(void);


static struct window *
CurrentWindow(void)
{
   int id = glutGetWindow();
   struct window *w;
   for (w = FirstWindow; w; w = w->next) {
      if (w->id == id)
         return w;
   }
   return NULL;
}


static GLboolean
AnyAnimating(void)
{
   struct window *w;
   for (w = FirstWindow; w; w = w->next) {
      if (w->anim)
         return 1;
   }
   return 0;
}


static void
KillWindow(struct window *w)
{
   struct window *win, *prev = NULL;
   for (win = FirstWindow; win; win = win->next) {
      if (win == w) {
         if (prev) {
            prev->next = win->next;
         }
         else {
            FirstWindow = win->next;
         }
         glutDestroyWindow(win->id);
         win->next = NULL;
         free(win);
         return;
      }
      prev = win;
   }
}


static void
KillAllWindows(void)
{
   while (FirstWindow)
      KillWindow(FirstWindow);
}


static GLuint
MakeTable(void)
{
   static GLfloat table_mat[] = { 1.0, 1.0, 1.0, 0.6 };
   static GLfloat gray[] = { 0.4, 0.4, 0.4, 1.0 };
   GLuint table_list;

   table_list = glGenLists(1);
   glNewList( table_list, GL_COMPILE );

   /* load table's texture */
   glMaterialfv( GL_FRONT, GL_AMBIENT_AND_DIFFUSE, table_mat );
   /*glMaterialfv( GL_FRONT, GL_EMISSION, gray );*/
   glMaterialfv( GL_FRONT, GL_DIFFUSE, table_mat );
   glMaterialfv( GL_FRONT, GL_AMBIENT, gray );
   
   /* draw textured square for the table */
   glPushMatrix();
   glScalef( 4.0, 4.0, 4.0 );
   glBegin( GL_POLYGON );
   glNormal3f( 0.0, 1.0, 0.0 );
   glTexCoord2f( 0.0, 0.0 );   glVertex3f( -1.0, 0.0,  1.0 );
   glTexCoord2f( 1.0, 0.0 );   glVertex3f(  1.0, 0.0,  1.0 );
   glTexCoord2f( 1.0, 1.0 );   glVertex3f(  1.0, 0.0, -1.0 );
   glTexCoord2f( 0.0, 1.0 );   glVertex3f( -1.0, 0.0, -1.0 );
   glEnd();
   glPopMatrix();

   glDisable( GL_TEXTURE_2D );

   glEndList();
   return table_list;
}


static void
MakeObjects(GLuint *objects_list)
{
   GLUquadricObj *q;

   static GLfloat cyan[] = { 0.0, 1.0, 1.0, 1.0 };
   static GLfloat green[] = { 0.2, 1.0, 0.2, 1.0 };
   static GLfloat black[] = { 0.0, 0.0, 0.0, 0.0 };

   q = gluNewQuadric();
   gluQuadricDrawStyle( q, GLU_FILL );
   gluQuadricNormals( q, GLU_SMOOTH );

   objects_list[0] = glGenLists(1);
   glNewList( objects_list[0], GL_COMPILE );
   glMaterialfv( GL_FRONT, GL_AMBIENT_AND_DIFFUSE, cyan );
   glMaterialfv( GL_FRONT, GL_EMISSION, black );
   gluCylinder( q, 0.5, 0.5,  1.0, 15, 1 );
   glEndList();

   objects_list[1] = glGenLists(1);
   glNewList( objects_list[1], GL_COMPILE );
   glMaterialfv( GL_FRONT, GL_AMBIENT_AND_DIFFUSE, green );
   glMaterialfv( GL_FRONT, GL_EMISSION, black );
   gluCylinder( q, 1.5, 0.0,  2.5, 15, 1 );
   glEndList();

   gluDeleteQuadric(q);
}


static void
InitWindow(struct window *w)
{
   GLint imgWidth, imgHeight;
   GLenum imgFormat;
   GLubyte *image = NULL;

   w->table_list = MakeTable();
   MakeObjects(w->objects_list);

   image = LoadRGBImage( TABLE_TEXTURE, &imgWidth, &imgHeight, &imgFormat );
   if (!image) {
      printf("Couldn't read %s\n", TABLE_TEXTURE);
      exit(0);
   }

   gluBuild2DMipmaps(GL_TEXTURE_2D, 3, imgWidth, imgHeight,
                     imgFormat, GL_UNSIGNED_BYTE, image);
   free(image);

   glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT );
   glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT );
   glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
   glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );

   glShadeModel( GL_FLAT );
   
   glEnable( GL_LIGHT0 );
   glEnable( GL_LIGHTING );

   glClearColor( 0.5, 0.5, 0.9, 0.0 );

   glEnable( GL_NORMALIZE );
}


static void
Reshape(int width, int height)
{
   struct window *w = CurrentWindow();
   GLfloat yAspect = 2.5;
   GLfloat xAspect = yAspect * (float) width / (float) height;
   w->width = width;
   w->height = height;
   glViewport(0, 0, width, height);
   glMatrixMode(GL_PROJECTION);
   glLoadIdentity();
   glFrustum( -xAspect, xAspect, -yAspect, yAspect, 10.0, 30.0 );
   glMatrixMode(GL_MODELVIEW);
   glLoadIdentity();
}


static void
DrawObjects(struct window *w, GLfloat eyex, GLfloat eyey, GLfloat eyez)
{
   (void) eyex;
   (void) eyey;
   (void) eyez;
#ifndef USE_ZBUFFER
   if (eyex<0.5) {
#endif
      glPushMatrix();
      glTranslatef( 1.0, 1.5, 0.0 );
      glRotatef( w->spin, 1.0, 0.5, 0.0 );
      glRotatef( 0.5*w->spin, 0.0, 0.5, 1.0 );
      glCallList( w->objects_list[0] );
      glPopMatrix();
      
      glPushMatrix();
      glTranslatef( -1.0, 0.85+3.0*fabs( cos(0.01*w->spin) ), 0.0 );
      glRotatef( 0.5*w->spin, 0.0, 0.5, 1.0 );
      glRotatef( w->spin, 1.0, 0.5, 0.0 );
      glScalef( 0.5, 0.5, 0.5 );
      glCallList( w->objects_list[1] );
      glPopMatrix();
#ifndef USE_ZBUFFER
   }
   else {	
      glPushMatrix();
      glTranslatef( -1.0, 0.85+3.0*fabs( cos(0.01*w->spin) ), 0.0 );
      glRotatef( 0.5*w->spin, 0.0, 0.5, 1.0 );
      glRotatef( w->spin, 1.0, 0.5, 0.0 );
      glScalef( 0.5, 0.5, 0.5 );
      glCallList( w->objects_list[1] );
      glPopMatrix();

      glPushMatrix();
      glTranslatef( 1.0, 1.5, 0.0 );
      glRotatef( w->spin, 1.0, 0.5, 0.0 );
      glRotatef( 0.5*w->spin, 0.0, 0.5, 1.0 );
      glCallList( w->objects_list[0] );
      glPopMatrix();
   }
#endif
}


static void
DrawTable(struct window *w)
{
   glCallList(w->table_list);
}


static void
DrawWindow(void)
{
   struct window *w = CurrentWindow();
   static GLfloat light_pos[] = { 0.0, 20.0, 0.0, 1.0 };
   GLfloat dist = 20.0;
   GLfloat eyex, eyey, eyez;

   if (w->drawBuffer == GL_NONE) {
      glDrawBuffer(GL_BACK);
      glReadBuffer(GL_BACK);
   }
   else {
      glDrawBuffer(w->drawBuffer);
      glReadBuffer(w->drawBuffer);
   }

   glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

   if (w->drawBuffer == GL_NONE) {
      glDrawBuffer(GL_NONE);
   }

   eyex = dist  *  cos(w->yrot * DEG2RAD)  *  cos(w->xrot * DEG2RAD);
   eyez = dist  *  sin(w->yrot * DEG2RAD)  *  cos(w->xrot * DEG2RAD);
   eyey = dist  *  sin(w->xrot * DEG2RAD);

   /* view from top */
   glPushMatrix();
   gluLookAt( eyex, eyey, eyez, 0.0, 0.0, 0.0,  0.0, 1.0, 0.0 );

   glLightfv( GL_LIGHT0, GL_POSITION, light_pos );
 
   /* draw table into stencil planes */
   glDisable( GL_DEPTH_TEST );
   glEnable( GL_STENCIL_TEST );
   glStencilFunc( GL_ALWAYS, 1, 0xffffffff );
   glStencilOp( GL_REPLACE, GL_REPLACE, GL_REPLACE );
   glColorMask( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE );
   DrawTable(w);
   glColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE );

   glEnable( GL_DEPTH_TEST );

   /* render view from below (reflected viewport) */
   /* only draw where stencil==1 */
   if (eyey>0.0) {
      glPushMatrix();
 
      glStencilFunc( GL_EQUAL, 1, 0xffffffff );  /* draw if ==1 */
      glStencilOp( GL_KEEP, GL_KEEP, GL_KEEP );
      glScalef( 1.0, -1.0, 1.0 );

      /* Reposition light in reflected space. */
      glLightfv(GL_LIGHT0, GL_POSITION, light_pos);

      DrawObjects(w, eyex, eyey, eyez);
      glPopMatrix();

      /* Restore light's original unreflected position. */
      glLightfv(GL_LIGHT0, GL_POSITION, light_pos);
   }

   glDisable( GL_STENCIL_TEST );

   glEnable( GL_BLEND );
   glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );

   glEnable( GL_TEXTURE_2D );
   DrawTable(w);
   glDisable( GL_TEXTURE_2D );
   glDisable( GL_BLEND );

   /* view from top */
   glPushMatrix();

   DrawObjects(w, eyex, eyey, eyez);

   glPopMatrix();

   glPopMatrix();

   if (w->showBuffer == GL_DEPTH) {
      ShowDepthBuffer(w->width, w->height, 1.0, 0.0);
   }
   else if (w->showBuffer == GL_STENCIL) {
      ShowStencilBuffer(w->width, w->height, 255.0, 0.0);
   }
   else if (w->showBuffer == GL_ALPHA) {
      ShowAlphaBuffer(w->width, w->height);
   }

   if (w->drawBuffer == GL_FRONT)
      glFinish();
   else
      glutSwapBuffers();

   /* calc/show frame rate */
   {
      static GLint t0 = 0;
      static GLint frames = 0;
      GLint t = glutGet(GLUT_ELAPSED_TIME);
      frames++;
      if (t - t0 >= 5000) {
         GLfloat seconds = (t - t0) / 1000.0;
         GLfloat fps = frames / seconds;
         printf("%d frames in %g seconds = %g FPS\n", frames, seconds, fps);
         fflush(stdout);
         t0 = t;
         frames = 0;
      }
   }
}


static void
Idle(void)
{
   double t = glutGet(GLUT_ELAPSED_TIME) / 1000.0;
   struct window *w;
   for (w = FirstWindow; w; w = w->next) {
      if (w->anim) {
         double dt;
         if (w->t0 < 0.0)
            w->t0 = t;
         dt = t - w->t0;
         w->t0 = t;
         w->spin += 60.0 * dt;
         w->yrot += 90.0 * dt;
         assert(w->id);
         glutSetWindow(w->id);
         glutPostRedisplay();
      }
   }
}


static void
UpdateIdleFunc(void)
{
   if (AnyAnimating())
      glutIdleFunc(Idle);
   else
      glutIdleFunc(NULL);
}

static void
Key(unsigned char key, int x, int y)
{
   struct window *w = CurrentWindow();
   (void) x;
   (void) y;

   switch (key) {
   case 'd':
      w->showBuffer = GL_DEPTH;
      glutPostRedisplay();
      break;
   case 's':
      w->showBuffer = GL_STENCIL;
      glutPostRedisplay();
      break;
   case 'a':
      w->showBuffer = GL_ALPHA;
      glutPostRedisplay();
      break;
   case 'c':
      w->showBuffer = GL_NONE;
      glutPostRedisplay();
      break;
   case 'f':
      if (w->drawBuffer == GL_FRONT)
         w->drawBuffer = GL_BACK;
      else
         w->drawBuffer = GL_FRONT;
      glutPostRedisplay();
      break;
   case '0':
      w->drawBuffer = GL_NONE;
      glutPostRedisplay();
      break;
   case ' ':
      w->anim = !w->anim;
      w->t0 = -1;
      UpdateIdleFunc();
      glutPostRedisplay();
      break;
   case 'n':
      CreateWindow();
      UpdateIdleFunc();
      break;
   case 'k':
      KillWindow(w);
      if (FirstWindow == NULL)
         exit(0);
      break;
   case 27:
      KillAllWindows();
      exit(0);
      break;
   default:
      ;
   }
}


static void
SpecialKey(int key, int x, int y)
{
   struct window *w = CurrentWindow();
   (void) x;
   (void) y;
   switch (key) {
      case GLUT_KEY_UP:
         w->xrot += 3.0;
         if (w->xrot > 85)
            w->xrot = 85;
         break;
      case GLUT_KEY_DOWN:
         w->xrot -= 3.0;
         if (w->xrot < 5)
            w->xrot = 5;
         break;
      case GLUT_KEY_LEFT:
         w->yrot += 3.0;
         break;
      case GLUT_KEY_RIGHT:
         w->yrot -= 3.0;
         break;
   }
   glutPostRedisplay();
}


static void
CreateWindow(void)
{
   char title[1000];
   struct window *w = (struct window *) calloc(1, sizeof(struct window));
   
   glutInitWindowSize(INIT_WIDTH, INIT_HEIGHT);
   w->id = glutCreateWindow("foo");
   sprintf(title, "reflect window %d", w->id);
   glutSetWindowTitle(title);
   assert(w->id);
   w->width = INIT_WIDTH;
   w->height = INIT_HEIGHT;
   w->anim = GL_TRUE;
   w->xrot = 30.0;
   w->yrot = 50.0;
   w->spin = 0.0;
   w->showBuffer = GL_NONE;
   w->drawBuffer = GL_BACK;

   InitWindow(w);

   glutReshapeFunc(Reshape);
   glutDisplayFunc(DrawWindow);
   glutKeyboardFunc(Key);
   glutSpecialFunc(SpecialKey);

   /* insert at head of list */
   w->next = FirstWindow;
   FirstWindow = w;
}


static void
Usage(void)
{
   printf("Keys:\n");
   printf("  a      - show alpha buffer\n");
   printf("  d      - show depth buffer\n");
   printf("  s      - show stencil buffer\n");
   printf("  c      - show color buffer\n");
   printf("  f      - toggle rendering to front/back color buffer\n");
   printf("  n      - create new window\n");
   printf("  k      - kill window\n");
   printf("  SPACE  - toggle animation\n");
   printf("  ARROWS - rotate scene\n");
}


int
main(int argc, char *argv[])
{
   glutInit(&argc, argv);
   glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH |
                       GLUT_STENCIL | GLUT_ALPHA);
   CreateWindow();
   glutIdleFunc(Idle);
   Usage();
   glutMainLoop();
   return 0;
}