// cube.c - draw a 3D, rotatable, red cube

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

#define M_PI 3.1415927

// used for mouse-controlled rotations
static int window_width, window_height;
static int start_x, start_y;
static double current_rotation[16], start_rotation[16];
static float triangles[100][3][3];

static void generateTriangles()
{
  int i,j,k;
  for (i=0; i<100;i++)
    for (j=0;j<3;j++)
      for (k=0;k<3;k++)
        triangles[i][j][k]=-1.0+2.0*((float)rand()/RAND_MAX);
}

void
display(void)
{
  GLfloat red[] = {1.0, 0.0, 0.0, 1.0};
  GLfloat green[] = {0.0, 1.0, 0.0, 1.0};
  GLfloat c_random[]={1.0,0.0,0.0,1.0};
  int i;

  // Clear the back buffer
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  // Make the cube red
  glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, red);

  // Set up viewing transformation
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();

  // look from (0,0,5) to (0,0,0) with up vector of (0,1,0)
  gluLookAt(0.0, 0.0, 5.0,
            0.0, 0.0, 0.0,
            0.0, 1.0, 0.0);

  // apply the current rotation
  glMultMatrixd(current_rotation);

  // Draw the cube
  // glutSolidCube(2.0);

  // Draw a green triangle
  glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, green);
  glNormal3f(0.0, 0.0, 1.0);
  glBegin(GL_TRIANGLES);
  glVertex3f(-1.0, 1.0, 0.0);
  glVertex3f(1.0, 1.0, 0.0);
  glVertex3f(0.0, 2.0, 0.0);
  glEnd();

  // Draw 100 triangles
  for (i=0;i<100;i++) {
    c_random[0]=(float)rand()/RAND_MAX;
    c_random[1]=(float)rand()/RAND_MAX;
    c_random[2]=(float)rand()/RAND_MAX;
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE,c_random);
    glBegin(GL_TRIANGLES);
       glVertex3fv(triangles[i][0]);
       glVertex3fv(triangles[i][1]);
       glVertex3fv(triangles[i][2]);
    glEnd();
  }
  // Swap front and back buffers
  glutSwapBuffers();
}

void
arcball_rotation(int start_x, int start_y, int end_x, int end_y)
{
  double sx, sy, sz, ex, ey, ez;
  double scale;
  double sl, el;
  double dotprod;

  // find vectors from center of window
  sx = start_x - (window_width  / 2);
  sy = start_y - (window_height / 2);
  ex = end_x -   (window_width  / 2);
  ey = end_y -   (window_height / 2);

  // invert y coordinates (raster versus device coordinates)
  sy = -sy;
  ey = -ey;
  
  // scale by inverse of size of window
  if (window_width > window_height) {
    scale = 1.0 / (double) window_height;
  } else {
    scale = 1.0 / (double) window_width;
  }

  sx *= scale;
  sy *= scale;
  ex *= scale;
  ey *= scale;

  // project points to unit circle
  sl = hypot(sx, sy);
  el = hypot(ex, ey);

  if (sl > 1.0) {
    sx /= sl;
    sy /= sl;
    sl = 1.0;
  }
  if (el > 1.0) {
    ex /= el;
    ey /= el;
    el = 1.0;
  }

  // project up to unit sphere - find Z coordinate
  sz = sqrt(1.0 - sl * sl);
  ez = sqrt(1.0 - el * el);

  // rotate (sx,sy,sz) into (ex,ey,ez)
  
  // compute angle from dot-product of unit vectors (and double it).
  // compute axis from cross product.
  dotprod = sx * ex + sy * ey + sz * ez;
  glRotatef(2.0 * acos(dotprod) * 180.0 / M_PI,
            sy * ez - ey * sz,
            sz * ex - ez * sx,
            sx * ey - ex * sy);
}

void
mouse(int button, int state, int x, int y)
{ 
  if (state == GLUT_DOWN) {
    // Start of drag.  
    int i;

    // Store off mouse location and current rotation matrix
    start_x = x;
    start_y = y;
    for (i = 0; i < 16; i++) start_rotation[i] = current_rotation[i];
  } else {
    // nothing to do here, as rotation has already been multiplied into current_rotation
    ;
  }

  // redraw.
  glutPostRedisplay();
}

void
motion(int x, int y)
{
  // Mouse has been dragged.

  // Update the current rotation matrix.
  glLoadIdentity();
  arcball_rotation(start_x, start_y, x, y);
  glMultMatrixd(start_rotation);
  glGetDoublev(GL_MODELVIEW_MATRIX, current_rotation);

  // redraw.
  glutPostRedisplay();
}

void keyboard(unsigned char key, int x, int y)
{
  switch (key) {
  case 'Q':
  case 'q':
    exit(0);
  }
}

void
init(void)
{
  // Use depth buffering for hidden surface elimination.
  glEnable(GL_DEPTH_TEST);
  
  // Enable a single OpenGL light.
  // White diffuse light.
  GLfloat light_diffuse[] = {1.0, 1.0, 1.0, 1.0};
  // Put the light at infinity in the direction (1,1,1)
  GLfloat light_position[] = {1.0, 1.0, 1.0, 0.0};  
  glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse);
  glLightfv(GL_LIGHT0, GL_POSITION, light_position);

  // Turn on the light and enable lighting.
  glEnable(GL_LIGHT0);
  glEnable(GL_LIGHTING);
  glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 1);

  // Set up a perspective view, with square aspect ratio
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  // 50 degree fov, uniform aspect ratio, near = 1, far = 10
  gluPerspective(50.0,
                 1.0,
                 1.0, 10.0);

  // Initialize rotation of the cube
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  glGetDoublev(GL_MODELVIEW_MATRIX, current_rotation);
}

void
reshape(int w, int h)
{
  // store window dimensions (needed to compute rotations)
  window_width = w;
  window_height = h;

  // Always use the largest square viewport possible
  if (w > h) {
    glViewport((w - h) / 2, 0, h, h);
  } else {
    glViewport(0, (h - w) / 2, w, w);
  }
}

int
main(int argc, char **argv)
{
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
  glutCreateWindow("3D cube");
  glutDisplayFunc(display);
  glutReshapeFunc(reshape);
  glutMouseFunc(mouse);
  glutMotionFunc(motion);
  glutKeyboardFunc(keyboard);
  init();
  generateTriangles(); // generate 100 triangles
  glutMainLoop();
  return 0;
}
