#include <map>
#include <utility>
#include <vector>
#include <list>
#include <string>
#include <math.h>
#include <boost/thread.hpp>

#ifdef __APPLE__
    #include <GLUT/GLUT.h>
    #include <OpenGL/OpenGL.h>
#else
    #include <GL/glew.h>
    #include <GL/freeglut.h>
    #include <GL/freeglut_ext.h>
    #include <GL/glut.h>
    #include <GL/gl.h>
#endif

#include "Angel.h"
#include "Mesh.h"
#include "Grid.h"
#include "Piece.h"
//#define __DEBUG__

using namespace Angel;
using std::cout;
using std::endl;

Grid T_Grid(10,20,10);
Piece T_CurrentPiece(PieceType(rand()%5), &T_Grid);

int current = 0;

int mouseX;
int mouseY;
float angleX = 0.0;
float angleY = 0.0;

float cam_pitch = 0.0;
float cam_yaw = 0.0;

bool cam_zoom = false;
bool cam_pan = false;
bool cam_rotate = false;
GLfloat cam_angle = -90.0;

GLuint vPos;
GLuint vNormal;

color4 product;

color4 light_diffuse( 1.0, 1.0, 1.0, 1.0 );
color4 material_diffuse( 0.0, 0.0, 1.0, 1.0 );
point4 light_position( 20.0, 20.0, 20.0, 0.0 );

vec3 viewer_pos( T_Grid.getWidth(), T_Grid.getHeight()*1.75, 40 );
vec3 at = vec3( T_Grid.getWidth(),T_Grid.getHeight(), T_Grid.getDepth() );
vec3 up = cross(normalize(at-viewer_pos), cross(normalize(at-viewer_pos), vec3(0,-1,0)));

mat4 camera_view = LookAt(viewer_pos, at, up);

GLuint Projection;
GLuint ModelView;
GLuint CameraView;
GLuint main_window;
GLuint program;
GLuint buffer;
const int MAXLINE = 255;


void display();
void init();
void idle();
void gameloop();

boost::thread thread_1(gameloop);

/*
void orbit () {    
    glClearStencil(0);
    mat4 persp = Perspective ( 45.0, 1, 1.0, 3.0 );
    glUniformMatrix4fv(Projection, 1, GL_FALSE, persp);
    mat4 old_cam_view = camera_view;
    vec3 m_ctr_selected;
    
    if (current == 1)
        m_ctr_selected = Woman.getModelCenter();
    else
        m_ctr_selected = Terrain.getModelCenter();
    
    vec3 tmp_eye = vec3(0, 0, 15) + m_ctr_selected;
    GLfloat base_x = tmp_eye.x;
    GLfloat len = length(tmp_eye - m_ctr_selected);
    
    for (GLfloat i = 0.0; i <= 360.0; i += 1.0) {
        glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);     // clear the window
        cout << "tmp_eye: " << tmp_eye << endl;
        cout << "len: " << len << endl;
        cout << "m_ctr_selected: " << m_ctr_selected << endl;
        camera_view = LookAt(tmp_eye, m_ctr_selected, up);
        glUniformMatrix4fv( CameraView, 1, GL_TRUE, camera_view );
        
        DrawScene();
        
        glutSwapBuffers();
        tmp_eye.x = base_x + len*sin(i*M_PI/180.0);
        tmp_eye.z = len*cos(i*M_PI/180.0);
    }
    
    camera_view = old_cam_view;
    glutDisplayFunc(display);
}
*/

enum
{
    RESET_BUTTON
};

//  Declare the IDs of controls generating callbacks
void glui_callback (int control_id) {
    //  Behave based on control ID
    switch (control_id) {
        case RESET_BUTTON: {
            T_Grid.Clear();
            T_CurrentPiece = Piece(PieceType(rand()%5), &T_Grid);
        }
        /*
        case EXAMINE_BUTTON:
            //Camera->examine(CurrentlySelectedMesh);
            if (!current) {
                cout << "Please select an object." << endl;
                break;
            }
            glutDisplayFunc(orbit);            
            break;
         */
    }
}

//----------------------------------------------------------------------------

void init( void ) {
    glEnable(GL_STENCIL_TEST);
    glEnable(GL_DEPTH_TEST);
    
    // Create a vertex array object
    GLuint vao;
#ifdef __APPLE__
    glGenVertexArraysAPPLE( 1, &vao );
    glBindVertexArrayAPPLE( vao );
#else
    glGenVertexArrays( 1, &vao );
    glBindVertexArray( vao );
#endif
    
    glGenBuffers( 1, &buffer );
    glBindBuffer( GL_ARRAY_BUFFER, buffer );
    
    // Load shaders and use the resulting shader program
    program = InitShader( "vshader21.glsl", "fshader21.glsl" );
    glUseProgram( program );
    
    // Initialize the vertex position attribute from the vertex shader
    vPos = glGetAttribLocation( program, "vPosition" );
    glEnableVertexAttribArray( vPos );
    
    vNormal = glGetAttribLocation( program, "vNormal");
    glEnableVertexAttribArray(vNormal);
    
    // Initialize shader lighting parameters
    //point4 light_position( 0.0, 0.0, 1.0, 0.0 );
    color4 light_ambient( 0.5, 0.5, 0.5, 1.0 );
    //color4 light_diffuse( 0.65, 0.17, 0.17, 1.0 );
    //color4 light_diffuse( 1.0, 1.0, 1.0, 1.0 );
    color4 light_specular( 1.0, 1.0, 1.0, 1.0 );
    
    color4 material_ambient( 1.0, 1.0, 1.0, 1.0 );
    //color4 material_diffuse( 0.0, 0.0, 1.0, 1.0 );
    color4 material_specular( 1.0, 1.0, 1.0, 1.0 );
    //    float  material_shininess = 100.0;
    
    //color4 ambient_product = light_ambient * material_ambient;
    color4 diffuse_product = light_diffuse * material_diffuse;
    color4 specular_product = light_specular * material_specular;
    
    glUniform4fv( glGetUniformLocation(program, "LightAmbient"), 1, light_ambient );
    glUniform4fv( glGetUniformLocation(program, "MatAmbient"), 1, material_ambient );
    glUniform4fv( glGetUniformLocation(program, "DiffuseProduct"), 1, diffuse_product );
    glUniform4fv( glGetUniformLocation(program, "SpecularProduct"), 1, specular_product );
    glUniform4fv( glGetUniformLocation(program, "LightPosition"), 1, light_position );
    /*
    glUniform1f( glGetUniformLocation(program, "Shininess"), material_shininess );
    glUniform1i( glGetUniformLocation(program, "Shade"), ___Shade );
    glUniform1i( glGetUniformLocation(program, "SpecToggle"), ___Spec );
     */
    // Retrieve transformation uniform variable locations
    ModelView = glGetUniformLocation(program, "ModelView");
    Projection = glGetUniformLocation(program, "Projection");
    CameraView = glGetUniformLocation(program, "CameraView");
    
    glShadeModel(GL_SMOOTH);
    glEnable(GL_BLEND);
    glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    
    glClearColor( 1.0, 1.0, 1.0, 1.0 ); // black background
}

/*
//----------------------------------------------------------------------------

void transformations ( Mesh *model ) {
    product = light_diffuse * vec4(1.0, 0.0, 0.0, 1.0);
    
    if (y_increasing || x_increasing || y_decreasing || x_decreasing) {
        if (k_scale) {
            GLfloat epsilon = 1.0;
            if (y_increasing)
                epsilon += 0.05;
            if (y_decreasing)
                epsilon -= 0.05;
            model->mScale(epsilon, epsilon, epsilon);
        }
        else if (k_translate) {
            
            GLfloat epsilonX = 0.0;
            GLfloat epsilonY = 0.0;
            if (x_decreasing)
                epsilonX -= 0.3;
            if (x_increasing)
                epsilonX += 0.3;
            
            if (y_decreasing)
                epsilonY -= 0.3;
            if (y_increasing)
                epsilonY += 0.3;
            
            model->mTranslate(epsilonX, epsilonY, 0);
        }
        else if (k_rotate_h) {
            if (x_increasing)
                model->mRotate(0.0, 2.0, 0.0);
            else if (x_decreasing)
                model->mRotate(0.0, -2.0, 0.0);
        }
        else if (k_rotate_v) {
            if (y_increasing)
                model->mRotate(2.0, 0.0, 0.0);
            else if (y_decreasing)
                model->mRotate(-2.0, 0.0, 0.0);
        }
    }
}
 */

void display( void ) {
    glClearStencil(0);
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);     // clear the window

//    mat4 ortho = Ortho(-15.0, 15.0, -15.0, 15.0, -150.0, 150.0);
    mat4 persp = Perspective ( 45.0, 1, 1.0, 3.0 );

    glUniformMatrix4fv(Projection, 1, GL_FALSE, persp);
/*
    if (cam_zoom) {
        if (y_increasing) 
            viewer_pos.z *= 0.9;
        else if (y_decreasing)
            viewer_pos.z *= 1.1;
    }
    else if (cam_pan) {
        if (y_increasing) {
            viewer_pos += vec3(0, 1, 0);
            at += vec3(0, 1, 0);
            //change cam pitch to look down.
            cam_pitch = asin(viewer_pos.y / length(viewer_pos - at));
        }
        else if (y_decreasing) {
            viewer_pos -= vec3(0, 1, 0);
            at -= vec3(0, 1, 0);
            //change cam pitch to look up.
            cam_pitch = asin(viewer_pos.y / length(viewer_pos - at));
        }
        if (x_increasing) {
            viewer_pos += vec3(1, 0, 0);
            at += vec3(1, 0, 0);
            //change cam yaw to look left.
            cam_yaw = asin(viewer_pos.x / length(viewer_pos - at));
        }
        else if (x_decreasing) {
            viewer_pos -= vec3(1, 0, 0);
            at -= vec3(1, 0, 0);
            //change cam yaw to look right.
            cam_yaw = asin(viewer_pos.x / length(viewer_pos - at));
        }
#ifdef __DEBUG__
        std::cout << "pitch: " << cam_pitch << std::endl;
        std::cout << "yaw: " << cam_yaw << std::endl;
#endif
    }
    
    else if (cam_rotate) {
        GLfloat len = length(viewer_pos - at);
        if (cam_angle > 360.0) cam_angle -= 360.0;
        if (cam_angle < -360.0) cam_angle += 360.0;
        if (x_increasing) cam_angle += 1.0;
        else if (x_decreasing) cam_angle -= 1.0;
        at.x = len*cos(cam_angle*M_PI/180.0);
        at.z = len*sin(cam_angle*M_PI/180.0);
    }
*/
    camera_view = LookAt(viewer_pos, at, up);
    glUniformMatrix4fv( CameraView, 1, GL_TRUE, camera_view );
    
    if (!T_Grid.getLock()) {
        T_Grid.drawAll(program, vPos, vNormal, ModelView);
        glutSwapBuffers();
    }
}
/*
void motion(int x, int y){
	glutSwapBuffers();
}

void passive(int x, int y){        
    x_increasing = (mouseX < x);
    y_increasing = (mouseY > y);
    x_decreasing = (mouseX > x);
    y_decreasing = (mouseY < y);
    mouseX = x;
	mouseY = y;
}

void mouse(int button, int state, int x, int y) {
    if (state != GLUT_DOWN)
        return;
    
    // window_width = glutGet(GLUT_WINDOW_WIDTH);
    int window_height = glutGet(GLUT_WINDOW_HEIGHT);
    
    GLbyte color[4];
    GLfloat depth;
    GLuint index = 0;
    
    glReadPixels(x, window_height - y - 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, color);
    glReadPixels(x, window_height - y - 1, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth);
    glReadPixels(x, window_height - y - 1, 1, 1, GL_STENCIL_INDEX, GL_UNSIGNED_INT, &index);
    
    printf("Clicked on pixel %d, %d, color %02hhx%02hhx%02hhx%02hhx, depth %f, stencil index %u\n",
           x, y, color[0], color[1], color[2], color[3], depth, index);
    
    current = index;

	glutSwapBuffers();
}
//----------------------------------------------------------------------------
 */

void keyboard( unsigned char key, int x, int y ) {
    switch ( key ) {
        case 'w': { // 90 about x axis in -z direction
            T_CurrentPiece.Rotate(-90, 0, 0);
			T_Grid.drawAll(program, vPos, vNormal, ModelView);
            break;
        }
        case 's': { // 90 about x axis in z direction
            T_CurrentPiece.Rotate(90, 0, 0);
            T_Grid.drawAll(program, vPos, vNormal, ModelView);
            break;
        }
        case 'a': { // 90 about z axis in -x direction
            T_CurrentPiece.Rotate(0, 0, -90);
            if (!T_Grid.getLock()) T_Grid.drawAll(program, vPos, vNormal, ModelView);
            break;
        }
        case 'd': { // 90 about z axis in x direction
            T_CurrentPiece.Rotate(0, 0, 90);
            T_Grid.drawAll(program, vPos, vNormal, ModelView);
            break;
        }
        case 'q': { // 90 about y axis in -x direction
            T_CurrentPiece.Rotate(0, 90, 0);
            T_Grid.drawAll(program, vPos, vNormal, ModelView);
            break;
        }
        case 'e': { // 90 about y axis in x direction
            T_CurrentPiece.Rotate(0, -90, 0);
            T_Grid.drawAll(program, vPos, vNormal, ModelView);
            break;
        }
        case 'k': {
            viewer_pos += vec3( 0, 0, T_Grid.getDepth());
            break;
        }
        case 'i': {
            viewer_pos -= vec3( 0, 0, T_Grid.getDepth());
            break;
        }
        case 0x20: { // speed down movement (space)
            T_CurrentPiece.setPieceSpeed(T_CurrentPiece.getPieceSpeed()/2);
            break;
        }
        case 033: {
            exit( EXIT_SUCCESS );
            break;
        }
    }
}

void morekeys(int key, int x, int y) {
    switch (key) {
        case GLUT_KEY_DOWN:
            T_CurrentPiece.Translate(0, 0, 1);
            T_Grid.drawAll(program, vPos, vNormal, ModelView);
            break;
        case GLUT_KEY_UP:
            T_CurrentPiece.Translate(0, 0, -1);
            T_Grid.drawAll(program, vPos, vNormal, ModelView);
            break;
        case GLUT_KEY_RIGHT:
            T_CurrentPiece.Translate(1, 0, 0);
            T_Grid.drawAll(program, vPos, vNormal, ModelView);
            break;
        case GLUT_KEY_LEFT:
            T_CurrentPiece.Translate(-1, 0, 0);
            T_Grid.drawAll(program, vPos, vNormal, ModelView);
            break;
    }
}

//-------------------------------------------------------------------------
//  Idle Callback function.
//
//  Set the main_window as the current window to avoid sending the
//  redisplay to the GLUI window rather than the GLUT window.
//  Call the Sleep function to stop the GLUI program from causing
//  starvation.
//-------------------------------------------------------------------------
void idle () {
    glutSetWindow (main_window);
    if (!T_Grid.getLock())
        glutPostRedisplay();
}
//-------------------------------------------------------------------------

void gameloop() {
    for(;;) {   
        boost::this_thread::sleep(boost::posix_time::millisec(T_CurrentPiece.getPieceSpeed()));
        if (!T_CurrentPiece.Step()) {
            T_Grid.setLock(true);
            if (T_Grid.CheckLose())
                T_Grid.Clear();
            T_Grid.CheckLines();
            T_CurrentPiece = Piece(PieceType(rand()%5), &T_Grid);
            T_Grid.setLock(false);
        }
    }
}

int main( int argc, char **argv ) {
    srand(time(NULL));
    
    glutInit( &argc, argv );
    glutInitDisplayMode( GLUT_DOUBLE | GLUT_RGBA | GLUT_ALPHA | GLUT_DEPTH | GLUT_STENCIL );
    glutInitWindowSize( 1024, 1024 );
    
    main_window = glutCreateWindow( "3D Tetris" );
    
    //  pointer to a GLUI window
    GLUI * glui_window;
    
    //  Create GLUI window
    glui_window = GLUI_Master.create_glui_subwindow(main_window);
#ifndef __APPLE__
    glewInit();
#endif
    init();
    
    glutDisplayFunc( display );
    glutKeyboardFunc( keyboard );
    glutSpecialFunc( morekeys );
    
    GLUI_Master.set_glutIdleFunc (idle);
    GLUI_Master.set_glutDisplayFunc (display);
    glui_window->set_main_gfx_window (main_window);
    glui_window->add_button("Reset", RESET_BUTTON, glui_callback);
    glutSetWindow(main_window);

    glutMainLoop();
    thread_1.join();
    return 0;
}
