// $Id: Draw.cpp,v 1.95 2006/01/16 17:55:43 geuzaine Exp $
//
// Copyright (C) 1997-2006 C. Geuzaine, J.-F. Remacle
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
// USA.
// 
// Please report all bugs and problems to <gmsh@geuz.org>.

#include "Gmsh.h"
#include "GmshUI.h"
#include "Geo.h"
#include "CAD.h"
#include "Mesh.h"
#include "Draw.h"
#include "Context.h"
#include "Numeric.h"

extern Context_T CTX;
extern Mesh M;

// Global Draw functions

int NeedPolygonOffset()
{
  if(M.status == 2 &&
     (CTX.mesh.surfaces_edges || CTX.geom.lines || CTX.geom.surfaces))
    return 1;
  if(M.status == 3 && 
     (CTX.mesh.surfaces_edges || CTX.mesh.volumes_edges))
    return 1;
  for(int i = 0; i < List_Nbr(CTX.post.list); i++){
    Post_View *v = *(Post_View**)List_Pointer(CTX.post.list, i);
    if(v->Visible){
      if(v->ShowElement)
	return 1;
      if((v->NbST || v->NbSQ) && (v->Axes || v->IntervalsType == DRAW_POST_ISO))
	return 1;
    }
  }
  return 0;
}

void Draw3d(void)
{
  // We should only enable the polygon offset when there is a mix of
  // lines and polygons to be drawn; enabling it all the time can lead
  // to very small but annoying artifacts in the picture. Since there
  // are so many ways in Gmsh to combine polygons and lines
  // (geometries + meshes + views...), we do our best here to
  // automatically detect if we should enable it. Note: the formula
  // for the offset is "offset = factor*DZ+r*units", where DZ is a
  // measurement of the change in depth relative to the screen area of
  // the polygon, and r is the smallest value that is guaranteed to
  // produce a resolvable offset for a given implementation.
  glPolygonOffset(CTX.polygon_offset_factor, CTX.polygon_offset_units);
  if(CTX.polygon_offset_factor || CTX.polygon_offset_units)
    CTX.polygon_offset = CTX.polygon_offset_always ? 1 : NeedPolygonOffset();
  else
    CTX.polygon_offset = 0;

  glDepthFunc(GL_LESS);
  glEnable(GL_DEPTH_TEST);

  InitProjection();
  InitRenderModel();
  InitPosition();

  Draw_Mesh(&M);
}

void Draw2d(void)
{
  glDisable(GL_DEPTH_TEST);
  for(int i = 0; i < 6; i++)
    glDisable((GLenum)(GL_CLIP_PLANE0 + i));

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho((double)CTX.viewport[0], (double)CTX.viewport[2],
          (double)CTX.viewport[1], (double)CTX.viewport[3], -1., 1.);
  // hack to make the 2D primitives appear "in front" in GL2PS
  glTranslated(0., 0., CTX.clip_factor > 1. ? 1./CTX.clip_factor : CTX.clip_factor);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  Draw_Graph2D();
  Draw_Text2D();
  Draw_OnScreenMessages();
  if(CTX.post.draw)
    Draw_Scales();
  if(CTX.small_axes)
    Draw_SmallAxes();
}

void DrawPlugin(void (*draw)(void))
{
  CTX.post.plugin_draw_function = draw;
  int old = CTX.draw_bbox;
  CTX.draw_bbox = 1;
  if(CTX.fast_redraw){
    CTX.post.draw = 0;
    CTX.mesh.draw = 0;
  }
  if(!CTX.batch) 
    Draw();
  // this is reset in each plugin run/cancel callback:
  // CTX.post.plugin_draw_function = NULL;
  CTX.draw_bbox = old;
  CTX.post.draw = 1;
  CTX.mesh.draw = 1;
}

void ClearOpengl(void)
{
  glClearColor(CTX.UNPACK_RED(CTX.color.bg) / 255.,
               CTX.UNPACK_GREEN(CTX.color.bg) / 255.,
               CTX.UNPACK_BLUE(CTX.color.bg) / 255., 0.);
  glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
}

// Init

void InitProjection(int xpick, int ypick, int wpick, int hpick)
{
  double Va = 
    (GLdouble) (CTX.viewport[3] - CTX.viewport[1]) /
    (GLdouble) (CTX.viewport[2] - CTX.viewport[0]);
  double Wa = (CTX.max[1] - CTX.min[1]) / (CTX.max[0] - CTX.min[0]);

  // compute the viewport in World coordinates (with margins)
  if(Va > Wa) {
    CTX.vxmin = CTX.min[0];
    CTX.vxmax = CTX.max[0];
    CTX.vymin = 0.5 * (CTX.min[1] + CTX.max[1] - Va * (CTX.max[0] - CTX.min[0]));
    CTX.vymax = 0.5 * (CTX.min[1] + CTX.max[1] + Va * (CTX.max[0] - CTX.min[0]));
  }
  else {
    CTX.vxmin = 0.5 * (CTX.min[0] + CTX.max[0] - (CTX.max[1] - CTX.min[1]) / Va);
    CTX.vxmax = 0.5 * (CTX.min[0] + CTX.max[0] + (CTX.max[1] - CTX.min[1]) / Va);
    CTX.vymin = CTX.min[1];
    CTX.vymax = CTX.max[1];
  }
  CTX.vxmin -= (CTX.vxmax - CTX.vxmin) / 3.;
  CTX.vxmax += 0.25 * (CTX.vxmax - CTX.vxmin);
  CTX.vymin -= (CTX.vymax - CTX.vymin) / 3.;
  CTX.vymax += 0.25 * (CTX.vymax - CTX.vymin);

  // store what one pixel represents in world coordinates
  CTX.pixel_equiv_x = (CTX.vxmax - CTX.vxmin) / (CTX.viewport[2] - CTX.viewport[0]);
  CTX.pixel_equiv_y = (CTX.vymax - CTX.vymin) / (CTX.viewport[3] - CTX.viewport[1]);

  // no initial translation of the model
  CTX.t_init[0] = CTX.t_init[1] = CTX.t_init[2] = 0.;

  // setup ortho or perspective projection matrix
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();

  // restrict picking to a rectangular region around xpick,ypick in SELECT mode
  if(CTX.render_mode == GMSH_SELECT)
    gluPickMatrix((GLdouble)xpick, (GLdouble)(CTX.viewport[3] - ypick),
                  (GLdouble)wpick, (GLdouble)hpick, (GLint *)CTX.viewport);

  double grad_z, grad_xy;
  double zmax = MAX(fabs(CTX.min[2]), fabs(CTX.max[2]));
  if(zmax < CTX.lc) zmax = CTX.lc;

  if(CTX.ortho) {
    // setting up the near and far clipping planes so that the box is
    // large enough to manipulate the model and zoom, but not too big
    // (the z-buffer resolution, e.g., on software Mesa can become
    // insufficient)
    double clip = zmax * CTX.s[2] * CTX.clip_factor;
    glOrtho(CTX.vxmin, CTX.vxmax, CTX.vymin, CTX.vymax, -clip, clip);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    grad_z = 0.99 * clip;
    grad_xy = 1.;
  }
  else {
    double clip_near = 0.75 * CTX.clip_factor * zmax;
    double clip_far = 75. * CTX.clip_factor * zmax;
    double coef = (75./0.75) / 3.;
    // recenter the model such that the perspective is always at the
    // center of gravity (we should maybe add an option to choose
    // this, as we do for the rotation center)
    CTX.t_init[0] = CTX.cg[0];
    CTX.t_init[1] = CTX.cg[1];
    CTX.vxmin -= CTX.t_init[0];
    CTX.vxmax -= CTX.t_init[0];
    CTX.vymin -= CTX.t_init[1];
    CTX.vymax -= CTX.t_init[1];
    glFrustum(CTX.vxmin, CTX.vxmax, CTX.vymin, CTX.vymax, clip_near, clip_far);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glTranslated(-coef * CTX.t_init[0], -coef * CTX.t_init[1], -coef * clip_near);
    glScaled(coef, coef, coef);
    grad_z = 0.99 * clip_far;
    grad_xy = clip_far / clip_near;
  }

  // draw background gradient
  if(CTX.render_mode != GMSH_SELECT && CTX.bg_gradient){
    glPushMatrix();
    glLoadIdentity();
    glTranslated(0., 0., -grad_z);
    if(CTX.bg_gradient == 1){ // vertical
      glBegin(GL_QUADS);
      glColor4ubv((GLubyte *) & CTX.color.bg);
      glVertex3d(grad_xy * CTX.vxmin, grad_xy * CTX.vymin, 0.);
      glVertex3d(grad_xy * CTX.vxmax, grad_xy * CTX.vymin, 0.);
      glColor4ubv((GLubyte *) & CTX.color.bg_grad);
      glVertex3d(grad_xy * CTX.vxmax, grad_xy * CTX.vymax, 0.);
      glVertex3d(grad_xy * CTX.vxmin, grad_xy * CTX.vymax, 0.);
      glEnd();
    }
    else if(CTX.bg_gradient == 2){ // horizontal
      glBegin(GL_QUADS);
      glColor4ubv((GLubyte *) & CTX.color.bg);
      glVertex3d(grad_xy * CTX.vxmax, grad_xy * CTX.vymin, 0.);
      glVertex3d(grad_xy * CTX.vxmax, grad_xy * CTX.vymax, 0.);
      glColor4ubv((GLubyte *) & CTX.color.bg_grad);
      glVertex3d(grad_xy * CTX.vxmin, grad_xy * CTX.vymax, 0.);
      glVertex3d(grad_xy * CTX.vxmin, grad_xy * CTX.vymin, 0.);
      glEnd();
    }
    else{ // radial
      double cx = grad_xy * (CTX.vxmin + CTX.vxmax) / 2.;
      double cy = grad_xy * (CTX.vymin + CTX.vymax) / 2.;
      double r = grad_xy * MAX(CTX.vxmax - CTX.vxmin, CTX.vymax - CTX.vymin) / 2.;
      glBegin(GL_TRIANGLE_FAN);
      glColor4ubv((GLubyte *) & CTX.color.bg_grad);
      glVertex3d(cx, cy, 0.);
      glColor4ubv((GLubyte *) & CTX.color.bg);
      glVertex3d(cx + r, cy, 0.);
      int ntheta = 36;
      for(int i = 1; i < ntheta + 1; i ++){
	double theta = i * 2 * M_PI / (double)ntheta;
	glVertex3d(cx + r * cos(theta), cy + r * sin(theta), 0.);	
      }
      glEnd();
    }
    glPopMatrix();
  }
}

void InitRenderModel(void)
{
  GLfloat r, g, b;

  for(int i = 0; i < 6; i++) {
    if(CTX.light[i]) {
      GLfloat position[4] = {(GLfloat)CTX.light_position[i][0],
			     (GLfloat)CTX.light_position[i][1],
			     (GLfloat)CTX.light_position[i][2],
			     (GLfloat)CTX.light_position[i][3]};
      glLightfv((GLenum)(GL_LIGHT0 + i), GL_POSITION, position);

      r = CTX.UNPACK_RED(CTX.color.ambient_light[i])/255.;
      g = CTX.UNPACK_GREEN(CTX.color.ambient_light[i])/255.;
      b = CTX.UNPACK_BLUE(CTX.color.ambient_light[i])/255.;
      GLfloat ambient[4] = {r, g, b, 1.0};
      glLightfv((GLenum)(GL_LIGHT0 + i), GL_AMBIENT, ambient);

      r = CTX.UNPACK_RED(CTX.color.diffuse_light[i])/255.;
      g = CTX.UNPACK_GREEN(CTX.color.diffuse_light[i])/255.;
      b = CTX.UNPACK_BLUE(CTX.color.diffuse_light[i])/255.;
      GLfloat diffuse[4] = {r, g, b, 1.0};
      glLightfv((GLenum)(GL_LIGHT0 + i), GL_DIFFUSE, diffuse);

      r = CTX.UNPACK_RED(CTX.color.specular_light[i])/255.;
      g = CTX.UNPACK_GREEN(CTX.color.specular_light[i])/255.;
      b = CTX.UNPACK_BLUE(CTX.color.specular_light[i])/255.;
      GLfloat specular[4] = {r, g, b, 1.0};
      glLightfv((GLenum)(GL_LIGHT0 + i), GL_SPECULAR, specular);

      glEnable((GLenum)(GL_LIGHT0 + i));
    }
    else{
      glDisable((GLenum)(GL_LIGHT0 + i));
    }
  }

  // ambient and diffuse material colors track glColor automatically
  glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
  glEnable(GL_COLOR_MATERIAL);
  // "white"-only specular material reflection color
  GLfloat spec[4] = {CTX.shine, CTX.shine, CTX.shine, 1.0};
  glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, spec);
  // specular exponent in [0,128] (larger means more "focused"
  // reflection)
  glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, CTX.shine_exponent);

  glShadeModel(GL_SMOOTH);

  // Normalize the normals automatically. We could use the more
  // efficient glEnable(GL_RESCALE_NORMAL) instead (since we initially
  // specify unit normals), but GL_RESCALE_NORMAL does only work with
  // isotropic scalings (and we allow anistotropic scalings in
  // myZoom). Note that GL_RESCALE_NORMAL is only available in
  // GL_VERSION_1_2.
  glEnable(GL_NORMALIZE);

  // lighting is enabled/disabled for each particular primitive later
  glDisable(GL_LIGHTING);
}

void InitPosition(void)
{
  glScaled(CTX.s[0], CTX.s[1], CTX.s[2]);
  glTranslated(CTX.t[0], CTX.t[1], CTX.t[2]);

  if(CTX.rotation_center_cg)
    glTranslated(CTX.cg[0], CTX.cg[1], CTX.cg[2]);
  else
    glTranslated(CTX.rotation_center[0],
		 CTX.rotation_center[1],
		 CTX.rotation_center[2]);
  
  CTX.buildRotationMatrix();
  glMultMatrixd(CTX.rot);

  if(CTX.rotation_center_cg)
    glTranslated(-CTX.cg[0], -CTX.cg[1], -CTX.cg[2]);
  else
    glTranslated(-CTX.rotation_center[0],
		 -CTX.rotation_center[1],
		 -CTX.rotation_center[2]);

  // store the projection and modelview matrices at this precise
  // moment (so that we can use them at any later time, even if the
  // context has changed, i.e., even if we are out of Draw())
  glGetDoublev(GL_PROJECTION_MATRIX, CTX.proj);
  glGetDoublev(GL_MODELVIEW_MATRIX, CTX.model);
}

// Entity selection

typedef struct{
  unsigned int type, ient, depth;
} hit;

int fcmp_hit_depth(const void *a, const void *b)
{
  return ((hit*)a)->depth - ((hit*)b)->depth;
}

int Process_SelectionBuffer(int type, bool multi,
			    int x, int y, int w, int h,
			    Vertex *v[SELECTION_MAX_HITS],
			    Curve *c[SELECTION_MAX_HITS],
			    Surface *s[SELECTION_MAX_HITS],
			    Mesh *m)
{
  hit hits[SELECTION_BUFFER_SIZE];
  GLuint selectBuf[SELECTION_BUFFER_SIZE];

  glSelectBuffer(SELECTION_BUFFER_SIZE, selectBuf);

  glRenderMode(GL_SELECT);
  CTX.render_mode = GMSH_SELECT;

  glInitNames();
  glPushMatrix();
  InitProjection(x, y, w, h);
  InitPosition();
  Draw_Mesh(&M);
  glPopMatrix();

  GLint numhits = glRenderMode(GL_RENDER);
  CTX.render_mode = GMSH_RENDER;

  if(!numhits){ // no hits
    return 0;
  }
  else if(numhits < 0){ // overflow
    Msg(WARNING, "Selection buffer size exceeded");
    return 0;
  }

  GLint *ptr = (GLint *) selectBuf;

  int numentities = 0;
  for(int i = 0; i < numhits; i++) {
    GLint names = *ptr++; 
    GLint mindepth = *ptr++;
    GLint maxdepth = *ptr++;
    // in Gmsh 'names' should always be 2 (in which case the first is
    // the type of the entity (0=point, 1=line, ...) and the second
    // the entity number) or 0 (if there is nothing in the stack)
    if(names == 2){
      hits[numentities].depth = (mindepth+maxdepth)/2;
      hits[numentities].type = *ptr++; 
      hits[numentities].ient = *ptr++;
      numentities++;
    }
  }

  if(!numentities){ // no entities
    return 0;
  }

  // filter result: if type == ENT_NONE, return the closest entity of
  // "lowest dimension" (point < line < surface < volume). Otherwise,
  // return the closest entity of type "type"

  unsigned int typmin = 4;
  for(int i = 0; i < numentities; i++) {
    if(hits[i].type < typmin)
      typmin = hits[i].type;
  }

  // sort hits to get closest entities first
  qsort(hits, numentities, sizeof(hit), fcmp_hit_depth);
  
  int j = 0;
  for(int i = 0; i < numentities; i++) {
    if((type == ENT_NONE && hits[i].type == typmin) ||
       (type == ENT_POINT && hits[i].type == 0) ||
       (type == ENT_LINE && hits[i].type == 1) ||
       (type == ENT_SURFACE && hits[i].type == 2)){
      switch (hits[i].type) {
      case 0:
	if(!(v[j] = FindPoint(hits[i].ient, m))){
	  Msg(GERROR, "Problem in point selection processing");
	  return j;
	}
	j++;
	if(!multi) return 1;
	break;
      case 1:
	if(!(c[j] = FindCurve(hits[i].ient, m))){
	  Msg(GERROR, "Problem in line selection processing");
	  return j;
	}
	j++;
	if(!multi) return 1;
	break;
      case 2:
	if(!(s[j] = FindSurface(hits[i].ient, m))){
	  Msg(GERROR, "Problem in surface selection processing");
	  return j;
	}
	j++;
	if(!multi) return 1;
	break;
      }
    }
  }
  return j;
}

// Takes a cursor position in window coordinates and returns the line
// (given by a point and a unit direction vector), in real space, that
// corresponds to that cursor position

void unproject(double x, double y, double p[3], double d[3])
{
  GLint viewport[4];
  glGetIntegerv(GL_VIEWPORT, viewport);

  y = viewport[3]-y;

  GLdouble x0, y0, z0, x1, y1, z1;

  // we use CTX.model and CTX.proj instead of directly getGetDouble'ing
  // the matrices since unproject can be called in or after Draw2D
  
  if(!gluUnProject(x, y, 0.0, CTX.model, CTX.proj, viewport, &x0, &y0, &z0))
    Msg(WARNING, "unproject1 failed");
  if(!gluUnProject(x, y, 1.0, CTX.model, CTX.proj, viewport, &x1, &y1, &z1))
    Msg(WARNING, "unproject2 failed");
  
  p[0] = x0;
  p[1] = y0;
  p[2] = z0;
  d[0] = x1-x0;
  d[1] = y1-y0;
  d[2] = z1-z0;
  double len = sqrt(d[0]*d[0] + d[1]*d[1] + d[2]*d[2]);
  d[0] /= len;
  d[1] /= len;
  d[2] /= len;
}

void Viewport2World(double win[3], double xyz[3])
{
  GLint viewport[4];
  GLdouble model[16], proj[16];
  glGetIntegerv(GL_VIEWPORT, viewport);
  glGetDoublev(GL_PROJECTION_MATRIX, proj);
  glGetDoublev(GL_MODELVIEW_MATRIX, model);
  gluUnProject(win[0], win[1], win[2], model, proj, viewport, &xyz[0], &xyz[1], &xyz[2]);
}

void World2Viewport(double xyz[3], double win[3])
{
  GLint viewport[4];
  GLdouble model[16], proj[16];
  glGetIntegerv(GL_VIEWPORT, viewport);
  glGetDoublev(GL_PROJECTION_MATRIX, proj);
  glGetDoublev(GL_MODELVIEW_MATRIX, model);
  gluProject(xyz[0], xyz[1], xyz[2], model, proj, viewport, &win[0], &win[1], &win[2]);
}
