/* OpenGL GLUT example included with SciTech Software MGL developer's kit
 * by "the unknown programmer".
 *
 * Modified for stereoscopic displays implementing GL_STEREO quad-buffering
 * by Dave Milici.
 */

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

#include <math.h>
#ifndef	M_PI
#define M_PI 3.14159265
#endif
#include <sys/types.h>
#include <stdio.h>

/* For portability... */
#undef fcos
#undef fsin
#undef fsqrt
#define fcos  cos
#define fsin  sin
#define fsqrt sqrt

/* Global def for stereo3d code additions. */
#define STEREO

static unsigned char ccolors[] =
{0xff, 0x00, 0x00,
  0x00, 0xff, 0x00,
  0x00, 0x00, 0xff,
  0x99, 0x99, 0x99};
static int cptr = 0;

static int wd, ht;

static double d_near = 1.0;
static double d_far = 2000;
static int poo = 0;

typedef struct {
  float rad, wid;
} Profile;

#ifdef STEREO
/* control vars for stereo3d display */
GLboolean bStereoRequested = GL_FALSE;
GLboolean bStereoEnabled = GL_FALSE;
/* stereo3d parallax parameters */
GLfloat fDxViewpoint;
GLfloat fDxImageShift;
/* stereo3d frustum parameters */
GLfloat fFrustumWidth2;
GLfloat fFrustumHeight2;
GLfloat fFrustumZnear;
GLfloat fFrustumZfar;
/* stereo3d projection matrixes */
GLfloat mProjectionLeft[16];
GLfloat mProjectionRight[16];
#endif

void flat_face(float ir, float or, float wd);
void draw_inside(float w1, float w2, float rad);
void draw_outside(float w1, float w2, float rad);
void tooth_side(int nt, float ir, float or, float tp, float tip, float wd);

int circle_subdiv;

void
gear(int nt, float wd, float ir, float or, float sa, float tp, float tip, int ns, Profile * ip)
{
  /**
   * nt - number of teeth 
   * wd - width of gear at teeth
   * ir - inside radius absolute scale
   * or - radius at outside of wheel (tip of tooth) ratio of ir
   * sa - angle of slant of teeth on gear (1 = slant of one whole tooth)
   * tp - ratio of tooth in slice of circle (0..1] (1 = teeth are touching at base)
   * tip - ratio of tip of tooth (0..tp] (cant be wider that base of tooth)
   * ns - number of elements in wheel width profile
   * *ip - list of float pairs {start radius, width, ...} (width is ratio to wd)
   *
   */

  /* gear lying on xy plane, z for width. all normals calulated 
     (normalized) */

  float prev;
  int k, t;

  /* estimat # times to divide circle */
  if (nt <= 0)
    circle_subdiv = 64;
  else {
    /* lowest multiple of number of teeth */
    circle_subdiv = nt;
    while (circle_subdiv < 64)
      circle_subdiv += nt;
  }

  /* --- draw wheel face --- */

  /* draw horzontal, vertical faces for each section. if first
     section radius not zero, use wd for 0.. first if ns == 0
     use wd for whole face. last width used to edge.  */

  if (ns <= 0) {
    flat_face(0.0, ir, wd);
  } else {
    /* draw first flat_face, then continue in loop */
    if (ip[0].rad > 0.0) {
      flat_face(0.0, ip[0].rad * ir, wd);
      prev = wd;
      t = 0;
    } else {
      flat_face(0.0, ip[1].rad * ir, ip[0].wid * wd);
      prev = ip[0].wid;
      t = 1;
    }
    for (k = t; k < ns; k++) {
      if (prev < ip[k].wid) {
        draw_inside(prev * wd, ip[k].wid * wd, ip[k].rad * ir);
      } else {
        draw_outside(prev * wd, ip[k].wid * wd, ip[k].rad * ir);
      }
      prev = ip[k].wid;
      /* - draw to edge of wheel, add final face if needed - */
      if (k == ns - 1) {
        flat_face(ip[k].rad * ir, ir, ip[k].wid * wd);

        /* now draw side to match tooth rim */
        if (ip[k].wid < 1.0) {
          draw_inside(ip[k].wid * wd, wd, ir);
        } else {
          draw_outside(ip[k].wid * wd, wd, ir);
        }
      } else {
        flat_face(ip[k].rad * ir, ip[k + 1].rad * ir, ip[k].wid * wd);
      }
    }
  }

  /* --- tooth side faces --- */
  tooth_side(nt, ir, or, tp, tip, wd);

  /* --- tooth hill surface --- */
}

void 
tooth_side(int nt, float ir, float or, float tp, float tip, float wd)
{

  float i;
  float end = 2.0 * M_PI / nt;
  float x[6], y[6];
  float s[3], c[3];

  or = or * ir;         /* or is really a ratio of ir */
  for (i = 0; i < 2.0 * M_PI - end / 4.0; i += end) {

    c[0] = fcos(i);
    s[0] = fsin(i);
    c[1] = fcos(i + end * (0.5 - tip / 2));
    s[1] = fsin(i + end * (0.5 - tip / 2));
    c[2] = fcos(i + end * (0.5 + tp / 2));
    s[2] = fsin(i + end * (0.5 + tp / 2));

    x[0] = ir * c[0];
    y[0] = ir * s[0];
    x[5] = ir * fcos(i + end);
    y[5] = ir * fsin(i + end);
    /* ---treat veritices 1,4 special to match strait edge of
       face */
    x[1] = x[0] + (x[5] - x[0]) * (0.5 - tp / 2);
    y[1] = y[0] + (y[5] - y[0]) * (0.5 - tp / 2);
    x[4] = x[0] + (x[5] - x[0]) * (0.5 + tp / 2);
    y[4] = y[0] + (y[5] - y[0]) * (0.5 + tp / 2);
    x[2] = or * fcos(i + end * (0.5 - tip / 2));
    y[2] = or * fsin(i + end * (0.5 - tip / 2));
    x[3] = or * fcos(i + end * (0.5 + tip / 2));
    y[3] = or * fsin(i + end * (0.5 + tip / 2));

    /* draw face trapezoids as 2 tmesh */
    glNormal3f(0.0, 0.0, 1.0);
    glBegin(GL_TRIANGLE_STRIP);
    glVertex3f(x[2], y[2], wd / 2);
    glVertex3f(x[1], y[1], wd / 2);
    glVertex3f(x[3], y[3], wd / 2);
    glVertex3f(x[4], y[4], wd / 2);
    glEnd();

    glNormal3f(0.0, 0.0, -1.0);
    glBegin(GL_TRIANGLE_STRIP);
    glVertex3f(x[2], y[2], -wd / 2);
    glVertex3f(x[1], y[1], -wd / 2);
    glVertex3f(x[3], y[3], -wd / 2);
    glVertex3f(x[4], y[4], -wd / 2);
    glEnd();

    /* draw inside rim pieces */
    glNormal3f(c[0], s[0], 0.0);
    glBegin(GL_TRIANGLE_STRIP);
    glVertex3f(x[0], y[0], -wd / 2);
    glVertex3f(x[1], y[1], -wd / 2);
    glVertex3f(x[0], y[0], wd / 2);
    glVertex3f(x[1], y[1], wd / 2);
    glEnd();

    /* draw up hill side */
    {
      float a, b, n;
      /* calculate normal of face */
      a = x[2] - x[1];
      b = y[2] - y[1];
      n = 1.0 / fsqrt(a * a + b * b);
      a = a * n;
      b = b * n;
      glNormal3f(b, -a, 0.0);
    }
    glBegin(GL_TRIANGLE_STRIP);
    glVertex3f(x[1], y[1], -wd / 2);
    glVertex3f(x[2], y[2], -wd / 2);
    glVertex3f(x[1], y[1], wd / 2);
    glVertex3f(x[2], y[2], wd / 2);
    glEnd();
    /* draw top of hill */
    glNormal3f(c[1], s[1], 0.0);
    glBegin(GL_TRIANGLE_STRIP);
    glVertex3f(x[2], y[2], -wd / 2);
    glVertex3f(x[3], y[3], -wd / 2);
    glVertex3f(x[2], y[2], wd / 2);
    glVertex3f(x[3], y[3], wd / 2);
    glEnd();

    /* draw down hill side */
    {
      float a, b, c;
      /* calculate normal of face */
      a = x[4] - x[3];
      b = y[4] - y[3];
      c = 1.0 / fsqrt(a * a + b * b);
      a = a * c;
      b = b * c;
      glNormal3f(b, -a, 0.0);
    }
    glBegin(GL_TRIANGLE_STRIP);
    glVertex3f(x[3], y[3], -wd / 2);
    glVertex3f(x[4], y[4], -wd / 2);
    glVertex3f(x[3], y[3], wd / 2);
    glVertex3f(x[4], y[4], wd / 2);
    glEnd();
    /* inside rim part */
    glNormal3f(c[2], s[2], 0.0);
    glBegin(GL_TRIANGLE_STRIP);
    glVertex3f(x[4], y[4], -wd / 2);
    glVertex3f(x[5], y[5], -wd / 2);
    glVertex3f(x[4], y[4], wd / 2);
    glVertex3f(x[5], y[5], wd / 2);
    glEnd();
  }
}

void 
flat_face(float ir, float or, float wd)
{

  int i;
  float w;

  /* draw each face (top & bottom ) * */
  if (poo)
    printf("Face  : %f..%f wid=%f\n", ir, or, wd);
  if (wd == 0.0)
    return;
  for (w = wd / 2; w > -wd; w -= wd) {
    if (w > 0.0)
      glNormal3f(0.0, 0.0, 1.0);
    else
      glNormal3f(0.0, 0.0, -1.0);

    if (ir == 0.0) {
      /* draw as t-fan */
      glBegin(GL_TRIANGLE_FAN);
      glVertex3f(0.0, 0.0, w);  /* center */
      glVertex3f(or, 0.0, w);
      for (i = 1; i < circle_subdiv; i++) {
        glVertex3f(fcos(2.0 * M_PI * i / circle_subdiv) * or,
          fsin(2.0 * M_PI * i / circle_subdiv) * or,
          w);
      }
      glVertex3f(or, 0.0, w);
      glEnd();
    } else {
      /* draw as tmesh */
      glBegin(GL_TRIANGLE_STRIP);
      glVertex3f(or, 0.0, w);
      glVertex3f(ir, 0.0, w);
      for (i = 1; i < circle_subdiv; i++) {
        glVertex3f(fcos(2.0 * M_PI * i / circle_subdiv) * or,
          fsin(2.0 * M_PI * i / circle_subdiv) * or,
          w);
        glVertex3f(fcos(2.0 * M_PI * i / circle_subdiv) * ir,
          fsin(2.0 * M_PI * i / circle_subdiv) * ir,
          w);
      }
      glVertex3f(or, 0.0, w);
      glVertex3f(ir, 0.0, w);
      glEnd();

    }
  }
}

void 
draw_inside(float w1, float w2, float rad)
{

  int i, j;
  float c, s;
  if (poo)
    printf("Inside: wid=%f..%f rad=%f\n", w1, w2, rad);
  if (w1 == w2)
    return;

  w1 = w1 / 2;
  w2 = w2 / 2;
  for (j = 0; j < 2; j++) {
    if (j == 1) {
      w1 = -w1;
      w2 = -w2;
    }
    glBegin(GL_TRIANGLE_STRIP);
    glNormal3f(-1.0, 0.0, 0.0);
    glVertex3f(rad, 0.0, w1);
    glVertex3f(rad, 0.0, w2);
    for (i = 1; i < circle_subdiv; i++) {
      c = fcos(2.0 * M_PI * i / circle_subdiv);
      s = fsin(2.0 * M_PI * i / circle_subdiv);
      glNormal3f(-c, -s, 0.0);
      glVertex3f(c * rad,
        s * rad,
        w1);
      glVertex3f(c * rad,
        s * rad,
        w2);
    }
    glNormal3f(-1.0, 0.0, 0.0);
    glVertex3f(rad, 0.0, w1);
    glVertex3f(rad, 0.0, w2);
    glEnd();
  }
}

void 
draw_outside(float w1, float w2, float rad)
{

  int i, j;
  float c, s;
  if (poo)
    printf("Outsid: wid=%f..%f rad=%f\n", w1, w2, rad);
  if (w1 == w2)
    return;

  w1 = w1 / 2;
  w2 = w2 / 2;
  for (j = 0; j < 2; j++) {
    if (j == 1) {
      w1 = -w1;
      w2 = -w2;
    }
    glBegin(GL_TRIANGLE_STRIP);
    glNormal3f(1.0, 0.0, 0.0);
    glVertex3f(rad, 0.0, w1);
    glVertex3f(rad, 0.0, w2);
    for (i = 1; i < circle_subdiv; i++) {
      c = fcos(2.0 * M_PI * i / circle_subdiv);
      s = fsin(2.0 * M_PI * i / circle_subdiv);
      glNormal3f(c, s, 0.0);
      glVertex3f(c * rad,
        s * rad,
        w1);
      glVertex3f(c * rad,
        s * rad,
        w2);
    }
    glNormal3f(1.0, 0.0, 0.0);
    glVertex3f(rad, 0.0, w1);
    glVertex3f(rad, 0.0, w2);
	glEnd();
  }
}

#ifdef STEREO
/* Additional routine for building stereo projection matrixes. */
void calcStereoMatrix(void)
{
	glMatrixMode(GL_PROJECTION);
	/* rebuild frustum with rightward image shift for left eye view */
	glPushMatrix();
	glLoadIdentity();
	glFrustum(-fFrustumWidth2+fDxImageShift, fFrustumWidth2+fDxImageShift,
			  -fFrustumHeight2, fFrustumHeight2,
			  fFrustumZnear, fFrustumZfar);
	/* offset viewpoint for left eye perspective */
	glTranslatef(fDxViewpoint, 0.0, 0.0);
	glGetFloatv(GL_PROJECTION_MATRIX, &mProjectionLeft);
	glPopMatrix();
	/* rebuild frustum with leftward image shift for right eye view */
	glPushMatrix();
	glLoadIdentity();
	glFrustum(-fFrustumWidth2-fDxImageShift, fFrustumWidth2-fDxImageShift,
			  -fFrustumHeight2, fFrustumHeight2,
			  fFrustumZnear, fFrustumZfar);
	/* offset viewpoint for right eye perspective */
	glTranslatef(-fDxViewpoint, 0.0, 0.0);
	glGetFloatv(GL_PROJECTION_MATRIX, &mProjectionRight);
	glPopMatrix();
	glMatrixMode(GL_MODELVIEW);
}
#endif

Profile gear_profile[] =
{0.000, 0.0,
  0.300, 7.0,
  0.340, 0.4,
  0.550, 0.64,
  0.600, 0.4,
  0.950, 1.0
};

float a1 = 27.0;
float a2 = 67.0;
float a3 = 47.0;
float a4 = 87.0;
float i1 = 1.2;
float i2 = 3.1;
float i3 = 2.3;
float i4 = 1.1;
void
oneFrame(void)
{
#ifdef STEREO
  int i;

  /* enclose original rendering code in 2-pass loop */
  for (i=0; i<2; i++)
  {
  /* adjust projection matrix for stereo viewpoint */
  if (bStereoEnabled) {
	  glDrawBuffer((i==0) ? GL_BACK_LEFT : GL_BACK_RIGHT);
	  glMatrixMode(GL_PROJECTION);
	  glPushMatrix();
	  glLoadMatrixf((i==0) ? &mProjectionLeft : &mProjectionRight);
	  glMatrixMode(GL_MODELVIEW);
	  }
#endif

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glPushMatrix();
  glTranslatef(0.0, 0.0, -4.0);
  glRotatef(a3, 1.0, 1.0, 1.0);
  glRotatef(a4, 0.0, 0.0, -1.0);
  glTranslatef(0.14, 0.2, 0.0);
  gear(76,
    0.4, 2.0, 1.1,
    0.0, 0.4, 0.04,
    sizeof(gear_profile) / sizeof(Profile), gear_profile);
  glPopMatrix();

  glPushMatrix();
  glTranslatef(0.1, 0.2, -3.8);
  glRotatef(a2, -4.0, 2.0, -1.0);
  glRotatef(a1, 1.0, -3.0, 1.0);
  glTranslatef(0.0, -0.2, 0.0);
  gear(36,
    0.4, 2.0, 1.1,
    0.0, 0.7, 0.2,
    sizeof(gear_profile) / sizeof(Profile), gear_profile);
  glPopMatrix();

#ifdef STEREO
  /* render twice for stereo, or once for mono */
  if (bStereoEnabled) {
	  glFlush();
	  glMatrixMode(GL_PROJECTION);
	  glPopMatrix();
	  glMatrixMode(GL_MODELVIEW);
	  }
  else
	  break;
  } /* for loop */
#endif

  a1 += i1;
  if (a1 > 360.0)
    a1 -= 360.0;
  if (a1 < 0.0)
    a1 -= 360.0;
  a2 += i2;
  if (a2 > 360.0)
    a2 -= 360.0;
  if (a2 < 0.0)
    a2 -= 360.0;
  a3 += i3;
  if (a3 > 360.0)
    a3 -= 360.0;
  if (a3 < 0.0)
    a3 -= 360.0;
  a4 += i4;
  if (a4 > 360.0)
    a4 -= 360.0;
  if (a4 < 0.0)
    a4 -= 360.0;
  glutSwapBuffers();
}

void
display(void)
{
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}

void
myReshape(int w, int h)
{
  wd = w;
  ht = h;
  glViewport(0, 0, w, h);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glFrustum(-1.0, 1.0, -1.0, 1.0, d_near, d_far);
  /**
    use perspective instead:

    if (w <= h){
        glOrtho( 0.0, 1.0,
                 0.0, 1.0 * (GLfloat) h / (GLfloat) w,
                -16.0, 4.0);
    }else{
        glOrtho( 0.0, 1.0 * (GLfloat) w / (GLfloat) h,
                 0.0, 1.0,
                -16.0, 4.0);
    }
   */
#ifdef STEREO
  /* remember frustum settings for stereo3d perspective adjustments */
  fFrustumZnear = d_near;
  fFrustumZfar = d_far;
  fFrustumHeight2 = 1.0;
  fFrustumWidth2 = 1.0;
  fDxImageShift = 0.075;
  fDxViewpoint = 0.23;
  calcStereoMatrix();
#endif

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}

void
visibility(int status)
{
  if (status == GLUT_VISIBLE) {
    glutIdleFunc(oneFrame);
  } else {
    glutIdleFunc(NULL);
  }

}

void
myinit(void)
{
  float f[20];

#ifdef STEREO
  /* check if GL_STEREO quad-buffered page-flipping display enabled ? */
  if (bStereoRequested)
	  glGetBooleanv(GL_STEREO, &bStereoEnabled);
  /* lower background contrast to reduce ghosting in LCS glasses */
  if (bStereoEnabled)
      glClearColor(0.2, 0.2, 0.2, 0.0);
  else
#endif
  glClearColor(0.0, 0.0, 0.0, 0.0);
  myReshape(640, 480);
  /* glShadeModel(GL_FLAT); */
  glEnable(GL_DEPTH_TEST);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glEnable(GL_LIGHTING);

  glLightf(GL_LIGHT0, GL_SHININESS, 1.0);
  f[0] = 1.3;
  f[1] = 1.3;
  f[2] = -3.3;
  f[3] = 1.0;
  glLightfv(GL_LIGHT0, GL_POSITION, f);
  f[0] = 0.8;
  f[1] = 1.0;
  f[2] = 0.83;
  f[3] = 1.0;
  glLightfv(GL_LIGHT0, GL_SPECULAR, f);
  glLightfv(GL_LIGHT0, GL_DIFFUSE, f);
  glEnable(GL_LIGHT0);

  glLightf(GL_LIGHT1, GL_SHININESS, 1.0);
  f[0] = -2.3;
  f[1] = 0.3;
  f[2] = -7.3;
  f[3] = 1.0;
  glLightfv(GL_LIGHT1, GL_POSITION, f);
  f[0] = 1.0;
  f[1] = 0.8;
  f[2] = 0.93;
  f[3] = 1.0;
  glLightfv(GL_LIGHT1, GL_SPECULAR, f);
  glLightfv(GL_LIGHT1, GL_DIFFUSE, f);
  glEnable(GL_LIGHT1);

  /* gear material */
  f[0] = 0.1;
  f[1] = 0.15;
  f[2] = 0.2;
  f[3] = 1.0;
  glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, f);

  f[0] = 0.9;
  f[1] = 0.3;
  f[2] = 0.3;
  f[3] = 1.0;
  glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, f);

  f[0] = 0.4;
  f[1] = 0.9;
  f[2] = 0.6;
  f[3] = 1.0;
  glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, f);

  glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 4);
}

void
keys(unsigned char c, int x, int y)
{

  if (c == 0x1b)
	glutExit();            /* escape */
#ifdef STEREO
  else switch (c) {
	/* adjust stereo3d parallax settings */
	case '+': 	fDxViewpoint += 0.010;
				calcStereoMatrix();
				break;
	case '-': 	fDxViewpoint -= 0.010;
				if (fDxViewpoint < 0.0)
					fDxViewpoint = 0.0;
				calcStereoMatrix();
				break;
	case '*': 	fDxImageShift += 0.001;
				calcStereoMatrix();
				break;
	case '/': 	fDxImageShift -= 0.001;
				calcStereoMatrix();
				break;
  }
#endif
}

int
main(int argc, char **argv)
{
  int mode = GLUT_DOUBLE;

  glutInit(&argc, argv);

#ifdef  STEREO
  if (argc > 1)
	  if (stricmp(argv[1],"-stereo") == 0)
		  bStereoRequested = GL_TRUE;
  /* use GLUT to request GL_STEREO quad-buffered display visual */
  if (bStereoRequested)
	  mode |= GLUT_STEREO;
#endif

  glutInitDisplayMode(mode | GLUT_RGB | GLUT_DEPTH);
  glutInitWindowPosition(100, 100);
  glutInitWindowSize(640, 480);
  glutCreateWindow(argv[0]);

  myinit();
  glutReshapeFunc(myReshape);
  glutDisplayFunc(display);
  glutKeyboardFunc(keys);
  glutVisibilityFunc(visibility);
  glutPostRedisplay();
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glutMainLoop();
  return 0;             /* ANSI C requires main to return int. */
}
