/*  
 * .obj file viewer based on "smooth" by Nate Robins, 1997
 *
 * Brian Paul
 * 1 Oct 2009
 */

#include <math.h>
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
#include <stdarg.h>
#include <glad/glad.h>
#include "glut_wrap.h"
#include "glm.h"
#include "skybox.h"
#include "trackball.h"
#include "shaderutil.h"


static char *Model_file = NULL;		/* name of the obect file */
static GLMmodel *Model;
static GLfloat Scale = 4.0;			/* scaling factor */
static GLboolean Performance = GL_FALSE;
static GLboolean Stats = GL_FALSE;
static GLboolean Animate = GL_TRUE;
static GLuint SkyboxTex;
static GLboolean Skybox = GL_TRUE;
static GLboolean Cull = GL_TRUE;
static GLboolean WireFrame = GL_FALSE;
static GLenum FrontFace = GL_CCW;
static GLfloat Yrot = 0.0;
static GLint WinWidth = 1024, WinHeight = 768;
static GLuint NumInstances = 1;



typedef struct
{
   float CurQuat[4];
   float Distance;
   /* When mouse is moving: */
   GLboolean Rotating, Translating;
   GLint StartX, StartY;
   float StartDistance;
} ViewInfo;

static ViewInfo View;

static void
InitViewInfo(ViewInfo *view)
{
   view->Rotating = GL_FALSE;
   view->Translating = GL_FALSE;
   view->StartX = view->StartY = 0;
   view->Distance = 12.0;
   view->StartDistance = 0.0;
   view->CurQuat[0] = 0.0;
   view->CurQuat[1] = 1.0;
   view->CurQuat[2] = 0.0;
   view->CurQuat[3] = 0.0;
}



/* text: general purpose text routine.  draws a string according to
 * format in a stroke font at x, y after scaling it by the scale
 * specified (scale is in window-space (lower-left origin) pixels).  
 *
 * x      - position in x (in window-space)
 * y      - position in y (in window-space)
 * scale  - scale in pixels
 * format - as in printf()
 */
static void 
text(GLuint x, GLuint y, GLfloat scale, char* format, ...)
{
  va_list args;
  char buffer[255], *p;
  GLfloat font_scale = 119.05 + 33.33;

  va_start(args, format);
  vsprintf(buffer, format, args);
  va_end(args);

  glMatrixMode(GL_PROJECTION);
  glPushMatrix();
  glLoadIdentity();
  gluOrtho2D(0, glutGet(GLUT_WINDOW_WIDTH), 0, glutGet(GLUT_WINDOW_HEIGHT));

  glMatrixMode(GL_MODELVIEW);
  glPushMatrix();
  glLoadIdentity();

  glPushAttrib(GL_ENABLE_BIT);
  glDisable(GL_LIGHTING);
  glDisable(GL_TEXTURE_2D);
  glDisable(GL_DEPTH_TEST);
  glTranslatef(x, y, 0.0);

  glScalef(scale/font_scale, scale/font_scale, scale/font_scale);

  for(p = buffer; *p; p++)
    glutStrokeCharacter(GLUT_STROKE_ROMAN, *p);
  
  glPopAttrib();

  glPopMatrix();
  glMatrixMode(GL_PROJECTION);
  glPopMatrix();
  glMatrixMode(GL_MODELVIEW);
}


static float
ComputeFPS(void)
{
   static double t0 = -1.0;
   static int frames = 0;
   double t = glutGet(GLUT_ELAPSED_TIME) / 1000.0;
   static float fps = 0;

   frames++;

   if (t0 < 0.0) {
      t0 = t;
      fps = 0.0;
   }
   else if (t - t0 >= 4.0) {
      fps = (frames / (t - t0) + 0.5);
      t0 = t;
      frames = 0;
      return fps;
   }

   return 0.0;
}


static void
init_model(void)
{
   /* read in the model */
   Model = glmReadOBJ(Model_file);
   glmUnitize(Model);
   glmFacetNormals(Model);
   if (Model->numnormals == 0) {
      GLfloat smoothing_angle = 90.0;
      printf("Generating normals.\n");
      glmVertexNormals(Model, smoothing_angle);
   }

   glmLoadTextures(Model);
   glmReIndex(Model);
   glmMakeVBOs(Model);
   if (0)
      glmPrint(Model);
}

static void
init_skybox(void)
{
   SkyboxTex = LoadSkyBoxCubeTexture("alpine_east.rgb",
                                     "alpine_west.rgb",
                                     "alpine_up.rgb",
                                     "alpine_down.rgb",
                                     "alpine_south.rgb",
                                     "alpine_north.rgb");
   glmSpecularTexture(Model, SkyboxTex);
}


static void
init_gfx(void)
{
   glEnable(GL_DEPTH_TEST);
   glEnable(GL_CULL_FACE);
   glEnable(GL_NORMALIZE);
   glClearColor(0.3, 0.3, 0.9, 0.0);
}


static void
reshape(int width, int height)
{
   float ar = 0.5 * (float) width / (float) height;

   WinWidth = width;
   WinHeight = height;

   glViewport(0, 0, width, height);

   glMatrixMode(GL_PROJECTION);
   glLoadIdentity();
   glFrustum(-ar, ar, -0.5, 0.5, 1.0, 300.0);
   glMatrixMode(GL_MODELVIEW);
   glLoadIdentity();
   glTranslatef(0.0, 0.0, -3.0);
}


static void
Idle(void)
{
   float q[4];
   trackball(q, 100, 0, 99.99, 0);
   add_quats(q, View.CurQuat, View.CurQuat);

   glutPostRedisplay();
}


static void
display(void)
{
   GLfloat rot[4][4];
   float fps;

   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

   glPushMatrix();
      glTranslatef(0.0, 0.0, -View.Distance);
      glRotatef(Yrot, 0, 1, 0);
      build_rotmatrix(rot, View.CurQuat);
      glMultMatrixf(&rot[0][0]);
      glScalef(Scale, Scale, Scale );

      glUseProgram(0);

      if (Skybox)
         DrawSkyBoxCubeTexture(SkyboxTex);

      if (WireFrame)
         glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
      else
         glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

      if (Cull)
         glEnable(GL_CULL_FACE);
      else
         glDisable(GL_CULL_FACE);

      if (NumInstances == 1) {
         glmDrawVBO(Model);
      }
      else {
         /* draw > 1 instance */
         float dr = 360.0 / NumInstances;
         float r;
         for (r = 0.0; r < 360.0; r += dr) {
            glPushMatrix();
            glRotatef(r, 0, 1, 0);
            glTranslatef(1.4, 0.0, 0.0);
            glmDrawVBO(Model);
            glPopMatrix();
         }
      }

      glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
      glDisable(GL_CULL_FACE);

   glPopMatrix();

   if (Stats) {
      glColor3f(1.0, 1.0, 1.0);
      text(5, glutGet(GLUT_WINDOW_HEIGHT) - (5+20*1), 20, "%s", 
           Model->pathname);
      text(5, glutGet(GLUT_WINDOW_HEIGHT) - (5+20*2), 20, "%d vertices", 
           Model->numvertices);
      text(5, glutGet(GLUT_WINDOW_HEIGHT) - (5+20*3), 20, "%d triangles", 
           Model->numtriangles);
      text(5, glutGet(GLUT_WINDOW_HEIGHT) - (5+20*4), 20, "%d normals", 
           Model->numnormals);
      text(5, glutGet(GLUT_WINDOW_HEIGHT) - (5+20*5), 20, "%d texcoords", 
           Model->numtexcoords);
      text(5, glutGet(GLUT_WINDOW_HEIGHT) - (5+20*6), 20, "%d groups", 
           Model->numgroups);
      text(5, glutGet(GLUT_WINDOW_HEIGHT) - (5+20*7), 20, "%d materials", 
           Model->nummaterials);
   }

   glutSwapBuffers();

   fps = ComputeFPS();
   if (fps) {
      printf("%f FPS\n", fps);
      fflush(stdout);
   }
}


static void
keyboard(unsigned char key, int x, int y)
{
   switch (key) {
   case 'h':
      printf("help\n\n");
      printf("a            -  Toggle animation\n");
      printf("d/D          -  Decrease/Incrase number of models\n");
      printf("w            -  Toggle wireframe/filled\n");
      printf("c            -  Toggle culling\n");
      printf("n            -  Toggle facet/smooth normal\n");
      printf("r            -  Reverse polygon winding\n");
      printf("p            -  Toggle performance indicator\n");
      printf("s            -  Toggle skybox\n");
      printf("z/Z          -  Scale model smaller/larger\n");
      printf("i            -  Show model info/stats\n");
      printf("q/escape     -  Quit\n\n");
      break;
   case 'a':
      Animate = !Animate;
      if (Animate)
         glutIdleFunc(Idle);
      else
         glutIdleFunc(NULL);
      break;
   case 'd':
      if (NumInstances > 1)
         NumInstances--;
      break;
   case 'D':
      NumInstances++;
      break;
   case 'i':
      Stats = !Stats;
      break;
   case 'p':
      Performance = !Performance;
      break;
   case 'w':
      WireFrame = !WireFrame;
      break;
   case 'c':
      Cull = !Cull;
      printf("Polygon culling: %d\n", Cull);
      break;
   case 'r':
      if (FrontFace == GL_CCW)
         FrontFace = GL_CW;
      else
         FrontFace = GL_CCW;
      glFrontFace(FrontFace);
      printf("Front face:: %s\n", FrontFace == GL_CCW ? "CCW" : "CW");
      break;
   case 's':
      Skybox = !Skybox;
      if (Skybox)
         glmSpecularTexture(Model, SkyboxTex);
      else
         glmSpecularTexture(Model, 0);
      break;
   case 'z':
      Scale *= 0.9;
      break;
   case 'Z':
      Scale *= 1.1;
      break;
   case 'q':
   case 27:
      exit(0);
      break;
   }

   glutPostRedisplay();
}


static void
menu(int item)
{
    keyboard((unsigned char)item, 0, 0);
}


/**
 * Handle mouse button.
 */
static void
Mouse(int button, int state, int x, int y)
{
   if (button == GLUT_LEFT_BUTTON) {
      if (state == GLUT_DOWN) {
         View.StartX = x;
         View.StartY = y;
         View.Rotating = GL_TRUE;
      }
      else if (state == GLUT_UP) {
         View.Rotating = GL_FALSE;
      }
   }
   else if (button == GLUT_MIDDLE_BUTTON) {
      if (state == GLUT_DOWN) {
         View.StartX = x;
         View.StartY = y;
         View.StartDistance = View.Distance;
         View.Translating = GL_TRUE;
      }
      else if (state == GLUT_UP) {
         View.Translating = GL_FALSE;
      }
   }
}


/**
 * Handle mouse motion
 */
static void
Motion(int x, int y)
{
   int i;
   if (View.Rotating) {
      float x0 = (2.0 * View.StartX - WinWidth) / WinWidth;
      float y0 = (WinHeight - 2.0 * View.StartY) / WinHeight;
      float x1 = (2.0 * x - WinWidth) / WinWidth;
      float y1 = (WinHeight - 2.0 * y) / WinHeight;
      float q[4];

      trackball(q, x0, y0, x1, y1);
      View.StartX = x;
      View.StartY = y;
      for (i = 0; i < 1; i++)
         add_quats(q, View.CurQuat, View.CurQuat);

      glutPostRedisplay();
   }
   else if (View.Translating) {
      float dz = 0.02 * (y - View.StartY);
      View.Distance = View.StartDistance + dz;
      glutPostRedisplay();
   }
}


static void
DoFeatureChecks(void)
{
   if (!GLAD_GL_VERSION_2_0) {
      /* check for individual extensions */
      if (!GLAD_GL_ARB_texture_cube_map) {
         printf("Sorry, GL_ARB_texture_cube_map is required.\n");
         exit(1);
      }
      if (!GLAD_GL_ARB_vertex_shader) {
         printf("Sorry, GL_ARB_vertex_shader is required.\n");
         exit(1);
      }
      if (!GLAD_GL_ARB_fragment_shader) {
         printf("Sorry, GL_ARB_fragment_shader is required.\n");
         exit(1);
      }
      if (!GLAD_GL_ARB_vertex_buffer_object) {
         printf("Sorry, GL_ARB_vertex_buffer_object is required.\n");
         exit(1);
      }
   }
   if (!ShadersSupported()) {
      printf("Sorry, GLSL is required\n");
      exit(1);
   }
}


int
main(int argc, char** argv)
{
   glutInitWindowSize(WinWidth, WinHeight);
   glutInit(&argc, argv);

   if (argc > 1) {
      Model_file = argv[1];
   }
   if (!Model_file) {
      fprintf(stderr, "usage: objview file.obj\n");
      fprintf(stderr, "(using default bunny.obj)\n");
      Model_file = "bunny.obj";
   }

   glutInitDisplayMode(GLUT_RGB | GLUT_DEPTH | GLUT_DOUBLE);
   glutCreateWindow("objview");

   gladLoadGL();

   DoFeatureChecks();

   glutReshapeFunc(reshape);
   glutDisplayFunc(display);
   glutKeyboardFunc(keyboard);
   glutMouseFunc(Mouse);
   glutMotionFunc(Motion);
   if (Animate)
      glutIdleFunc(Idle);

   glutCreateMenu(menu);
   glutAddMenuEntry("[a] Toggle animate", 'a');
   glutAddMenuEntry("[d] Fewer models", 'd');
   glutAddMenuEntry("[D] More models", 'D');
   glutAddMenuEntry("[w] Toggle wireframe/filled", 'w');
   glutAddMenuEntry("[c] Toggle culling on/off", 'c');
   glutAddMenuEntry("[r] Reverse polygon winding", 'r');
   glutAddMenuEntry("[z] Scale model smaller", 'z');
   glutAddMenuEntry("[Z] Scale model larger", 'Z');
   glutAddMenuEntry("[p] Toggle performance indicator", 'p');
   glutAddMenuEntry("[i] Show model stats", 'i');
   glutAddMenuEntry("", 0);
   glutAddMenuEntry("[q] Quit", 27);
   glutAttachMenu(GLUT_RIGHT_BUTTON);

   InitViewInfo(&View);

   init_model();
   init_skybox();
   init_gfx();

   glutMainLoop();

   return 0;
}
