/*
 * This program is under the GNU GPL.
 * Use at your own risk.
 *
 * written by David Bucciarelli (tech.hmw@plus.it)
 *            Humanware s.r.l.
 *
 * based on a Mikael SkiZoWalker's (MoDEL) / France (Skizo@Hol.Fr) demo
 */

#include <assert.h>
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#ifdef WIN32
#include <windows.h>
#endif

#include <GL/glut.h>

#ifdef XMESA
#include "GL/xmesa.h"
static int fullscreen = 1;
#endif

#ifndef M_PI
#define M_PI 3.14159265
#endif

#define heightMnt	450
#define	lenghtXmnt	62
#define lenghtYmnt	62

#define stepXmnt     96.0
#define stepYmnt     96.0

#define WIDTH 640
#define HEIGHT 480

static GLint T0 = 0;
static GLint Frames = 0;

#define TSCALE 4

#define FOV 85

static GLfloat terrain[256 * 256];
static GLfloat terraincolor[256 * 256][3];

static int win = 0;

static int fog = 1;
static int bfcull = 1;
static int usetex = 1;
static int poutline = 0;
static int help = 1;
static int joyavailable = 0;
static int joyactive = 0;
static float ModZMnt;
static long GlobalMnt = 0;

static int scrwidth = WIDTH;
static int scrheight = HEIGHT;

#define OBSSTARTX 992.0
#define OBSSTARTY 103.0

static float obs[3] = { OBSSTARTX, heightMnt * 1.3, OBSSTARTY };
static float dir[3], v1[2], v2[2];
static float v = 900.0;
static float alpha = 75.0;
static float beta = 90.0;

static void
calcposobs(void)
{
   float alpha1, alpha2;
   static double t0 = -1.;
   double dt, t = glutGet(GLUT_ELAPSED_TIME) / 1000.0;
   if (t0 < 0.0)
      t0 = t;
   dt = t - t0;
   t0 = t;

   dir[0] = sin(alpha * M_PI / 180.0);
   dir[2] = cos(alpha * M_PI / 180.0) * sin(beta * M_PI / 180.0);
   dir[1] = cos(beta * M_PI / 180.0);

   if (dir[0] < 1.0e-5 && dir[0] > -1.0e-5)
      dir[0] = 0;
   if (dir[1] < 1.0e-5 && dir[1] > -1.0e-5)
      dir[1] = 0;
   if (dir[2] < 1.0e-5 && dir[2] > -1.0e-5)
      dir[2] = 0;

   alpha1 = alpha + FOV / 2.0;
   v1[0] = sin(alpha1 * M_PI / 180.0);
   v1[1] = cos(alpha1 * M_PI / 180.0);

   alpha2 = alpha - FOV / 2.0;
   v2[0] = sin(alpha2 * M_PI / 180.0);
   v2[1] = cos(alpha2 * M_PI / 180.0);

   obs[0] += v * dir[0] * dt;
   obs[1] += v * dir[1] * dt;
   obs[2] += v * dir[2] * dt;

   if (obs[1] < 0.0)
      obs[1] = 0.0;
}

static void
reshape(int width, int height)
{
   scrwidth = width;
   scrheight = height;
   glViewport(0, 0, (GLint) width, (GLint) height);
   glMatrixMode(GL_PROJECTION);
   glLoadIdentity();
   gluPerspective(50.0, ((GLfloat) width / (GLfloat) height),
		  lenghtXmnt * stepYmnt * 0.01, lenghtXmnt * stepYmnt * 0.7);
   glMatrixMode(GL_MODELVIEW);
   glLoadIdentity();
}

static int
clipstrip(float y, float *start, float *end)
{
   float x1, x2, t1, t2, tmp;

   if (v1[1] == 0.0) {
      t1 = 0.0;
      x1 = -HUGE_VAL;
   }
   else {
      t1 = y / v1[1];
      x1 = t1 * v1[0];
   }

   if (v2[1] == 0.0) {
      t2 = 0.0;
      x2 = HUGE_VAL;
   }
   else {
      t2 = y / v2[1];
      x2 = t2 * v2[0];
   }

   if (((x1 < -(lenghtXmnt * stepXmnt) / 2) && (t2 <= 0.0)) ||
       ((t1 <= 0.0) && (x2 > (lenghtXmnt * stepXmnt) / 2)) ||
       ((t1 < 0.0) && (t2 < 0.0)))
      return 0;

   if ((t1 == 0.0) && (t2 == 0.0)) {
      if ((v1[0] < 0.0) && (v1[1] > 0.0) && (v2[0] < 0.0) && (v2[1] < 0.0)) {
	 *start = -(lenghtXmnt * stepXmnt) / 2;
	 *end = stepXmnt;
	 return 1;
      }
      else {
	 if ((v1[0] > 0.0) && (v1[1] < 0.0) && (v2[0] > 0.0) && (v2[1] > 0.0)) {
	    *start = -stepXmnt;
	    *end = (lenghtXmnt * stepXmnt) / 2;
	    return 1;
	 }
	 else
	    return 0;
      }
   }
   else {
      if (t2 < 0.0) {
	 if (x1 < 0.0)
	    x2 = -(lenghtXmnt * stepXmnt) / 2;
	 else
	    x2 = (lenghtXmnt * stepXmnt) / 2;
      }

      if (t1 < 0.0) {
	 if (x2 < 0.0)
	    x1 = -(lenghtXmnt * stepXmnt) / 2;
	 else
	    x1 = (lenghtXmnt * stepXmnt) / 2;
      }
   }

   if (x1 > x2) {
      tmp = x1;
      x1 = x2;
      x2 = tmp;
   }

   x1 -= stepXmnt;
   if (x1 < -(lenghtXmnt * stepXmnt) / 2)
      x1 = -(lenghtXmnt * stepXmnt) / 2;

   x2 += stepXmnt;
   if (x2 > (lenghtXmnt * stepXmnt) / 2)
      x2 = (lenghtXmnt * stepXmnt) / 2;

   *start = ((int) (x1 / stepXmnt)) * stepXmnt;
   *end = ((int) (x2 / stepXmnt)) * stepXmnt;

   return 1;
}

static void
printstring(void *font, char *string)
{
   int len, i;

   len = (int) strlen(string);
   for (i = 0; i < len; i++)
      glutBitmapCharacter(font, string[i]);
}

static void
printhelp(void)
{
   glEnable(GL_BLEND);
   glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
   glColor4f(0.0, 0.0, 0.0, 0.5);
   glRecti(40, 40, 600, 440);
   glDisable(GL_BLEND);

   glColor3f(1.0, 0.0, 0.0);
   glRasterPos2i(300, 420);
   printstring(GLUT_BITMAP_TIMES_ROMAN_24, "Help");

   glRasterPos2i(60, 390);
   printstring(GLUT_BITMAP_TIMES_ROMAN_24, "h - Toggle Help");
   glRasterPos2i(60, 360);
   printstring(GLUT_BITMAP_TIMES_ROMAN_24, "t - Toggle Textures");
   glRasterPos2i(60, 330);
   printstring(GLUT_BITMAP_TIMES_ROMAN_24, "f - Toggle Fog");
   glRasterPos2i(60, 300);
   printstring(GLUT_BITMAP_TIMES_ROMAN_24, "p - Wire frame");
   glRasterPos2i(60, 270);
   printstring(GLUT_BITMAP_TIMES_ROMAN_24, "b - Toggle Back face culling");
   glRasterPos2i(60, 240);
   printstring(GLUT_BITMAP_TIMES_ROMAN_24, "Arrow Keys - Rotate");
   glRasterPos2i(60, 210);
   printstring(GLUT_BITMAP_TIMES_ROMAN_24, "a - Increase velocity");
   glRasterPos2i(60, 180);
   printstring(GLUT_BITMAP_TIMES_ROMAN_24, "z - Decrease velocity");

   glRasterPos2i(60, 150);
   if (joyavailable)
      printstring(GLUT_BITMAP_TIMES_ROMAN_24,
		  "j - Toggle jostick control (Joystick control available)");
   else
      printstring(GLUT_BITMAP_TIMES_ROMAN_24,
		  "(No Joystick control available)");
}

static void
drawterrain(void)
{
   int h, i, idx, ox, oy;
   float j, k, start, end;

   ox = (int) (obs[0] / stepXmnt);
   oy = (int) (obs[2] / stepYmnt);
   GlobalMnt = ((ox * TSCALE) & 255) + ((oy * TSCALE) & 255) * 256;

   glPushMatrix();
   glTranslatef((float) ox * stepXmnt, 0, (float) oy * stepYmnt);

   for (h = 0, k = -(lenghtYmnt * stepYmnt) / 2; h < lenghtYmnt;
	k += stepYmnt, h++) {
      if (!clipstrip(k, &start, &end))
	 continue;

      glBegin(GL_TRIANGLE_STRIP);	/* I hope that the optimizer will be able to improve this code */
      for (i = (int) (lenghtXmnt / 2 + start / stepXmnt), j = start; j <= end;
	   j += stepXmnt, i++) {
	 idx = (i * TSCALE + h * 256 * TSCALE + GlobalMnt) & 65535;
	 glColor3fv(terraincolor[idx]);
	 glTexCoord2f((ox + i) / 8.0, (oy + h) / 8.0);
	 glVertex3f(j, terrain[idx], k);

	 idx =
	    (i * TSCALE + h * 256 * TSCALE + 256 * TSCALE +
	     GlobalMnt) & 65535;
	 glColor3fv(terraincolor[idx]);
	 glTexCoord2f((ox + i) / 8.0, (oy + h + 1) / 8.0);
	 glVertex3f(j, terrain[idx], k + stepYmnt);
      }
      glEnd();
   }

   glDisable(GL_CULL_FACE);
   glDisable(GL_TEXTURE_2D);
   glEnable(GL_BLEND);
   glBegin(GL_QUADS);
   glColor4f(0.1, 0.7, 1.0, 0.4);
   glVertex3f(-(lenghtXmnt * stepXmnt) / 2.0, heightMnt * 0.6,
	      -(lenghtYmnt * stepYmnt) / 2.0);
   glVertex3f(-(lenghtXmnt * stepXmnt) / 2.0, heightMnt * 0.6,
	      (lenghtYmnt * stepYmnt) / 2.0);
   glVertex3f((lenghtXmnt * stepXmnt) / 2.0, heightMnt * 0.6,
	      (lenghtYmnt * stepYmnt) / 2.0);
   glVertex3f((lenghtXmnt * stepXmnt) / 2.0, heightMnt * 0.6,
	      -(lenghtYmnt * stepYmnt) / 2.0);
   glEnd();
   glDisable(GL_BLEND);
   if (bfcull)
      glEnable(GL_CULL_FACE);
   glEnable(GL_TEXTURE_2D);

   glPopMatrix();

}

static void
dojoy(void)
{
#ifdef WIN32
   static UINT max[2] = { 0, 0 };
   static UINT min[2] = { 0xffffffff, 0xffffffff }, center[2];
   MMRESULT res;
   JOYINFO joy;

   res = joyGetPos(JOYSTICKID1, &joy);

   if (res == JOYERR_NOERROR) {
      joyavailable = 1;

      if (max[0] < joy.wXpos)
	 max[0] = joy.wXpos;
      if (min[0] > joy.wXpos)
	 min[0] = joy.wXpos;
      center[0] = (max[0] + min[0]) / 2;

      if (max[1] < joy.wYpos)
	 max[1] = joy.wYpos;
      if (min[1] > joy.wYpos)
	 min[1] = joy.wYpos;
      center[1] = (max[1] + min[1]) / 2;

      if (joyactive) {
	 if (fabs(center[0] - (float) joy.wXpos) > 0.1 * (max[0] - min[0]))
	    alpha +=
	       2.5 * (center[0] - (float) joy.wXpos) / (max[0] - min[0]);
	 if (fabs(center[1] - (float) joy.wYpos) > 0.1 * (max[1] - min[1]))
	    beta += 2.5 * (center[1] - (float) joy.wYpos) / (max[1] - min[1]);

	 if (joy.wButtons & JOY_BUTTON1)
	    v += 0.5;
	 if (joy.wButtons & JOY_BUTTON2)
	    v -= 0.5;
      }
   }
   else
      joyavailable = 0;
#endif
}

static void
drawscene(void)
{
   static char frbuf[80] = "";

   dojoy();

   glShadeModel(GL_SMOOTH);
   glEnable(GL_DEPTH_TEST);

   if (usetex)
      glEnable(GL_TEXTURE_2D);
   else
      glDisable(GL_TEXTURE_2D);

   if (fog)
      glEnable(GL_FOG);
   else
      glDisable(GL_FOG);

   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

   glPushMatrix();

   calcposobs();
   gluLookAt(obs[0], obs[1], obs[2],
	     obs[0] + dir[0], obs[1] + dir[1], obs[2] + dir[2],
	     0.0, 1.0, 0.0);

   drawterrain();
   glPopMatrix();

   glDisable(GL_TEXTURE_2D);
   glDisable(GL_DEPTH_TEST);
   glDisable(GL_FOG);
   glShadeModel(GL_FLAT);

   glMatrixMode(GL_PROJECTION);
   glLoadIdentity();
   glOrtho(-0.5, 639.5, -0.5, 479.5, -1.0, 1.0);
   glMatrixMode(GL_MODELVIEW);
   glLoadIdentity();

   glColor3f(1.0, 0.0, 0.0);
   glRasterPos2i(10, 10);
   printstring(GLUT_BITMAP_HELVETICA_18, frbuf);
   glRasterPos2i(350, 470);
   printstring(GLUT_BITMAP_HELVETICA_10,
	       "Terrain V1.2 Written by David Bucciarelli (tech.hmw@plus.it)");
   glRasterPos2i(434, 457);
   printstring(GLUT_BITMAP_HELVETICA_10,
	       "Based on a Mickael's demo (Skizo@Hol.Fr)");

   if (help)
      printhelp();

   reshape(scrwidth, scrheight);

   glutSwapBuffers();

   Frames++;
   {
      GLint t = glutGet(GLUT_ELAPSED_TIME);
      if (t - T0 >= 2000) {
         GLfloat seconds = (t - T0) / 1000.0;
         GLfloat fps = Frames / seconds;
         sprintf(frbuf, "Frame rate: %f", fps);
         T0 = t;
         Frames = 0;
      }
   }
}

static void
key(unsigned char k, int x, int y)
{
   switch (k) {
   case 27:
      exit(0);
      break;
   case 'a':
      v += 50.;
      break;
   case 'z':
      v -= 50.;
      break;
   case 'p':
      if (poutline) {
	 glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
	 poutline = 0;
      }
      else {
	 glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
	 poutline = 1;
      }
      break;
   case 'j':
      joyactive = (!joyactive);
      break;
   case 'h':
      help = (!help);
      break;
   case 'f':
      fog = (!fog);
      break;
   case 't':
      usetex = (!usetex);
      break;
   case 'b':
      if (bfcull) {
	 glDisable(GL_CULL_FACE);
	 bfcull = 0;
      }
      else {
	 glEnable(GL_CULL_FACE);
	 bfcull = 1;
      }
      break;
#ifdef XMESA
   case ' ':
      XMesaSetFXmode(fullscreen ? XMESA_FX_FULLSCREEN : XMESA_FX_WINDOW);
      fullscreen = (!fullscreen);
      break;
#endif
   }
}

static void
special(int k, int x, int y)
{
   switch (k) {
   case GLUT_KEY_LEFT:
      alpha += 2.0;
      break;
   case GLUT_KEY_RIGHT:
      alpha -= 2.0;
      break;
   case GLUT_KEY_DOWN:
      beta -= 2.0;
      break;
   case GLUT_KEY_UP:
      beta += 2.0;
      break;
   }
}

static void
calccolor(GLfloat height, GLfloat c[3])
{
   GLfloat color[4][3] = {
      {1.0, 1.0, 1.0},
      {0.0, 0.8, 0.0},
      {1.0, 1.0, 0.3},
      {0.0, 0.0, 0.8}
   };
   GLfloat fact;

   height = height * (1.0 / 255.0);

   if (height >= 0.9) {
      c[0] = color[0][0];
      c[1] = color[0][1];
      c[2] = color[0][2];
      return;
   }

   if ((height < 0.9) && (height >= 0.7)) {
      fact = (height - 0.7) * 5.0;
      c[0] = fact * color[0][0] + (1.0 - fact) * color[1][0];
      c[1] = fact * color[0][1] + (1.0 - fact) * color[1][1];
      c[2] = fact * color[0][2] + (1.0 - fact) * color[1][2];
      return;
   }

   if ((height < 0.7) && (height >= 0.6)) {
      fact = (height - 0.6) * 10.0;
      c[0] = fact * color[1][0] + (1.0 - fact) * color[2][0];
      c[1] = fact * color[1][1] + (1.0 - fact) * color[2][1];
      c[2] = fact * color[1][2] + (1.0 - fact) * color[2][2];
      return;
   }

   if ((height < 0.6) && (height >= 0.5)) {
      fact = (height - 0.5) * 10.0;
      c[0] = fact * color[2][0] + (1.0 - fact) * color[3][0];
      c[1] = fact * color[2][1] + (1.0 - fact) * color[3][1];
      c[2] = fact * color[2][2] + (1.0 - fact) * color[3][2];
      return;
   }

   c[0] = color[3][0];
   c[1] = color[3][1];
   c[2] = color[3][2];
}

static void
loadpic(void)
{
   GLubyte bufferter[256 * 256], terrainpic[256 * 256];
   FILE *FilePic;
   int i, tmp;
   GLenum gluerr;
   size_t result;

   if ((FilePic = fopen("terrain.dat", "r")) == NULL) {
      fprintf(stderr, "Error loading terrain.dat\n");
      exit(-1);
   }
   result = fread(bufferter, 256 * 256, 1, FilePic);
   assert(result == 1);
   fclose(FilePic);

   for (i = 0; i < (256 * 256); i++) {
      terrain[i] = (bufferter[i] * (heightMnt / 255.0f));
      calccolor((GLfloat) bufferter[i], terraincolor[i]);
      tmp = (((int) bufferter[i]) + 96);
      terrainpic[i] = (tmp > 255) ? 255 : tmp;
   }

   glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
   if ((gluerr = gluBuild2DMipmaps(GL_TEXTURE_2D, 1, 256, 256, GL_LUMINANCE,
				   GL_UNSIGNED_BYTE,
				   (GLvoid *) (&terrainpic[0])))) {
      fprintf(stderr, "GLULib%s\n", (char *) gluErrorString(gluerr));
      exit(-1);
   }

   glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
   glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

   glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
		   GL_LINEAR_MIPMAP_LINEAR);
   glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

   glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
   glEnable(GL_TEXTURE_2D);
}

static void
init(void)
{
   float fogcolor[4] = { 0.6, 0.7, 0.7, 1.0 };

   glClearColor(fogcolor[0], fogcolor[1], fogcolor[2], fogcolor[3]);
   glClearDepth(1.0);
   glDepthFunc(GL_LEQUAL);
   glShadeModel(GL_SMOOTH);
   glEnable(GL_DEPTH_TEST);
   glEnable(GL_CULL_FACE);

   glDisable(GL_BLEND);
   glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

   glEnable(GL_FOG);
   glFogi(GL_FOG_MODE, GL_EXP2);
   glFogfv(GL_FOG_COLOR, fogcolor);
   glFogf(GL_FOG_DENSITY, 0.0007);
#ifdef FX
   glHint(GL_FOG_HINT, GL_NICEST);
#endif

   reshape(scrwidth, scrheight);
}


int
main(int ac, char **av)
{
   glutInitWindowSize(WIDTH, HEIGHT);
   glutInit(&ac, av);

   glutInitDisplayMode(GLUT_RGB | GLUT_DEPTH | GLUT_DOUBLE);

   if (!(win = glutCreateWindow("Terrain"))) {
      fprintf(stderr, "Error, couldn't open window\n");
      return -1;
   }

   ModZMnt = 0.0f;
   loadpic();

   init();

#ifndef FX
   glDisable(GL_TEXTURE_2D);
   usetex = 0;
#endif

   glutReshapeFunc(reshape);
   glutDisplayFunc(drawscene);
   glutKeyboardFunc(key);
   glutSpecialFunc(special);
   glutIdleFunc(drawscene);

   glutMainLoop();

   return 0;
}
