// rendercube.cpp
// Glenn G. Chappell
// 8 Nov 2012
//
// For CS 381 Fall 2012
// Render a Cube Map

// OpenGL/GLUT includes - DO THESE FIRST
#include <cstdlib>       // Do this before GL/GLUT includes
using std::exit;
#ifndef __APPLE__
# include <GL/glew.h>
# include <GL/glut.h>    // GLUT stuff, includes OpenGL headers as well
#else
# include <GLEW/glew.h>
# include <GLUT/glut.h>  // Apple puts glut.h in a different place
#endif

// Other includes
#include "lib381/bitmapprinter.h"
                         // For class BitmapPrinter
#include "lib381/glslprog.h"
                         // For GLSL code-handling functions
#include "lib381/tshapes.h"
                         // For shape-drawing functions
#include "lib381/rtt.h"  // For class RTT
#include <iomanip>
using std::setprecision;
using std::fixed;
#include <sstream>
using std::ostringstream;
#include <string>
using std::string;
#include <iostream>
using std::cout;
using std::cerr;
using std::endl;


// Global variables
// Keyboard
const int ESCKEY = 27;         // ASCII value of Escape

// Window/viewport
const int startwinsize = 600;  // Start window width & height (pixels)
int winw = 1, winh = 1;        // Window width, height (pixels)
                               //  (Initialize to avoid spurious errors)

// Objects
double savetime;               // Time of previous movement (sec)
double rotangle;               // Rotation angle for object (deg)
const double rotspeed = 40.;   // Rotation speed (deg/sec)
bool rotateflag;               // True if object rotating
double lightrotang;            // Angle for light source 0 move (deg)
const double lightrotspeed = 50.;
                               // Speed for above (deg/sec)
bool lightrotflag;             // True if light source is moving
int whichdraw;                 // Which object to display
int numsubdivs;                // Number of subdivisions for object
const int minsubdivs = 1;      //  Minumum of above
const int maxsubdivs = 50;     //  Maximum of above
int shade;                     // 0: shader, 1: no shader
                               //  2: wireframe-no shader
bool grayback;                 // False: black background; true: gray

// Shaders
string vshader1fname;          // Filename for vertex shader source
string fshader1fname;          // Filename for fragment shader source
GLhandleARB prog1;             // GLSL Program Object

bool shaderbool1;              // Boolean to send to shaders
GLfloat shaderfloat1;          // Float to send to shaders

// Textures
const int IMG_WIDTH = 256, IMG_HEIGHT = IMG_WIDTH;
RTT cube0;


// drawCubeArray
// Draws 3-D array of small cubes, centered at origin
// Uses drawCube; subdivisions are not used.
void drawCubeArray(int tanloc = -1)
{
    const int arraysize = 10;
    for (int i = -arraysize; i <= arraysize; ++i)
    {
        for (int j = -arraysize; j <= arraysize; ++j)
        {
            for (int k = -arraysize; k <= arraysize; ++k)
            {
                glPushMatrix();
                glTranslated(2.*i, 2.*j, 2.*k);
                glScaled(0.125, 0.125, 0.125);
                drawCube(2.0, tanloc);
                glPopMatrix();
            }
        }
    }
}


// drawBigPlane
// Draws large square in x,y-plane, centered at origin
// Texture coordinates at edges are significantly larger
//  than 1 or smaller than 0.
void drawBigPlane(int tanloc = -1)
{
    const double h = 50.;  // Half side of square
    if (tanloc != -1)
        glVertexAttrib3dARB(tanloc, 1., 0., 0.);
    glBegin(GL_QUADS);
        glNormal3d(0., 0., 1.);
        glTexCoord2d(-h, -h);
        glVertex3d(-h, -h, 0.);
        glTexCoord2d( h, -h);
        glVertex3d( h, -h, 0.);
        glTexCoord2d( h,  h);
        glVertex3d( h,  h, 0.);
        glTexCoord2d(-h,  h);
        glVertex3d(-h,  h, 0.);
    glEnd();
}


// myDisplay
// The GLUT display function
void myDisplay()
{
    if (grayback)
        glClearColor(0.7f, 0.7f, 0.7f, 1.0f);
    else
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glEnable(GL_DEPTH_TEST);

    GLhandleARB theprog;  // CURRENTLY-used program object or 0 if none

    // Shaders used? Wireframe?
    switch (shade)
    {
        case 0:  // 0: filled polygons, use shaders, smooth
            glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
            theprog = prog1;
            break;
        case 1:  // 1: filled polygons, no shader
            glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
            theprog = 0;
            break;
        case 2:  // 2: wireframe, no shader
            glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
            theprog = 0;
            break;
    }

    // Camera transformation
    glLoadIdentity();
    glTranslated(0., 0., -4.);

    // Position light source 0 & draw ball there
    // Also give spot direction
    glPushMatrix();
    glTranslated(0.0, 0.0, 1.0);
    glRotated(lightrotang, 1.,0.,0.);
    glTranslated(-1., 1., 1.);
    GLfloat origin4[] = { 0.0f, 0.0f, 0.0f, 1.0f };
    glLightfv(GL_LIGHT0, GL_POSITION, origin4);
    GLfloat spotdir[] = { 1.0f, -1.0f, -1.0f };
    glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, spotdir);
    glUseProgramObjectARB(0);
    glColor3d(1., 1., 1.);
    glutSolidSphere(0.1, 20, 15);
    glPopMatrix();

    // Position light source 1
    glPushMatrix();
    glTranslated(1.0, 0.0, 1.0);
    glLightfv(GL_LIGHT1, GL_POSITION, origin4);
    glPopMatrix();

    // Make program object (if any) active
    glUseProgramObjectARB(theprog);

    // Send values to shaders
    GLint tanloc = -1;
    if (theprog)
    {
        GLint bloc = glGetUniformLocationARB(theprog, "myb1");
        if (bloc != -1)
        {
            glUniform1i(bloc, shaderbool1);
        }
        GLint floc = glGetUniformLocationARB(theprog, "myf1");
        if (floc != -1)
        {
            glUniform1f(floc, shaderfloat1);
        }
        GLint tloc = glGetUniformLocationARB(theprog, "mycube0");
        if (tloc != -1)
        {
            glUniform1i(tloc, 0);   // Send texture channel
        }

        // Get location of tangent-vector attribute vec, if available
        tanloc = glGetAttribLocationARB(theprog, "vtangent_in");
    }

    // Draw the appropriate object
    glRotated(rotangle, 1.,2.,0.);
    glColor3d(0.8, 0.4, 0.6);
    switch(whichdraw)
    {
    case 1:  // draw square, with varying subdivisions
        drawSquare(numsubdivs, tanloc);
        break;
    case 2:  // draw torus, with varying subdivisions
        //glutSolidTorus(0.3, 1.0, 3*numsubdivs, 10*numsubdivs);
        // Above has no texture coordinates <sigh>
        drawTorus(0.3, 1.0, 3*numsubdivs, 10*numsubdivs, tanloc);
        break;
    case 3:  // draw sphere, with varying subdivisions
        //glutSolidSphere(1., 4*numsubdivs, 3*numsubdivs);
        // Above has no texture coordinates <sigh>
        drawSphere(1., 4*numsubdivs, 3*numsubdivs, tanloc);
        break;
    case 4:  // draw that silly teapot
        if (tanloc != -1)
            glVertexAttrib3dARB(tanloc, 1., 2., 3.);
                // Kludge; teapot has no tangent vecs
        glFrontFace(GL_CW);  // Teapot has front of polygons facing in
        glutSolidTeapot(1.);
        glFrontFace(GL_CCW);
        break;
    case 5:  // draw array of cubes
        drawCubeArray(tanloc);
        break;
    case 6:  // draw cylinder, with varying subdivisions
        drawCylinder(numsubdivs, tanloc);
        break;
    case 7:  // draw large square, no subdivisions
        drawBigPlane(tanloc);
        break;
    }

    // Reset GL state
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

    // Draw documentation
    glUseProgramObjectARB(0);     // No shaders
    glDisable(GL_DEPTH_TEST);
    glLoadIdentity();
    glMatrixMode(GL_PROJECTION);  // Set up simple ortho projection
    glPushMatrix();
    glLoadIdentity();
    gluOrtho2D(0., double(winw), 0., double(winh));
    if (grayback)  // Draw text in a contrasting color
        glColor3d(0., 0., 0.);
    else
        glColor3d(0.7, 0.7, 0.7);
    BitmapPrinter p(20., winh-20., 20.);
    p.print("1234567  Choose object to draw");
    ostringstream os;
    os << numsubdivs;
    p.print("<- ->    Change # of subdivisions (" + os.str() + ")");
    string shstrs[3] = {
        "SHADER/no shader/wireframe",
        "shader/NO SHADER/wireframe",
        "shader/no shader/WIREFRAME"
    };
    p.print("W        " + shstrs[shade]);
    p.print("R        Toggle object rotation");
    p.print("L        Toggle light-source movement");
    p.print(string("Space    Toggle shader boolean (") +
        (shaderbool1 ? "true" : "false") + ")");
    ostringstream os2;
    os2 << fixed << setprecision(2) << shaderfloat1;
    p.print("[ ]      Change shader float (" + os2.str() + ")");
    p.print("B        Change background color");
    p.print("Esc      Quit");
    glPopMatrix();                // Restore prev projection
    glMatrixMode(GL_MODELVIEW);

    glutSwapBuffers();
}


// myIdle
// The GLUT idle function
void myIdle()
{
    // Compute elapsed time since last movement
    double currtime = glutGet(GLUT_ELAPSED_TIME)/1000.;
    double elapsedtime = currtime - savetime;
    savetime = currtime;
    if (elapsedtime > 0.1)
        elapsedtime = 0.1;

    // Move objects

    // Rotate main object
    if (rotateflag)
    {
        rotangle += rotspeed * elapsedtime;
        glutPostRedisplay();
    }

    // Move light source
    if (lightrotflag)
    {
        lightrotang += lightrotspeed * elapsedtime;
        glutPostRedisplay();
    }

    // Print OpenGL errors, if there are any (for debugging)
    static int error_count = 0;
    if (GLenum err = glGetError())
    {
        ++error_count;
        cerr << "OpenGL ERROR " << error_count << ": "
             << gluErrorString(err) << endl;
    }
}


// myKeyboard
// The GLUT keyboard function
void myKeyboard(unsigned char key, int x, int y)
{
    switch (key)
    {
    case ESCKEY:  // Esc: quit
        exit(0);
        break;
    case '1':     // Digits: select object
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
        whichdraw = key - '0';
        glutPostRedisplay();
        break;
    case 'w':     // W: cycle shader use & wireframe
        shade = (shade+1)%3;
        glutPostRedisplay();
        break;
    case 'W':
        shade = (shade+3-1)%3;
        glutPostRedisplay();
        break;
    case 'r':     // R: toggle object rotation
    case 'R':
        rotateflag = !rotateflag;
        break;
    case 'l':     // L: toggle light-source movement
    case 'L':
        lightrotflag = !lightrotflag;
        break;
    case ' ':     // Space: toggle shader bool
        shaderbool1 = !shaderbool1;
        glutPostRedisplay();
        break;
    case '[':     // '[': Decrease shader float
        shaderfloat1 -= 0.02f;
        if (shaderfloat1 <= 0.0f)
            shaderfloat1 = 0.0f;
        glutPostRedisplay();
        break;
    case ']':     // ']': Increase shader float
        shaderfloat1 += 0.02f;
        if (shaderfloat1 >= 1.0f)
            shaderfloat1 = 1.0f;
        glutPostRedisplay();
        break;
    case 'b':     // 'B': Change background color
    case 'B':
        grayback = !grayback;
        glutPostRedisplay();
        break;
    }
}


// mySpecial
// The GLUT special function
void mySpecial(int key, int x, int y)
{
    switch (key)
    {
    case GLUT_KEY_RIGHT:  // ->: increase subdivisions
        ++numsubdivs;
        if (numsubdivs > maxsubdivs)
            numsubdivs = maxsubdivs;
        glutPostRedisplay();
        break;
    case GLUT_KEY_LEFT:   // <-: decrease subdivisions
        --numsubdivs;
        if (numsubdivs < minsubdivs)
            numsubdivs = minsubdivs;
        glutPostRedisplay();
        break;
    }
}


// myReshape
// The GLUT reshape function
void myReshape(int w, int h)
{
    // Set viewport & save window dimensions in globals
    glViewport(0, 0, w, h);
    winw = w;
    winh = h;

    // Set up projection
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(60., double(w)/h, 0.5, 100.);

    glMatrixMode(GL_MODELVIEW);  // Always go back to model/view mode
}


// drawLittleScene
// Render a small scene to make into a texture. Given color is used in
//  some significant way.
// Does glClear, but not glutSwapBuffers
void drawLittleScene(GLubyte r, GLubyte g, GLubyte b)
{
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluOrtho2D(-1., 1., -1., 1.);
    glMatrixMode(GL_MODELVIEW);

    glDisable(GL_DEPTH_TEST);
    glUseProgramObjectARB(0);

    glClearColor(0.65f, 0.65f, 0.65f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    glBegin(GL_TRIANGLES);
        glColor3ub(r, g, b);
        glVertex2d(0., 0.8);
        glVertex2d(-0.4, 0.0);
        glColor3d(1.0, 0.7, 0.4);
        glVertex2d(0.4, 0.0);

        glColor3ub(r, g, b);
        glVertex2d(-0.8, -0.8);
        glVertex2d(0.0, -0.8);
        glColor3d(0.4, 1.0, 0.7);
        glVertex2d(-0.4, 0.0);

        glColor3ub(r, g, b);
        glVertex2d(0.8, -0.8);
        glVertex2d(0.4, 0.0);
        glColor3d(0.7, 0.4, 1.0);
        glVertex2d(0.0, -0.8);
    glEnd();
}


// init
// Initialize GL states & global data
// Called by main after window creation
void init()
{
    // Objects
    savetime = glutGet(GLUT_ELAPSED_TIME)/1000.;
    whichdraw = 1;
    numsubdivs = 10;
    shade = 0;
    rotangle = 0.;
    rotateflag = false;
    lightrotang = 0.;
    lightrotflag = false;
    grayback = true;

    shaderbool1 = true;
    shaderfloat1 = 1.0;

    // OpenGL Stuff

    // Shaders
    prog1 = makeProgramObjectFromFiles(vshader1fname, fshader1fname);

    // Textures

    // Make Texture 0
    glActiveTexture(GL_TEXTURE0);
    cube0.init(GL_TEXTURE_CUBE_MAP, IMG_WIDTH, IMG_HEIGHT);

    // Make cube-map faces
    cube0.beginRender(GL_TEXTURE_CUBE_MAP_POSITIVE_X);
    drawLittleScene(192, 64, 64);
    cube0.endRender(false);
    cube0.beginRender(GL_TEXTURE_CUBE_MAP_NEGATIVE_X);
    drawLittleScene(64, 192, 192);
    cube0.endRender(false);
    cube0.beginRender(GL_TEXTURE_CUBE_MAP_POSITIVE_Y);
    drawLittleScene(64, 192, 64);
    cube0.endRender(false);
    cube0.beginRender(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y);
    drawLittleScene(192, 64, 192);
    cube0.endRender(false);
    cube0.beginRender(GL_TEXTURE_CUBE_MAP_POSITIVE_Z);
    drawLittleScene(64, 64, 192);
    cube0.endRender(false);
    cube0.beginRender(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z);
    drawLittleScene(192, 192, 64);
    cube0.endRender();

    // Set params
    glTexParameteri(GL_TEXTURE_CUBE_MAP,
        GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_CUBE_MAP,
        GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP,
        GL_TEXTURE_WRAP_S, GL_CLAMP);
    glTexParameteri(GL_TEXTURE_CUBE_MAP,
        GL_TEXTURE_WRAP_T, GL_CLAMP);
}


int main(int argc, char ** argv)
{
    // Initialize OpenGL/GLUT
    glutInit(&argc, argv);
    getShaderFilenames(vshader1fname, fshader1fname, argc, argv);
        // Set shader source filenames. Done here, as opposed to in
        //  function init, so that we can use command-line arguments.
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);

    // Make a window
    glutInitWindowSize(startwinsize, startwinsize);
    glutInitWindowPosition(50, 50);
    glutCreateWindow("CS 381 - Render a Cube Map");

    // Init GLEW & check status
    if (glewInit() != GLEW_OK)
    {
        cerr << "glewInit failed" << endl;
        exit(1);
    }

    // Initialize GL states & register GLUT callbacks
    init();
    glutDisplayFunc(myDisplay);
    glutIdleFunc(myIdle);
    glutKeyboardFunc(myKeyboard);
    glutSpecialFunc(mySpecial);
    glutReshapeFunc(myReshape);

    // Do something
    glutMainLoop();

    return 0;
}

