/*
	Mathematical surface, with the ability to move the surface around and
	see all parts.  The surface is animated -- it's from a one-parameter
	family of surfaces, and that parameter is animated by the  idle  function.
	The keyboard is enabled to let the user manipulate the surface, with
		X-rotations			:  q  w
		Y-rotations			:  a  s
		Forward and back	:  z  x
	The mouse is also enabled to manipulate the surface.
   
	Source file to be used with
	Cunningham, Computer Graphics: Programming in OpenGL for Visual Communication, Prentice-Hall, 2007

	Intended for class use only
*/
#include <GLUT/glut.h>
#include <stdlib.h>
#include <stdio.h>
#include <math.h>

#define SMOOTH		// determine whether flat or smooth shading is to be used

// variables to keep track of the current rotation on each axis
GLfloat camPitch = 1.0, camYaw = 0.0, camRadius = 16.0;
int mx,my; // used to calculate mouse displacement
int mdown; // is a button down?
int buttonDown; // if so, which button?

char *outfile = "windowDump.raw";

typedef GLfloat point3[3];

GLfloat t=0.0;        // parameter for the family of functions

//  Define the parameters of the surface grid
//  For an N x M grid you need XSIZE = N+1 and ZSIZE = M+1
#define XSIZE 100  // needs to be much larger than this for best results
#define ZSIZE XSIZE
#define MINX  -3.0 // was -3..3, -3..3 with original equation -- CHOICE = 4
#define MAXX   3.0
#define MINZ  -3.0
#define MAXZ   3.0

int BUF_WIDTH = 500;
int BUF_HEIGHT = 500;
static  GLubyte bufImage[500][500][3];

static GLfloat vertices[XSIZE][ZSIZE];

// function prototypes follow
GLfloat XX( int );
GLfloat ZZ( int );
void myinit( void );
void surface( void );
void display( void );
void reshape( int ,int );
void keyboard(unsigned char, int, int );
void animate( void );

void saveWindow(char *, int, int);

// two functions to return X and Z values for array indices  i  and  j  respectively
GLfloat XX(int i) {
     return (MINX+((MAXX-MINX)/(float)(XSIZE-1))*(float)(i));
}

GLfloat ZZ(int j) {
     return (MINZ+((MAXZ-MINZ)/(float)(ZSIZE-1))*(float)(j));
}

// initialize OpenGL and all necessary global variables
void myinit(void)
{
        GLfloat light_pos0[]={  0.0, 10.0, 10.0,  1.0 }; // first light over z-axis
        GLfloat light_col0[]={  1.0,  0.0,  0.0,  1.0 }; // and red
        GLfloat amb_color0[]={  0.3,  0.0,  0.0,  1.0 }; // even ambiently
        
        GLfloat light_pos1[]={  5.0, 10.0, -7.26, 1.0 }; // second light back/right
        GLfloat light_col1[]={  0.0,  1.0,  0.0,  1.0 }; // and green
        GLfloat amb_color1[]={  0.0,  0.3,  0.0,  1.0 }; // even ambiently
        
        GLfloat light_pos2[]={ -5.0, 10.0, -7.26, 1.0 }; // third light back/left
        GLfloat light_col2[]={  0.0,  0.0,  1.0,  1.0 }; // and blue
        GLfloat amb_color2[]={  0.0,  0.0,  0.3,  1.0 }; // even ambiently
        
//      set up overall light data
        GLfloat mat_specular[]  ={ 0.8, 0.8, 0.8, 1.0 };

        glClearColor( 0.0, 0.0, 1.0, 0.0 );
#ifdef SMOOTH
        glShadeModel(GL_SMOOTH);
#else
		glShadeModel(GL_FLAT);
#endif
        glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, mat_specular );
        
        glLightfv(GL_LIGHT0, GL_POSITION, light_pos0 ); // light 0
        glLightfv(GL_LIGHT0, GL_AMBIENT, amb_color0 );
        glLightfv(GL_LIGHT0, GL_SPECULAR, light_col0 );
        glLightfv(GL_LIGHT0, GL_DIFFUSE, light_col0 );
        
        glLightfv(GL_LIGHT1, GL_POSITION, light_pos1 ); // light 1
        glLightfv(GL_LIGHT1, GL_AMBIENT, amb_color1 );
        glLightfv(GL_LIGHT1, GL_SPECULAR, light_col1 );
        glLightfv(GL_LIGHT1, GL_DIFFUSE, light_col1 );
        
        glLightfv(GL_LIGHT2, GL_POSITION, light_pos2 ); // light 2
        glLightfv(GL_LIGHT2, GL_AMBIENT, amb_color2 );
        glLightfv(GL_LIGHT2, GL_SPECULAR, light_col2 );
        glLightfv(GL_LIGHT2, GL_DIFFUSE, light_col2 );
        
        glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 1 ); // two-sided lighting

		/* attributes */
        glEnable(GL_LIGHTING);   // so lighting models are used
        glEnable(GL_LIGHT0);     // we'll use LIGHT0
        glEnable(GL_LIGHT1);     // ... and LIGHT1
        glEnable(GL_LIGHT2);     // ... and LIGHT2
        glEnable(GL_DEPTH_TEST); // allow z-buffer display
        glEnable(GL_NORMALIZE);  // make normal vectors 1-unit long after transform
}

// draw the surface
void surface(void)
{
    point3 vec1, vec2, triNormal;
    
    int  i, j;
    float x, z;

//  Set up the surface color
    GLfloat white[] = {1.0, 1.0, 1.0, 1.0};
    GLfloat yellow[]= {1.0, 1.0, 0.0, 1.0};
    GLfloat mat_shininess[]={ 30.0 };
    
//  Calculate the points of the surface on the grid

    #define EPSILON .001;
    for ( i=0; i<XSIZE; i++ )
       for ( j=0; j<ZSIZE; j++ )
       {
			x = XX(i);
			z = ZZ(j);
			vertices[i][j] = 0.3*cos(x*x+z*z+t);
       }

/*  actually draw the surface */
    for ( i=0; i<XSIZE-1; i++ )
       for ( j=0; j<ZSIZE-1; j++ )
       {
       // first triangle in the quad, front face
          glMaterialfv(GL_FRONT, GL_DIFFUSE, white );
          glMaterialfv(GL_BACK, GL_DIFFUSE, yellow );
          glMaterialfv(GL_FRONT_AND_BACK, GL_SHININESS, mat_shininess );
          glBegin(GL_POLYGON);
			 // build the normal for the triangle
             vec1[0] = XX(i+1)-XX(i);
             vec1[1] = vertices[i+1][j]-vertices[i][j];
             vec1[2] = ZZ(j)-ZZ(j);
             vec2[0] = XX(i+1)-XX(i+1);
             vec2[1] = vertices[i+1][j+1]-vertices[i+1][j];
             vec2[2] = ZZ(j+1)-ZZ(j);
             triNormal[0] = vec1[1] * vec2[2] - vec1[2] * vec2[1];
             triNormal[1] = vec1[2] * vec2[0] - vec1[0] * vec2[2];
             triNormal[2] = vec1[0] * vec2[1] - vec1[1] * vec2[0];
             glNormal3fv(triNormal); // hack together the normal vector...

             glVertex3f(XX(i),vertices[i  ][j  ],ZZ(j));
             glVertex3f(XX(i+1),vertices[i+1][j  ],ZZ(j));
             glVertex3f(XX(i+1),vertices[i+1][j+1],ZZ(j+1));
          glEnd();

        // second triangle in the quad, front face  
          glMaterialfv(GL_FRONT, GL_DIFFUSE, white );
          glMaterialfv(GL_BACK, GL_DIFFUSE, yellow );
          glMaterialfv(GL_FRONT_AND_BACK, GL_SHININESS, mat_shininess );
          glBegin(GL_POLYGON);
             vec1[0] = XX(i+1)-ZZ(i);
             vec1[1] = vertices[i+1][j+1]-vertices[i][j];
             vec1[2] = ZZ(j+1)-ZZ(j);
             vec2[0] = XX(i)-XX(i+1);
             vec2[1] = vertices[i][j+1]-vertices[i+1][j+1];
             vec2[2] = ZZ(j+1)-ZZ(j+1);
             triNormal[0] = vec1[1] * vec2[2] - vec1[2] * vec2[1];
             triNormal[1] = vec1[2] * vec2[0] - vec1[0] * vec2[2];
             triNormal[2] = vec1[0] * vec2[1] - vec1[1] * vec2[0];
             glNormal3fv(triNormal);

             glVertex3f(XX(i),vertices[i  ][j  ],ZZ(j));
             glVertex3f(XX(i+1),vertices[i+1][j+1],ZZ(j+1));
             glVertex3f(XX(i),vertices[i  ][j+1],ZZ(j+1));
          glEnd();
       }
}

void display( void )
{
	static GLfloat camX, camY, camZ;

	// convert from spherical coords to cartesian
	camX = sin(camYaw) * cos(camPitch) * camRadius;
	camY = sin(camPitch) * camRadius;
	camZ = cos(camYaw) * cos(camPitch) * camRadius;

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    //  Put the identity onto the modelview stack to start the viewing transform
	glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    /*           eye point        center of view      up   */
    gluLookAt( camX, camY, camZ, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

    surface();
    
    glutSwapBuffers();
 }

void reshape(int w,int h)
{
        glViewport(0,0,(GLsizei)w,(GLsizei)h);
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        gluPerspective(30.0,(float)w/(float)h,1.0,100.0);
}

void keyboard(unsigned char key, int x, int y)
{
        switch (key)
        {
			case 'w' :
				camYaw += .08;
				if(camYaw > 6.28)
					camYaw = camYaw-6.28;
				break;
    	    case 'q' :
				camYaw -= .08;
				if(camYaw < 0.0)
					camYaw = 6.28+camYaw;
				break;
    	    case 's' :
				camPitch += .08;
				if(camPitch > 1.57)
					camPitch = 1.57;
				break;
    	    case 'a' :
				camPitch -= .08;
				if(camPitch < -1.57)
					camPitch = -1.57;
				break;
    	    case 'x' :
				camRadius += 0.5; break;
    	    case 'z' :
    			camRadius -= 0.5;
				if(camRadius < 1.0)
					camRadius = 1.0;
				break;
			case 'f':
				saveWindow(outfile, 500, 500);
				break;
        }
}

void mouse(int button, int state, int x, int y) {
	// save the mouse position for the camera rotation
	mx = x;
	my = y;
	if(state == GLUT_DOWN)
		mdown = 1; //TRUE;
	else
		mdown = 0; //FALSE;

	buttonDown = button; // remember which button is down
}

void motion(int x, int y) {
	if(buttonDown == GLUT_LEFT_BUTTON) {
		// adjust the camera's yaw
		camYaw += 0.02*(GLfloat)(mx-x);

		// wrap the yaw values at 0 and 2*pi
		if(camYaw < 0.0)
				camYaw = 6.28+camYaw;
		else if(camYaw > 6.28)
				camYaw = camYaw-6.28;

		// adjust the camera's pitch
		camPitch += 0.02*(GLfloat)(y-my);

		// make sure we don't pitch too far
		if(camPitch > 1.57)
			camPitch = 1.57;
		else if(camPitch < -1.57)
			camPitch = -1.57;
	}
	else {
		// zoom in and out if right mouse button is down
		camRadius += 0.5*(GLfloat)(y-my);
		if(camRadius < 1.0)
			camRadius = 1.0;
	}
	mx = x;
	my = y;
}

// update the surface
void animate(void)
{
    t += 0.1;
    glutPostRedisplay();
}

// function to read the contents of the front screen buffer into an array whose
// name and dimensions are passed to the function.  The resulting file will be a
// raw RGB file that can be opened and manipulated by any application that can
// work with that format (e.g. Photoshop).

void saveWindow(char *outfile, int WIDTH, int HEIGHT)
{
        FILE * fd;
        GLubyte ch; //char c;
        int i,j,k;

        fd = fopen(outfile, "w");
		glReadBuffer(GL_FRONT);			// set up to read the front buffer
		glReadPixels(0, 0, WIDTH, HEIGHT, GL_RGB, GL_UNSIGNED_BYTE, bufImage);
        for (i=WIDTH; i>0; i--)     // for each row
        {
           for (j=0; j<HEIGHT; j++) // for each column
           {
              for (k=0; k<3; k++)       // read RGB components of the pixel
				{
                 ch = bufImage[i][j][k];
                 fwrite(&ch, 1, 1, fd);
				}
           }
        }
        fclose(fd);
}

int main(int argc, char** argv)
{
        
		// Standard GLUT initialization
        glutInit(&argc,argv);
        glutInitDisplayMode (GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
        glutInitWindowSize(800, 800);
        glutInitWindowPosition(0, 0);
        glutCreateWindow("moving waves of math");
        glutDisplayFunc(display);
        glutReshapeFunc(reshape);
        glutIdleFunc(animate);
        glutKeyboardFunc(keyboard);
		glutMouseFunc(mouse);
		glutMotionFunc(motion);

        myinit();

        glutMainLoop();

		return 0;
}
