/**

  Description: Interactive 3D graphics, Assignment #1
               Miniature Steam Engine Simulation.
  Author:      Troy Robinette
  Date:        29/9/95
  Email:       troyr@yallara.cs.rmit.edu.au
  Notes:       - Transparence doesn't quite work. The color of the
                 underlying object doesn't show through. 
	       - Also only the front side of the transparent objects are
		 transparent.

**/

#include <stdio.h>
#include <GL/glut.h>
#include <math.h>

#define TRUE  1
#define FALSE 0

/* Dimensions of texture image. */
#define IMAGE_WIDTH  64
#define IMAGE_HEIGHT 64

/* Step to be taken for each rotation. */
#define ANGLE_STEP 10

/* Magic numbers for relationship b/w cylinder head and crankshaft. */
#define MAGNITUDE  120
#define PHASE      270.112
#define FREQ_DIV   58
#define ARC_LENGHT 2.7
#define ARC_RADIUS 0.15

/* Rotation angles */
GLdouble view_h = 270, view_v = 0, head_angle = 0;
GLint crank_angle = 0;

/* Crank rotation step. */
GLdouble crank_step = 5;

/* Toggles */
GLshort shaded = TRUE, anim = FALSE;
GLshort texture = FALSE, transparent = FALSE;
GLshort light1 = TRUE, light2 = FALSE;

/* Storage for the angle look up table and the texture map */
GLdouble head_look_up_table[361];
GLubyte image[IMAGE_WIDTH][IMAGE_HEIGHT][3];

/* Indentifiers for each Display list */
GLint list_piston_shaded = 1;
GLint list_piston_texture = 2;
GLint list_flywheel_shaded = 4;
GLint list_flywheel_texture = 8;

/* Variable used in the creaton of glu objects */
GLUquadricObj *obj;

/* Draws a box by scaling a glut cube of size 1. Also checks the shaded 
   toggle to see which rendering style to use. NB Texture doesn't work
   correctly due to the cube being scaled. */
void 
myBox(GLdouble x, GLdouble y, GLdouble z)
{
  glPushMatrix();
    glScalef(x, y, z);
    if (shaded)
      glutSolidCube(1);
    else
      glutWireCube(1);
  glPopMatrix();
}

/* Draws a cylinder using glu function, drawing flat disc's at each end,
   to give the appearence of it being solid. */
void 
myCylinder(GLUquadricObj * object, GLdouble outerRadius,
  GLdouble innerRadius, GLdouble lenght)
{
  glPushMatrix();
    gluCylinder(object, outerRadius, outerRadius, lenght, 20, 1);
    glPushMatrix();
      glRotatef(180, 0.0, 1.0, 0.0);
      gluDisk(object, innerRadius, outerRadius, 20, 1);
    glPopMatrix();

    glTranslatef(0.0, 0.0, lenght);
    gluDisk(object, innerRadius, outerRadius, 20, 1);
  glPopMatrix();
}

/* Draws a piston.  */
void 
draw_piston(void)
{
  glPushMatrix();
    glColor4f(0.3, 0.6, 0.9, 1.0);

    glPushMatrix();
      glRotatef(90, 0.0, 1.0, 0.0);
      glTranslatef(0.0, 0.0, -0.07);
      myCylinder(obj, 0.125, 0.06, 0.12);
    glPopMatrix();

    glRotatef(-90, 1.0, 0.0, 0.0);
    glTranslatef(0.0, 0.0, 0.05);
    myCylinder(obj, 0.06, 0.0, 0.6);
    glTranslatef(0.0, 0.0, 0.6);
    myCylinder(obj, 0.2, 0.0, 0.5);
  glPopMatrix();
}

/* Draws the engine pole and the pivot pole for the cylinder head. */
void 
draw_engine_pole(void)
{
  glPushMatrix();
    glColor4f(0.9, 0.9, 0.9, 1.0);
    myBox(0.5, 3.0, 0.5);

    glColor3f(0.5, 0.1, 0.5);
    glRotatef(90, 0.0, 1.0, 0.0);
    glTranslatef(0.0, 0.9, -0.4);
    myCylinder(obj, 0.1, 0.0, 2);
  glPopMatrix();
}

/* Draws the cylinder head at the appropreate angle, doing the necesary 
   translations for the rotation. */
void 
draw_cylinder_head(void)
{
  glPushMatrix();
    glColor4f(0.5, 1.0, 0.5, 0.1);
    glRotatef(90, 1.0, 0.0, 0.0);
    glTranslatef(0, 0.0, 0.4);
    glRotatef(head_angle, 1, 0, 0);
    glTranslatef(0, 0.0, -0.4);
    myCylinder(obj, 0.23, 0.21, 1.6);
    glRotatef(180, 1.0, 0.0, 0.0);
    gluDisk(obj, 0, 0.23, 20, 1);
  glPopMatrix();
}

/* Draws the flywheel.  */
void 
draw_flywheel(void)
{
  glPushMatrix();
    glColor4f(0.5, 0.5, 1.0, 1.0);
    glRotatef(90, 0.0, 1.0, 0.0);
    myCylinder(obj, 0.625, 0.08, 0.5);
  glPopMatrix();
}

/* Draws the crank bell, and the pivot pin for the piston. Also calls the
   appropreate display list of a piston doing the nesacary rotations before
   hand.  */
void 
draw_crankbell(void)
{
  glPushMatrix();
    glColor4f(1.0, 0.5, 0.5, 1.0);
    glRotatef(90, 0.0, 1.0, 0.0);
    myCylinder(obj, 0.3, 0.08, 0.12);

    glColor4f(0.5, 0.1, 0.5, 1.0);
    glTranslatef(0.0, 0.2, 0.0);
    myCylinder(obj, 0.06, 0.0, 0.34);

    glTranslatef(0.0, 0.0, 0.22);
    glRotatef(90, 0.0, 1.0, 0.0);
    glRotatef(crank_angle - head_angle, 1.0, 0.0, 0.0);
    if (shaded) {
      if (texture)
        glCallList(list_piston_texture);
      else
        glCallList(list_piston_shaded);
    } else
      draw_piston();
  glPopMatrix();
}

/* Draws the complete crank. Piston also gets drawn through the crank bell
   function. */
void 
draw_crank(void)
{
  glPushMatrix();
    glRotatef(crank_angle, 1.0, 0.0, 0.0);

    glPushMatrix();
      glRotatef(90, 0.0, 1.0, 0.0);
      glTranslatef(0.0, 0.0, -1.0);
      myCylinder(obj, 0.08, 0.0, 1.4);
    glPopMatrix();

    glPushMatrix();
      glTranslatef(0.28, 0.0, 0.0);
      draw_crankbell();
    glPopMatrix();

    glPushMatrix();
      glTranslatef(-0.77, 0.0, 0.0);
      if (shaded) {
        if (texture)
          glCallList(list_flywheel_texture);
        else
          glCallList(list_flywheel_shaded);
      } else
        draw_flywheel();
    glPopMatrix();
  glPopMatrix();
}

/* Main display routine. Clears the drawing buffer and if transparency is
   set, displays the model twice, 1st time accepting those fragments with 
   a ALPHA value of 1 only, then with DEPTH_BUFFER writing disabled for 
   those with other values. */
void 
display(void)
{
  int pass;

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glPushMatrix();
    if (transparent) {
      glEnable(GL_ALPHA_TEST);
      pass = 2;
    } else {
      glDisable(GL_ALPHA_TEST);
      pass = 0;
    }

    /* Rotate the whole model */
    glRotatef(view_h, 0, 1, 0);
    glRotatef(view_v, 1, 0, 0);

    do {
      if (pass == 2) {
        glAlphaFunc(GL_EQUAL, 1);
        glDepthMask(GL_TRUE);
        pass--;
      } else if (pass != 0) {
        glAlphaFunc(GL_NOTEQUAL, 1);
        glDepthMask(GL_FALSE);
        pass--;
      }
      draw_engine_pole();

      glPushMatrix();
        glTranslatef(0.5, 1.4, 0.0);
        draw_cylinder_head();
      glPopMatrix();

      glPushMatrix();
        glTranslatef(0.0, -0.8, 0.0);
        draw_crank();
      glPopMatrix();
    } while (pass > 0);
    glDepthMask(GL_TRUE);
    glutSwapBuffers();
  glPopMatrix();
}

/* Called when the window is idle. When called increments the crank angle
   by ANGLE_STEP, updates the head angle and notifies the system that
   the screen needs to be updated. */
void 
animation(void)
{
  if ((crank_angle += crank_step) >= 360)
    crank_angle = 0;
  head_angle = head_look_up_table[crank_angle];
  glutPostRedisplay();
}

/* Called when a key is pressed. Checks if it reconises the key and if so
   acts on it, updateing the screen. */
/* ARGSUSED1 */
void 
keyboard(unsigned char key, int x, int y)
{
  switch (key) {
  case 's':
    if (shaded == FALSE) {
      shaded = TRUE;
      glShadeModel(GL_SMOOTH);
      glEnable(GL_LIGHTING);
      glEnable(GL_DEPTH_TEST);
      glEnable(GL_COLOR_MATERIAL);
      gluQuadricNormals(obj, GLU_SMOOTH);
      gluQuadricDrawStyle(obj, GLU_FILL);
    } else {
      shaded = FALSE;
      glShadeModel(GL_FLAT);
      glDisable(GL_LIGHTING);
      glDisable(GL_DEPTH_TEST);
      glDisable(GL_COLOR_MATERIAL);
      gluQuadricNormals(obj, GLU_NONE);
      gluQuadricDrawStyle(obj, GLU_LINE);
      gluQuadricTexture(obj, GL_FALSE);
    }
    if (texture && !shaded);
    else
      break;
  case 't':
    if (texture == FALSE) {
      texture = TRUE;
      glEnable(GL_TEXTURE_2D);
      gluQuadricTexture(obj, GL_TRUE);
    } else {
      texture = FALSE;
      glDisable(GL_TEXTURE_2D);
      gluQuadricTexture(obj, GL_FALSE);
    }
    break;
  case 'o':
    if (transparent == FALSE) {
      transparent = TRUE;
    } else {
      transparent = FALSE;
    }
    break;

  case 'a':
    if ((crank_angle += crank_step) >= 360)
      crank_angle = 0;
    head_angle = head_look_up_table[crank_angle];
    break;
  case 'z':
    if ((crank_angle -= crank_step) <= 0)
      crank_angle = 360;
    head_angle = head_look_up_table[crank_angle];
    break;
  case '0':
    if (light1) {
      glDisable(GL_LIGHT0);
      light1 = FALSE;
    } else {
      glEnable(GL_LIGHT0);
      light1 = TRUE;
    }
    break;
  case '1':
    if (light2) {
      glDisable(GL_LIGHT1);
      light2 = FALSE;
    } else {
      glEnable(GL_LIGHT1);
      light2 = TRUE;
    }
    break;
  case '4':
    if ((view_h -= ANGLE_STEP) <= 0)
      view_h = 360;
    break;
  case '6':
    if ((view_h += ANGLE_STEP) >= 360)
      view_h = 0;
    break;
  case '8':
    if ((view_v += ANGLE_STEP) >= 360)
      view_v = 0;
    break;
  case '2':
    if ((view_v -= ANGLE_STEP) <= 0)
      view_v = 360;
    break;
  case ' ':
    if (anim) {
      glutIdleFunc(0);
      anim = FALSE;
    } else {
      glutIdleFunc(animation);
      anim = TRUE;
    }
    break;
  case '+':
    if ((++crank_step) > 45)
      crank_step = 45;
    break;
  case '-':
    if ((--crank_step) <= 0)
      crank_step = 0;
    break;
  default:
    return;
  }
  glutPostRedisplay();
}

/* ARGSUSED1 */
void
special(int key, int x, int y)
{
  switch (key) {
  case GLUT_KEY_LEFT:
    if ((view_h -= ANGLE_STEP) <= 0)
      view_h = 360;
    break;
  case GLUT_KEY_RIGHT:
    if ((view_h += ANGLE_STEP) >= 360)
      view_h = 0;
    break;
  case GLUT_KEY_UP:
    if ((view_v += ANGLE_STEP) >= 360)
      view_v = 0;
    break;
  case GLUT_KEY_DOWN:
    if ((view_v -= ANGLE_STEP) <= 0)
      view_v = 360;
    break;
  default:
    return;
  }
  glutPostRedisplay();
}

/* Called when a menu option has been selected. Translates the menu item
   identifier into a keystroke, then call's the keyboard function. */
void 
menu(int val)
{
  unsigned char key;

  switch (val) {
  case 1:
    key = 's';
    break;
  case 2:
    key = ' ';
    break;
  case 3:
    key = 't';
    break;
  case 4:
    key = 'o';
    break;
  case 5:
    key = '0';
    break;
  case 6:
    key = '1';
    break;
  case 7:
    key = '+';
    break;
  case 8:
    key = '-';
    break;
  default:
    return;
  }
  keyboard(key, 0, 0);
}

/* Initialises the menu of toggles. */
void 
create_menu(void)
{
  glutCreateMenu(menu);
  glutAttachMenu(GLUT_LEFT_BUTTON);
  glutAttachMenu(GLUT_RIGHT_BUTTON);
  glutAddMenuEntry("Shaded", 1);
  glutAddMenuEntry("Animation", 2);
  glutAddMenuEntry("Texture", 3);
  glutAddMenuEntry("Transparency", 4);
  glutAddMenuEntry("Right Light (0)", 5);
  glutAddMenuEntry("Left Light (1)", 6);
  glutAddMenuEntry("Speed UP", 7);
  glutAddMenuEntry("Slow Down", 8);
}

/* Makes a simple check pattern image. (Copied from the redbook example
   "checker.c".) */
void
make_image(void)
{
  int i, j, c;

  for (i = 0; i < IMAGE_WIDTH; i++) {
    for (j = 0; j < IMAGE_HEIGHT; j++) {
      c = (((i & 0x8) == 0) ^ ((j & 0x8) == 0)) * 255;
      image[i][j][0] = (GLubyte) c;
      image[i][j][1] = (GLubyte) c;
      image[i][j][2] = (GLubyte) c;
    }
  }
}

/* Makes the head look up table for all possible crank angles. */
void 
make_table(void)
{
  GLint i;
  GLdouble k;

  for (i = 0, k = 0.0; i < 360; i++, k++) {
    head_look_up_table[i] =
      MAGNITUDE * atan(
      (ARC_RADIUS * sin(PHASE - k / FREQ_DIV)) /
      ((ARC_LENGHT - ARC_RADIUS * cos(PHASE - k / FREQ_DIV))));
  }
}

/* Initialises texturing, lighting, display lists, and everything else 
   associated with the model. */
void 
myinit(void)
{
  GLfloat mat_specular[] = {1.0, 1.0, 1.0, 1.0};
  GLfloat mat_shininess[] = {50.0};
  GLfloat light_position1[] = {1.0, 1.0, 1.0, 0.0};
  GLfloat light_position2[] = {-1.0, 1.0, 1.0, 0.0};

  glClearColor(0.0, 0.0, 0.0, 0.0);

  obj = gluNewQuadric();
  make_table();
  make_image();

  /* Set up Texturing */
  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
  glTexImage2D(GL_TEXTURE_2D, 0, 3, IMAGE_WIDTH,
    IMAGE_HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE,
    image);
  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

  /* Set up Lighting */
  glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
  glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess);
  glLightfv(GL_LIGHT0, GL_POSITION, light_position1);
  glLightfv(GL_LIGHT1, GL_POSITION, light_position2);

  /* Initial render mode is with full shading and LIGHT 0
     enabled. */
  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);
  glDepthFunc(GL_LEQUAL);
  glEnable(GL_DEPTH_TEST);
  glDisable(GL_ALPHA_TEST);

  glColorMaterial(GL_FRONT_AND_BACK, GL_DIFFUSE);
  glEnable(GL_COLOR_MATERIAL);
  glShadeModel(GL_SMOOTH);

  /* Initialise display lists */
  glNewList(list_piston_shaded, GL_COMPILE);
    draw_piston();
  glEndList();
  glNewList(list_flywheel_shaded, GL_COMPILE);
    draw_flywheel();
  glEndList();

  gluQuadricTexture(obj, GL_TRUE);
  glNewList(list_piston_texture, GL_COMPILE);
    draw_piston();
  glEndList();
  glNewList(list_flywheel_texture, GL_COMPILE);
    draw_flywheel();
  glEndList();
  gluQuadricTexture(obj, GL_FALSE);
}

/* Called when the model's window has been reshaped.  */
void 
myReshape(int w, int h)
{
  glViewport(0, 0, w, h);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(65.0, (GLfloat) w / (GLfloat) h, 1.0, 20.0);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  glTranslatef(0.0, 0.0, -5.0);  /* viewing transform  */
  glScalef(1.5, 1.5, 1.5);
}

/* Main program. An interactive model of a miniture steam engine.
   Sets system in Double Buffered mode and initialises all the call-back
   functions. */
int 
main(int argc, char **argv)
{
  puts("Miniature Steam Engine                    Troy Robinette\n");

  puts("Keypad Arrow keys (with NUM_LOCK on) rotates object.");
  puts("Rotate crank: 'a' = anti-clock wise 'z' = clock wise");
  puts("Crank Speed : '+' = Speed up by 1   '-' = Slow Down by 1");
  puts("Toggle      : 's' = Shading         't' = Texture");
  puts("            : ' ' = Animation       'o' = Transparency");
  puts("            : '0' = Right Light     '1' = Left Light");
  puts(" Alternatively a pop up menu with all toggles is attached");
  puts("   to the left mouse button.\n");

  glutInitWindowSize(400, 400);
  glutInit(&argc, argv);

  /* Transperancy won't work properly without GLUT_ALPHA */
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_MULTISAMPLE);
  glutCreateWindow("Miniature Steam Engine by Troy Robinette");

  glutDisplayFunc(display);
  glutKeyboardFunc(keyboard);
  glutSpecialFunc(special);
  create_menu();

  myinit();

  glutReshapeFunc(myReshape);
  glutMainLoop();
  return 0;             /* ANSI C requires main to return int. */
}
