/*****************************************************************************
 * Wave Simulation in OpenGL
 * (C) 2002 Jakob Thomsen
 * http://home.in.tum.de/~thomsen
 * Modified for GLFW by Sylvain Hellegouarch - sh@programmationworld.com
 * Modified for variable frame rate by Marcus Geelnard
 * 2003-Jan-31: Minor cleanups and speedups / MG
 *****************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <GL/glfw3.h>

#ifndef M_PI
 #define M_PI 3.1415926535897932384626433832795
#endif

/* Maximum delta T to allow for differential calculations */
#define MAX_DELTA_T 0.01

/* Animation speed (10.0 looks good) */
#define ANIMATION_SPEED 10.0


GLfloat alpha = 210.0f, beta = -70.0f;
GLfloat zoom = 2.0f;

int running = 1;

struct Vertex
{
  GLfloat x,y,z;
  GLfloat r,g,b;
};

#define GRIDW 50
#define GRIDH 50
#define VERTEXNUM (GRIDW*GRIDH)

#define QUADW (GRIDW-1)
#define QUADH (GRIDH-1)
#define QUADNUM (QUADW*QUADH)

GLuint quad[4*QUADNUM];
struct Vertex vertex[VERTEXNUM];

/* The grid will look like this:
 *
 *      3   4   5
 *      *---*---*
 *      |   |   |
 *      | 0 | 1 |
 *      |   |   |
 *      *---*---*
 *      0   1   2
 */

void initVertices( void )
{
  int x,y,p;

  /* place the vertices in a grid */
  for(y=0;y<GRIDH;y++)
    for(x=0;x<GRIDW;x++)
    {
      p = y*GRIDW + x;

      //vertex[p].x = (-GRIDW/2)+x+sin(2.0*M_PI*(double)y/(double)GRIDH);
      //vertex[p].y = (-GRIDH/2)+y+cos(2.0*M_PI*(double)x/(double)GRIDW);
      vertex[p].x = (GLfloat)(x-GRIDW/2)/(GLfloat)(GRIDW/2);
      vertex[p].y = (GLfloat)(y-GRIDH/2)/(GLfloat)(GRIDH/2);
      vertex[p].z = 0;//sin(d*M_PI);
      //vertex[p].r = (GLfloat)x/(GLfloat)GRIDW;
      //vertex[p].g = (GLfloat)y/(GLfloat)GRIDH;
      //vertex[p].b = 1.0-((GLfloat)x/(GLfloat)GRIDW+(GLfloat)y/(GLfloat)GRIDH)/2.0;
      if((x%4<2)^(y%4<2))
      {
        vertex[p].r = 0.0;
      }
      else
      {
        vertex[p].r=1.0;
      }

      vertex[p].g = (GLfloat)y/(GLfloat)GRIDH;
      vertex[p].b = 1.f-((GLfloat)x/(GLfloat)GRIDW+(GLfloat)y/(GLfloat)GRIDH)/2.f;
    }

  for(y=0;y<QUADH;y++)
    for(x=0;x<QUADW;x++)
    {
      p = 4*(y*QUADW + x);

      /* first quad */
      quad[p+0] = y    *GRIDW+x;    /* some point */
      quad[p+1] = y    *GRIDW+x+1;  /* neighbor at the right side */
      quad[p+2] = (y+1)*GRIDW+x+1;  /* upper right neighbor */
      quad[p+3] = (y+1)*GRIDW+x;    /* upper neighbor */
    }
}

double dt;
double p[GRIDW][GRIDH];
double vx[GRIDW][GRIDH], vy[GRIDW][GRIDH];
double ax[GRIDW][GRIDH], ay[GRIDW][GRIDH];



void initSurface( void )
{
  int    x, y;
  double dx, dy, d;

  for(y = 0; y<GRIDH; y++)
  {
    for(x = 0; x<GRIDW; x++)
    {
      dx = (double)(x-GRIDW/2);
      dy = (double)(y-GRIDH/2);
      d = sqrt( dx*dx + dy*dy );
      if(d < 0.1 * (double)(GRIDW/2))
      {
        d = d * 10.0;
        p[x][y] = -cos(d * (M_PI / (double)(GRIDW * 4))) * 100.0;
      }
      else
      {
        p[x][y] = 0.0;
      }
      vx[x][y] = 0.0;
      vy[x][y] = 0.0;
    }
  }
}


/* Draw view */
void draw_screen( void )
{
  /* Clear the color and depth buffers. */
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  /* We don't want to modify the projection matrix. */
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();

  /* Move back. */
  glTranslatef(0.0, 0.0, -zoom);
  /* Rotate the view */
  glRotatef(beta, 1.0, 0.0, 0.0);
  glRotatef(alpha, 0.0, 0.0, 1.0);

  //glDrawArrays(GL_POINTS,0,VERTEXNUM); /* Points only */
  glDrawElements(GL_QUADS, 4*QUADNUM, GL_UNSIGNED_INT, quad);
  //glDrawElements(GL_LINES, QUADNUM, GL_UNSIGNED_INT, quad);

  glfwSwapBuffers();
}


/* Initialize OpenGL */
void setup_opengl( void )
{
  /* Our shading model--Gouraud (smooth). */
  glShadeModel(GL_SMOOTH);

  /* Culling. */
  //glCullFace(GL_BACK);
  //glFrontFace(GL_CCW);
  //glEnable(GL_CULL_FACE);

  /* Switch on the z-buffer. */
  glEnable(GL_DEPTH_TEST);

  glEnableClientState(GL_VERTEX_ARRAY);
  glEnableClientState(GL_COLOR_ARRAY);
  glVertexPointer(3/*3 components per vertex (x,y,z)*/, GL_FLOAT, sizeof(struct Vertex), vertex);
  glColorPointer(3/*3 components per vertex (r,g,b)*/, GL_FLOAT, sizeof(struct Vertex), &vertex[0].r);  //Pointer to the first color
  glPointSize(2.0);

  /* Background color is black. */
  glClearColor(0, 0, 0, 0);
}


/* Modify the height of each vertex according to the pressure. */
void adjustGrid( void )
{
  int pos;
  int x, y;

  for(y = 0; y<GRIDH; y++)
  {
    for(x = 0; x<GRIDW; x++)
    {
      pos = y*GRIDW + x;
      vertex[pos].z = (float) (p[x][y]*(1.0/50.0));
    }
  }
}


/* Calculate wave propagation */
void calc( void )
{
  int    x, y, x2, y2;
  double time_step = dt * ANIMATION_SPEED;

  /* compute accelerations */
  for(x = 0; x < GRIDW; x++)
  {
    x2 = (x + 1) % GRIDW;
    for(y = 0; y < GRIDH; y++)
    {
      ax[x][y] = p[x][y] - p[x2][y];
    }
  }

  for(y = 0; y < GRIDH;y++)
  {
    y2 = (y + 1) % GRIDH;
    for(x = 0; x < GRIDW; x++)
    {
      ay[x][y] = p[x][y] - p[x][y2];
    }
  }

  /* compute speeds */
  for(x = 0; x < GRIDW; x++)
  {
    for(y = 0; y < GRIDH; y++)
    {
      vx[x][y] = vx[x][y] + ax[x][y] * time_step;
      vy[x][y] = vy[x][y] + ay[x][y] * time_step;
    }
  }

  /* compute pressure */
  for(x = 1; x < GRIDW; x++)
  {
    x2 = x - 1;
    for(y = 1; y < GRIDH; y++)
    {
      y2 = y - 1;
      p[x][y] = p[x][y] + (vx[x2][y] - vx[x][y] + vy[x][y2] - vy[x][y]) * time_step;
    }
  }
}


/* Handle key strokes */
void handle_key_down(GLFWwindow window, int key, int action)
{
  if( action != GLFW_PRESS )
  {
    return;
  }

  switch(key) {
    case GLFW_KEY_ESC:
      running = 0;
      break;
    case GLFW_KEY_SPACE:
      initSurface();
      break;
    case GLFW_KEY_LEFT:
      alpha+=5;
      break;
    case GLFW_KEY_RIGHT:
      alpha-=5;
      break;
    case GLFW_KEY_UP:
      beta-=5;
      break;
    case GLFW_KEY_DOWN:
      beta+=5;
      break;
    case GLFW_KEY_PAGEUP:
      if(zoom>1) zoom-=1;
      break;
    case GLFW_KEY_PAGEDOWN:
      zoom+=1;
      break;
    default:
      break;
  }
}


/* Callback function for window resize events */
void handle_resize( GLFWwindow window, int width, int height )
{
  float ratio = 1.0f;

  if( height > 0 )
  {
      ratio = (float) width / (float) height;
  }

  /* Setup viewport (Place where the stuff will appear in the main window). */
  glViewport(0, 0, width, height);

  /*
   * Change to the projection matrix and set
   * our viewing volume.
   */
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(60.0, ratio, 1.0, 1024.0);
}


/* Program entry point */
int main(int argc, char* argv[])
{
  /* Dimensions of our window. */
  int width, height;
  /* Style of our window. */
  int mode;
  /* Frame time */
  double t, t_old, dt_total;
  GLFWwindow window;

  /* Initialize GLFW */
  if(glfwInit() == GL_FALSE)
  {
    fprintf(stderr, "GLFW initialization failed\n");
    exit(-1);
  }

  /* Desired window properties */
  width  = 640;
  height = 480;
  mode   = GLFW_WINDOWED;

  glfwOpenWindowHint(GLFW_DEPTH_BITS, 16);

  /* Open window */
  window = glfwOpenWindow(width,height,mode);
  if (!window)
  {
    fprintf(stderr, "Could not open window\n");
    glfwTerminate();
    exit(-1);
  }

  /* Set title */
  glfwSetWindowTitle( window, "Wave Simulation" );

  glfwSwapInterval( 1 );

  /* Keyboard handler */
  glfwSetKeyCallback( window, handle_key_down );
  glfwEnable( window, GLFW_KEY_REPEAT );

  /* Window resize handler */
  glfwSetWindowSizeCallback( window, handle_resize );

  /* Initialize OpenGL */
  setup_opengl();

  /* Initialize simulation */
  initVertices();
  initSurface();
  adjustGrid();

  /* Initialize timer */
  t_old = glfwGetTime() - 0.01;

  /* Main loop */
  while(running)
  {
    /* Timing */
    t = glfwGetTime();
    dt_total = t - t_old;
    t_old = t;

    /* Safety - iterate if dt_total is too large */
    while( dt_total > 0.0f )
    {
        /* Select iteration time step */
        dt = dt_total > MAX_DELTA_T ? MAX_DELTA_T : dt_total;
        dt_total -= dt;

        /* Calculate wave propagation */
        calc();
    }

    /* Compute height of each vertex */
    adjustGrid();

    /* Draw wave grid to OpenGL display */
    draw_screen();

    glfwPollEvents();

    /* Still running? */
    running = running && glfwIsWindow( window );
  }

  glfwTerminate();

  return 0;
}