/*  xtraceroute - graphically show traceroute information.
 *  Copyright (C) 1996-1998  Björn Augustsson 
 *
 *  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.
*/

#include "xt.h"
#include <netinet/in.h>

extern void sphere(int level);

extern texture *earth_texture;
extern int scanning;

static GLUquadricObj *lineobj;
static GLUquadricObj *earthobj;

const static GLfloat hiaccColor[]    = {0.1, 1.0, 0.1, 1.0};      /* Green for OK. */
const static GLfloat loaccColor[]    = {1.0, 1.0, 0.1, 1.0};      /* Yellow for bad */
const static GLfloat noaccColor[]    = {1.0, 0.1, 0.1, 1.0};      /* Red for no info */

const static GLfloat lineColor[]         = {1.0, 1.0, 0.1, 1.0};      /* Yellow */
const static GLfloat defaultColor[]      = {0.8, 0.8, 0.8, 1.0};

const static GLfloat selectedEmission[]  = {0.6, 0.6, 0.6, 1.0};
const static GLfloat defaultEmission[]   = {0.0, 0.0, 0.0, 1.0};

const static GLfloat lightZeroPosition[] = { 10.0,  4.0, 10.0, 1.0};
const static GLfloat lightZeroColor[]    = {  0.8,  1.0,  0.8, 1.0}; /* green-tinted */
const static GLfloat lightOnePosition[]  = { -1.0, -2.0,  1.0, 0.0};
const static GLfloat lightOneColor[]     = {  0.9,  0.9,  0.9, 1.0}; /* bright white */

static int sphere_lod = 3;
static GLUquadricObj *sitemarker;
static int render_mode = NORMAL_MODE;

/**
 * Set the Level Of Detail on the spheres generated in the future.
 * Only affects the earth-sphere, not the ones that represent
 * sites.
 */

void
set_sphere_lod(int lod)
{
  sphere_lod = lod;
}

/**
 * Set the mode in which the earth is to be rendered. Valid modes are:
 * NORMAL_MODE (default, does nothing special)
 * SELECT_MODE (uses glLoadName so we can do picking.)
 */

void 
set_render_mode(int mode)
{
  render_mode = mode;
}

/**
 * between puts the coordinates for a point between the two
 * specified ones in (rlat, rlon).
 */

static void 
between(GLdouble lat1, GLdouble lon1,
	GLdouble lat2, GLdouble lon2,
	GLdouble *rlat, GLdouble *rlon)
  { 
   GLdouble x1 = tox(lat1, lon1);
   GLdouble y1 = toy(lat1, lon1);
   GLdouble z1 = toz(lat1, lon1); 
   GLdouble x2 = tox(lat2, lon2);
   GLdouble y2 = toy(lat2, lon2);
   GLdouble z2 = toz(lat2, lon2); 

   GLdouble  x = (x1 + x2) / 2.0;
   GLdouble  y = (y1 + y2) / 2.0;
   GLdouble  z = (z1 + z2) / 2.0;

/*  'Normalize' to the radius of the earth. (Which is always 1, since this
     is the Unit Earth (TM))  */

   GLdouble f  = 1 / sqrt(x*x + y*y + z*z);
    x*=f;
    y*=f;
    z*=f;
    *rlat = tolat(x,y,z);
    *rlon = tolon(x,y,z);    
  }

/**
 * This is only used from makeearth, to plot a line on the globe.
 * There is some difficultish maths in this one.
 *
 * "to" and "from" in the comments in the code refers to the two
 * points given as arguments.
 */

static void
plot_line(GLfloat fromlat, GLfloat fromlon, GLfloat tolat, GLfloat tolon)
{
  GLfloat vink;
  GLfloat linesweep;
  
  glPushMatrix();
  glMaterialfv(GL_FRONT, GL_DIFFUSE, lineColor);
  
  /*    Angle is arccos(Ax*Bx + Ay*By + Az*Bz)    */
  
  linesweep = acos(tox(tolat, tolon)*tox(fromlat, fromlon)
		   +toy(tolat, tolon)*toy(fromlat, fromlon)
		   +toz(tolat, tolon)*toz(fromlat, fromlon))
    * todeg;
  
  /*   This mess calculates the angle to rotate "to" to a position
       'south' of "from". It's a somewhat adapted version of a formula
       that calculates an angle in a triangle on a sphere given all three
       sides. (Found the original in Beta.)                           */
  
  vink = todeg*acos((sin(tolat*torad)-sin(fromlat*torad)*cos(linesweep*torad))/
		    ( cos(fromlat*torad) * sin(linesweep*torad) )      );
  
  /*   Compensate for the usual arccos problem. (It checks if "to" is
       'to the right' or 'to the left' of "from")                 */
  
  if(fromlon > 0.0)
    {
      if(tolon > fromlon || tolon < fromlon - 180.0)
	vink = -vink;
    }
  else
    {
      if(tolon > fromlon && tolon < fromlon + 180.0)
	vink = -vink;
    }
  
  /*   Rotate "to" to a position 'south' of "from".   */
  
  glRotatef(vink, tox(fromlat, fromlon),
	    toy(fromlat, fromlon),
	    toz(fromlat, fromlon));
  
  /*   ...and rotate them onto the z=0 plane.   */
  
  glRotatef(90.0+fromlon, 0.0, 1.0, 0.0);
  
  /*   Fix incorrect names on the lines, identifying them as the 
       sitemarker we just drew.             */
  
  if(render_mode == SELECT_MODE)
    glLoadName(-1);
  
  partialtorus((GLfloat)fromlat-180.0, (GLfloat)linesweep);
  glPopMatrix();
}

/**
 * This is only used from makeearth, to plot a site on the globe.
 */
static void
plot_site(int site_nr, GLfloat lat, GLfloat lon, int height, int selected, int accuracy)
{
  GLfloat factor;

  glPushMatrix();
  if(selected)
    glMaterialfv(GL_FRONT, GL_EMISSION, selectedEmission);
  switch(accuracy)
    {
    case ACC_NDG_HOST:
    case ACC_NDG_NET:
    case ACC_LOCAL_SITE:
    case ACC_LOCAL_USER:
    case ACC_INCLUDED:
      glMaterialfv(GL_FRONT, GL_DIFFUSE, hiaccColor);	    
      break;
    case ACC_SUFFIX:
      glMaterialfv(GL_FRONT, GL_DIFFUSE, loaccColor);
      break;
    case ACC_NONE:
      glMaterialfv(GL_FRONT, GL_DIFFUSE, noaccColor);
      break;
    }
  factor = 1+2*SITE_MARKER_SIZE*(height);
  glTranslated(tox(lat, lon)*factor,
	       toy(lat, lon)*factor,
	       toz(lat, lon)*factor);
  
  if(render_mode == SELECT_MODE)
    glLoadName(site_nr);
  
  gluSphere(sitemarker,SITE_MARKER_SIZE,5,5);
  
  if (selected)
    glMaterialfv(GL_FRONT, GL_EMISSION, defaultEmission);
  
  glPopMatrix();
}


void init_gl(GtkWidget *wi, gpointer data)
{
  if (gtk_gl_area_begingl(GTK_GL_AREA(wi)))
    {
      int i;
      GLenum appropriate_format = GL_RGBA;
      lineobj  = gluNewQuadric();
      earthobj = gluNewQuadric();
      sitemarker = gluNewQuadric();
      gluQuadricDrawStyle(lineobj, GLU_FILL);
      gluQuadricNormals(lineobj, GLU_NONE);
      
      
      glEnable(GL_NORMALIZE);  /* Or zoom'll break the lighting */
      glEnable(GL_DEPTH_TEST);
      glEnable(GL_LIGHTING);
      glMatrixMode(GL_PROJECTION);
      glLoadIdentity();
      gluPerspective( /* field of view in degree */ 40.0,
		      /* aspect ratio                    */  1.0,
		      /* Z near                          */  1.0, 
		      /* Z far                           */ 10.0);
      glMatrixMode(GL_MODELVIEW);
      gluLookAt(0.0, 0.0, 4.0,    /* eye is at (0,0,4) */
		0.0, 0.0, 0.0,      /* center is at (0,0,0) */
		0.0, 1.0, 0.0);     /* up is in postive Y direction */
      glPushMatrix(); /* dummy push so we can pop on model recalc */
		
      /* Lighting stuff. */
      glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, 1);
      glLightfv(GL_LIGHT0, GL_POSITION, lightZeroPosition);
      glLightfv(GL_LIGHT0, GL_DIFFUSE, lightZeroColor);
      glLightf(GL_LIGHT0, GL_CONSTANT_ATTENUATION, 0.1);
      glLightf(GL_LIGHT0, GL_LINEAR_ATTENUATION, 0.05);
      glLightfv(GL_LIGHT1, GL_POSITION, lightOnePosition);
      glLightfv(GL_LIGHT1, GL_DIFFUSE, lightOneColor);
      glEnable(GL_LIGHT0);
      glEnable(GL_LIGHT1);
		
      glDisable(GL_BLEND);
      glEnable(GL_CULL_FACE);
		
#ifdef FLAT_SHADING
      glShadeModel(GL_FLAT);
#endif
		
#ifdef TEXTUREMAPPING
		
      /*  It is BROKEN behavior to support GL_EXT_abgr in gl* but not glu*! 
	  (gluBuild2DMipmaps)
    
	  Something like
    
	  #ifdef GL_EXT_abgr
	  if(glutExtensionSupported("GL_EXT_abgr"))
	  {
	  appropriate_format = GL_ABGR_EXT;
	  }
	  else
	  {
    
	  (byteswap)
    
	  }
	  #endif
	  would have been so nice here...
    
      */
		
		
      if(htons(1234) == 1234)  /* We're on a real machine */
	{
	  /* ABGR -> RGBA */
#ifdef DEBUG
	  printf("Byteswapping texture...");
#endif
	  for (i = 0; i < earth_texture->width * earth_texture->height; i++)
	    {
	      register unsigned char *cp = (unsigned char *) &(earth_texture->bits[i]);
	      int t;

	      t = cp[3];
	      cp[3] = cp[0];
	      cp[0] = t;
	      t = cp[2];
	      cp[2] = cp[1];
	      cp[1] = t;
	    }
#ifdef DEBUG
	  printf("done.");
#endif
	}
      gluQuadricTexture(earthobj,GL_TRUE);
      glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
		
      /* I would be clever and use the nice appropriate_format variable
	 here, but that doesn't work, because gluBuild2DMipmaps doesn't
	 support that. See above for rant on that. */
      gluBuild2DMipmaps(GL_TEXTURE_2D,
			earth_texture->depth,
			earth_texture->width,
			earth_texture->height,
			appropriate_format, GL_UNSIGNED_BYTE, 
			earth_texture->bits);
      glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
      glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
#ifdef UGLY_LINES
      glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
      glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
#else
      glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
      glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
#endif
      glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
#endif

      gtk_gl_area_endgl(GTK_GL_AREA(wi));
    }
  gtk_gl_area_swapbuffers(GTK_GL_AREA(wi));
}

/**
 * This determines wether to plot a site on top of another or put it 
 * on the ground and connect it and the previous one with a line.
 */

static int far_enough_apart(site a, site b)
{
  float londiff = fabs(a.lon - b.lon);
  float latdiff = fabs(a.lat - b.lat);
  
  /* This test should really check true distance, but hey... */
  return (londiff > 3.0 || latdiff > 3.0);
}


/**
 * This one builds the WORLD displaylist. It gets called whenever there
 * is new information to be "plotted". (The function, not the list)
 *
 * This is the kind of code everyone hates. 300 lines, one bazillion
 * levels of indentation, obscure maths, and weird names for stuff.
 *
 * It's certainly scheduled for a rewrite soon, but until then...
 *
 * It's a little better now, as of 0.8.8. Not good, better.
 **/

void makeearth(void)
{
  int i;
  int sites_on_top = 0;  
  int bad_behind   = 0;
  int any_good_sites_plotted = 0;

  if (gtk_gl_area_begingl(GTK_GL_AREA(glarea)))
    {
      glNewList(WORLD, GL_COMPILE);
      glPushMatrix();

#ifdef TEXTUREMAPPING
      glMaterialfv(GL_FRONT, GL_DIFFUSE, defaultColor);
#else
      glMaterialfv(GL_FRONT, GL_DIFFUSE,earthlandColor);
#endif

#ifdef TEXTUREMAPPING
      glEnable(GL_TEXTURE_2D);
#endif
    
      sphere(sphere_lod);
#ifdef TEXTUREMAPPING
      glDisable(GL_TEXTURE_2D);
#endif
      glPopMatrix();

      for(i=0 ; i<MAX_SITES && sites[i].draw ; i++) /* Here we go */
	{
	  /* If the first sites doesn't reply, stack them 
	     under the first known one */
	  if(sites[i].accuracy == ACC_NONE && !any_good_sites_plotted)
	    continue;
	  
	  if(!any_good_sites_plotted) /* [ == if(accuracy > ACC_NONE) ] */
	    {
	      int ii;
	      for(ii=0 ; ii<i ; ii++)
		{
		  plot_site(ii, sites[i].lat, 
			    sites[i].lon,
			    ii,
			    sites[ii].selected,
			    sites[ii].accuracy);
		}
	      plot_site(i, sites[i].lat, sites[i].lon, ii, 
			sites[i].selected, sites[i].accuracy);
	      any_good_sites_plotted = 1;
	      continue;
	    }
	  else
	    {
	      plot_site(i, sites[i].lat, sites[i].lon, 0, 
			sites[i].selected, sites[i].accuracy);
	    }
	  if(i > 0 && far_enough_apart(sites[i], sites[i-1]))
	    {
	      sites_on_top = 0;
	      
	      /* Make a line to it from the last one. */
	      plot_line(sites[i-1].lat, sites[i-1].lon,
			sites[ i ].lat, sites[ i ].lon);
	      if(bad_behind) /* Then stack them on the line. */
		{
		  int ii;
		  GLdouble lat, lon;
		  between(sites[ i ].lat,sites[ i ].lon,
			  sites[i-1].lat,sites[i-1].lon,
			  &lat, &lon);
		  for(ii=0;ii<bad_behind;ii++)
		    {
		      plot_site(i-bad_behind+ii, lat, lon, ii+1,
				sites[i-bad_behind+ii].selected, ACC_NONE);
		    }
		  bad_behind = 0;
		}
	    }
	  else   /* This site and the one before it is very close */ 
	    {
	      if(sites[i].accuracy == ACC_NONE)
		bad_behind++;
	      else
		{
		  if(bad_behind)
		    {
		      int ii;
		      for(ii=0 ; ii<bad_behind ; ii++)
			{
			  plot_site(i-bad_behind+ii, 
				    sites[i].lat, 
				    sites[i].lon, 
				    1+sites_on_top++,
				    sites[i-bad_behind+ii].selected,
				    ACC_NONE);
			}
		      bad_behind = 0;
		    }
		  plot_site(i, sites[i].lat, sites[i].lon, 1+sites_on_top,
			    sites[i].selected, sites[i].accuracy);
		  sites_on_top++;
		}
	    }
	}

      /* If there are still bad_behind, plot them on   */
      /* top of the last known site.                   */
      
      /* !draw , or MAX_SITES sites. */	  
      
      if(bad_behind && !scanning)
	{
	  int ii;
	  //	  i--;   FIXME! FIXME!
	  for(ii=0 ; ii<bad_behind ; ii++)
	    {
	      plot_site(i-bad_behind+ii,
			sites[i-1].lat,
			sites[i-1].lon,
			1+sites_on_top++, 
			sites[i-bad_behind+ii].selected,
			ACC_NONE);
	    }
	  bad_behind = 0;
	}

      glEndList();
      gtk_gl_area_endgl(GTK_GL_AREA(glarea));
    } 
  
  if(render_mode != SELECT_MODE)
    redraw(GTK_WIDGET(glarea), NULL);
}
