/* */

#define GL_GLEXT_PROTOTYPES

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <glad/glad.h>
#include "glm.h"
#include "readtex.h"
#include "shaderutil.h"


/* defines */
#define T(x) model->triangles[(x)]


/* glmDraw: Renders the model to the current OpenGL context using the
 * mode specified.
 *
 * model    - initialized GLMmodel structure
 * mode     - a bitwise OR of values describing what is to be rendered.
 *            GLM_NONE     -  render with only vertices
 *            GLM_FLAT     -  render with facet normals
 *            GLM_SMOOTH   -  render with vertex normals
 *            GLM_TEXTURE  -  render with texture coords
 *            GLM_COLOR    -  render with colors (color material)
 *            GLM_MATERIAL -  render with materials
 *            GLM_COLOR and GLM_MATERIAL should not both be specified.  
 *            GLM_FLAT and GLM_SMOOTH should not both be specified.  
 */
GLvoid
glmDraw(GLMmodel* model, GLuint mode)
{
  GLuint i;
  GLMgroup* group;

  assert(model);
  assert(model->vertices);

  /* do a bit of warning */
  if (mode & GLM_FLAT && !model->facetnorms) {
    printf("glmDraw() warning: flat render mode requested "
	   "with no facet normals defined.\n");
    mode &= ~GLM_FLAT;
  }
  if (mode & GLM_SMOOTH && !model->normals) {
    printf("glmDraw() warning: smooth render mode requested "
	   "with no normals defined.\n");
    mode &= ~GLM_SMOOTH;
  }
  if (mode & GLM_TEXTURE && !model->texcoords) {
    printf("glmDraw() warning: texture render mode requested "
	   "with no texture coordinates defined.\n");
    mode &= ~GLM_TEXTURE;
  }
  if (mode & GLM_FLAT && mode & GLM_SMOOTH) {
    printf("glmDraw() warning: flat render mode requested "
	   "and smooth render mode requested (using smooth).\n");
    mode &= ~GLM_FLAT;
  }
  if (mode & GLM_COLOR && !model->materials) {
    printf("glmDraw() warning: color render mode requested "
	   "with no materials defined.\n");
    mode &= ~GLM_COLOR;
  }
  if (mode & GLM_MATERIAL && !model->materials) {
    printf("glmDraw() warning: material render mode requested "
	   "with no materials defined.\n");
    mode &= ~GLM_MATERIAL;
  }
  if (mode & GLM_COLOR && mode & GLM_MATERIAL) {
    printf("glmDraw() warning: color and material render mode requested "
	   "using only material mode\n");
    mode &= ~GLM_COLOR;
  }
  if (mode & GLM_COLOR)
    glEnable(GL_COLOR_MATERIAL);
  if (mode & GLM_MATERIAL)
    glDisable(GL_COLOR_MATERIAL);

  glPushMatrix();
  glTranslatef(model->position[0], model->position[1], model->position[2]);

  glBegin(GL_TRIANGLES);
  group = model->groups;
  while (group) {
    if (mode & GLM_MATERIAL) {
      glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, 
		   model->materials[group->material].ambient);
      glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, 
		   model->materials[group->material].diffuse);
      glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, 
		   model->materials[group->material].specular);
       glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 
  		  model->materials[group->material].shininess);
    }

    if (mode & GLM_COLOR) {
      glColor3fv(model->materials[group->material].diffuse);
    }

    for (i = 0; i < group->numtriangles; i++) {
      if (mode & GLM_FLAT)
	glNormal3fv(&model->facetnorms[3 * T(group->triangles[i]).findex]);
      
      if (mode & GLM_SMOOTH)
	glNormal3fv(&model->normals[3 * T(group->triangles[i]).nindices[0]]);
      if (mode & GLM_TEXTURE)
	glTexCoord2fv(&model->texcoords[2*T(group->triangles[i]).tindices[0]]);
      glVertex3fv(&model->vertices[3 * T(group->triangles[i]).vindices[0]]);
#if 0
      printf("%f %f %f\n", 
	     model->vertices[3 * T(group->triangles[i]).vindices[0] + X],
	     model->vertices[3 * T(group->triangles[i]).vindices[0] + Y],
	     model->vertices[3 * T(group->triangles[i]).vindices[0] + Z]);
#endif
      
      if (mode & GLM_SMOOTH)
	glNormal3fv(&model->normals[3 * T(group->triangles[i]).nindices[1]]);
      if (mode & GLM_TEXTURE)
	glTexCoord2fv(&model->texcoords[2*T(group->triangles[i]).tindices[1]]);
      glVertex3fv(&model->vertices[3 * T(group->triangles[i]).vindices[1]]);
#if 0
      printf("%f %f %f\n", 
	     model->vertices[3 * T(group->triangles[i]).vindices[1] + X],
	     model->vertices[3 * T(group->triangles[i]).vindices[1] + Y],
	     model->vertices[3 * T(group->triangles[i]).vindices[1] + Z]);
#endif
      
      if (mode & GLM_SMOOTH)
	glNormal3fv(&model->normals[3 * T(group->triangles[i]).nindices[2]]);
      if (mode & GLM_TEXTURE)
	glTexCoord2fv(&model->texcoords[2*T(group->triangles[i]).tindices[2]]);
      glVertex3fv(&model->vertices[3 * T(group->triangles[i]).vindices[2]]);
#if 0
      printf("%f %f %f\n", 
	     model->vertices[3 * T(group->triangles[i]).vindices[2] + X],
	     model->vertices[3 * T(group->triangles[i]).vindices[2] + Y],
	     model->vertices[3 * T(group->triangles[i]).vindices[2] + Z]);
#endif
      
    }
    
    group = group->next;
  }
  glEnd();

  glPopMatrix();
}


void
glmMakeVBOs(GLMmodel *model)
{
   uint bytes, vertexFloats, i;
   float *buffer;
   GLMgroup* group;
   unsigned totalIndexes;
   GLubyte *ibuffer, *ib;

   /*
    * Vertex data
    */
   vertexFloats = 3;
   model->posOffset = 0;

   if (model->numnormals > 0) {
      assert(model->numnormals == model->numvertices);
      model->normOffset = vertexFloats * sizeof(GLfloat);
      vertexFloats += 3;
   }      

   if (model->numtexcoords > 0) {
      assert(model->numtexcoords == model->numvertices);
      model->texOffset = vertexFloats * sizeof(GLfloat);
      vertexFloats += 2;
   }

   model->vertexSize = vertexFloats;

   bytes = (model->numvertices + 1) * vertexFloats * sizeof(float);

   buffer = (float *) malloc(bytes);
   for (i = 0; i < model->numvertices; i++) {
      /* copy vertex pos */
      uint j = 0;
      buffer[i * vertexFloats + j++] = model->vertices[i * 3 + 0];
      buffer[i * vertexFloats + j++] = model->vertices[i * 3 + 1];
      buffer[i * vertexFloats + j++] = model->vertices[i * 3 + 2];
      if (model->numnormals > 0) {
         buffer[i * vertexFloats + j++] = model->normals[i * 3 + 0];
         buffer[i * vertexFloats + j++] = model->normals[i * 3 + 1];
         buffer[i * vertexFloats + j++] = model->normals[i * 3 + 2];
      }
      if (model->numtexcoords > 0) {
         buffer[i * vertexFloats + j++] = model->texcoords[i * 2 + 0];
         buffer[i * vertexFloats + j++] = model->texcoords[i * 2 + 1];
      }
   }

   glGenBuffersARB(1, &model->vbo);
   glBindBufferARB(GL_ARRAY_BUFFER_ARB, model->vbo);
   glBufferDataARB(GL_ARRAY_BUFFER_ARB, bytes, buffer, GL_STATIC_DRAW_ARB);
   glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);

   free(buffer);

   /*
    * Index data
    */
   totalIndexes = 0;
   for (group = model->groups; group; group = group->next) {
      if (group->numtriangles > 0) {
         totalIndexes += 3 * group->numtriangles;
      }
   }

   glGenBuffersARB(1, &model->index_vbo);
   glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, model->index_vbo);
   glBufferDataARB(GL_ELEMENT_ARRAY_BUFFER_ARB, totalIndexes * sizeof(GLuint),
                   NULL, GL_STATIC_DRAW_ARB);
   ibuffer = (GLubyte *) glMapBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB,
                                        GL_WRITE_ONLY);
   ib = ibuffer;

   for (group = model->groups; group; group = group->next) {
      if (group->numtriangles > 0) {
         int bytes = 3 * group->numtriangles * sizeof(uint);
         memcpy(ib, group->triIndexes, bytes);
         totalIndexes += 3 * group->numtriangles;
         group->indexVboOffset = ib - ibuffer;
         ib += bytes;
      }
   }

   glUnmapBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB);

   glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);
}


static void
_glmLoadTexture(GLMmaterial *mat)
{
  if (mat->map_kd) {
     GLint imgWidth, imgHeight;
     GLenum imgFormat;
     GLubyte *image = NULL;
     
     glGenTextures(1, &mat->texture_kd);

     image = LoadRGBImage( mat->map_kd, &imgWidth, &imgHeight, &imgFormat );
     if (!image) {
        /*fprintf(stderr, "Couldn't open texture %s\n", mat->map_kd);*/
        free(mat->map_kd);
        mat->map_kd = NULL;
        mat->texture_kd = 0;
        return;
     }
     if (0)
        printf("load texture %s %d x %d\n", mat->map_kd, imgWidth, imgHeight);

     glBindTexture(GL_TEXTURE_2D, mat->texture_kd);
     gluBuild2DMipmaps(GL_TEXTURE_2D, 3, imgWidth, imgHeight,
                       imgFormat, GL_UNSIGNED_BYTE, image);
     free(image);

     glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT );
     glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT );
     glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR );
     glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
  }
}

void
glmLoadTextures(GLMmodel *model)
{
   uint i;

   for (i = 0; i < model->nummaterials; i++) {
      GLMmaterial *mat = &model->materials[i];
      _glmLoadTexture(mat);
   }
}


void
glmDrawVBO(GLMmodel *model)
{
   GLMgroup* group;
   uint prevMaterial = ~0;

   assert(model->vbo);

   glBindBufferARB(GL_ARRAY_BUFFER_ARB, model->vbo);

   glVertexPointer(3, GL_FLOAT, model->vertexSize * sizeof(float),
                   (const void *) (size_t) model->posOffset);
   glEnableClientState(GL_VERTEX_ARRAY);

   if (model->numnormals > 0) {
      glNormalPointer(GL_FLOAT, model->vertexSize * sizeof(float),
                      (const void *) (size_t) model->normOffset);
      glEnableClientState(GL_NORMAL_ARRAY);
   }

   if (model->numtexcoords > 0) {
      glTexCoordPointer(2, GL_FLOAT, model->vertexSize * sizeof(float),
                        (const void *) (size_t) model->texOffset);
      glEnableClientState(GL_TEXTURE_COORD_ARRAY);
   }

   glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, model->index_vbo);

   glPushMatrix();
   glTranslatef(model->position[0], model->position[1], model->position[2]);
   glScalef(model->scale, model->scale, model->scale);

   for (group = model->groups; group; group = group->next) {
      if (group->numtriangles > 0) {

         if (group->material != prevMaterial) {
            glmShaderMaterial(&model->materials[group->material]);
            prevMaterial = group->material;
         }

         glDrawRangeElements(GL_TRIANGLES,
                             group->minIndex, group->maxIndex,
                             3 * group->numtriangles,
                             GL_UNSIGNED_INT,
                             (void *) (GLintptr) group->indexVboOffset);
      }
   }

   glPopMatrix();

   glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);
   glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);
   glDisableClientState(GL_VERTEX_ARRAY);
   glDisableClientState(GL_NORMAL_ARRAY);
   glDisableClientState(GL_TEXTURE_COORD_ARRAY);
}



/* glmList: Generates and returns a display list for the model using
 * the mode specified.
 *
 * model    - initialized GLMmodel structure
 * mode     - a bitwise OR of values describing what is to be rendered.
 *            GLM_NONE     -  render with only vertices
 *            GLM_FLAT     -  render with facet normals
 *            GLM_SMOOTH   -  render with vertex normals
 *            GLM_TEXTURE  -  render with texture coords
 *            GLM_COLOR    -  render with colors (color material)
 *            GLM_MATERIAL -  render with materials
 *            GLM_COLOR and GLM_MATERIAL should not both be specified.  
 *            GLM_FLAT and GLM_SMOOTH should not both be specified.  
 */
GLuint
glmList(GLMmodel* model, GLuint mode)
{
  GLuint list;

  list = glGenLists(1);
  glNewList(list, GL_COMPILE);
  glmDraw(model, mode);
  glEndList();

  return list;
}



static const char *VertexShader =
   "varying vec3 normal; \n"
   "void main() { \n"
   "   gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; \n"
   "   normal = gl_NormalMatrix * gl_Normal; \n"
   "   gl_TexCoord[0] = gl_MultiTexCoord0; \n"
   "} \n";

/**
 * Two %s substitutions:
 *   diffuse texture? true/false
 *   specular texture? true/false
 */
static const char *TexFragmentShader =
   "uniform vec4 ambient, diffuse, specular; \n"
   "uniform vec4 ambientLight, diffuseLight, specularLight; \n"
   "uniform float shininess; \n"
   "uniform sampler2D diffTex; \n"
   "uniform samplerCube specTex; \n"
   "varying vec3 normal; \n"
   "\n"
   "void main() \n"
   "{ \n"
   "   vec4 diffTerm, specTerm; \n"
   "   float dotProd = max(dot(gl_LightSource[0].position.xyz, \n"
   "                           normalize(normal)), 0.0);\n"
   "   float dotProd2 = max(dot(-gl_LightSource[0].position.xyz, \n"
   "                           normalize(normal)), 0.0);\n"
   "   dotProd += dotProd2; \n"
   " \n"
   "   diffTerm = diffuse * diffuseLight * dotProd; \n"
   "   if (%s) \n"
   "      diffTerm *= texture2D(diffTex, gl_TexCoord[0].st); \n"
   " \n"
   "   specTerm = specular * specularLight * pow(dotProd, shininess); \n"
   "   if (%s) \n"
   "      specTerm *= textureCube(specTex, normal); \n"
   " \n"
   "   gl_FragColor = ambient * ambientLight + diffTerm + specTerm; \n"
   "} \n";


void
glmShaderMaterial(GLMmaterial *mat)
{
   static const float ambientLight[4] = { 0.1, 0.1, 0.1, 0.0 };
   static const float diffuseLight[4] = { 0.75, 0.75, 0.75, 1.0 };
   static const float specularLight[4] = { 1.0, 1.0, 1.0, 0.0 };

   if (!mat->prog) {
      /* make shader now */
      char newShader[10000];
      GLuint vs, fs;
      const char *diffuseTex = mat->texture_kd ? "true" : "false";
      const char *specularTex = mat->texture_ks ? "true" : "false";
      GLint uAmbientLight, uDiffuseLight, uSpecularLight;

      /* replace %d with 0 or 1 */
      sprintf(newShader, TexFragmentShader, diffuseTex, specularTex);
      if (0)
         printf("===== new shader =====\n%s\n============\n", newShader);

      vs = CompileShaderText(GL_VERTEX_SHADER, VertexShader);
      fs = CompileShaderText(GL_FRAGMENT_SHADER, newShader);
      mat->prog = LinkShaders(vs, fs);
      assert(mat->prog);

      glUseProgram(mat->prog);

      mat->uAmbient = glGetUniformLocation(mat->prog, "ambient");
      mat->uDiffuse = glGetUniformLocation(mat->prog, "diffuse");
      mat->uSpecular = glGetUniformLocation(mat->prog, "specular");
      mat->uShininess = glGetUniformLocation(mat->prog, "shininess");
      mat->uDiffTex = glGetUniformLocation(mat->prog, "diffTex");
      mat->uSpecTex = glGetUniformLocation(mat->prog, "specTex");

      uAmbientLight = glGetUniformLocation(mat->prog, "ambientLight");
      uDiffuseLight = glGetUniformLocation(mat->prog, "diffuseLight");
      uSpecularLight = glGetUniformLocation(mat->prog, "specularLight");

      glUniform4fv(mat->uAmbient, 1, mat->ambient);
      glUniform4fv(mat->uDiffuse, 1, mat->diffuse);
      glUniform4fv(mat->uSpecular, 1, mat->specular);
      glUniform1f(mat->uShininess, mat->shininess);
      glUniform1i(mat->uDiffTex, 0);
      glUniform1i(mat->uSpecTex, 1);

      glUniform4fv(uAmbientLight, 1, ambientLight);
      glUniform4fv(uDiffuseLight, 1, diffuseLight);
      glUniform4fv(uSpecularLight, 1, specularLight);
   }

   glActiveTexture(GL_TEXTURE1);
   if (mat->texture_ks)
      glBindTexture(GL_TEXTURE_CUBE_MAP, mat->texture_ks);
   else
      glBindTexture(GL_TEXTURE_CUBE_MAP, 0);

   glActiveTexture(GL_TEXTURE0);
   if (mat->texture_kd)
      glBindTexture(GL_TEXTURE_2D, mat->texture_kd);
   else
      glBindTexture(GL_TEXTURE_2D, 0);

   if (mat->diffuse[3] < 1.0) {
      glEnable(GL_BLEND);
      glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
   }
   else {
      glDisable(GL_BLEND);
   }

   glUseProgram(mat->prog);
}


void
glmSpecularTexture(GLMmodel *model, uint cubeTex)
{
   uint i;

   for (i = 0; i < model->nummaterials; i++) {
      model->materials[i].texture_ks = cubeTex;
   }
}
