/* oglrenderer.C: OpenGL VRML renderer */

#include <stdio.h>
#include "oglrenderer.H"

#include "Grouping.H"
#include "Transforming.H"
#include "Shape.H"
#include "Appearance.H"
#include "Material.H"
#include "TextureTransform.H"
#include "Texture.H"
#include "ImageTexture.H"
#include "PixelTexture.H"
#include "Geometry.H"
#include "ElevationGrid.H"
#include "IndexedFaceSet.H"
#include "DirectionalLight.H"
#include "PointLight.H"
#include "SpotLight.H"

#include "nodeCatalog.H"

#include <GL/gl.h>
#include <GL/glx.h>
#include <GL/glu.h>
#include <unistd.h>

#include "render.h"
#include "file.H"

// use the integer user data field for associating a display list ID with the node
#define DLISTID(node)	(node->data.i)
#define LIGHTID(node)	(GLenum)(GL_LIGHT0 + node->data.i) // for lights

static int max_lights;

void opengl_renderer::begin_frame(class world* world)
{
  // determine maximum number of lights
  glGetIntegerv(GL_MAX_LIGHTS, &max_lights);

  // set default material
  begin_Material(dynamic_cast<Material*>(builtin_nodes->lookup("Material")));
  views.size = 0;  	// will be read again
  nrlights = 0;

  texture_active = lighting_active = false;
  current_texture_components = 0;
  current_alpha = 1.0;

  // define and enable headlight is required.
  ninfo = world->stacks->navigationInfo.top();
  if (enable_headlight) {
    GLfloat pos[4] = {0., 0., 1., 0.};	// directional light shines along -Z
    GLfloat col[4] = {1., 1., 1., 1.};

    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();
    glLightfv(GL_LIGHT0, GL_POSITION, pos);
    glLightfv(GL_LIGHT0, GL_AMBIENT, col);
    glLightfv(GL_LIGHT0, GL_DIFFUSE, col);
    glLightfv(GL_LIGHT0, GL_SPECULAR, col);
    glPopMatrix();

    glEnable(GL_LIGHT0);
    nrlights = 1;
  }

  glMatrixMode(GL_TEXTURE);
  glLoadIdentity();
  glMatrixMode(GL_MODELVIEW);

  glColorMaterial(GL_FRONT_AND_BACK, GL_DIFFUSE);
#ifdef GL_VERSION_1_2
  glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL, GL_SEPARATE_SPECULAR_COLOR);
#endif
  glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, 1);
  glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 1);
  GLfloat Black[] = { 0., 0., 0., 1. };
  glLightModelfv(GL_LIGHT_MODEL_AMBIENT, Black);

  glEnable(GL_BLEND);
  glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
}

void opengl_renderer::end_frame(class world*)
{
  use_dlists = renderopts.use_display_lists;
}

void opengl_renderer::worldInfo(WorldInfo *info)
{
  if (!winfo) winfo = info;
}

void opengl_renderer::viewpoint(Viewpoint *vp)
{
  views.append(vp);
}

void opengl_renderer::navigationInfo(class NavigationInfo *info)
{
  // nothing to do
}

bool opengl_renderer::node_has_changed(SFNode* node)
{
  return (node->is_updated() || first_frame);
}

void opengl_renderer::begin_Grouping(Grouping *group)
{
  if (node_has_changed(group)) {
    if (group->bboxSize.x >= 0. || group->bboxSize.y >= 0. || group->bboxSize.z >= 0.) {
      gmin = (Vec3)(group->bboxCenter) - (Vec3)(group->bboxSize)/(float)2.;
      gmax = (Vec3)(group->bboxCenter) + (Vec3)(group->bboxSize)/(float)2.;      
    } else {
      gmin = Vec3(HUGE, HUGE, HUGE);
      gmax = Vec3(-HUGE, -HUGE, -HUGE);
    }
  }
}

void opengl_renderer::end_Grouping(Grouping *group)
{
  if (node_has_changed(group)) {
    group->bboxCenter = ((gmin + gmax)/(float)2.);
    group->bboxSize = (gmax - gmin);
    group->clear_update();
  }
}

void opengl_renderer::begin_Transforming(Transforming *xf)
{
  renderer::begin_Transforming(xf);
  glMatrixMode(GL_MODELVIEW);
  glPushMatrix();
  glMultMatrixf((const GLfloat *)(xf->xf).m);
}

void opengl_renderer::end_Transforming(Transforming *xf)
{
  glMatrixMode(GL_MODELVIEW);
  glPopMatrix();
  renderer::end_Transforming(xf);
}

void opengl_renderer::begin_Shape(Shape *shape)
{
  glDisable(GL_LIGHTING);
  lighting_active = false;

  GLfloat white[4] = {1., 1., 1., 1.};
  glColor4fv(white);
  current_alpha = 1.;
}

// called after begin_Texture (if there is a texture)
void opengl_renderer::begin_Material(Material *m)
{
  if (current_texture_components != 2 && current_texture_components != 4) {
    // texture without alpha channel: set material alpha. For textures with
    // alpha channel, the texture alpha shall be used instead. With modulated
    // textures, this means that we should keep the previously initialised value
    // 1 for the alpha 
    current_alpha = 1. - m->transparency;
  }

  GLfloat diffuse[4]; diffuse[3] = current_alpha;
  //#ifdef NEVER
  if (current_texture_components == 3 || current_texture_components == 4) {
    // diffuse material color of RGB and RGBA textures shall be texture color.
    // With modulated textures, this means that we should set the diffuse
    // material color to 1.
    diffuse[0] = diffuse[1] = diffuse[2] = 1.;
  } else {
    //#endif
    diffuse[0] = m->diffuseColor.r;
    diffuse[1] = m->diffuseColor.g;
    diffuse[2] = m->diffuseColor.b;
    //#ifdef NEVER
  }
  //#endif

  GLfloat ambient[] = {
    diffuse[0] * m->ambientIntensity,
    diffuse[1] * m->ambientIntensity,
    diffuse[2] * m->ambientIntensity,
    current_alpha
  };
  GLfloat specular[] = {
    m->specularColor.r, 
    m->specularColor.g, 
    m->specularColor.b, 
    current_alpha
  };
  GLfloat emission[] = {
    m->emissiveColor.r, 
    m->emissiveColor.g, 
    m->emissiveColor.b, 
    current_alpha
  };
  glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, ambient);
  glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, diffuse);
  glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, specular);
  glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, emission);
  glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, m->shininess*128. > 127. ? 127. : m->shininess*128.);

  if (!renderopts.disable_lighting) {
    glEnable(GL_LIGHTING);
    lighting_active = true;
  }

  glColor4fv(diffuse);
  glDisable(GL_COLOR_MATERIAL);
}

void opengl_renderer::end_Material(Material *)
{
  // nothing to do.
}

void opengl_renderer::begin_TextureTransform(TextureTransform* txf)
{
  glMatrixMode(GL_TEXTURE);
  glLoadMatrixf((const GLfloat *)(txf->xf).m);
  glMatrixMode(GL_MODELVIEW);
}

void opengl_renderer::end_TextureTransform(TextureTransform*)
{
  glMatrixMode(GL_TEXTURE);
  glLoadIdentity();
  glMatrixMode(GL_MODELVIEW);
}

int opengl_renderer::load_Texture(int width, int height, int numcomponents, unsigned char* image, bool repeatS, bool repeatT)
{
  // create a texture object for the image texture
  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

  GLuint texid;
  glGenTextures(1, &texid);
  glBindTexture(GL_TEXTURE_2D, texid);

  if (repeatS)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
  if (repeatT)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
 
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

  GLint formats[5] = { 0, GL_LUMINANCE, GL_LUMINANCE_ALPHA, GL_RGB, GL_RGBA };
  current_texture_components = numcomponents;

#ifdef NO_MIPMAPPING
  // TODO: use gluScaleImage if image width or height is not a power of 2.
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexImage2D(GL_TEXTURE_2D, 0, numcomponents, width, height, 0, formats[numcomponents], GL_UNSIGNED_BYTE, image);
#endif /*NO_MIPMAPPING*/

  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
  gluBuild2DMipmaps(GL_TEXTURE_2D, numcomponents, width, height, formats[numcomponents], GL_UNSIGNED_BYTE, image);

  return texid;
}

void opengl_renderer::begin_Texture(Texture* t)
{
  if (disable_textures) return;

  if (t->data.i == 0) {
    if (!t->Map)  // read the texture image if not done so before
      t->load();

    if (t->Map)   // succesfully read the image, create OpenGL texture object
      t->data.i = load_Texture(t->Width, t->Height, t->Channels, t->Map, t->repeatS, t->repeatT);
    else
      t->data.i = -1;  // indicates a problem loading the texture
  }

  if (t->data.i > 0) {
    current_texture_components = t->Channels;
    // make texture object current and enable texturing
    glEnable(GL_TEXTURE_2D);
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, current_texture_components < 3 ? GL_MODULATE : GL_MODULATE);
    glBindTexture(GL_TEXTURE_2D, t->data.i);
    texture_active = true;
  }
}

void opengl_renderer::end_Texture(Texture* t)
{
  if (t->data.i <= 0 || disable_textures)
    return;

  // disable texturing
  glDisable(GL_TEXTURE_2D);
  texture_active = false;
  current_texture_components = 0;
}

void opengl_renderer::begin_LightSource(LightSource *l)
{
  if (!l->on || nrlights>=max_lights)
    return;

  /* LIGHTID(l) */ l->data.i = nrlights;
  nrlights++;

  GLfloat amb[] = { 
    l->ambientIntensity * l->color.r, 
    l->ambientIntensity * l->color.g, 
    l->ambientIntensity * l->color.b, 
    1.
  };
  GLfloat diff[] = {
    l->intensity * l->color.r, 
    l->intensity * l->color.g, 
    l->intensity * l->color.b, 
    1.
  };
  GLfloat spec[] = {
    l->intensity * l->color.r, 
    l->intensity * l->color.g, 
    l->intensity * l->color.b, 
    1.
  };
  glLightfv(LIGHTID(l), GL_AMBIENT, amb);
  glLightfv(LIGHTID(l), GL_DIFFUSE, diff);
  glLightfv(LIGHTID(l), GL_SPECULAR, spec);

  glEnable(LIGHTID(l));
}

void opengl_renderer::end_LightSource(LightSource *l)
{
  if (!l->on || nrlights>=max_lights)
    return;

  nrlights--;
  glDisable(LIGHTID(l));
}

void opengl_renderer::begin_DirectionalLight(DirectionalLight *l)
{
  if (!l->on || nrlights>=max_lights) 
    return;
  begin_LightSource(l);

  GLfloat dir[4] = { -l->direction.x, -l->direction.y, -l->direction.z, 0.0 };
  glLightfv(LIGHTID(l), GL_POSITION, dir);
}

void opengl_renderer::begin_PointLight(PointLight *l)
{
  if (!l->on || nrlights>=max_lights) 
    return;
  begin_LightSource(l);

  GLfloat pos[4] = { l->location.x, l->location.y, l->location.z, 1.0 };
  glLightfv(LIGHTID(l), GL_POSITION, pos);

  glLightf(LIGHTID(l), GL_CONSTANT_ATTENUATION, l->attenuation.x);
  glLightf(LIGHTID(l), GL_LINEAR_ATTENUATION, l->attenuation.y);
  glLightf(LIGHTID(l), GL_QUADRATIC_ATTENUATION, l->attenuation.z);
}

void opengl_renderer::begin_SpotLight(SpotLight *l)
{
  if (!l->on || nrlights>=max_lights) 
    return;
  begin_LightSource(l);

  GLfloat pos[4] = { l->location.x, l->location.y, l->location.z, 1.0 };
  glLightfv(LIGHTID(l), GL_POSITION, pos);

  glLightf(LIGHTID(l), GL_CONSTANT_ATTENUATION, l->attenuation.x);
  glLightf(LIGHTID(l), GL_LINEAR_ATTENUATION, l->attenuation.y);
  glLightf(LIGHTID(l), GL_QUADRATIC_ATTENUATION, l->attenuation.z);

  GLfloat dir[3] = { l->direction.x, l->direction.y, l->direction.z };
  glLightfv(LIGHTID(l), GL_SPOT_DIRECTION, dir);

  GLfloat cutoff = (l->cutOffAngle > M_PI/2.) ? (M_PI/2.) : (double)l->cutOffAngle;
  glLightf(LIGHTID(l), GL_SPOT_CUTOFF, cutoff * 180. / M_PI);

  GLfloat expon = 0.0;
  if (l->beamWidth < cutoff - 1e-3 && l->beamWidth > 1e-3)
    // exponent is such that we have half intensity at beamWidth angle
    expon = log(0.5) / log(cos(l->beamWidth));
  glLightf(LIGHTID(l), GL_SPOT_EXPONENT, (expon > 127) ? 127 : expon);
}

void opengl_renderer::geometry(Geometry *geom)
{
  renderer::normals_required = lighting_active;
  renderer::texcoords_required = texture_active;

  // per-vertex or per-face colors are only relevant if there is
  // no texture or the texture is a luminance texture or luminance+alpha
  // texture. For RGB and RGBA textures, the texture color replaces
  // the fragment color.
  renderer::colors_required = current_texture_components < 3;

  //  if (texture_active && lighting_active) {
  //  glEnable(GL_COLOR_MATERIAL);
  // }
#ifdef NEVER
  cerr << __FILE__ << ":" << __LINE__ << ": "
       << "lighting = " << (glIsEnabled(GL_LIGHTING) ? "ON" : "OFF") << ", "
       << "light 0 = " << (glIsEnabled(GL_LIGHT0) ? "ON" : "OFF") << ", "
       << "light 1 = " << (glIsEnabled(GL_LIGHT1) ? "ON" : "OFF") << ", "
       << "light 2 = " << (glIsEnabled(GL_LIGHT2) ? "ON" : "OFF") << ", "
       << "light 3 = " << (glIsEnabled(GL_LIGHT3) ? "ON" : "OFF") << "\n";
#endif
  if (node_has_changed(geom) || use_dlists != renderopts.use_display_lists) {
    geom->clear_update();

    if (renderopts.use_display_lists) {
      if (DLISTID(geom) > 0)
	glDeleteLists(DLISTID(geom), 1);

      DLISTID(geom) = (long)glGenLists(1);
      glNewList(DLISTID(geom), GL_COMPILE);
      renderer::geometry(geom);
      glEndList();
    }
  }

  if (geom->solid) {
    if (geom->ccw) glCullFace(GL_BACK);	// by default CCW faces are front facing
    else	   glCullFace(GL_FRONT);
    if (dynamicBackfaceCulling) glEnable(GL_CULL_FACE);
  } else
    if (dynamicBackfaceCulling) glDisable(GL_CULL_FACE);

  if (renderopts.use_display_lists)
    glCallList(DLISTID(geom));
  else
    renderer::geometry(geom);
}

void opengl_renderer::elevationGrid(ElevationGrid* geom)
{
  if (geom->color && current_texture_components < 3)
    glEnable(GL_COLOR_MATERIAL);
  geometry(geom);
}

void opengl_renderer::indexedFaceSet(IndexedFaceSet* geom)
{
  if (geom->color && current_texture_components < 3)
    glEnable(GL_COLOR_MATERIAL);
  geometry(geom);
}

void opengl_renderer::begin_faces(Geometry * geom)
{
  if (geom->colorPerVertex)
    glShadeModel(GL_SMOOTH);
  else
    glShadeModel(GL_FLAT);

  stripsize = -1; lastnrverts = -1;
}

void opengl_renderer::begin_face(int, int nverts)
{
#ifdef NEVER
  cerr << "begin face:\n";
#endif
  if (draw_outlines) {
    if (stripsize>=0) glEnd();
    glColor3f(1.,1.,1.);
    glBegin(GL_LINE_LOOP);
    return;
  }
  else if (lastnrverts!=3 || lastnrverts!=4 || 
      nverts != lastnrverts || stripsize > 200) {
    if (stripsize >= 0)
      glEnd();

    switch (nverts) {
    case 3:  glBegin(GL_TRIANGLES); break;
    case 4:  glBegin(GL_QUADS); break;
    default: glBegin(GL_POLYGON); break;
    }
    stripsize = 0;
  }
  lastnrverts = nverts;
}

void opengl_renderer::face_normal(int, const SFVec3f& norm)
{
  glNormal3f(norm.x, norm.y, norm.z);
}

void opengl_renderer::face_color(int, const SFColor& col)
{
  glColor4f(col.r, col.g, col.b, current_alpha);
}

void opengl_renderer::vertex_normal(int, const SFVec3f& norm)
{
  glNormal3f(norm.x, norm.y, norm.z);
}

void opengl_renderer::vertex_color(int, const SFColor& col)
{
  glColor4f(col.r, col.g, col.b, current_alpha);
}

void opengl_renderer::vertex_texCoord(int id, const SFVec2f& texco)
{
  glTexCoord2f(texco.s, texco.t);
}

void opengl_renderer::vertex_coord(int id, const SFVec3f& vert)
{
  Vec3 v = Vec3(vert) * xf;	// compute bounding box
  min <<= v;
  max >>= v;
  gmin <<= v;
  gmax >>= v;

  glVertex3f(vert.x, vert.y, vert.z);
  stripsize++;
}

void opengl_renderer::end_face(int, int)
{
  // nothing to do
}

void opengl_renderer::end_faces(Geometry *)
{
  if (stripsize >= 0)
    glEnd();
}
