#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <GL/glut.h>
#include <sys/resource.h>
#include <sys/time.h>
#include "paulslib.h"
#include "bitmaplib.h"
#include "opengllib.h"

/*
   Demo of SuperEllipsoids
*/

typedef struct {
   XYZ vp;              /* View position           */
   XYZ vd;              /* View direction vector   */
   XYZ vu;              /* View up direction       */
   XYZ pr;              /* Point to rotate about   */
   double focallength;  /* Focal Length along vd   */
   double aperture;     /* Camera aperture         */
   double eyesep;       /* Eye separation          */
   int screenwidth,screenheight;
} CAMERA;

void Display(void);
void MakeGeometry(void);
void HandleKeyboard(unsigned char key,int x, int y);
void HandleSpecialKeyboard(int key,int x, int y);
void HandleMouse(int,int,int,int);
void HandleMainMenu(int);
void HandleSpeedMenu(int);
void HandleVisibility(int vis);
void HandleReshape(int,int);
void HandleMouseMotion(int,int);
void HandlePassiveMotion(int,int);
void HandleIdle(void);
void GiveUsage(char *);
void RotateCamera(int,int,int);
void TranslateCamera(int,int);
void CameraHome(int);

/* GLobals */
int fullscreen = FALSE;
int stereo = FALSE;
int windowdump = FALSE;
int debug = FALSE;
int currentbutton = -1;
CAMERA camera;
XYZ origin = {0.0,0.0,0.0};
#define WIREFRAME 1
#define SHADED    2
#define TEXTURED  3
int rendermode = WIREFRAME;

/* Texture */
int twidth,theight;
BITMAP *rgbtex;
unsigned int texid;

/* Superellipse */
double power1 = 1,power2 = 1;
double spinangle = 0;

int main(int argc,char **argv)
{
   int i;
   int mainmenu;
   COLOUR white = {1.0,1.0,1.0};

   camera.screenwidth = 500;
   camera.screenheight = 500;

   /* Parse the command line arguments */
   for (i=1;i<argc;i++) {
      if (strstr(argv[i],"-h") != NULL) 
         GiveUsage(argv[0]);
      if (strstr(argv[i],"-f") != NULL)
         fullscreen = TRUE;
      if (strstr(argv[i],"-s") != NULL)
         stereo = TRUE;
      if (strstr(argv[i],"-d") != NULL)
         debug = TRUE;
   }

   /* Set things up and go */
   glutInit(&argc,argv);
   if (!stereo)
      glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
   else
      glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH | GLUT_STEREO);

   glutCreateWindow("SuperEllipse");
   glutReshapeWindow(camera.screenwidth,camera.screenheight);
   if (fullscreen)
      glutFullScreen();
   glutDisplayFunc(Display);
   glutReshapeFunc(HandleReshape);
   glutVisibilityFunc(HandleVisibility);
   glutKeyboardFunc(HandleKeyboard);
   glutSpecialFunc(HandleSpecialKeyboard);
   glutMouseFunc(HandleMouse);
   glutMotionFunc(HandleMouseMotion);
   glutSetCursor(GLUT_CURSOR_NONE);
   glEnable(GL_DEPTH_TEST);
   glDisable(GL_LINE_SMOOTH);
   glDisable(GL_POINT_SMOOTH);
   glDisable(GL_POLYGON_SMOOTH);
   glDisable(GL_DITHER);
   glDisable(GL_CULL_FACE);
   glFrontFace(GL_CCW);
   glClearColor(0.0,0.0,0.0,0.0);
   glColorMaterial(GL_FRONT_AND_BACK,GL_AMBIENT_AND_DIFFUSE);
   glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER,GL_TRUE);
   glLightModeli(GL_LIGHT_MODEL_TWO_SIDE,GL_TRUE);
   glEnable(GL_COLOR_MATERIAL);
   glPixelStorei(GL_UNPACK_ALIGNMENT,1);
   CameraHome(0);

   /* Set up the main menu */
   mainmenu = glutCreateMenu(HandleMainMenu);
   glutAddMenuEntry("Wireframe",WIREFRAME);
   glutAddMenuEntry("Shaded",SHADED);
   glutAddMenuEntry("Textured",TEXTURED);
   glutAddMenuEntry("Quit",9);
   glutAttachMenu(GLUT_RIGHT_BUTTON);

   rgbtex = ReadPPMTexture("mercury.ppm",white,'o',&twidth,&theight,2);
   glGenTextures(1,&texid);
   glBindTexture(GL_TEXTURE_2D,texid);
   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_LINEAR_MIPMAP_LINEAR);
   glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
   glTexImage2D(GL_TEXTURE_2D,0,4,twidth,theight,0,GL_RGBA,GL_UNSIGNED_BYTE,rgbtex);
   gluBuild2DMipmaps(GL_TEXTURE_2D,4,twidth,theight,GL_RGBA,GL_UNSIGNED_BYTE,rgbtex);
   glTexEnvf(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_MODULATE);

   /* Ready to go! */
   glutMainLoop();
   return(0);
}

/*
   This is the basic display callback routine
   It creates the geometry, lighting, and viewing position
   In this case it rotates the camera around the scene
*/
void Display(void)
{
   XYZ r;
   double dist,ratio,radians,scale,wd2,ndfl;
   double left,right,top,bottom,near=0.1,far=100.0;

   static int framecount = -1;
   static double tstart,tstop;         /* Frame rate calculations */
   double framerate = 0;

   /* Set the time the first time around */
   if (framecount < 0) {
      framecount = 0;
      tstart = GetRunTime();
   }
   framecount++;

   /* Clip to avoid extreme stereo */
   if (stereo)
      near = camera.focallength / 5;

   /* Misc stuff */
   ratio  = camera.screenwidth / (double)camera.screenheight;
   radians = DTOR * camera.aperture / 2;
   wd2     = near * tan(radians);
   ndfl    = near / camera.focallength;

   /* Clear the buffers */
   glDrawBuffer(GL_BACK_LEFT);
   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
   if (stereo) {
      glDrawBuffer(GL_BACK_RIGHT);
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
   }

   if (stereo) {

      /* Derive the two eye positions */
      CROSSPROD(camera.vd,camera.vu,r);
      Normalise(&r);
      r.x *= camera.eyesep / 2.0;
      r.y *= camera.eyesep / 2.0;
      r.z *= camera.eyesep / 2.0;

      glMatrixMode(GL_PROJECTION);
      glLoadIdentity();
      left  = - ratio * wd2 - 0.5 * camera.eyesep * ndfl;
      right =   ratio * wd2 - 0.5 * camera.eyesep * ndfl;
      top    =   wd2;
      bottom = - wd2;
      glFrustum(left,right,bottom,top,near,far);

      glMatrixMode(GL_MODELVIEW);
      glDrawBuffer(GL_BACK_RIGHT);
      glLoadIdentity();
      gluLookAt(camera.vp.x + r.x,camera.vp.y + r.y,camera.vp.z + r.z,
                camera.vp.x + r.x + camera.vd.x,
                camera.vp.y + r.y + camera.vd.y,
                camera.vp.z + r.z + camera.vd.z,
                camera.vu.x,camera.vu.y,camera.vu.z);
      MakeGeometry();

      glMatrixMode(GL_PROJECTION);
      glLoadIdentity();
      left  = - ratio * wd2 + 0.5 * camera.eyesep * ndfl;
      right =   ratio * wd2 + 0.5 * camera.eyesep * ndfl;
      top    =   wd2;
      bottom = - wd2;
      glFrustum(left,right,bottom,top,near,far);

      glMatrixMode(GL_MODELVIEW);
      glDrawBuffer(GL_BACK_LEFT);
      glLoadIdentity();
      gluLookAt(camera.vp.x - r.x,camera.vp.y - r.y,camera.vp.z - r.z,
                camera.vp.x - r.x + camera.vd.x,
                camera.vp.y - r.y + camera.vd.y,
                camera.vp.z - r.z + camera.vd.z,
                camera.vu.x,camera.vu.y,camera.vu.z);
      MakeGeometry();

   } else {

      glMatrixMode(GL_PROJECTION);
      glLoadIdentity();
      left  = - ratio * wd2;
      right =   ratio * wd2;
      top    =   wd2;
      bottom = - wd2;
      glFrustum(left,right,bottom,top,near,far);

      glMatrixMode(GL_MODELVIEW);
      glDrawBuffer(GL_BACK_LEFT);
      glLoadIdentity();
      gluLookAt(camera.vp.x,camera.vp.y,camera.vp.z,
                camera.vp.x + camera.vd.x,
                camera.vp.y + camera.vd.y,
                camera.vp.z + camera.vd.z,
                camera.vu.x,camera.vu.y,camera.vu.z);
      MakeGeometry();
   }

   /* glFlush(); This isn't necessary for double buffers */
   glutSwapBuffers();

   /*
      Handle frame rate dependent features
      Vary the resolution of spheres
   */
   tstop = GetRunTime();
   if (tstop - tstart > 2) {
      framerate = framecount / (tstop - tstart);
      if (debug)
         fprintf(stderr,"Frame rate = %.1f frames/second\n",framerate);
      framecount = 0;
      tstart = tstop;
   }

   if (windowdump)
      WindowDump("",camera.screenwidth,camera.screenheight,stereo,-5);

   /* Update global geometry stuff */
   spinangle += 0.1;
}

/*
   Create the geometry
   One would normally make a display list here
*/
void MakeGeometry(void)
{
   GLfloat position[4] = {0.0,0.0,3.0,0.0};
   GLfloat ambient[4]  = {0.2,0.2,0.2,1.0};
   GLfloat diffuse[4]  = {1.0,1.0,1.0,1.0};
   GLfloat white[4] = {1.0,1.0,1.0,1.0};
   GLfloat black[4] = {0.0,0.0,0.0,1.0};
   GLfloat shin[1]  = {40.0};

   /* Turn lighting on */
   if (rendermode == WIREFRAME) {
      glDisable(GL_LIGHTING);
      glShadeModel(GL_FLAT);
      glLightfv(GL_LIGHT0,GL_SPECULAR,black);
      glPolygonMode(GL_FRONT_AND_BACK,GL_LINE);
      glLightModelfv(GL_LIGHT_MODEL_AMBIENT,white);
      glMaterialfv(GL_FRONT_AND_BACK,GL_SPECULAR,white);
      glMaterialfv(GL_FRONT_AND_BACK,GL_SHININESS,shin);
   } else {
      glEnable(GL_LIGHTING);
      glShadeModel(GL_SMOOTH);
      glLightfv(GL_LIGHT0,GL_SPECULAR,white);
      glPolygonMode(GL_FRONT_AND_BACK,GL_FILL);
      glLightModelfv(GL_LIGHT_MODEL_AMBIENT,ambient);
      glEnable(GL_LIGHT0);
      glLightfv(GL_LIGHT0,GL_POSITION,position);
      glLightfv(GL_LIGHT0,GL_AMBIENT,ambient);
      glLightfv(GL_LIGHT0,GL_DIFFUSE,diffuse);
      glMaterialfv(GL_FRONT_AND_BACK,GL_SPECULAR,white);
      glMaterialfv(GL_FRONT_AND_BACK,GL_SHININESS,shin);
   }

   /* Draw superellipse */
   glPushMatrix();
   /* glTranslatef(); */
   glRotatef(spinangle,0.0,1.0,0.0);
   if (rendermode == TEXTURED) {
      glColor3f(1.0,1.0,1.0);
      glEnable(GL_TEXTURE_2D);
      glBindTexture(GL_TEXTURE_2D,texid);
   } else {
      glColor3f(0.0,1.0,0.0);
   }
   CreateSuperEllipse(power1,power2,60,1);
   if (rendermode == TEXTURED) 
      glDisable(GL_TEXTURE_2D);
   glPopMatrix();

   /* Draw some stars */
   glPointSize(2);
   glColor3f(1.0,1.0,1.0);
   glDisable(GL_LIGHTING);
   CreateRandomStars(1000);
   glShadeModel(GL_FLAT);
   CreateStars(origin,100.0);
}

/*
   Deal with plain key strokes
*/
void HandleKeyboard(unsigned char key,int x, int y)
{
   switch (key) {
   case ESC:                            /* Quit */
   case 'Q':
   case 'q': 
      exit(0); 
      break;
   case 'h':                           /* Go home     */
   case 'H':
      CameraHome(0);
      break;
   case '[':                           /* Roll anti clockwise */
      RotateCamera(0,0,-1);
      break;
   case ']':                           /* Roll clockwise */
      RotateCamera(0,0,1);
      break;
   case 'i':                           /* Translate camera up */
   case 'I':
      TranslateCamera(0,1);
      break;
   case 'k':                           /* Translate camera down */
   case 'K':
      TranslateCamera(0,-1);
      break;
   case 'j':                           /* Translate camera left */
   case 'J':
      TranslateCamera(-1,0);
      break;
   case 'l':                           /* Translate camera right */
   case 'L':
      TranslateCamera(1,0);
      break;
   case 'w':                           /* Write the image to disk */
   case 'W':
      windowdump = !windowdump;
      break;
   }
}

/*
   Deal with special key strokes
   Control the supeellipse powers
*/
void HandleSpecialKeyboard(int key,int x,int y)
{
   switch (key) {
   case GLUT_KEY_LEFT:
      power1 *= 0.9;
      break;
   case GLUT_KEY_RIGHT:
      power1 *= 1.1;
      break;
   case GLUT_KEY_UP:
      power2 *= 1.1;
      break;
   case GLUT_KEY_DOWN:
      power2 *= 0.9;
      break;
   }
   if (debug)
      fprintf(stderr,"Power1: %g, Power2: %g\n",power1,power2);
}

/*
   Rotate (ix,iy) or roll (iz) the camera about the focal point
   ix,iy,iz are flags, 0 do nothing, +- 1 rotates in opposite directions
   Correctly updating all camera attributes
*/
void RotateCamera(int ix,int iy,int iz)
{
   XYZ vp,vu,vd;
   XYZ right;
   XYZ newvp,newr;
   double radius,dd,radians;
   double dx,dy,dz;
   double dtheta = 2;

   vu = camera.vu;
   Normalise(&vu);
   vp = camera.vp;
   vd = camera.vd;
   Normalise(&vd);
   CROSSPROD(vd,vu,right);
   Normalise(&right);
   radians = dtheta * PI / 180.0;

   /* Handle the roll */
   if (iz != 0) {
      camera.vu.x += iz * right.x * radians;
      camera.vu.y += iz * right.y * radians;
      camera.vu.z += iz * right.z * radians;
      Normalise(&camera.vu);
      return;
   }

   /* Distance from the rotate point */
   dx = camera.vp.x - camera.pr.x;
   dy = camera.vp.y - camera.pr.y;
   dz = camera.vp.z - camera.pr.z;
   radius = sqrt(dx*dx + dy*dy + dz*dz);

   /* Determine the new view point */
   dd = radius * radians;
   newvp.x = vp.x + dd * ix * right.x + dd * iy * vu.x - camera.pr.x;
   newvp.y = vp.y + dd * ix * right.y + dd * iy * vu.y - camera.pr.y;
   newvp.z = vp.z + dd * ix * right.z + dd * iy * vu.z - camera.pr.z;
   Normalise(&newvp);
   camera.vp.x = camera.pr.x + radius * newvp.x;
   camera.vp.y = camera.pr.y + radius * newvp.y;
   camera.vp.z = camera.pr.z + radius * newvp.z;

   /* Determine the new right vector */
   newr.x = camera.vp.x + right.x - camera.pr.x;
   newr.y = camera.vp.y + right.y - camera.pr.y;
   newr.z = camera.vp.z + right.z - camera.pr.z;
   Normalise(&newr);
   newr.x = camera.pr.x + radius * newr.x - camera.vp.x;
   newr.y = camera.pr.y + radius * newr.y - camera.vp.y;
   newr.z = camera.pr.z + radius * newr.z - camera.vp.z;

   camera.vd.x = camera.pr.x - camera.vp.x;
   camera.vd.y = camera.pr.y - camera.vp.y;
   camera.vd.z = camera.pr.z - camera.vp.z;
   Normalise(&camera.vd);

   /* Determine the new up vector */
   CROSSPROD(newr,camera.vd,camera.vu);
   Normalise(&camera.vu);
}

/*
   Translate (pan) the camera view point
   In response to i,j,k,l keys
   Also move the camera rotate location in parallel
*/
void TranslateCamera(int ix,int iy)
{
   XYZ vp,vu,vd;
   XYZ right;
   XYZ newvp,newr;
   double radians,delta;
   double dtheta = 2;

   vu = camera.vu;
   Normalise(&vu);
   vp = camera.vp;
   vd = camera.vd;
   Normalise(&vd);
   CROSSPROD(vd,vu,right);
   Normalise(&right);
   radians = dtheta * PI / 180.0;
   delta = dtheta * camera.focallength / 90.0;

   camera.vp.x += iy * vu.x * delta;
   camera.vp.y += iy * vu.y * delta;
   camera.vp.z += iy * vu.z * delta;
   camera.pr.x += iy * vu.x * delta;
   camera.pr.y += iy * vu.y * delta;
   camera.pr.z += iy * vu.z * delta;

   camera.vp.x += ix * right.x * delta;
   camera.vp.y += ix * right.y * delta;
   camera.vp.z += ix * right.z * delta;
   camera.pr.x += ix * right.x * delta;
   camera.pr.y += ix * right.y * delta;
   camera.pr.z += ix * right.z * delta;
}

/*
   Handle mouse events
   Right button events are passed to menu handlers
*/
void HandleMouse(int button,int state,int x,int y)
{
   if (state == GLUT_DOWN) {
      if (button == GLUT_LEFT_BUTTON) {
         currentbutton = GLUT_LEFT_BUTTON;
      } else if (button == GLUT_MIDDLE_BUTTON) {
         currentbutton = GLUT_MIDDLE_BUTTON;
      } 
   }
}

/*
   Handle the main menu
*/
void HandleMainMenu(int whichone)
{
   switch (whichone) {
   case WIREFRAME:
   case SHADED:
   case TEXTURED:
      rendermode = whichone;
      break;
   case 9: 
      exit(0); 
      break;
   }
}

/*
   How to handle visibility
*/
void HandleVisibility(int visible)
{
   if (visible == GLUT_VISIBLE)
      glutIdleFunc(HandleIdle);
   else
      glutIdleFunc(NULL);
}

/*
   What to do on an idle event
*/
void HandleIdle(void)
{
   glutPostRedisplay();
}

/*
   Handle a window reshape/resize
*/
void HandleReshape(int w,int h)
{
   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
   glViewport(0,0,(GLsizei)w,(GLsizei)h);
   camera.screenwidth = w;
   camera.screenheight = h;
}

/*
   Display the program usage information
*/
void GiveUsage(char *cmd)
{
   fprintf(stderr,"Usage: %s [-h] [-f] [-s] [-c] [-q n]\n",cmd);
   fprintf(stderr,"          -h   this text\n");
   fprintf(stderr,"          -f   full screen\n");
   fprintf(stderr,"          -s   stereo\n");
   fprintf(stderr,"Key Strokes\n");
   fprintf(stderr,"  arrow keys   change superellipse powers\n");
   fprintf(stderr,"  left mouse   rotate\n");
   fprintf(stderr,"middle mouse   roll\n");
   fprintf(stderr,"           i   translate up\n");
   fprintf(stderr,"           k   translate down\n");
   fprintf(stderr,"           j   translate left\n");
   fprintf(stderr,"           l   translate right\n");
   fprintf(stderr,"           [   roll clockwise\n");
   fprintf(stderr,"           ]   roll anti clockwise\n");
   fprintf(stderr,"           q   quit\n");
   exit(-1);
}

/*
   Move the camera to the home position 
*/
void CameraHome(int mode)
{
   power1 = 1;
   power2 = 1;

   camera.aperture = 50;
   camera.focallength = 5;
   camera.eyesep = camera.focallength / 20;
   camera.pr = origin;

   camera.vp.x = camera.focallength - 1; 
   camera.vp.y = 0; 
   camera.vp.z = 0;
   camera.vd.x = -camera.vp.x;
   camera.vd.y = -camera.vp.y;
   camera.vd.z = -camera.vp.z;
   Normalise(&camera.vd);

   camera.vu.x = 0;  
   camera.vu.y = 1; 
   camera.vu.z = 0;
}

/*
   Handle mouse motion
*/
void HandleMouseMotion(int x,int y)
{
   static int xlast=-1,ylast=-1;
   int dx,dy;

   dx = x - xlast;
   dy = y - ylast;
   if (dx < 0)      dx = -1;
   else if (dx > 0) dx =  1;
   if (dy < 0)      dy = -1;
   else if (dy > 0) dy =  1;

   if (currentbutton == GLUT_LEFT_BUTTON)
      RotateCamera(-dx,dy,0);
   else if (currentbutton == GLUT_MIDDLE_BUTTON)
      RotateCamera(0,0,dx);

   xlast = x;
   ylast = y;
}

