// Source file for the OFF file viewer



////////////////////////////////////////////////////////////
// INCLUDE FILES
////////////////////////////////////////////////////////////

#ifdef _WIN32
#include <windows.h>
#define M_PI 3.1415925
#endif

#include <GL/glut.h>
#include "R3/R3.h"
#include "R3Mesh.h"




////////////////////////////////////////////////////////////
// GLOBAL VARIABLES
////////////////////////////////////////////////////////////

// Program arguments

static char *filename = NULL;
static int print_verbose = 0;



// Display variables

static R3Mesh *mesh = NULL;
static R3Point camera_eye (0, 0, 4);
static R3Vector camera_towards(0, 0, -1);
static R3Vector camera_up(0, 1, 0);
static double camera_yfov = 0.75;
static int show_faces = 1;
static int show_edges = 0;
static int show_vertices = 0;
static int show_ids = 0;



// GLUT variables 

static int GLUTwindow = 0;
static int GLUTwindow_height = 800;
static int GLUTwindow_width = 800;
static int GLUTmouse[2] = { 0, 0 };
static int GLUTbutton[3] = { 0, 0, 0 };
static int GLUTmodifiers = 0;



////////////////////////////////////////////////////////////
// GLUT USER INTERFACE CODE
////////////////////////////////////////////////////////////

void GLUTMainLoop(void)
{
  // Setup initial camera parameters
  double mesh_radius = mesh->bbox.DiagonalRadius();
  R3Point mesh_center = mesh->bbox.Centroid();
  camera_eye = mesh_center + 2.5 * mesh_radius * camera_towards;

  // Run main loop -- never returns 
  glutMainLoop();
}



void GLUTDrawText(const R3Point& p, const char *s)
{
  // Draw text string s and position p
  glRasterPos3d(p[0], p[1], p[2]);
  while (*s) glutBitmapCharacter(GLUT_BITMAP_HELVETICA_12, *(s++));
}
  



void GLUTStop(void)
{
  // Destroy window 
  glutDestroyWindow(GLUTwindow);

  // Delete mesh
  delete mesh;

  // Exit
  exit(0);
}



void GLUTResize(int w, int h)
{
  // Resize window
  glViewport(0, 0, w, h);

  // Remember window size 
  GLUTwindow_width = w;
  GLUTwindow_height = h;

  // Redraw
  glutPostRedisplay();
}



void GLUTMotion(int x, int y)
{
  // Invert y coordinate
  y = GLUTwindow_height - y;
  
  // Compute mouse movement
  int dx = x - GLUTmouse[0];
  int dy = y - GLUTmouse[1];
  
  // Process mouse motion event
  if ((dx != 0) || (dy != 0)) {
    R3Point mesh_center = mesh->bbox.Centroid();
    if (GLUTbutton[0]) {
      // Rotate world
      double vx = (double) dx / (double) GLUTwindow_width;
      double vy = (double) dy / (double) GLUTwindow_height;
      double theta = 4.0 * (fabs(vx) + fabs(vy));
      R3Vector camera_right = camera_towards % camera_up;
      R3Vector vector = (camera_right * vx) + (camera_up * vy);
      R3Vector rotation_axis = vector % camera_towards;
      rotation_axis.Normalize();
      camera_eye.Rotate(R3Line(mesh_center, rotation_axis), theta);
      camera_towards.Rotate(rotation_axis, theta);
      camera_up.Rotate(rotation_axis, theta);
      camera_right = camera_towards % camera_up;
      camera_up = camera_right % camera_towards;
      camera_towards.Normalize();
      camera_up.Normalize();
      glutPostRedisplay();
    }
    else if (GLUTbutton[1]) {
      // Scale world 
      double factor = (double) dx / (double) GLUTwindow_width;
      factor += (double) dy / (double) GLUTwindow_height;
      factor = exp(2.0 * factor);
      factor = (factor - 1.0) / factor;
      R3Vector translation = (mesh_center - camera_eye) * factor;
      camera_eye += translation;
      glutPostRedisplay();
    }
    else if (GLUTbutton[2]) {
      // Translate world
      double length = R3Distance(mesh_center, camera_eye) * tan(camera_yfov);
      double vx = length * (double) dx / (double) GLUTwindow_width;
      double vy = length * (double) dy / (double) GLUTwindow_height;
      R3Vector camera_right = camera_towards % camera_up;
      R3Vector translation = -((camera_right * vx) + (camera_up * vy));
      camera_eye += translation;
      glutPostRedisplay();
    }
  }

  // Remember mouse position 
  GLUTmouse[0] = x;
  GLUTmouse[1] = y;
}



void GLUTMouse(int button, int state, int x, int y)
{
  // Invert y coordinate
  y = GLUTwindow_height - y;
  
  // Process mouse button event
  if (state == GLUT_DOWN) {
    if (button == GLUT_LEFT_BUTTON) {
    }
    else if (button == GLUT_MIDDLE_BUTTON) {
    }
    else if (button == GLUT_RIGHT_BUTTON) {
    }
  }

  // Remember button state 
  int b = (button == GLUT_LEFT_BUTTON) ? 0 : ((button == GLUT_MIDDLE_BUTTON) ? 1 : 2);
  GLUTbutton[b] = (state == GLUT_DOWN) ? 1 : 0;

  // Remember modifiers 
  GLUTmodifiers = glutGetModifiers();

   // Remember mouse position 
  GLUTmouse[0] = x;
  GLUTmouse[1] = y;

  // Redraw
  glutPostRedisplay();
}



void GLUTSpecial(int key, int x, int y)
{
  // Invert y coordinate
  y = GLUTwindow_height - y;

  // Process keyboard button event 
  switch (key) {
  case GLUT_KEY_DOWN:
    break;

  case GLUT_KEY_UP:
    break;

  case GLUT_KEY_LEFT:
    break;

  case GLUT_KEY_RIGHT:
    break;
  }

  // Remember mouse position 
  GLUTmouse[0] = x;
  GLUTmouse[1] = y;

  // Remember modifiers 
  GLUTmodifiers = glutGetModifiers();

  // Redraw
  glutPostRedisplay();
}



void GLUTKeyboard(unsigned char key, int x, int y)
{
  // Invert y coordinate
  y = GLUTwindow_height - y;

  // Process keyboard button event 
  switch (key) {
  case 'F':
  case 'f':
    show_faces = !show_faces;
    break;

  case 'E':
  case 'e':
    show_edges = !show_edges;
    break;

  case 'I':
  case 'i':
    show_ids = !show_ids;
    break;

  case 'V':
  case 'v':
    show_vertices = !show_vertices;
    break;

  case 'Q':
  case 'q':
    GLUTStop();
    break;

  case 27: // ESCAPE
    GLUTStop();
    break;
  }

  // Remember mouse position 
  GLUTmouse[0] = x;
  GLUTmouse[1] = y;

  // Remember modifiers 
  GLUTmodifiers = glutGetModifiers();

  // Redraw
  glutPostRedisplay();
}



void GLUTRedraw(void)
{
  // Set projection transformation
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(180.0*camera_yfov/M_PI, (GLdouble) GLUTwindow_width /(GLdouble) GLUTwindow_height, 0.001, 100.0);

  // Set camera transformation
  R3Vector& t = camera_towards;
  R3Vector& u = camera_up;
  R3Vector r = camera_towards % camera_up;
  GLdouble camera_matrix[16] = { r[0], u[0], t[0], 0, r[1], u[1], t[1], 0, r[2], u[2], t[2], 0, 0, 0, 0, 1 };
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  glMultMatrixd(camera_matrix);
  glTranslated(-camera_eye[0], -camera_eye[1], -camera_eye[2]);

  // Clear window 
  glClearColor(1.0, 1.0, 1.0, 1.0);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  // Set lights
  static GLfloat light0_position[] = { 3.0, 4.0, 5.0, 0.0 };
  glLightfv(GL_LIGHT0, GL_POSITION, light0_position);
  static GLfloat light1_position[] = { -3.0, -2.0, -3.0, 0.0 };
  glLightfv(GL_LIGHT1, GL_POSITION, light1_position);

  // Draw faces
  if (show_faces) {
    glEnable(GL_LIGHTING);
    static GLfloat diffuse[] = { 0.8, 0.8, 0.8, 1.0 };
    static GLfloat specular[] = { 0.2, 0.2, 0.2, 1.0 };
    static GLfloat shininess[] = { 64 };
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, diffuse); 
    glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, specular); 
    glMaterialfv(GL_FRONT_AND_BACK, GL_SHININESS, shininess); 
    for (int i = 0; i < mesh->NFaces(); i++) {
      glBegin(GL_POLYGON);
      R3MeshFace *face = mesh->Face(i);
      const R3Vector& normal = face->plane.Normal();
      glNormal3d(normal[0], normal[1], normal[2]);
      for (int j = 0; j < 3; j++) {
        R3MeshVertex *vertex = face->vertices[j];
        const R3Point& p = vertex->position;
        glVertex3f(p[0], p[1], p[2]);
      }
      glEnd();
    }
  }

  // Draw edges
  if (show_edges) {
    glDisable(GL_LIGHTING);
    glColor3d(0.3, 0.3, 0.3);
    glLineWidth(3);
    for (int i = 0; i < mesh->NFaces(); i++) {
      glBegin(GL_LINE_LOOP);
      R3MeshFace *face = mesh->Face(i);
      for (int j = 0; j < 3; j++) {
        R3MeshVertex *vertex = face->vertices[j];
        const R3Point& p = vertex->position;
        glVertex3f(p[0], p[1], p[2]);
      }
      glEnd();
    }
  }

  // Draw vertices
  if (show_vertices) {
    glDisable(GL_LIGHTING);
    glColor3d(0, 0, 0);
    glPointSize(5);
    glBegin(GL_POINTS);
    for (int i = 0; i < mesh->NFaces(); i++) {
      R3MeshFace *face = mesh->Face(i);
      for (int j = 0; j < 3; j++) {
        R3MeshVertex *vertex = face->vertices[j];
        const R3Point& p = vertex->position;
        glVertex3f(p[0], p[1], p[2]);
      }
    }
    glEnd();
  }

  // Draw vertex IDs
  if (show_ids) {
    glDisable(GL_LIGHTING);
    glColor3d(0, 0, 0);
    for (int i = 0; i < mesh->NFaces(); i++) {
      R3MeshFace *face = mesh->Face(i);
      for (int j = 0; j < 3; j++) {
        char buffer[256];
        R3MeshVertex *vertex = face->vertices[j];
        sprintf(buffer, "%d", vertex->id);
        GLUTDrawText(vertex->position, buffer);
      }
    }
  }

  // Swap buffers 
  glutSwapBuffers();
}    



void GLUTInit(int *argc, char **argv)
{
  // Open window 
  glutInit(argc, argv);
  glutInitWindowPosition(100, 100);
  glutInitWindowSize(GLUTwindow_width, GLUTwindow_height);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH); // | GLUT_STENCIL
  GLUTwindow = glutCreateWindow("OpenGL Viewer");

  // Initialize GLUT callback functions 
  glutReshapeFunc(GLUTResize);
  glutDisplayFunc(GLUTRedraw);
  glutKeyboardFunc(GLUTKeyboard);
  glutSpecialFunc(GLUTSpecial);
  glutMouseFunc(GLUTMouse);
  glutMotionFunc(GLUTMotion);
    
  // Initialize lights 
  static GLfloat lmodel_ambient[] = { 0.2, 0.2, 0.2, 1.0 };
  glLightModelfv(GL_LIGHT_MODEL_AMBIENT, lmodel_ambient);
  glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE);
  static GLfloat light0_diffuse[] = { 1.0, 1.0, 1.0, 1.0 };
  glLightfv(GL_LIGHT0, GL_DIFFUSE, light0_diffuse);
  glEnable(GL_LIGHT0);
  static GLfloat light1_diffuse[] = { 0.5, 0.5, 0.5, 1.0 };
  glLightfv(GL_LIGHT1, GL_DIFFUSE, light1_diffuse);
  glEnable(GL_LIGHT1);
  glEnable(GL_NORMALIZE);
  glEnable(GL_LIGHTING);

  // Initialize graphics modes 
  glEnable(GL_DEPTH_TEST);
}




////////////////////////////////////////////////////////////
// PROGRAM ARGUMENT PARSING
////////////////////////////////////////////////////////////

int 
ParseArgs(int argc, char **argv)
{
  // Innocent until proven guilty
  int print_usage = 0;

  // Parse arguments
  argc--; argv++;
  while (argc > 0) {
    if ((*argv)[0] == '-') {
      if (!strcmp(*argv, "-help")) { print_usage = 1; }
      else if (!strcmp(*argv, "-v")) { print_verbose = 1; }
      else { fprintf(stderr, "Invalid program argument: %s", *argv); exit(1); }
      argv++; argc--;
    }
    else {
      if (!filename) filename = *argv;
      else { fprintf(stderr, "Invalid program argument: %s", *argv); exit(1); }
      argv++; argc--;
    }
  }

  // Check filename
  if (!filename || print_usage) {
    printf("Usage: meshview <filename>\n");
    return 0;
  }

  // Return OK status 
  return 1;
}



////////////////////////////////////////////////////////////
// MESH READING
////////////////////////////////////////////////////////////


R3Mesh *
ReadMesh(const char *filename)
{
  // Allocate mesh
  R3Mesh *mesh = new R3Mesh();
  if (!mesh) {
    fprintf(stderr, "Unable to allocate mesh\n");
    return NULL;
  }

  // Read file
  if (!mesh->Read(filename)) {
    fprintf(stderr, "Unable to read mesh from %s\n", filename);
    return NULL;
  }

  // Print information
  if (print_verbose) {
    R3Box b = mesh->bbox;
    R3Point c = b.Centroid();
    printf("Read %s\n", filename);
    printf("  # Vertices = %d\n", mesh->NVertices());
    printf("  # Faces = %d\n", mesh->NFaces());
    printf("  Box = (%g %g %g) (%g %g %g)\n", b[0][0], b[0][1], b[0][2], b[1][0], b[1][1], b[1][2]);
    printf("  Center = %g %g %g\n", c[0], c[1], c[2]);
  }

  // Return mesh
  return mesh;
}



////////////////////////////////////////////////////////////
// MAIN
////////////////////////////////////////////////////////////

int 
main(int argc, char **argv)
{
  // Initialize GLUT
  GLUTInit(&argc, argv);

  // Parse program arguments
  if (!ParseArgs(argc, argv)) exit(1);

  // Read mesh
  mesh = ReadMesh(filename);
  if (!mesh) exit(-1);

  // Run GLUT interface
  GLUTMainLoop();

  // Return success 
  return 0;
}









