// 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 {

public:
  float t;
  int mode;
private:
  GLuint fboID;
  GLuint depthID;
  int fboTexSize;
  int width;
  int height;
public:
  // constructor
  Renderer() : t(0.0), mode(1),
  fboID(0), depthID(0),
  fboTexSize(512), width(0), height(0) {}
  
  // destructor
  ~Renderer() {
    if(fboID !=0) glDeleteFramebuffers(1, &fboID);
    if(depthID != 0) glDeleteTextures(1, &depthID);
  }
  
public:
  void init() {
    glEnable(GL_DEPTH_TEST);

    // create a frame buffer object
    glGenFramebuffers(1, &fboID);
    
    // bind the frame buffer
    glBindFramebuffer(GL_FRAMEBUFFER, fboID);
    
    // Generate render texture
    // this is not used (but some drivers require it)
    GLuint texID;
    glGenTextures (1, &texID);
    glBindTexture(GL_TEXTURE_2D, texID);
    
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    int level = 0;
    glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA, fboTexSize, fboTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
    
    // Attach the texture to the fbo
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texID, level);
    if(!checkFramebufferStatus()) exit(1);
    
    // Generate depth render texture
    glGenTextures (1, &depthID);
    glBindTexture(GL_TEXTURE_2D, depthID);
    
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexImage2D(GL_TEXTURE_2D, level, GL_DEPTH_COMPONENT, fboTexSize, fboTexSize, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
    
    // Attach the texture to the fbo
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthID, level);
    if(!checkFramebufferStatus()) exit(1);
    
    // unbind texture
    glBindTexture(GL_TEXTURE_2D, 0);
    //unbind fbo
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
  }
  
  void resize(int w, int h) {
    width = w;
    height = h;
    changeViewport(w, h);
  }
  
  void display() {
    
    if(mode > 0) {
      // bind the fbo
      glBindFramebuffer(GL_FRAMEBUFFER, fboID);
      changeViewport(fboTexSize,fboTexSize);
    }else{
      changeViewport(width, height);
    }
    
    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
    // render to attached fbo
    drawScene();
    
    if(mode == 1) {
      // unbind frame buffer
      glBindFramebuffer(GL_FRAMEBUFFER, 0);
      
      changeViewportOrtho(width, height);
      
      // using first render pass result as texture
      glEnable(GL_TEXTURE_2D);
      glBindTexture(GL_TEXTURE_2D, depthID);
      
      //draw quad with depth texture
      glColor3f(1.0f,0.0f,0.0f);
      glBegin(GL_POLYGON);
      glTexCoord2f(0.00f,0.00f); glVertex3f(-1.0f,-1.0f, 0.0f);
      glTexCoord2f(1.00f,0.00f); glVertex3f( 1.0f,-1.0f, 0.0f);
      glTexCoord2f(1.00f,1.00f); glVertex3f( 1.0f, 1.0f, 0.0f);
      glTexCoord2f(0.00f,1.00f); glVertex3f(-1.0f, 1.0f, 0.0f);
      glEnd();
      
      glDisable(GL_TEXTURE_2D);
    }  
  }
  
private:
  void changeViewport(int w, int h) {
    glViewport(0, 0, w, h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective (30.0, (float)w/(float)h, 7.0, 12.0);
  }
  
  void changeViewportOrtho(int w, int h) {
    float aspect = (float)w/(float)h;
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-1.0*aspect, 1.0*aspect, -1.0, 1.0, -1.0, 1.0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
  }
  
  
  void drawScene() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    // set camera
    gluLookAt(8.0, -2.0, 5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0);
    // draw scene
    glRotatef(t, 0.0f, 0.0f, 1.0f);
    drawTexturedCube();
  }
  
  bool checkFramebufferStatus() {
    GLenum status;
    status = (GLenum) glCheckFramebufferStatus(GL_FRAMEBUFFER);
    switch(status) {
    case GL_FRAMEBUFFER_COMPLETE:
      return true;
    case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
      printf("Framebuffer incomplete, incomplete attachment\n");
      return false;
    case GL_FRAMEBUFFER_UNSUPPORTED:
      printf("Unsupported framebuffer format\n");
      return false;
    case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
      printf("Framebuffer incomplete, missing attachment\n");
      return false;
    case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER:
      printf("Framebuffer incomplete, missing draw buffer\n");
      return false;
    case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER:
      printf("Framebuffer incomplete, missing read buffer\n");
      return false;
    }
    return false;
  }
  
  void drawTexturedCube() {
    
    glColor3f(1.0f,0.0f,0.0f);
    glBegin(GL_POLYGON);
    glTexCoord2f(0.00f,0.00f); glVertex3f(-1.0f, 1.0f, 1.0f);
    glTexCoord2f(1.00f,0.00f); glVertex3f(-1.0f,-1.0f, 1.0f);
    glTexCoord2f(1.00f,1.00f); glVertex3f( 1.0f,-1.0f, 1.0f);
    glTexCoord2f(0.00f,1.00f); glVertex3f( 1.0f, 1.0f, 1.0f);
    glEnd();
    glColor3f(1.0f,1.0f,0.0f);
    glBegin(GL_POLYGON);
    glTexCoord2f(0.00f,0.00f); glVertex3f(-1.0f, 1.0f,-1.0f);
    glTexCoord2f(1.00f,0.00f); glVertex3f(-1.0f,-1.0f,-1.0f);
    glTexCoord2f(1.00f,1.00f); glVertex3f(-1.0f,-1.0f, 1.0f);
    glTexCoord2f(0.00f,1.00f); glVertex3f(-1.0f, 1.0f, 1.0f);
    glEnd();
    glColor3f(0.0f,0.0f,1.0f);
    glBegin(GL_POLYGON);
    glTexCoord2f(0.00f,0.00f); glVertex3f( 1.0f, 1.0f, 1.0f);
    glTexCoord2f(1.00f,0.00f); glVertex3f( 1.0f,-1.0f, 1.0f);
    glTexCoord2f(1.00f,1.00f); glVertex3f( 1.0f,-1.0f,-1.0f);
    glTexCoord2f(0.00f,1.00f); glVertex3f( 1.0f, 1.0f,-1.0f);
    glEnd();
    glColor3f(1.0f,0.0f,1.0f);
    glBegin(GL_POLYGON);
    glTexCoord2f(0.00f,0.00f); glVertex3f(-1.0f, 1.0f,-1.0f);
    glTexCoord2f(1.00f,0.00f); glVertex3f(-1.0f, 1.0f, 1.0f);
    glTexCoord2f(1.00f,1.00f); glVertex3f( 1.0f, 1.0f, 1.0f);
    glTexCoord2f(0.00f,1.00f); glVertex3f( 1.0f, 1.0f,-1.0f);
    glEnd();
    glColor3f(0.0f,1.0f,1.0f);
    glBegin(GL_POLYGON);
    glTexCoord2f(0.00f,0.00f); glVertex3f(-1.0f, -1.0f, 1.0f);
    glTexCoord2f(1.00f,0.00f); glVertex3f(-1.0f, -1.0f,-1.0f);
    glTexCoord2f(1.00f,1.00f); glVertex3f( 1.0f, -1.0f,-1.0f);
    glTexCoord2f(0.00f,1.00f); glVertex3f( 1.0f, -1.0f, 1.0f);
    glEnd();
    glColor3f(0.0f,1.0f,0.0f);
    glBegin(GL_POLYGON);
    glTexCoord2f(0.00f,0.00f); glVertex3f( 1.0f, 1.0f,-1.0f);
    glTexCoord2f(1.00f,0.00f); glVertex3f( 1.0f,-1.0f,-1.0f);
    glTexCoord2f(1.00f,1.00f); glVertex3f(-1.0f,-1.0f,-1.0f);
    glTexCoord2f(0.00f,1.00f); glVertex3f(-1.0f, 1.0f,-1.0f);
    glEnd();
  }
};

//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;
  std::string modeStr;
  switch(key) {
     case '1':
       if(renderer->mode == 1) {
         renderer->mode = 0;
         modeStr = "first render pass result";
       } else {
         renderer->mode = 1;
         modeStr = "second render pass result";
       }
       redraw = true;
       break;
  }
  if(redraw) {
    glutDisplay();
    cout << modeStr << endl;
    glutSetWindowTitle(modeStr.c_str());
  }
}


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 toggle between first/second render pass");
  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();
}