// 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
    float vertexData[] = {
      0.0f, 0.0f, 2.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.5f, 1.0f, 0.0000f,-0.9701f, 0.2425f,
      -0.5f,-0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0000f,-0.9701f, 0.2425f,
      0.5f,-0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0000f,-0.9701f, 0.2425f,
      0.0f, 0.0f, 2.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.5f, 1.0f, 0.9701f, 0.0000f, 0.2425f,
      0.5f,-0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.9701f, 0.0000f, 0.2425f,
      0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.9701f, 0.0000f, 0.2425f,
      0.0f, 0.0f, 2.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f, 1.0f, 0.0000f, 0.9701f, 0.2425f,
      0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0000f, 0.9701f, 0.2425f,
      -0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0000f, 0.9701f, 0.2425f,
      0.0f, 0.0f, 2.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.5f, 1.0f,-0.9701f, 0.0000f, 0.2425f,
      -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f,-0.9701f, 0.0000f, 0.2425f,
      -0.5f,-0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f,-0.9701f, 0.0000f, 0.2425f
    };

    vertNo = 12;

    // generating vertex VBO
    glGenBuffers(1, &vertBufID);
    glBindBuffer(GL_ARRAY_BUFFER, vertBufID);
    glBufferData(GL_ARRAY_BUFFER, vertNo * sizeof(Vertex),
      vertexData, 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, 2.0, 10.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(3.0, -1.0, 4.5, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0);
    // draw scene
    glRotatef(t, 0.0f, 0.0f, 1.0f);

    // 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 == 0) {
      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;
  }
};

//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("VBO Stride Demo");
  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();
}