/****************************************************************************

 Copyright (C) 2002-2008 Gilles Debunne. All rights reserved.

 This file is part of the QGLViewer library version 2.3.6.

 http://www.libqglviewer.com - contact@libqglviewer.com

 This file may be used under the terms of the GNU General Public License
 versions 2.0 or 3.0 as published by the Free Software Foundation and
 appearing in the LICENSE file included in the packaging of this file.
 In addition, as a special exception, Gilles Debunne gives you certain
 additional rights, described in the file GPL_EXCEPTION in this package.

 libQGLViewer uses dual licensing. Commercial/proprietary software must
 purchase a libQGLViewer Commercial License.

 This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
 WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.

*****************************************************************************/

#include "simpleViewer.h"
#include "../interfaces/visitor.h"
#include "../glnodes/scenegroup.h"
#include "../glnodes/shapes.h"

#include <qopengl.h>
#include <QOpenGLShaderProgram>

#include <qopengl.h>

#include <iostream>
#include <stack>
#include <QMouseEvent>
#include <typeinfo>
using namespace std;

#define BUFFER_OFFSET(i) ((char *)NULL + (i))
#define GRID_SIZE 10;

namespace
{
	const int numVerticesCube = 12*3;
	const double frame_limit = 5;
	const double inc_mult = 5;
	const double inc_offset = 1.05;

    int m_lDirectionLocation;
    int m_normalMatrixLoc;
}

class SkyboxCamera : public qglviewer::Camera
{
    virtual qreal zNear() const { return 0.01; }
    virtual qreal zFar() const { return 100.0; }
};

Viewer::Viewer()
{
	activeColor = new QColor(255, 255, 255, 255);
	activeCell = nullptr;
	activeShape = 0;
}

Viewer::~Viewer()
{
	cleanup();
}

void Viewer::cleanup()
{
	makeCurrent();

	// Delete shaders
	delete m_program;
	m_program = 0;

	// Delete buffers
	glDeleteBuffers(NumBuffers, m_Buffers);
	glDeleteVertexArrays(NumVAOs, m_VAOs);

	doneCurrent();
}

void Viewer::drawSkybox()
{
    // Use the Skybox Shaders
    //skyboxRenderShaderProgram->bind();
    s_texture->bind();

    // Get projection and camera transformations
    QMatrix4x4 projectionMatrix;
    QMatrix4x4 modelViewMatrix;
    camera()->getProjectionMatrix(projectionMatrix);
    camera()->getModelViewMatrix(modelViewMatrix);

    // Increase size of skybox

    modelViewMatrix.scale(101);


    m_program->setUniformValue(m_mvMatrixLocation, modelViewMatrix);
    m_program->setUniformValue(m_isSkyLoc, true);

    int faces = floor(numVerticesCube/6);
    for(int i = 0; i < faces; i++){ // 6 vertexes par face
        glBindVertexArray(m_VAOs[VAO_Cube]);

        glDrawArrays(GL_TRIANGLES, i*6, 6);
    }
}

void Viewer::draw()
{
    drawSkybox();

	// Bind our vertex/fragment shaders
	m_program->bind();

	// Get projection and camera transformations
	QMatrix4x4 projectionMatrix;
	QMatrix4x4 modelViewMatrix;
	camera()->getProjectionMatrix(projectionMatrix);
	camera()->getModelViewMatrix(modelViewMatrix);

	// Prepare a transformation stack

    //  stack<QMatrix4x4> modelStack;

	//modelViewMatrix.rotate(45, 0, 1, 0);
	// uncomment this and the cube translation in init()
	// to see the cube spin
	/*rot = QQuaternion::fromAxisAndAngle(1, 0, 1, 90/60 * frame);
	modelViewMatrix.rotate(rot);
	projectionMatrix.translate(0, -5, 0);
	projectionMatrix.rotate(15, 1, 0, 0);//*/

    modelViewMatrix.rotate(30,0,1,0);

	m_program->setUniformValue(m_projMatrixLocation, projectionMatrix);
	m_program->setUniformValue(m_mvMatrixLocation, modelViewMatrix);

	// Traverse the Scene in order to draw its components

	modelStack.push(modelViewMatrix);
    root.accept(*this);
    frame++;
	update();
}

void Viewer::mouseMoveEvent(QMouseEvent* e) {
	//cout << "Viewer::mouseMoveEvent(QMouseEvent* e)" << endl;
	// Normal QGLViewer behavior.
    QGLViewer::mouseMoveEvent(e);
}

void Viewer::mousePressEvent(QMouseEvent* e) {
    // Auto Return, but old code left in as reference

	// TODO: figure out how to get this weird offset  ↓↓  frame position maybe?
	int x = this->x() + e->x() + 10;
	int y = this->parentWidget()->height() - e->y() + 21;
    std::cout << "--------------------------------------------------\nPicking shape at " << x << " (" << this->x() << " + " << e->x() << "), " << y << endl;
    std::cout << "Window geom: " << this->window()->size().width() << "w, " << this->window()->size().height() << "h" << endl;
    Shape* selectedShape = pickGeom(x, y);
    // OpenGL coords start at the window's bottom-left, not the frame's. ugh.

    QGLViewer::mousePressEvent(e);
}

void Viewer::deselect(){
	std::cout << "Deselecting cell " << activeCell << endl;
	if(activeCell != nullptr && activeCell->getChildren()->size()){
		std::cout << "Cell has children..." << endl;
		Shape* shape = dynamic_cast<Shape*> (activeCell->childAt(0));
		QColor newColor = shape->getColor();
		std::cout << newColor.Rgb << endl;
		newColor.setAlpha(180);
		shape->setColor(newColor);
	}else{
		std::cout << "Cell has no children, moving on" << endl;
	}
	this->update();
}

void Viewer::mouseReleaseEvent(QMouseEvent* e) {
	//cout << "Viewer::mouseReleaseEvent(QMouseEvent* e)" << endl;
    QGLViewer::mouseReleaseEvent(e);
}

void Viewer::init()
{
    SkyboxCamera *_cam = new SkyboxCamera();
    setCamera(_cam);
    //camera()->setType(qglviewer::Camera::PERSPECTIVE);
    //setMouseBinding(Qt::NoModifier, Qt::LeftButton, CAMERA, SCREEN_ROTATE);
    //setMouseBinding(Qt::AltModifier, Qt::LeftButton, CAMERA, NO_MOUSE_ACTION);
    //setMouseBinding(Qt::NoModifier, Qt::MouseButton(Qt::LeftButton + Qt::MidButton), CAMERA, NO_MOUSE_ACTION);
    //setMouseBinding(Qt::ControlModifier, Qt::MouseButton(Qt::LeftButton + Qt::MidButton), CAMERA, NO_MOUSE_ACTION);
    //setMouseBinding(Qt::ShiftModifier, Qt::MouseButton(Qt::LeftButton + Qt::MidButton), CAMERA, NO_MOUSE_ACTION);

	// Our scene will be from -5 to 5 in X and Y (the grid will be 10x10).
	setSceneRadius(5);
	showEntireScene();

	// Init OpenGL objects
	connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, &Viewer::cleanup);
	initializeOpenGLFunctions();

	// Init shaders & geometry
	initShaders();
	initGeometries();

	{
		Shape* cube = new Cube();
		cube->setColor(*activeColor);

		// uncomment this and the quaternion transformation in draw()
		// to see the cube rotate. This is how we can easily make the sun.
		// cube->transform.translate(3, 0, 0);

		SceneGroup *c = new SceneGroup();
		c->addChild(cube);
		root.addChild(c);
	}
}

void Viewer::initShaders()
{


	// Load vertex and fragment shaders
	m_program = new QOpenGLShaderProgram;
	colorPickerShaderProgram = m_program;
	if (!m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, "src/shaders/basicShader.vert")) {
		cerr << "Unable to load Shader" << endl
			 << "Log file:" << endl;
		qDebug() << m_program->log();
	}
	if (!m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, "src/shaders/basicShader.frag")) {
		cerr << "Unable to load Shader" << endl
			 << "Log file:" << endl;
		qDebug() << m_program->log();
	}
	m_program->link();
	m_program->bind();	// Note: This is equivalent to glUseProgram(programId());

	// Specify shader input paramters
	// The strings "vPosition", "mvMatrix", etc. have to match an attribute name in the vertex shader.
	if ((m_vPositionLocation = m_program->attributeLocation("vPosition")) < 0)
		qDebug() << "Unable to find shader location for " << "vPosition";

	if ((m_colorLocation = m_program->uniformLocation("color")) < 0)
		qDebug() << "Unable to find shader location for " << "color";

	if ((m_mvMatrixLocation = m_program->uniformLocation("mvMatrix")) < 0)
		qDebug() << "Unable to find shader location for " << "mvMatrix";

	if ((m_projMatrixLocation = m_program->uniformLocation("projMatrix")) < 0)
        qDebug() << "Unable to find shader location for " << "projMatrix";

    if ((m_lDirectionLocation = m_program->uniformLocation("lDirection")) < 0)
        qDebug() << "Unable to find m_shader location for" << "lDirection";

    if ((m_normalMatrixLoc = m_program->uniformLocation("normalMatrix")) < 0)
        qDebug() << "Unable to find m_shader location for" << "normalMatrix";

    if ((m_vNormalLocation = m_program->attributeLocation("vNormal")) < 0)
        qDebug() << "Unable to find m_shader location for" << "vNormal";

    if ((s_vUvLocation = m_program->attributeLocation("vUv")) < 0)
        qDebug() << "Unable to find shader location for " << "vUv";

    if ((m_isSkyLoc = m_program->uniformLocation("isSky")) < 0)
        qDebug() << "Unable to find m_shader location for" << "isSky";

    if ((m_isPhongLoc = m_program->uniformLocation("isPhong")) < 0)
        qDebug() << "Unable to find m_shader location for" << "isPhong";

    if ((m_lDirLoc = m_program->uniformLocation("lDirection")) < 0)
        qDebug() << "Unable to find m_shader location for" << "lDirection";


    /*
     * Adding Texture Shader
     */
     /*
    textureRenderShaderprogram = new QOpenGLShaderProgram;
    if (!textureRenderShaderprogram->addShaderFromSourceFile(QOpenGLShader::Vertex, "src/shaders/textureShader.vert")) {
        cerr << "Unable to load Shader" << endl
             << "Log file:" << endl;
        qDebug() << textureRenderShaderprogram->log();
    }
    if (!textureRenderShaderprogram->addShaderFromSourceFile(QOpenGLShader::Fragment, "src/shaders/textureShader.frag")) {
        cerr << "Unable to load Shader" << endl
             << "Log file:" << endl;
        qDebug() << textureRenderShaderprogram->log();
    }

    textureRenderShaderprogram->link();
*/
    /*
     * Adding Skybox Shaders
     *
     */
    skyboxRenderShaderProgram = new QOpenGLShaderProgram;
    if (!skyboxRenderShaderProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, "src/shaders/skyboxShader.vert")) {
        cerr << "Unable to load Shader" << endl
             << "Log file:" << endl;
        qDebug() << skyboxRenderShaderProgram->log();
    }
    if (!skyboxRenderShaderProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, "src/shaders/skyboxShader.frag")) {
        cerr << "Unable to load Shader" << endl
             << "Log file:" << endl;
        qDebug() << skyboxRenderShaderProgram->log();
    }

    skyboxRenderShaderProgram->link();

//    if ((s_texCoordsLocation = m_program->attributeLocation("texCoords")) < 0)
//        qDebug() << "Unable to find shader location for " << "vPosition";

    if ((s_vPositionLocation = skyboxRenderShaderProgram->attributeLocation("vPosition")) < 0)
        qDebug() << "Unable to find shader location for " << "vPosition";


    if ((s_projMatrixLocation = skyboxRenderShaderProgram->uniformLocation("projMatrix")) < 0)
        qDebug() << "Unable to find shader location for " << "projMatrix";


    if ((s_mvMatrixLocation = skyboxRenderShaderProgram->uniformLocation("mvMatrix")) < 0)
        qDebug() << "Unable to find shader location for " << "mvMatrix";

	s_texture = new QOpenGLTexture(QImage("src/data/skybox.jpg"));/*/
	s_texture = new QOpenGLTexture(QImage("src/data/uvLayoutGrid.png"));//*/
    s_texture->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
    s_texture->setMagnificationFilter(QOpenGLTexture::Linear);
}

// Creates the basic shapes in memory. We only have 3, so we just prep them all in advance.
void Viewer::initGeometries()
{
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	glEnable( GL_BLEND );
    //glEnable( GL_CULL_FACE );
	glFrontFace( GL_CCW );
    //glCullFace( GL_BACK );
	glClearColor(0.0,0.0,0.0,0.0);

	// Create our VertexArrays Objects and VertexBuffer Objects
	glGenVertexArrays(NumVAOs, m_VAOs);
	glGenBuffers(NumBuffers, m_Buffers);

	// Create our pentagone object, store its vertices on the graphic card, and
	// bind the data to the vPosition attribute of the shader
	GLfloat verticesCube[numVerticesCube][3] = { // 12 triangles == 6 squares
		//Front, if Z is towards us
		{ -0.5,  0.5, 0.5 }, { -0.5, -0.5, 0.5 }, {  0.5, -0.5, 0.5 },
		{  0.5, -0.5, 0.5 }, {  0.5,  0.5, 0.5 }, { -0.5,  0.5, 0.5 },
		//Back
		{ -0.5,  0.5, -0.5 }, {  0.5,  0.5, -0.5 }, {  0.5, -0.5, -0.5 },
		{  0.5, -0.5, -0.5 }, { -0.5, -0.5, -0.5 }, { -0.5,  0.5, -0.5 },
		//Right
		{ -0.5,  0.5,  0.5 }, { -0.5,  0.5, -0.5 }, { -0.5, -0.5, -0.5 },
		{ -0.5, -0.5, -0.5 }, { -0.5, -0.5,  0.5 }, { -0.5,  0.5,  0.5 },
		//Left
		{ 0.5,  0.5,  0.5 }, { 0.5, -0.5, -0.5 }, { 0.5,  0.5, -0.5 },
		{ 0.5, -0.5, -0.5 }, { 0.5,  0.5,  0.5 }, { 0.5, -0.5,  0.5 },
		//Top
		{ -0.5, 0.5,  0.5 }, {  0.5, 0.5,  0.5 }, {  0.5, 0.5, -0.5 },
		{  0.5, 0.5, -0.5 }, { -0.5, 0.5, -0.5 }, { -0.5, 0.5,  0.5 },
		//Bottom
		{ -0.5, -0.5,  0.5 }, {  0.5, -0.5, -0.5 }, {  0.5, -0.5,  0.5 },
		{  0.5, -0.5, -0.5 }, { -0.5, -0.5,  0.5 }, { -0.5, -0.5, -0.5 }
	};

	GLfloat uvs[numVerticesCube][2] = {

		{ 1.0, .25 }, { 1.0, .50 }, { .75, .50 },
		{ .75, .50 }, { .75, .25 }, { 1.0, .25 },

		{ .25, .25 }, { .50, .25 }, { .50, .50 },
		{ .50, .50 }, { .25, .50 }, { .25, .25 },

		{ .00, .25}, { .25, .25}, { .25, .50},
		{ .25, .50}, { .00, .50}, { .00, .25},

		{ .75, .25}, { .50, .50}, { .50, .25},
		{ .50, .50}, { .75, .25}, { .75, .50},

		{ .25, .00 }, { .49, .00 }, { .49, .25 },
		{ .49, .25 }, { .25, .25 }, { .25, .00 },

		{ .25, .50 }, { .49, .75 }, { .49, .50 },
		{ .49, .75 }, { .25, .50 }, { .25, .75 },

    };

    GLfloat normals[numVerticesCube][3] = {
        { 0.0, 0.0, 1.0 }, {0.0, 0.0, 1.0}, {0.0, 0.0, 1.0},
        { 0.0, 0.0, 1.0 }, {0.0, 0.0, 1.0}, {0.0, 0.0, 1.0},

        { 0.0, 0.0, -1.0 }, {0.0, 0.0, -1.0}, {0.0, 0.0, -1.0},
        { 0.0, 0.0, -1.0 }, {0.0, 0.0, -1.0}, {0.0, 0.0, -1.0},

        { -1.0, 0.0, 0.0 }, {-1.0, 0.0, 0.0}, {-1.0, 0.0, 0.0},
        { -1.0, 0.0, 0.0 }, {-1.0, 0.0, 0.0}, {-1.0, 0.0, 0.0},

        { 1.0, 0.0, 0.0}, {1.0, 0.0, 0.0}, {1.0, 0.0, 0.0},
        { 1.0, 0.0, 0.0 }, {1.0, 0.0, 0.0}, {1.0, 0.0, 0.0},

        { 0.0, 1.0, 0.0 }, {0.0, 1.0, 0.0}, {0.0, 1.0, 0.0},
        { 0.0, 1.0, 0.0 }, {0.0, 1.0, 0.0}, {0.0, 1.0, 0.0},

        { 0.0, -1.0, 0.0 }, {0.0, -1.0, 0.0}, {0.0, -1.0, 0.0},
        { 0.0, -1.0, 0.0 }, {0.0, -1.0, 0.0}, {0.0, -1.0, 0.0}
    };


    // Fill vertex VBO
    GLsizeiptr offsetVertices = 0;
    GLsizeiptr offsetNormals = sizeof(verticesCube);
    GLsizeiptr offsetUV = offsetNormals + sizeof(normals);
    GLsizeiptr dataSize = offsetUV + sizeof(uvs);


    glBindVertexArray(m_VAOs[VAO_Cube]);
//    glBindBuffer(GL_ARRAY_BUFFER, m_Buffers[VBO_Cube]);
    glGenBuffers(NumBuffers, m_Buffers);
    glBindBuffer(GL_ARRAY_BUFFER, m_Buffers[VBO_Cube]);
    glBufferData(GL_ARRAY_BUFFER, dataSize,
                 NULL, GL_STATIC_DRAW);

    glBufferSubData(GL_ARRAY_BUFFER, offsetVertices, sizeof(verticesCube), verticesCube);
    glBufferSubData(GL_ARRAY_BUFFER, offsetNormals, sizeof(normals), normals);
    glBufferSubData(GL_ARRAY_BUFFER, offsetUV, sizeof(uvs), uvs);

    glVertexAttribPointer(m_vPositionLocation, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
    glEnableVertexAttribArray(m_vPositionLocation);

    glVertexAttribPointer(m_vNormalLocation, 3, GL_FLOAT, GL_TRUE, 0, BUFFER_OFFSET(offsetNormals));
    glEnableVertexAttribArray(m_vNormalLocation);

    glVertexAttribPointer(s_vUvLocation, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(offsetUV));
    glEnableVertexAttribArray(s_vUvLocation);

}

void Viewer::visit(Cube &s)
{
	QMatrix4x4 modelViewMatrix = modelStack.top() * QMatrix4x4(s.transform);

	int faces = floor(numVerticesCube/6);
	for(int i = 0; i < faces; i++){ // 6 vertexes par face
        glBindVertexArray(m_VAOs[VAO_Cube]);
        m_program->setUniformValue(m_isSkyLoc, false);
        m_program->setUniformValue(m_isPhongLoc, true);
        m_program->setUniformValue(m_mvMatrixLocation, modelViewMatrix);
        m_program->setUniformValue(m_normalMatrixLoc, modelViewMatrix.normalMatrix());
		m_program->setUniformValue(m_colorLocation, s.getColor());

		glDrawArrays(GL_TRIANGLES, i*6, 6);
	}
}

void Viewer::visit(SceneGroup &s)
{
	// Build compound transformation matrix
	QMatrix4x4 currentMatrix = modelStack.top() * QMatrix4x4(s.transform);
	modelStack.push(currentMatrix);

	while(s.hasNext()) {
		// Get next leaf
		GlNode* current = s.getChild();

		// Draw/Traverse child
		current->accept(*this);
	}

	// Return model matrix to previous state
	modelStack.pop();
}

void Viewer::changeColor(QColor c){
	if(activeCell->getChildren()->size()){
		Shape* shape = dynamic_cast<Shape*> (activeCell->childAt(0));
		shape->setColor(c);
	}
	activeColor = new QColor(c);
	this->update();
}

Shape* Viewer::pickGeom(int x, int y){
	QMap<QRgb, Shape*> mapColorToShape;
	QColor c;
	QRgb startColor = 0xFF0001; // alpha must be 100%, glReadPixels doesn't resturn alpha
    unsigned char pixelData[3];
	// Traverse tree
	/* TODO: Make this recurse through SceneGroups, with like "populateMap(map, root, color)"
	 *       Right now it's fine because we have simple Minecraft rules
	 *       but could be important in the future
	 */
	QOpenGLShaderProgram *lastshader = m_program;

	//colorPickerShaderProgram->bind();

	std::cout << "Iterating through " << root.getChildren()->size() << "items"<<endl;

	for(int i = 0, l = root.getChildren()->size(); i<l; i++)
	{
		std::cout << "	iterating... " << i << endl;
		SceneGroup* current = dynamic_cast<SceneGroup*>(root.childAt(i));
		Shape* currentCube;

		if(current->getChildren()->size())
		{
			currentCube = dynamic_cast<Shape*>(current->childAt(0));
		}

		if(currentCube)
		{
			c.setRgb(startColor);
			mapColorToShape.insert(c.rgb(), currentCube);
			std::cout << "Setting " << currentCube << " to " << c.red() << " " << c.green() << " " << c.blue() << " " << c.alpha() << endl;
			currentCube->setColor(c);
			startColor+=6;
		}
	}

	update();

	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    // for debugging purposes
	/*QImage *im = new QImage(128,128, QImage::Format_RGB32);
	for(int i = 0; i< 128; i++){
		for(int j = 0; j< 128; j++){
			glReadPixels(x-64+i, y+64-j, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixelData);
            QColor* pickedColor = new QColor(pixelData[0], pixelData[1], pixelData[2]);
			if(i==64&&j==64) pickedColor->setRgba(0xFFFFFFFF);
            im->setPixelColor(i, j, pickedColor->rgb());
        }
    }
    im->save("./screenshot.bmp");//*/

    glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixelData);
    QColor* pickedColor = new QColor(pixelData[0], pixelData[1], pixelData[2]);
	unsigned int pickedInt = pickedColor->rgb();
	Shape* pickedShape = mapColorToShape.value(pickedInt - (pickedInt % 6) + 1);
    std::cout << "Picked Color: " << pickedColor->red() << " " << pickedColor->green() << " " << pickedColor->blue() << " " << pickedColor->alpha() << endl;
    std::cout << "Picked Shape: " << pickedShape << endl;



	//lastshader->bind();
    return pickedShape;
}
