// flyer.cpp
// Glenn G. Chappell
// 27 Sep 2012
//
// For CS 381 Fall 2012
// Flight Simulator

// OpenGL/GLUT includes - DO THESE FIRST
#include <cstdlib>       // Do this before GL/GLUT includes
using std::exit;
#ifndef __APPLE__
# include <GL/glut.h>    // GLUT stuff, includes OpenGL headers as well
#else
# include <GLUT/glut.h>  // Apple puts glut.h in a different place
#endif

// Other includes
#include "lib381/bitmapprinter.h"
                         // For class BitmapPrinter
#include "lib381/whereami.h"
                         // For function whereAmI
#include <cstdlib>
using std::rand;
using std::srand;
// Also using RAND_MAX
#include <iomanip>
using std::setw;
using std::setprecision;
using std::fixed;
#include <sstream>
using std::ostringstream;
#include <cmath>
using std::sqrt;
#include <vector>
using std::vector;
#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)

const double angstep = 5.;     // Amt to rotate on single keypress (deg)
GLdouble cammat[16];           // Saved rotation matrix for object
const double speed = 1.;       // Fwd speed (camunits/sec)
const double angspeed = 0.1;   // Turning speed
                               //  (deg/sec per pixel of mouse movement
const double rollamt = 5;      // Roll amount (deg)

// Mouse
bool leftdown;                 // True if left mouse down
bool rightdown;                // True if right mouse down
int mx, my;                    // Mouse coords from window center,
                               //  x increasing right, y increasing up
                               //  (pixels)


// drawSquare
// Draws a filled square, using current GL states,
//  in the x,y plane, centered at the origin, aligned w/ x & y axes,
//  with side 2.
void drawSquare()
{
    glBegin(GL_QUADS);
        glVertex2d(-1., -1.);
        glVertex2d( 1., -1.);
        glVertex2d( 1.,  1.);
        glVertex2d(-1.,  1.);
    glEnd();
}


// drawCube
// Draws a cube, centered at origin, side length 2, axis aligned,
// each face a different color.
void drawCube()
{
    // +x face
    glColor3d(1., 0., 0.);
    glPushMatrix();
    glTranslated(1., 0., 0.);
    glRotated(90., 0., 1., 0.);
    drawSquare();
    glColor3d(0.7, 0., 0.);
    //drawAxis();
    glPopMatrix();

    // -x face
    glColor3d(0., 1., 1.);
    glPushMatrix();
    glTranslated(-1., 0., 0.);
    glRotated(90., 0., 1., 0.);
    drawSquare();
    glPopMatrix();

    // +y face
    glColor3d(0., 1., 0.);
    glPushMatrix();
    glTranslated(0., 1., 0.);
    glRotated(90., 1., 0., 0.);
    drawSquare();
    glColor3d(0., 0.7, 0.);
    //drawAxis();
    glPopMatrix();

    // -y face
    glColor3d(1., 0., 1.);
    glPushMatrix();
    glTranslated(0., -1., 0.);
    glRotated(90., 1., 0., 0.);
    drawSquare();
    glPopMatrix();

    // +z face
    glColor3d(0., 0., 1.);
    glPushMatrix();
    glTranslated(0., 0., 1.);
    drawSquare();
    glColor3d(0., 0., 0.7);
    //drawAxis();
    glPopMatrix();

    /*
    // -z face
    glColor3d(1., 1., 0.);
    glPushMatrix();
    glTranslated(0., 0., -1.);
    drawSquare();
    glPopMatrix();
    */
}


// drawCity
// Draw "city" on x,y-plane, ground centered at origin,
// buildings made of scaled cubes drawn by drawCube.
void drawCity()
{
    const int citysize = 20;  // Half width of city

    // Draw ground
    glColor3d(0.0, 0.5, 0.0);
    glBegin(GL_QUADS);
        glVertex3d( citysize,  citysize, 0.0);
        glVertex3d(-citysize,  citysize, 0.0);
        glVertex3d(-citysize, -citysize, 0.0);
        glVertex3d( citysize, -citysize, 0.0);
    glEnd();

    // Draw buildings, bases centered at (x,y,0) where x & y are odd
    for (int x = 1-citysize; x < citysize; x += 2)
    {
        for (int y = 1-citysize; y < citysize; y += 2)
        {
            double pmone = (double(rand())/RAND_MAX)*2.-1.;
                // Random number in [-1,1]
            double dist2 = x*x+y*y;
                // Square of ground distance from city center
            double ht = (3.+2.*pmone)/(dist2+50.)*100.;

            // Draw building based at (x, y, 0), base side = 1, height = ht
            glPushMatrix();
            glTranslated(x, y, ht/2.);
            glScaled(0.5, 0.5, ht/2.);
            drawCube();
            glPopMatrix();
        }
    }
}


// myDisplay
// The GLUT display function
void myDisplay()
{
    // Restart pseudorandom numbers
    srand(1);

    glClearColor(0.7f, 0.7f, 0.7f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // Draw objects

    // Set up transformation
    glEnable(GL_DEPTH_TEST);
    glLoadIdentity();
    glMultMatrixd(cammat);

    // Get camera position
    vector<GLdouble> campos = whereAmI(cammat);

    // Draw scene
    drawCity();

    // Draw documentation
    glDisable(GL_DEPTH_TEST);
    glLoadIdentity();
    glMatrixMode(GL_PROJECTION);  // Set up simple ortho projection
    glPushMatrix();
    glLoadIdentity();
    gluOrtho2D(0., double(winw), 0., double(winh));
    glColor3d(0., 0., 0.);        // Black text
    BitmapPrinter p(20., winh-20., 20.);
    p.print("Mouse          Turn");
    p.print("Mouse buttons  Forward/reverse");
    p.print("<- ->          Roll");
    p.print("Z              Reset position");
    p.print("Esc            Quit");
    p.print("Camera position:");
    ostringstream ostr;
    ostr << "(";
    for (int i = 0; i < 3; ++i)
    {
        if (i > 0)
            ostr << ",  ";
        ostr << fixed << setw(10) << setprecision(4) << campos[i];
    }
    ostr << ")";
    p.print(ostr.str());
    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;

    // Fly!
    glLoadIdentity();
    double turnamt = sqrt(mx*mx+my*my) * angspeed * elapsedtime;
    glRotated(turnamt, -my, mx, 0.);
    if (leftdown)
        glTranslated(0., 0., speed * elapsedtime);
    if (rightdown)
        glTranslated(0., 0., -speed * elapsedtime);
    glMultMatrixd(cammat);
    glGetDoublev(GL_MODELVIEW_MATRIX, cammat);
    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;
    }
}


// resetCamMat
// Reset Camera transformation matrix to initial value
void resetCamMat()
{
    glLoadIdentity();
    glRotated(-90., 1.,0.,0.);
    glTranslated(0., 12., -0.3);
    glGetDoublev(GL_MODELVIEW_MATRIX, cammat);
}


// myKeyboard
// The GLUT keyboard function
void myKeyboard(unsigned char key, int x, int y)
{
    switch (key)
    {
    case ESCKEY:  // Esc: quit
        exit(0);
        break;
    case 'Z':     // Z: reset rotations
    case 'z':
        resetCamMat();
        glutPostRedisplay();
    }
}


// mySpecial
// The GLUT special function
void mySpecial(int key, int x, int y)
{
    switch (key)
    {
    case GLUT_KEY_LEFT:   // <- left roll
        glLoadIdentity();
        glRotated(-rollamt, 0.,0.,1.);
        glMultMatrixd(cammat);
        glGetDoublev(GL_MODELVIEW_MATRIX, cammat);
        glutPostRedisplay();
        break;
    case GLUT_KEY_RIGHT:  // -> right roll
        glLoadIdentity();
        glRotated(rollamt, 0.,0.,1.);
        glMultMatrixd(cammat);
        glGetDoublev(GL_MODELVIEW_MATRIX, cammat);
        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
    // Standard perspective projection
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(60., double(w)/h, 0.01, 100.);

    glMatrixMode(GL_MODELVIEW);  // Always go back to model/view mode
}


// saveMouse
// Given mouse pos in GLUT format, save it in globals mx, my
//  in pixels from window center, increasing up & to right.
void saveMouse(int x, int y)
{
    mx = x - winw/2;
    my = winh/2 - y;
}


// myMouse
// The GLUT mouse function
void myMouse(int button, int state, int x, int y)
{
    // Save left & right mouse button states in globals
    if (button == GLUT_LEFT_BUTTON)
    {
        leftdown = (state == GLUT_DOWN);
    }
    else if (button == GLUT_RIGHT_BUTTON)
    {
        rightdown = (state == GLUT_DOWN);
    }

    // Save mouse position in globals
    saveMouse(x, y);
}


// myMouse
// The GLUT motion function
void myMotion(int x, int y)
{
    // Save mouse position in globals
    saveMouse(x, y);
}

// myMouse
// The GLUT passiveMotion function
void myPassiveMotion(int x, int y)
{
    // Save mouse position in globals
    saveMouse(x, y);
}


// init
// Initialize GL states & global data
// Called by main after window creation
void init()
{
    // Objects
    savetime = glutGet(GLUT_ELAPSED_TIME)/1000.;

    glMatrixMode(GL_MODELVIEW);
    resetCamMat();

    // Mouse
    leftdown = false;
    rightdown = false;
    mx = 0;
    my = 0;

    // OpenGL Stuff
    glLineWidth(2.0);
}


int main(int argc, char ** argv)
{
    // Initialize OpenGL/GLUT
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);

    // Make a window
    glutInitWindowSize(startwinsize, startwinsize);
    glutInitWindowPosition(50, 50);
    glutCreateWindow("CS 381 - Object Manipulation Improved");

    // Initialize GL states & register GLUT callbacks
    init();
    glutDisplayFunc(myDisplay);
    glutIdleFunc(myIdle);
    glutKeyboardFunc(myKeyboard);
    glutSpecialFunc(mySpecial);
    glutReshapeFunc(myReshape);
    glutMouseFunc(myMouse);
    glutMotionFunc(myMotion);
    glutPassiveMotionFunc(myPassiveMotion);

    // Do something
    glutMainLoop();

    return 0;
}

