// GLUT drawing code

// by including draw.h, we ensure that the exported prototypes
//   match the function definitions
#include "draw.h"

#include <GL/glut.h>
extern "C" {
#include <OpenGL/glext.h>
}

#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/time.h>
#include <sys/unistd.h>
#include <math.h>

using namespace std;

// style to use when drawing
DrawStyle drawStyle = DRAW_TEAPOT;

double SPF=1./15;
void adjustFrameRate(int d) {
    // convert into seconds per frame for SPF>=1
    // and 2-frames per second for SPF<=1
    if (SPF < 1) SPF = 2-1/SPF;

    // adjust (unit steps of FPS or SPF, whichever is >1)
    SPF += d;
    if (SPF >= 1)
	printf("seconds per frame >= %g\n", SPF);
    else
	printf("frames per second <= %g\n", 2-SPF);

    // convert back to true seconds per frame
    if (SPF < 1) SPF = 1/(2-SPF);
}

// set shader filenames (unfortunately global, but at least local to this file)
// shader updates determined by file stat time
static string vsname, fsname;
static struct stat vsb={0}, fsb={0};
void setVSname(string filename) {
    // reset file time
    struct stat resetstat={0};
    vsb = resetstat;

    // update filename
    vsname = filename;
}
void setFSname(string filename) {
    // reset file time
    struct stat resetstat={0};
    fsb = resetstat;
    
    // update filename
    fsname = filename;
}

// read and load shader update (set*name must have already been called)

extern "C"
void updateShaders()
{
    // read vertex shader (using mmap to map file directly to memory)
    if (vsname.length()) {
	// reload shader if changed
	int fd = open(vsname.c_str(), O_RDONLY, 0);
	struct stat sb;
	fstat(fd,&sb);
	if (vsb.st_mtime != sb.st_mtime) {
	    vsb = sb;		// remember new change time

	    // load using memory mapping of file
	    char *sh = static_cast<char*>(mmap(0,sb.st_size,PROT_READ,
					       MAP_FILE,fd,0));
	    glProgramStringARB(GL_VERTEX_PROGRAM_ARB, 
			       GL_PROGRAM_FORMAT_ASCII_ARB,
			       sb.st_size, sh);
	    munmap(sh,sb.st_size);

	    // report errors
	    GLint err;
	    glGetIntegerv(GL_PROGRAM_ERROR_POSITION_ARB,&err);
	    if (err != -1) {
		string estr = reinterpret_cast<const char *>(
		    glGetString(GL_PROGRAM_ERROR_STRING_ARB));
		printf("%s: %s\n",vsname.c_str(), estr.c_str());
	    }

	    // make sure shader is enabled and schedule redraw
	    glEnable(GL_VERTEX_PROGRAM_ARB);
	    glutPostRedisplay();
	}
	close(fd);
    }

    // read fragment shader (same method)
    if (fsname.length()) {
	// reload shader if changed
	int fd = open(fsname.c_str(), O_RDONLY, 0);
	struct stat sb;
	fstat(fd,&sb);
	if (fsb.st_mtime != sb.st_mtime) {
	    fsb = sb;		// remember new change time

	    // load using memory mapping of file
	    char *sh = static_cast<char*>(mmap(0,sb.st_size,PROT_READ,
					       MAP_FILE,fd,0));
	    glProgramStringARB(GL_FRAGMENT_PROGRAM_ARB, 
			       GL_PROGRAM_FORMAT_ASCII_ARB,
			       sb.st_size, sh);
	    munmap(sh,sb.st_size);

	    // report errors
	    GLint err;
	    glGetIntegerv(GL_PROGRAM_ERROR_POSITION_ARB,&err);
	    if (err != -1) {
		string estr = reinterpret_cast<const char *>(
		    glGetString(GL_PROGRAM_ERROR_STRING_ARB));
		printf("%s: %s\n",vsname.c_str(), estr.c_str());
	    }

	    // make sure shader is enabled and schedule redraw
	    glEnable(GL_FRAGMENT_PROGRAM_ARB);
	    glutPostRedisplay();
	}
	close(fd);
    }
}


// update frame count
void updateFrame(int m)
{
    static int frame[4]={0,0,0,0};
    static int inc[4]={0,0,0,0};
    const int max=500;

    if (m==9)			// reset to start
	frame[0] = frame[1] = frame[2] = frame[3] = 0;
    else if (m==1) --inc[0];	// red
    else if (m==2) ++inc[0];
    else if (m==3) --inc[1];	// green
    else if (m==4) ++inc[1];
    else if (m==5) --inc[2];	// blue
    else if (m==6) ++inc[2];
    else if (m==7) --inc[3];	// alpha
    else if (m==8) ++inc[3];

    if (m==0 || m==9) {		// stop changing
	inc[0] = inc[1] = inc[2] = inc[3] = 0;
	glutPostRedisplay();
    }
    else if (inc[0] != 0 || inc[1] != 0 || inc[2] != 0 || inc[3] != 0)
	glutPostRedisplay();

    float color[4];
    for(int i=0; i<4; ++i) {
	frame[i] += inc[i];
	if (frame[i] < 0)   {inc[i] = 0; frame[i] = 0;}
	if (frame[i] > max) {inc[i] = 0; frame[i] = max;}
	color[i] = (float)frame[i]/max;
    }

    glColor4fv(color);
}

// draw teapot (scaled to 90, center of rotation translated to middle)
void drawTeapot()
{
    // draw teapot
    glPushMatrix();
    glTranslatef(-10,0,0);
    glutSolidTeapot(90);
    glPopMatrix();
}

// draw sphere
// hand coded since GLU reuses some texture coordinates along
// the seam, which isn't too good for my "unwrapping" examples
void drawSphere()
{
    // draw sphere
    glPushMatrix();
    glScalef(90,90,90);

    const int n_s=40, n_t=20;

    GLfloat ds=1./n_s, dt=1./n_t;
    GLfloat s=0, cos_s=1, sin_s=0;
    GLfloat cos_ds = cos(2*M_PI*ds), sin_ds = sin(2*M_PI*ds);
    GLfloat cos_dt = cos(  M_PI*dt), sin_dt = sin(  M_PI*dt);

    for(int i = 0; i<n_s; ++i) {
	GLfloat s1 = s+ds;
	GLfloat cos_s1 = cos_s*cos_ds - sin_s*sin_ds;
	GLfloat sin_s1 = sin_s*cos_ds + cos_s*sin_ds;

	glBegin(GL_QUAD_STRIP);

	GLfloat t=0, cos_t=1, sin_t=0;
	for (int j=0; j<=n_t; ++j) {
	    GLfloat t1 = t+dt;
	    GLfloat cos_t1 = cos_t*cos_dt - sin_t*sin_dt;
	    GLfloat sin_t1 = sin_t*cos_dt + cos_t*sin_dt;
	    
	    glTexCoord2f(s1, t);
	    glNormal3f(cos_s1*sin_t, sin_s1*sin_t, cos_t);
	    glVertex3f(cos_s1*sin_t, sin_s1*sin_t, cos_t);

	    glTexCoord2f(s, t);
	    glNormal3f(cos_s*sin_t, sin_s*sin_t, cos_t);
	    glVertex3f(cos_s*sin_t, sin_s*sin_t, cos_t);

	    t=t1; cos_t=cos_t1; sin_t=sin_t1;
	}
	glEnd();
	
	s=s1; cos_s=cos_s1; sin_s=sin_s1;
    }

    glPopMatrix();
}

// draw torus (donut)
// hand-coded since GLUT's doesn't provide texture coordinates
void drawDonut()
{
    // draw torus
    glPushMatrix();
    glScalef(90,90,90);

    // loosely based on code found in the MESA distribution of GLUT
    const int n_s=40, n_t=20;
    const float r=.5, R=1;

    GLfloat ds=1./n_s, dt=1./n_t;
    GLfloat s=0, cos_s=1, sin_s=0;
    GLfloat cos_ds = cos(2*M_PI*ds), sin_ds = sin(2*M_PI*ds);
    GLfloat cos_dt = cos(2*M_PI*dt), sin_dt = sin(2*M_PI*dt);

    for(int i = 0; i<n_s; ++i) {
	GLfloat s1 = s+ds;
	GLfloat cos_s1 = cos_s*cos_ds - sin_s*sin_ds;
	GLfloat sin_s1 = sin_s*cos_ds + cos_s*sin_ds;

	glBegin(GL_QUAD_STRIP);

	GLfloat t=0, cos_t=1, sin_t=0;
	for (int j=0; j<=n_t; ++j) {
	    GLfloat t1 = t+dt;
	    GLfloat cos_t1 = cos_t*cos_dt - sin_t*sin_dt;
	    GLfloat sin_t1 = sin_t*cos_dt + cos_t*sin_dt;
	    
	    GLfloat dist = R + r * cos_t;

	    glTexCoord2f(s1, t);
	    glNormal3f(cos_s1*cos_t, -sin_s1*cos_t, sin_t);
	    glVertex3f(cos_s1*dist, -sin_s1*dist, r*sin_t);
	    
	    glTexCoord2f(s,t);
	    glNormal3f(cos_s*cos_t, -sin_s*cos_t, sin_t);
	    glVertex3f(cos_s*dist, -sin_s*dist, r*sin_t);

	    t=t1; cos_t=cos_t1; sin_t=sin_t1;
	}
	glEnd();
	
	s=s1; cos_s=cos_s1; sin_s=sin_s1;
    }
    glPopMatrix();
}

// draw a square
void drawPlane()
{
    // draw square
    glBegin(GL_QUADS);
    glTexCoord2f(0,0); glNormal3f(0,0,1); glVertex3f(-90,-90,0);
    glTexCoord2f(1,0); glNormal3f(0,0,1); glVertex3f( 90,-90,0);
    glTexCoord2f(1,1); glNormal3f(0,0,1); glVertex3f( 90, 90,0);
    glTexCoord2f(0,1); glNormal3f(0,0,1); glVertex3f(-90, 90,0);
    glPushMatrix();
    glEnd();
}

// GLUT draw callback
// inserts delay to hit a 'target' FPS to demo reducing frame rate
// updates color animation
// draws currently selected object
extern "C" void
draw()
{
    // delay if necessary to hit target FPS
    static struct timeval tOld = {0,0};
    struct timeval tNew;
    gettimeofday(&tNew, 0);
    double elapsed=(tNew.tv_sec-tOld.tv_sec)+(tNew.tv_usec-tOld.tv_usec)*1e-6;
    double delay=SPF-elapsed;
    if (delay>0) usleep((useconds_t)(delay*1e6));
    tOld=tNew;
    

    // clear old screen contents
    glClearColor(1,1,1,1);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // send current frame
    updateFrame(-1);

    // draw appropriate object
    switch (drawStyle) {
    case DRAW_TEAPOT:
	drawTeapot();
	break;
    case DRAW_SPHERE:
	drawSphere();
	break;
    case DRAW_DONUT:
	drawDonut();
	break;
    case DRAW_PLANE:
	drawPlane();
	break;
    }

    // tell OpenGL to finish drawing and switch the double buffer
    glutSwapBuffers();
}
