// 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 {

private:
  struct Vertex
  {
    float position[3];
    float color[4];
    float texCoord[2];
    float normal[3];
  };

public:
  float t;
  int mode;
private:
  GLuint vertBufID;
  GLuint texID;
  int vertNo;

public:
  // constructor
  Renderer() : t(0.0), mode(0),
               vertBufID(0), texID(0), vertNo(0) {}
  //destructor
  ~Renderer() {
    if(vertBufID !=0) glDeleteBuffers( 1, &vertBufID);
    if(texID !=0) glDeleteTextures( 1, &texID);
  }

public:
  void init() {
    glEnable(GL_DEPTH_TEST);
    
    // generating VBO input data
    std::vector <float> data;
    int perVertexFloats = (3+4+2+3);
    loadVertexData(string("pushbike.vbo"), data, perVertexFloats);
    
    // The binary version of a float array is faster to read but less portable
    // becaue of endianness. The provided binary array 
    // "pushbike_binary.vbo" was created on a machine with Little-Endian byte order.
    // Use this function to load the binary version:
    // loadVertexDataBinary(string("pushbike_binary.vbo"), data, perVertexFloats);

    vertNo = int(data.size()) / perVertexFloats;
    
    // generating vertex VBO
    glGenBuffers(1, &vertBufID);
    glBindBuffer(GL_ARRAY_BUFFER, vertBufID);
    glBufferData(GL_ARRAY_BUFFER, vertNo*sizeof(Vertex),
                 &data[0], GL_STATIC_DRAW);
    
    std::string fileName("checkerboard.ppm");
    texID = loadTexture(fileName);
  }
  

  void resize(int w, int h) {
    glViewport(0, 0, w, h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective (30.0, (float)w/(float)h, 1.0, 10.0);
  }

  void display() {
    glClearColor(0.3f, 0.3f, 0.3f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    // camera orbits in the z=1.5 plane
    // and looks at origin
    double rad = M_PI / 180.0f * t;
    gluLookAt(1.5f*float(cos(rad)), 1.5f*float(sin(rad)), 1.5f, // eye
              0.0f, 0.0f, 0.0f, // look at
              0.0f, 0.0f, 1.0f); // up

    // activating VBO
    glBindBuffer(GL_ARRAY_BUFFER, vertBufID);
    int stride = sizeof(Vertex); // stride in bytes
    int offset = 0; // offset in bytes

    // position
    glVertexPointer(3, GL_FLOAT, stride, (const void*)(intptr_t)offset);
    glEnableClientState(GL_VERTEX_ARRAY);

    // color
    offset = 3 * sizeof(float);
    glColorPointer(4, GL_FLOAT, stride, (const void*)(intptr_t)offset);
    glEnableClientState(GL_COLOR_ARRAY);

    // texture
    offset = (3 + 4) * sizeof(float);
    glTexCoordPointer(2, GL_FLOAT, stride, (const void*)(intptr_t)offset);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);

    // normals
    offset = (3 + 4 + 2) * sizeof(float);
    glNormalPointer(GL_FLOAT, stride, (const void*)(intptr_t)offset);
    glEnableClientState(GL_NORMAL_ARRAY);

    // bind texture
    if(mode == 1) {
      glEnable(GL_TEXTURE_2D);
      glBindTexture(GL_TEXTURE_2D, texID);
    }else{
      glDisable(GL_TEXTURE_2D);
      glBindTexture(GL_TEXTURE_2D, 0);
    }

    // render data
    glDrawArrays(GL_TRIANGLES, 0, vertNo);

    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_COLOR_ARRAY);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);
    glDisable(GL_TEXTURE_2D);
  }

private:
   // returns a valid textureID on success, otherwise 0
  GLuint loadTexture(std::string &filename) {

    unsigned width;
    unsigned height;
    int level = 0;
    int border = 0;
    std::vector<unsigned char> imgData;

    // load image data
    if(!loadPPMImageFlipped(filename, width, height, imgData)) return 0;

    // data is aligned in byte order
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    //request textureID
    GLuint textureID;
    glGenTextures( 1, &textureID);

    // bind texture
    glBindTexture( GL_TEXTURE_2D, textureID);

    //define how to filter the texture (important but ignore for now)
    glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

    //texture colors should modulate the original color values
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

    // specify the 2D texture map
    glTexImage2D(GL_TEXTURE_2D, level, GL_RGB, width, height, border, GL_RGB, GL_UNSIGNED_BYTE, &imgData[0]);

    // return unique texture identifier
    return textureID;
  }

  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;
  }

  bool loadVertexData(std::string &filename, std::vector<float> &data, unsigned perVertexFloats) {
    // read vertex data from vbo file in plain text format
    ifstream input(filename.c_str());
    if(!input) { // cast istream to bool to see if something went wrong
      cerr << "Can not find vertex data file " << filename << endl;
      return false;
    } 

    int numFloats;
    double vertData;
    if(input >> numFloats) {
      if(numFloats > 0) {
        data.resize(numFloats);
        int i = 0;
        while(input >> vertData && i < numFloats) {
          // store it in the vector
          data[i] = float(vertData);
          i++;
        }
        if(i != numFloats || numFloats % perVertexFloats) return false;
      }
    }else{
      return false;
    }
    return true;
  }

  bool loadVertexDataBinary(std::string &filename, std::vector<float> &data, unsigned perVertexFloats) {
     // read vertex data from vbo file in binary format
    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 vertex data file " << filename << endl;
      return false;
    } else {
      unsigned int numFloats = 0;
      input.read((char*)(&numFloats), sizeof(unsigned int));
      if(!input) return false;
      data.resize(numFloats);
      input.read((char*)(&data[0]), numFloats * sizeof(float));
      if(!input || numFloats % perVertexFloats) return false;
    }
    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 = 1.0f;
  renderer->t += offset;
  glutDisplay();
  glutTimerFunc(unsigned(20), timer, ++v);
}

static void glutKeyboard(unsigned char key, int x, int y) {
  bool redraw = false;
  std::string modeStr;
  switch(key) {
    case '1':
      if(renderer->mode == 1) renderer->mode = 0;
      else renderer->mode = 1;
      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("ObjToVboExample");
  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();
}