// 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 <vector>
using namespace std;

class Renderer {

private:
  struct Vertex
  {
    float position[3];
    float color[4];
  };

public:
  float t;
private:
  enum { Pyramid, Cube, numVAOs };
  enum { PyramidAll, CubePos, CubeCol, CubeIndices, numVBOs };
  GLuint vaoID[numVAOs];
  GLuint bufID[numVBOs];
  int pyramidVertNo;
  int cubeIndicesNo;

public:
  // constructor
  Renderer() : t(0.0), pyramidVertNo(0), cubeIndicesNo(0) {}
  //destructor
  ~Renderer() {
    glDeleteVertexArrays(numVAOs, vaoID);
    glDeleteBuffers(numVBOs, bufID);
  }

public:
  void init() {
    glEnable(GL_DEPTH_TEST);

    // now we create 2 Vertex Array Objects (VAO):
    // one for a pyramid and one for a cube.
    // The pyramid uses one interleaved VBO
    // which is later drawn with "glDrawArrays",
    // while the cube uses 3 separate VBOs
    // which are later drawn with "glDrawElements"

    // create the Vertex Array Objects
    glGenVertexArrays(numVAOs, vaoID);

    // generating Vertex Buffer Objects (VBO)
    glGenBuffers(numVBOs, bufID);

    // specifying the pyramid VAO
    glBindVertexArray(vaoID[Pyramid]);

    float pyramidVertexData[] = {
      0.0f, 0.0f, 2.0f, 1.0f, 0.0f, 0.0f, 0.0f,
      -0.5f,-0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
      0.5f,-0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
      0.0f, 0.0f, 2.0f, 0.0f, 1.0f, 0.0f, 0.0f,
      0.5f,-0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
      0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
      0.0f, 0.0f, 2.0f, 0.0f, 0.0f, 1.0f, 0.0f,
      0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f,
      -0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f,
      0.0f, 0.0f, 2.0f, 1.0f, 1.0f, 0.0f, 0.0f,
      -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f,
      -0.5f,-0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f,
    };
    pyramidVertNo = 12;

    glBindBuffer(GL_ARRAY_BUFFER, bufID[PyramidAll]);
    glBufferData(GL_ARRAY_BUFFER, pyramidVertNo * sizeof(Vertex),
      pyramidVertexData, GL_STATIC_DRAW);

    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);

    // specifying the cube VAO
    glBindVertexArray(vaoID[Cube]);

    float cubePosData[] = {
      -1.0f,  1.0f,  1.0f,
      -1.0f, -1.0f,  1.0f,
      1.0f, -1.0f,  1.0f,
      1.0f,  1.0f,  1.0f,
      -1.0f,  1.0f, -1.0f,
      -1.0f, -1.0f, -1.0f,
      1.0f, -1.0f, -1.0f,
      1.0f,  1.0f, -1.0f
    };

    float cubeColorData[] = {
      1.0f, 1.0f, 0.0f,
      1.0f, 0.0f, 0.0f,
      0.0f, 1.0f, 0.0f,
      0.0f, 0.0f, 1.0f,
      1.0f, 1.0f, 0.0f,
      1.0f, 0.0f, 0.0f,
      0.0f, 1.0f, 0.0f,
      0.0f, 0.0f, 1.0f,
    };

    int cubeIndicesData[] =
    { 0, 1, 2, 3,
    0, 4, 5, 1,
    4, 7, 6, 5,
    2, 6, 7, 3,
    1, 5, 6, 2,
    3, 7, 4, 0
    };

    cubeIndicesNo = 24;

    // position
    glBindBuffer(GL_ARRAY_BUFFER, bufID[CubePos]);
    glBufferData(GL_ARRAY_BUFFER, sizeof(cubePosData), cubePosData, GL_STATIC_DRAW);
    glVertexPointer(3, GL_FLOAT, 0, NULL);
    glEnableClientState(GL_VERTEX_ARRAY);

    // color
    glBindBuffer(GL_ARRAY_BUFFER, bufID[CubeCol]);
    glBufferData(GL_ARRAY_BUFFER, sizeof(cubeColorData), cubeColorData, GL_STATIC_DRAW);
    glColorPointer(3, GL_FLOAT, 0, NULL);
    glEnableClientState(GL_COLOR_ARRAY);

    // indices
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bufID[CubeIndices]);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(cubeIndicesData), cubeIndicesData, GL_STATIC_DRAW);
  }


  void resize(int w, int h) {
    glViewport(0, 0, w, h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(60.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(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);
    // bind cube VAO
    glBindVertexArray(vaoID[Cube]);
    // render data
    glDrawElements(GL_QUADS, cubeIndicesNo, GL_UNSIGNED_INT, NULL);

    glTranslatef(0.0f, 0.0f, 1.0f);
    // bind pyramid VAO
    glBindVertexArray(vaoID[Pyramid]);
    // render data
    glDrawArrays(GL_TRIANGLES, 0, pyramidVertNo);
  }
};

//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);
}

int main(int argc, char** argv)
{
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
  glutInitWindowPosition(100, 100);
  glutInitWindowSize(320, 320);

  glutCreateWindow("Vertex Array Object (VAO) Demo");
  GLenum err = glewInit();
  if (GLEW_OK != err) {
    fprintf(stderr, "Glew error: %s\n", glewGetErrorString(err));
  }
  glutDisplayFunc(glutDisplay);
  //glutIdleFunc(glutDisplay);
  glutReshapeFunc(glutResize);

  renderer = new Renderer;
  renderer->init();

  glutTimerFunc(unsigned(20), timer, 0);

  glutMainLoop();
}