// blast.cpp
// VERSION 2
// Glenn G. Chappell
// 1 Mov 2012
//
// History:
// - v2: added smooth camera reset
//
// For CS 381 Fall 2012
// Billboarding & Blending Demo

// 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/whereami.h"
                         // For function whereAmI
#include <cstdlib>
using std::rand;
using std::srand;
// Also using RAND_MAX
#include <cmath>
using std::exp;
using std::sqrt;
using std::acos;
#include <vector>
using std::vector;
#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)
bool shade;                    // True if shaders used
bool trans;                    // True if translucency simulated
bool bill;                     // True if billboarding done
bool running;                  // True if simulation running
double sideang;                // Side-to-side rotation for camera (deg)
double sideang_start = 0.;     // Intial value for above
double upang;                  // Upward rotation for camera (deg)
double upang_start = 8.;       // Initial value for above
bool resetingCamera;           // True if in process of camera reset
const int NUM_OBJECTS = 500;   // Number of objects
const double velfact = 7.0;    // Multiplier for velocity
const double gravity = 3.0;    // Gravity accel (world coords/sec/sec)
vector<vector<GLdouble> > locs;
                               // Object locations (world coords)
                               //  Each item is a vector of size 3
vector<vector<GLdouble> > vels;
                               // Object velocities (world coords/sec)
                               //  Each item is a vector of size 3

// 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 NUM_TEXTURES = 1;
GLuint texnames[NUM_TEXTURES];
const int IMG_WIDTH = 256, IMG_HEIGHT = IMG_WIDTH;
GLubyte teximage[IMG_HEIGHT][IMG_WIDTH][4];  // Texture temp storage
   // The image
   // 3rd subscript 0 = R, 1 = G, 2 = B, 3 = Alpha


// sphericalBillboard
// Does spherical billboarding.
// Given camera & object location, does glRotate to make
//  vector (0, 0, 1) point in direction from object to camera.
void sphericalBillboard(const vector<GLdouble> & campos,
                        const vector<GLdouble> & objpos)
{
    const double pi = 3.1415926535898;
    vector<GLdouble> v(3);  // Vector from object to camera
    for (int i = 0; i < 3; ++i)
        v[i] = campos[i] - objpos[i];
    double dot = v[2] / sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);
    double ang = acos(dot) * 180./pi;
    glRotated(ang, -v[1],v[0],0.);
}


// initObjects
// Set values of globals locs, vels to initial object
//  locations & velocities.
void initObjects()
{
    srand(2);
    vector<GLdouble> loc(3, 0.);  // Holds values to push
    vector<GLdouble> vel(3);      // Holds values to push
    vels.clear();
    locs.clear();
    for (int i = 0; i < NUM_OBJECTS; ++i)
    {
        locs.push_back(loc);
        do {
        vel[0] = double(rand())/RAND_MAX * 2.0 - 1.0;
        vel[1] = double(rand())/RAND_MAX;
        vel[2] = double(rand())/RAND_MAX * 2.0 - 1.0;
        } while (vel[0]*vel[0]+vel[1]*vel[1]+vel[2]*vel[2] > 1.);
        for (int j = 0; j < 3; ++j)
            vel[j] *= velfact;
        vels.push_back(vel);
    }
}


// drawObjects
// Draws all objects.
// Given camera position in world coordinates, for billboarding.
// Does billboarding if dobill is true.
void drawObjects(const vector<GLdouble> & campos,
                 bool dobill)
{
    for (size_t i = 0; i != locs.size(); ++i)
    {
        glPushMatrix();
        glTranslated(locs[i][0], locs[i][1], locs[i][2]);
        if (bill)
            sphericalBillboard(campos, locs[i]);
        glScaled(0.5, 0.5, 0.5);

        glBegin(GL_QUADS);
            glNormal3d(0., 0., 1.);
            glTexCoord2d(0., 0.);
            glVertex3d(-1., -1., 0.);
            glTexCoord2d(1., 0.);
            glVertex3d( 1., -1., 0.);
            glTexCoord2d(1., 1.);
            glVertex3d( 1.,  1., 0.);
            glTexCoord2d(0., 1.);
            glVertex3d(-1.,  1., 0.);
        glEnd();

        glPopMatrix();
    }
}


// myDisplay
// The GLUT display function
void myDisplay()
{
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    GLhandleARB theprog;  // CURRENTLY-used program object or 0 if none
    theprog = shade ? prog1 : 0;

    // Initialize transformation
    glLoadIdentity();

    // To keep shaders happy, position light sources
    GLfloat origin4[] = { 0.0f, 0.0f, 0.0f, 1.0f };

    glPushMatrix();
    glTranslated(-1., 1., 1.);
    glLightfv(GL_LIGHT0, GL_POSITION, origin4);
    GLfloat spotdir[] = { 1.0f, -1.0f, -1.0f };
    glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, spotdir);
    glPopMatrix();

    glPushMatrix();
    glTranslated(1., 1., 1.);
    glLightfv(GL_LIGHT1, GL_POSITION, origin4);
    glPopMatrix();

    // Camera transformation
    glTranslated(0., 0., -15.);
    glRotated(upang, 1.,0.,0.);
    glRotated(sideang, 0.,1.,0.);
    GLdouble cammat[16];  // Camera matrix
    glGetDoublev(GL_MODELVIEW_MATRIX, cammat);
    // Find camera position
    vector<GLdouble> campos = whereAmI(cammat);

    // Make program object (if any) active
    glUseProgramObjectARB(theprog);

    // Send values to shaders
    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, "mytex0");
        if (tloc != -1)
        {
            glUniform1i(tloc, 0);  // Send texture channel
        }
    }

    // Draw objects
    if (trans)
    {
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        glDisable(GL_DEPTH_TEST);
    }
    else
    {
        glDisable(GL_BLEND);
        glEnable(GL_DEPTH_TEST);
    }
    glColor4d(1.0, 0.5, 1.0, 0.25);
    drawObjects(campos, bill);

    // 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));
    glColor3d(0.7, 0.7, 0.7);     // Gray text
    BitmapPrinter p(20., winh-20., 20.);
    p.print("S        Toggle shaders       " + string(shade ? "ON" : "off"));
    p.print("T        Toggle translucency  " + string(trans ? "ON" : "off"));
    p.print("B        Toggle billboarding  " + string(bill ? "ON" : "off"));
    p.print("P        Pause/unpause motion");
    p.print("Space    Restart motion");
    p.print("Arrows   Move camera");
    p.print("Z        Reset camera");
    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?
    if (running)
    {
        for (int i = 0; i < NUM_OBJECTS; ++i)
        {
            vels[i][1] -= gravity * elapsedtime;
            for (int j = 0; j < 3; ++j)
                locs[i][j] += vels[i][j] * elapsedtime;
            if (locs[i][1] < 0.)
            {
                // Bounce object up & remove potential energy
                double vy = vels[i][1];
                double potential = vy*vy-2.*gravity*(2.*-locs[i][1]);
                if (potential < 0.)
                {
                    vels[i][1] = 0.;
                    locs[i][1] = 0.;
                }
                else
                {
                    vels[i][1] = sqrt(potential);
                    locs[i][1] = -locs[i][1];
                }

                // Damp velocity
                const double damp = 0.6;
                for (int j = 0; j < 3; ++j)
                   vels[i][j] *= damp;
            }
        }

        glutPostRedisplay();
    }

    // Reset camera?
    if (resetingCamera)
    {
        const double minmove = 0.001;  // Min move angle (deg)
        const double speedfact = 7.0;  // Base angle speed =
                                       //  this * angle difference
        const double maxspeed = 100.;  // Max angle speed (deg/sec)

        while (sideang > 180.)
           sideang -= 360.;
        while (sideang < -180.)
           sideang += 360.;

        // Compute diffs between current angles, start angles
        double sidediff = sideang_start - sideang;
        double sidesign = (sidediff < 0.) ? -1. : 1.;

        double updiff = upang_start - upang;
        double upsign = (updiff < 0.) ? -1. : 1.;

        // If move is small, just do it, and we are done
        if (sidediff*sidesign < minmove && updiff*upsign < minmove)
        {
            sideang = sideang_start;
            upang = upang_start;
            resetingCamera = false;
        }
        else
        {
            // Compute base angle speeds
            double sidespeed = speedfact * sidediff * sidesign;
            double upspeed = speedfact * updiff * upsign;

            // If speed too high, reduce both in proportion
            if (sidespeed > maxspeed || upspeed > maxspeed)
            {
                if (sidespeed > upspeed)
                {
                    sidespeed = maxspeed;
                    upspeed *= maxspeed / sidespeed;
                }
                else
                {
                    upspeed = maxspeed;
                    sidespeed *= maxspeed / upspeed;
                }
            }

            // Set the new camera angles
            sideang += sidespeed * sidesign * elapsedtime;
            upang += upspeed * upsign * 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 's':     // 'S': Toggle shaders
    case 'S':
        shade = !shade;
        glutPostRedisplay();
        break;
    case 't':     // 'T': Toggle translucency
    case 'T':
        trans = !trans;
        glutPostRedisplay();
        break;
    case 'b':     // 'B': Toggle billboarding
    case 'B':
        bill = !bill;
        glutPostRedisplay();
        break;
    case 'p':     // 'P': Pause/unpause
    case 'P':
        running = !running;
        break;
    case ' ':     // Space: Reset motion
        initObjects();
        running = true;
        glutPostRedisplay();
        break;
    case 'z':     // 'Z': Start camera reset
    case 'Z':
        resetingCamera = true;
        break;
    case '\'':    // Quote: toggle shader bool
    case '"':
        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();
    }
}


// mySpecial
// The GLUT special function
void mySpecial(int key, int x, int y)
{
    switch (key)
    {
    case GLUT_KEY_RIGHT:  // ->: move right
        resetingCamera = false;
        sideang -= 2.;
        glutPostRedisplay();
        break;
    case GLUT_KEY_LEFT:   // <-: move left
        resetingCamera = false;
        sideang += 2.;
        glutPostRedisplay();
        break;
    case GLUT_KEY_UP:     // up: move up
        resetingCamera = false;
        upang += 2.;
        if (upang > 90.)
            upang = 90.;
        glutPostRedisplay();
        break;
    case GLUT_KEY_DOWN:   // down: move down
        resetingCamera = false;
        upang -= 2.;
        if (upang < 0.)
            upang = 0.;
        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
}


// makeTexImage
// Make texture image in global teximage
// using given color (components 0..255)
void makeTexImage(GLubyte r, GLubyte g, GLubyte b)
{
    // Make Gaussian blob
    // All pixels given color, alpha max in center, 0 at edgews
    double k = 4.0;
    for (int i = 0; i < IMG_HEIGHT; ++i)
    {
        double x = double(i)/(IMG_HEIGHT-1) * 2.0 - 1.0;
        for (int j = 0; j < IMG_WIDTH; ++j)
        {
            double y = double(j)/(IMG_WIDTH-1) * 2.0 - 1.0;
            double alpha = 0.;
            double distsq = x*x+y*y;
            if (distsq < 1.)
                alpha = (exp(-k*distsq) - exp(-k))/(1.-exp(-k));
            teximage[i][j][0] = r;
            teximage[i][j][1] = g;
            teximage[i][j][2] = b;
            teximage[i][j][3] = GLubyte(alpha*255);
        }
    }
}


// init
// Initialize GL states & global data
// Called by main after window creation
void init()
{
    // Objects
    savetime = glutGet(GLUT_ELAPSED_TIME)/1000.;
    shade = true;
    trans = true;
    bill = true;
    running = true;
    sideang = sideang_start;
    upang = upang_start;
    resetingCamera = false;
    initObjects();

    shaderbool1 = true;
    shaderfloat1 = 1.0;

    // OpenGL Stuff

    // Shaders
    prog1 = makeProgramObjectFromFiles(vshader1fname, fshader1fname);

    // Textures
    glGenTextures(NUM_TEXTURES, texnames);

    // Make Texture
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, texnames[0]);

    makeTexImage(255, 255, 96);
    gluBuild2DMipmaps(GL_TEXTURE_2D,
        GL_RGBA,
        IMG_WIDTH, IMG_HEIGHT,
        GL_RGBA, // Make sure to get this right!
        GL_UNSIGNED_BYTE,
        &teximage[0][0][0]);
    glTexParameteri(GL_TEXTURE_2D,
        GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D,
        GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D,
        GL_TEXTURE_WRAP_S, GL_CLAMP);
    glTexParameteri(GL_TEXTURE_2D,
        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 - Billboarding & Blending Demo");

    // 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;
}

