// This code example is created for educational purpose
// by Thorsten Thormaehlen (contact: www.thormae.de).
// It is distributed without any warranty.

#include <GL/glew.h>
#include <GL/freeglut.h> // we use glut here as window manager

#define _USE_MATH_DEFINES
#include <math.h>


#include <iostream>
#include <string>
#include <sstream>
#include <fstream>
#include <vector>
using namespace std;

class Renderer {

public:
  float t;
private:
  GLuint texID;
  std::vector <float> terrain;

public:
  // constructor
  Renderer() : t(0.0), texID(0) {}
  //destructor
  ~Renderer() {
    if(texID !=0) glDeleteTextures( 1, &texID);
  }

public:
  void init() {
    glEnable(GL_DEPTH_TEST);
    std::vector< std::string > filenames;
    filenames.push_back("deep_water.ppm");
    filenames.push_back("shallow_water.ppm");
    filenames.push_back("shore.ppm");
    filenames.push_back("fields.ppm");
    filenames.push_back("rocks.ppm");
    filenames.push_back("snow.ppm");
    texID = loadTexture3D(filenames);

    rebuildTerrain();
  }

  void resize(int w, int h) {
    glViewport(0, 0, w, h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective (30.0, (float)w/(float)h, 0.1, 50.0);
  }

  void display() {
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    // set camera
    gluLookAt(1.5, -1.0, 1.5, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0);
    // draw scene
    glRotatef(t, 0.0f, 0.0f, 1.0f);
    drawTerrain();
  }

  void rebuildTerrain() {

    //create random values
    int dim = 40;
    terrain.resize(dim*dim);

    for(int r=0; r < dim*dim; r++) {
      int rval = rand();
      terrain[r] = (fabs(float(rval))/float(RAND_MAX));
    }

    if(true) { // generate smooth terrain values
      std::vector<float> smoothTerrain(dim*dim);
      for(unsigned k=0; k < 5; k++){
        float maxVal = 0.0f;
        float minVal = 1.0f;
        for(int x = 0; x < dim; x++) {
          for(int y = 0; y < dim; y++) {
            if(x == 0 || x == dim-1) terrain[x*dim+y] = 0.0f;
            else if (y == 0 || y == dim-1) terrain[x*dim+y] = 0.0f;
            else {
              float a = 0.0f;
              int counter = 0;
              for(int s=-1; s <= 1; s++) {
                for(int r=-1; r <= 1; r++) {
                  a += terrain[(x+s)*dim+(y+r)];
                  counter++;
                }
              }
              float val = a / float(counter);
              smoothTerrain[x*dim+y] = val;
              if(val > maxVal) maxVal = val;
              if(val < minVal) minVal = val;
            }
          }
        }
        for(int r=0; r < dim*dim; r++) terrain[r] = (smoothTerrain[r] - minVal) / (maxVal-minVal);
      }
    }
  }

private:

  // returns a valid textureID on success, otherwise 0
  GLuint loadTexture3D(std::vector< std::string > &filenames) {

    unsigned width = 0;
    unsigned height = 0;
    unsigned depth = unsigned(filenames.size());
    int level = 0;
    int border = 0;
    std::vector<unsigned char> imgData;
    std::vector<unsigned char> data3d;
    unsigned prevWidth = 0;
    unsigned prevHeight = 0;

    for(unsigned i=0; i < depth; i++) {
      // load image data
      if(!loadPPMImageFlipped(filenames[i], width, height, imgData)) return 0;
      if(i != 0 && (prevWidth != width || prevHeight != height)) return 0;

      // pack 2D images subsequently into a large 3D buffer
      data3d.insert(data3d.end(), imgData.begin(), imgData.end());
      prevWidth = width;
      prevHeight = height;
    }

    // data is aligned in byte order
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    //request textureID
    GLuint textureID;
    glGenTextures( 1, &textureID);

    // bind texture
    glBindTexture(GL_TEXTURE_3D, textureID);

    //parameters the define how to warp the texture
    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER);
    GLfloat borderColor[4] = {0.0f,0.0f,0.0f,1.0f};
    glTexParameterfv(GL_TEXTURE_3D, GL_TEXTURE_BORDER_COLOR, borderColor);

    //define how to filter the texture
    glTexParameteri (GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri (GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

    //texture colors should replace the original color values
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); //GL_MODULATE

    // specify the 2D texture map
    glTexImage3D(GL_TEXTURE_3D, level, GL_RGB, width, height, depth, border, GL_RGB, GL_UNSIGNED_BYTE, &data3d[0]);

    // return unique texture identifier
    return textureID;
  }

  void drawTerrain() {
    glEnable(GL_TEXTURE_3D);

    glBindTexture(GL_TEXTURE_3D, texID);
    glColor3f(1.0f,0.0f,0.0f);
    unsigned dim = unsigned(sqrt(double(terrain.size())));
    float maxHeight = 0.2f;
    float texHeight = 0.9f;
    for(unsigned x = 1; x < dim; x++) {
      for(unsigned y = 1; y < dim; y++) {
        glBegin(GL_POLYGON);
        glTexCoord3f(float(x-1)/float(dim),
                     float(y-1)/float(dim),
                     terrain[(x-1)*dim+(y-1)]*texHeight);
        glVertex3f(float(x-1)/float(dim)-0.5f,
                   float(y-1)/float(dim)-0.5f,
                   terrain[(x-1)*dim+(y-1)]*maxHeight);
        glTexCoord3f(float(x)/float(dim),
                     float(y-1)/float(dim),
                     terrain[x*dim+(y-1)]*texHeight);
        glVertex3f(float(x)/float(dim)-0.5f,
                   float(y-1)/float(dim)-0.5f,
                    terrain[x*dim+(y-1)]*maxHeight);
        glTexCoord3f(float(x)/float(dim),
                   float(y)/float(dim),
                   terrain[x*dim+y]*texHeight);
        glVertex3f(float(x)/float(dim)-0.5f,
                   float(y)/float(dim)-0.5f,
                   terrain[x*dim+y]*maxHeight);
        glTexCoord3f(float(x-1)/float(dim),
                   float(y)/float(dim),
                   terrain[(x-1)*dim+y]*texHeight);
        glVertex3f(float(x-1)/float(dim)-0.5f,
                   float(y)/float(dim)-0.5f,
                   terrain[(x-1)*dim+y]*maxHeight);
        glEnd();
      }
    }
    glDisable(GL_TEXTURE_3D);
  }

  bool loadPPMImageFlipped(std::string &filename, unsigned &width, unsigned &height, std::vector<unsigned char> &imgData) {

    ifstream input(filename.c_str(), ifstream::in | ifstream::binary);
    if(!input) { // cast istream to bool to see if something went wrong
      cerr << "Can not find texture data file " << filename.c_str() << endl;
      return false;
    } 
    input.unsetf(std::ios_base::skipws);

    string line;
    input >> line >> std::ws;
    if (line != "P6") {
      cerr << "File is not PPM P6 raw format" << endl;
      return false;
    }

    width = 0;
    height = 0;
    unsigned depth = 0;
    unsigned readItems = 0;
    unsigned char lastCharBeforeBinary;

    while (readItems < 3) {
      input >> std::ws;
      if(input.peek() != '#') {
        if (readItems == 0) input >> width;
        if (readItems == 1) input >> height;
        if (readItems == 2) input >> depth >> lastCharBeforeBinary;
        readItems++;
      }else{ // skip comments
        std::getline(input, line);
      }
    }

    if(depth >= 256) {
	    cerr << "Only 8-bit PPM format is supported" << endl;
      return false;
    }

    unsigned byteCount = width * height * 3;
    imgData.resize(byteCount);
    input.read((char*)&imgData[0], byteCount*sizeof(unsigned char));

    // vertically flip the image because the image origin
    // in OpenGL is the lower-left corner
    unsigned char tmpData;
    for(unsigned y=0; y < height / 2; y++) {
      int sourceIndex = y * width * 3;
      int targetIndex = (height-1-y) * width *3;
      for(unsigned x=0; x < width*3; x++) {
          tmpData = imgData[targetIndex];
          imgData[targetIndex] = imgData[sourceIndex];
          imgData[sourceIndex] = tmpData;
          sourceIndex++;
          targetIndex++;
      }
    }

    return true;
  }
};

//this is a static pointer to a Renderer used in the glut callback functions
static Renderer *renderer;

//glut static callbacks start
static void glutResize(int w, int h) 
{
  renderer->resize(w,h);
}

static void glutDisplay() 
{
  renderer->display();
  glutSwapBuffers();
  glutReportErrors();
}

static void timer(int v) 
{
  float offset = 0.25f;
  renderer->t += offset;
  glutDisplay();
  glutTimerFunc(unsigned(20), timer, ++v);
}

static void glutKeyboard(unsigned char key, int x, int y) {
  bool redraw = false;
  switch(key) {
  case '1':
    renderer->rebuildTerrain();
    redraw = true;
    break;
  }
  if(redraw) {
    glutDisplay();
  }
}


int main(int argc, char **argv) 
{
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
  glutInitWindowPosition(100,100);
  glutInitWindowSize(320, 320);

  glutCreateWindow("Press 1 to generate a new random terrain");
  GLenum err = glewInit();
  if (GLEW_OK != err) {
    fprintf(stderr, "Glew error: %s\n", glewGetErrorString(err));
  }
  glutDisplayFunc(glutDisplay);
  //glutIdleFunc(glutDisplay);
  glutReshapeFunc(glutResize);
  glutKeyboardFunc(glutKeyboard);

  renderer = new Renderer;
  renderer->init();

  glutTimerFunc(unsigned(20), timer, 0);

  glutMainLoop();
}