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

 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 <math.h>

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

#include <qopengl.h>

#include <iostream>
#include <stack>
#include <QMouseEvent>
#include <typeinfo>
#include <time.h>

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;

	const int numRowSphere = 20;
	const int numColSphere = numRowSphere+2;
	const int numVerticesSphere = numColSphere * numRowSphere + 2;
	const int numTriSphere = numColSphere*(numRowSphere-1)*2 + 2*numColSphere;

	int m_lDirectionLocation;
	int m_normalMatrixLoc;

	QVector3D sun = QVector3D(0,-1,0);
	QMatrix4x4 sunRotate;

	SceneGroup* selection;
    PickedGeom selectedObj;

	int currentPoint = 0; // VERY lazy way of tracking light balls
}

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

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

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

    glActiveTexture(GL_TEXTURE0);
	m_program->bind();
	s_texture->bind();
	// Get projection and camera transformations
	QMatrix4x4 projectionMatrix;
	QMatrix4x4 modelViewMatrix;
	camera()->getProjectionMatrix(projectionMatrix);
	camera()->getModelViewMatrix(modelViewMatrix);

	// Increase size of skybox
	modelViewMatrix = QMatrix4x4(modelViewMatrix.normalMatrix());
	modelViewMatrix.scale(100);

	float colorMult = 0.2 + std::fabs(0.8 * cos(std::fmod(angle_mult * frame + 300, 360) / 360 * M_PI));

	m_program->setUniformValue(m_skyMultLoc, colorMult);
	m_program->setUniformValue(m_mvMatrixLocation, modelViewMatrix);
	m_program->setUniformValue(m_isSkyLoc, true);
	m_program->setUniformValue(m_drawTextLoc, true);
	m_program->setUniformValue(m_isLightLoc, false);
	m_program->setUniformValue(m_colorLocation, *(new QColor(0, 0, 0, 0)));

	glCullFace( GL_FRONT );
	glBindVertexArray(m_VAOs[VAO_Cube]);
	glDrawArrays(GL_TRIANGLES, 0, 36);
}

void Viewer::draw()
{
	// Bind our vertex/fragment shaders
	m_program->bind();

	glClear(GL_COLOR_BUFFER_BIT);

	if(!isPickingActivated)
		drawSkybox();
	glCullFace( GL_BACK );


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

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

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

	// Adjust sun position

    float rotAngle = std::fmod(angle_mult * 25 * frame, 360);
    sunRotate.rotate(rotAngle, 0, 0, 1);
	//sunRotate.rotate(15, 1, 0, 0);

    m_program->setUniformValue(m_lDirLoc, (QMatrix4x4(modelViewMatrix.normalMatrix())*sunRotate * sun));
	selection->transform.setToIdentity();
	selection->transform.rotate(rotAngle, 0, 1, 0);

	// Traverse the Scene in order to draw its components

	modelStack.push(modelViewMatrix);
	root.accept(*this);

	drawUi();

	sunRotate.setToIdentity();
	//float rotAngle = (frame * angle_mult) % 360;
}

void Viewer::drawUi(){

	glCullFace(GL_BACK);
	glClear(GL_DEPTH_BUFFER_BIT); // make ui always on top

	QMatrix4x4 projectionMatrix;
	QMatrix4x4 uiViewMatrix;

	uiViewMatrix.translate(0, -1.45, -4);
	camera()->getProjectionMatrix(projectionMatrix);

	float colorMult = 0.2 + std::fabs(0.8 * cos(std::fmod(angle_mult * frame + 300, 360) / 360 * M_PI));

	m_program->setUniformValue(m_isSkyLoc, false);
	m_program->setUniformValue(m_drawTextLoc, true);
	m_program->setUniformValue(m_isLightLoc, true);

	glBindVertexArray(m_VAOs[VAO_Cube]);
	uiViewMatrix.scale(.25, .25, .01);
	uiViewMatrix.translate(-TEX_LENGTH/2.0, 0, 0);
	for(int i = 0; i<TEX_LENGTH; i++)
    {
        glActiveTexture(GL_TEXTURE0);
		TexturePrograms[i]->bind();
		uiViewMatrix.translate(1.2, 0, 0);
		if(i == selectedTexture)
			uiViewMatrix.scale(1.2);
		m_program->setUniformValue(m_mvMatrixLocation, uiViewMatrix);
		glDrawArrays(GL_TRIANGLES, 0, 6);
		if(i == selectedTexture)
			uiViewMatrix.scale(1/1.2);
	}

}

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

void Viewer::mousePressEvent(QMouseEvent* e) {
	// 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() - 23;

	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;

    PickedGeom selectedGeom = pickGeom(x, y);

	if(!selectedGeom.position.isIdentity() && e->modifiers().testFlag(Qt::ControlModifier))
	{
		std::cout << "Selecting cube" << endl;
        if(selectedObj.shape != nullptr && selectedObj.shape->getParent() != nullptr) {
			// Remove previous selection
			selectedObj.shape->getParent()->getChildren()->erase(
						std::remove(selectedObj.shape->getParent()->getChildren()->begin(),
									selectedObj.shape->getParent()->getChildren()->end(),
									selection),
						selectedObj.shape->getParent()->getChildren()->end());
		}
		selectedObj.shape = selectedGeom.shape;
		selectedObj.shape->getParent()->addChild(selection);
        cubeSelected(true);
	}

	if(!selectedGeom.position.isIdentity() && e->modifiers().testFlag(Qt::ShiftModifier))
	{
		Cube* c = new Cube;
		c->setType(selectedTexture);
		SceneGroup* container = new SceneGroup;
		container->transform = selectedGeom.position;
		container->addChild(c);
		root.addChild(container);
	}
	else{
		QGLViewer::mousePressEvent(e);
	}
}

void Viewer::mouseReleaseEvent(QMouseEvent* e) {
	//cout << "Viewer::mouseReleaseEvent(QMouseEvent* e)" << endl;
	//m_program->setUniformValue(m_isPickingModeLoc, false);
	m_program->bind();
	m_program->setUniformValue(m_isPickingModeLoc, false);
	isPickingActivated = false;
	QGLViewer::mouseReleaseEvent(e);
}

void Viewer::keyPressEvent(QKeyEvent* e) {
	switch ( e->key() ) {
	case Qt::Key_W:
		break;
	case Qt::Key_A:
		break;
	case Qt::Key_S:
		break;
	case Qt::Key_D:
		break;
	case Qt::Key_Q:
		break;
	case Qt::Key_E:
		break;

	case Qt::Key_1: // 0x31; yaay, fall-through cases!
	case Qt::Key_2: // 0x32
	case Qt::Key_3: // 0x33
	case Qt::Key_4: // 0x34
	case Qt::Key_5: // 0x35
	case Qt::Key_6: // 0x36
		selectedTexture = e->key() - 0x31;
		if(selectedObj.shape != nullptr ){
			selectedObj.shape->setType(selectedTexture);
		}
		break;

	default: e->ignore(); break;
	}
}

void Viewer::keyReleaseEvent(QKeyEvent* e){
	switch ( e->key() ) {
	case Qt::Key_W:
		break;
	case Qt::Key_A:
		break;
	case Qt::Key_S:
		break;
	case Qt::Key_D:
		break;
	case Qt::Key_Q:
		break;
	case Qt::Key_E:
		break;

	default: e->ignore(); break;
	}
}

void Viewer::init()
{
	identityMatrix.setToIdentity();
	SkyboxCamera *_cam = new SkyboxCamera();
	setCamera(_cam);

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

	// Init OpenGL objects
	//setMouseBinding(Qt::NoModifier, Qt::LeftButton, CAMERA, LOOK_AROUND, true);
	connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, &Viewer::cleanup);
	initializeOpenGLFunctions();

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


	{
        // Default non existant cube
        selectedObj.shape = nullptr;

		QColor* c1 = new QColor(255, 0, 255, 255);
		QColor* c2 = new QColor(0, 255, 255, 255);
		QColor* c3 = new QColor(255, 255, 0, 255);

		Shape* s1 = new Sphere();
		Shape* s2 = new Sphere();
		Shape* s3 = new Sphere();

		selection = new SceneGroup();
		selection->addChild(s1);
		selection->addChild(s2);
		selection->addChild(s3);

		//SceneGroup *sc1 = new SceneGroup();
		//sc1->addChild(selection);
        //root.addChild(sc1);

		s1->transform.rotate(360 * 1/3,0,1,0);
		s1->transform.translate(0.3,1,0);
		s1->transform.scale(0.05);
		s1->setColor(*c1);
		s2->transform.rotate(360 * 2/3,0,1,0);
		s2->transform.translate(0.3,1,0);
		s2->transform.scale(0.05);
		s2->setColor(*c2);
		s3->transform.rotate(360 * 3/3,0,1,0);
		s3->transform.translate(0.3,1,0);
		s3->transform.scale(0.05);
		s3->setColor(*c3);

		for(int i = 0; i < 10; i++){
			for(int j = 0; j < 10; j++){
				Shape* cube = new Cube();
				SceneGroup *sc = new SceneGroup();
				cube->setColor(*c1);
                cube->setType(TEX_WOODFLOOR);
				sc->addChild(cube);
				sc->transform.translate(-5+i, 0, -5+j);
				root.addChild(sc);
			}
		}

	}
	startTime = clock();
	startAnimation();
}

void Viewer::resetSphereColors(){
    QColor* c1 = new QColor(255, 0, 255, 255);
    QColor* c2 = new QColor(0, 255, 255, 255);
    QColor* c3 = new QColor(255, 255, 0, 255);

    int i = 0;/*

    QColor* colors[3] = {c1, c2, c3};

    while(selection.hasNext()) {
        Shape currentSphere = dynamic_cast<Shape*>(selection->getChild());

        currentSphere->setColor(colors[i++]);
    }*/

}

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_useNormalMap = m_program->uniformLocation("useNormalMap")) < 0)
            qDebug() << "Unable to find shader location for " << "useNormalMap";

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

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

		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";

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

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

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

		if ((m_point1Loc = m_program->uniformLocation("pointLight[0]")) < 0)
			qDebug() << "Unable to find m_shader location for" << "pointLight[0]";

		if ((m_point2Loc = m_program->uniformLocation("pointLight[1]")) < 0)
			qDebug() << "Unable to find m_shader location for" << "pointLight[1]";

		if ((m_point3Loc = m_program->uniformLocation("pointLight[2]")) < 0)
			qDebug() << "Unable to find m_shader location for" << "pointLight[2]";

		if ((m_c1Loc = m_program->uniformLocation("pointLightCol[0]")) < 0)
			qDebug() << "Unable to find m_shader location for" << "pointLightCol[0]";

		if ((m_c2Loc = m_program->uniformLocation("pointLightCol[1]")) < 0)
			qDebug() << "Unable to find m_shader location for" << "pointLightCol[1]";

		if ((m_c3Loc = m_program->uniformLocation("pointLightCol[2]")) < 0)
			qDebug() << "Unable to find m_shader location for" << "pointLightCol[2]";

		if ((m_isPickingModeLoc = m_program->uniformLocation("isPickingMode")) < 0)
			qDebug() << "Unable to find m_shader location for" << "isPickingMode" << m_program->log();

	m_program->setUniformValue(m_isPhongLoc, true);
	m_program->setUniformValue(m_drawTextLoc, false);
	m_program->setUniformValue(m_isPickingModeLoc, false);
    m_program->setUniformValue(m_useNormalMap, true);

    m_program->setUniformValue(m_texColor, 0);
    m_program->setUniformValue(m_texNormal, 1);


	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);

	// load remaining textures
    for(int i = 0; i<TEX_LENGTH; i++){
        std::cout << "Binding " << TexturePaths[i].toStdString() << endl;
        TexturePrograms[i] = new QOpenGLTexture(QImage(TexturePaths[i]));
        TexturePrograms[i]->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
        TexturePrograms[i]->setMagnificationFilter(QOpenGLTexture::Linear);

        std::cout << "Binding " << NormalPaths[i].toStdString() << endl;
        TexturePrograms[TEX_LENGTH + i] = new QOpenGLTexture(QImage(NormalPaths[i]));
        TexturePrograms[TEX_LENGTH + i]->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
        TexturePrograms[TEX_LENGTH + i]->setMagnificationFilter(QOpenGLTexture::Linear);
	}//*/
}

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}
	};

	GLfloat sphereVertices[numVerticesSphere][3];
	GLfloat sphereNormals[numVerticesSphere][3];
	GLuint sphereIndices[numTriSphere * 3][3];

	// Create Sphere
	// Generate surrounding vertices
	unsigned int v = 0;
	float thetaInc = 2.0f*3.14159265f / static_cast<float>(numColSphere);
	float phiInc = 3.14159265f / static_cast<float>(numRowSphere+1);
	for (int row=0; row<numRowSphere; ++row)
	{
		float phi = 3.14159265f - (static_cast<float>(row+1) * phiInc);
		for (int col=0; col<numColSphere; ++col, ++v)
		{
			float theta = col*thetaInc;
			sphereVertices[v][0] = 0.5*sin(theta)*sin(phi);
			sphereVertices[v][1] = 0.5*cos(phi);
			sphereVertices[v][2] = 0.5*cos(theta)*sin(phi);

			sphereNormals[v][0] = sphereVertices[v][0]*2.0f;	// Multiply by 2 because sphere radius is 0.5
			sphereNormals[v][1] = sphereVertices[v][1]*2.0f;
			sphereNormals[v][2] = sphereVertices[v][2]*2.0f;
		}
	}

	// Generate cap vertices
	sphereVertices[numColSphere*numRowSphere+0][0] = 0.0f;
	sphereVertices[numColSphere*numRowSphere+0][1] = -0.5f;
	sphereVertices[numColSphere*numRowSphere+0][2] = 0.0f;

	sphereVertices[numColSphere*numRowSphere+1][0] = 0.0f;
	sphereVertices[numColSphere*numRowSphere+1][1] = 0.5f;
	sphereVertices[numColSphere*numRowSphere+1][2] = 0.0f;

	sphereNormals[numColSphere*numRowSphere+0][0] = 0.0f;
	sphereNormals[numColSphere*numRowSphere+0][1] = -1.0f;
	sphereNormals[numColSphere*numRowSphere+0][2] = 0.0f;

	sphereNormals[numColSphere*numRowSphere+1][0] = 0.0f;
	sphereNormals[numColSphere*numRowSphere+1][1] = 1.0f;
	sphereNormals[numColSphere*numRowSphere+1][2] = 0.0f;

	// Generate surrounding indices (faces)
	unsigned int tri = 0;
	for (int row=0; row<numRowSphere-1; ++row)
	{
		unsigned int rowStart = row*numColSphere;
		unsigned int topRowStart = rowStart + numColSphere;

		for (int col=0; col<numColSphere; ++col, tri += 2)
		{
			// Compute quad vertices
			unsigned int v = rowStart + col;
			unsigned int vi = (col<numColSphere-1) ? v+1 : rowStart;
			unsigned int vj = topRowStart + col;
			unsigned int vji = (col<numColSphere-1) ? vj+1 : topRowStart;

			// Add to indices
			sphereIndices[tri+0][0] = v;
			sphereIndices[tri+0][1] = vi;
			sphereIndices[tri+0][2] = vj;
			sphereIndices[tri+1][0] = vi;
			sphereIndices[tri+1][1] = vji;
			sphereIndices[tri+1][2] = vj;
		}
	}

	// Generate cap indices (faces)
	for (int col=0; col<numColSphere; ++col, tri += 2)
	{
		sphereIndices[tri+0][0] = numColSphere*numRowSphere;
		sphereIndices[tri+0][1] = (col<numColSphere-1) ? col+1 : 0;
		sphereIndices[tri+0][2] = col;

		unsigned int rowStart = (numRowSphere-1)*numColSphere;
		sphereIndices[tri+1][0] = numColSphere*numRowSphere+1;
		sphereIndices[tri+1][1] = rowStart + col;
		sphereIndices[tri+1][2] = (col<numColSphere-1) ? (rowStart + col + 1) : rowStart;
	}

	// Prep buffers
	glGenBuffers(NumBuffers, m_Buffers);

	// Fill cube 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]);
	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);

	/*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]);
	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(sizeof(offsetNormals)));
	glEnableVertexAttribArray(m_vNormalLocation);

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

	// Fill Sphere VBO


	GLsizeiptr offsetSphereV = 0;
	GLsizeiptr offsetSphereN = offsetSphereV + sizeof(sphereVertices);
	GLsizeiptr sphereDataSize = offsetSphereN + sizeof(sphereNormals);


	glBindVertexArray(m_VAOs[VAO_Sphere]);
	glBindBuffer(GL_ARRAY_BUFFER, m_Buffers[VBO_Sphere]);
	glBufferData(GL_ARRAY_BUFFER, sphereDataSize,
				 NULL, GL_STATIC_DRAW);

	glBufferSubData(GL_ARRAY_BUFFER, offsetSphereV, sizeof(sphereVertices), sphereVertices);
	glBufferSubData(GL_ARRAY_BUFFER, offsetSphereN, sizeof(sphereNormals), sphereNormals);

	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);

	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_Buffers[EBO_Sphere]);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(sphereIndices), sphereIndices, GL_STATIC_DRAW);
}

void Viewer::initBuffers(){

	glGenRenderbuffers(1, m_RenderBuffers);
	glBindRenderbuffer(GL_RENDERBUFFER, m_RenderBuffers[RenderBuffer_Main]);
	glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB565, 1920, 1080);
	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_RenderBuffers[RenderBuffer_Main]);

}


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

    glBindVertexArray(m_VAOs[VAO_Cube]);

    glActiveTexture(GL_TEXTURE0);
    TexturePrograms[s.getType()]->bind();
    glActiveTexture(GL_TEXTURE1);
    TexturePrograms[TEX_LENGTH + s.getType()]->bind();

	m_program->setUniformValue(m_isSkyLoc, false);
	m_program->setUniformValue(m_drawTextLoc, true);
	m_program->setUniformValue(m_isLightLoc, false);
	m_program->setUniformValue(m_mvMatrixLocation, modelViewMatrix);
	m_program->setUniformValue(m_normalMatrixLoc, modelViewMatrix.normalMatrix());

	int faces = floor(numVerticesCube/6);
	for(int i =0; i<faces; i++)
	{
		faceColor->setRgb(s.getColor().rgb() + i);
		m_program->setUniformValue(m_colorLocation, *faceColor);/*/
		QColor* sideColor = new QColor(sideColors[i]);
		m_program->setUniformValue(m_colorLocation, *sideColor);//*/

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

void Viewer::visit(Sphere &s)
{
	// std::cout << "Sphere found";
	QMatrix4x4 modelViewMatrix = modelStack.top() * QMatrix4x4(s.transform);

	if(!isPickingActivated){
		glBindVertexArray(m_VAOs[VAO_Sphere]);
		m_program->setUniformValue(m_isSkyLoc, false);
		m_program->setUniformValue(m_mvMatrixLocation, modelViewMatrix);
		m_program->setUniformValue(m_normalMatrixLoc, modelViewMatrix.normalMatrix());
		m_program->setUniformValue(m_colorLocation, s.getColor());
		m_program->setUniformValue(m_drawTextLoc, false);
		m_program->setUniformValue(m_isLightLoc, true);


		// This will save the current position of this light ball as the next point
		// This means point lights are technically 1 frame behind

		QVector3D point = QVector3D(0,0,0);

		point = modelViewMatrix * point;


		int pointLocs[3] = {m_point1Loc, m_point2Loc, m_point3Loc};
		int pointCLocs[3] = {m_c1Loc, m_c2Loc, m_c3Loc};

		m_program->setUniformValue(pointLocs[currentPoint], point);
		m_program->setUniformValue(pointCLocs[currentPoint], s.getColor());

		currentPoint = (currentPoint + 1) % 3;

		glDrawElements(GL_TRIANGLES, numTriSphere*3, GL_UNSIGNED_INT, 0);
	}
}

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::setPhong(bool on) {
    m_program->bind();
    m_program->setUniformValue(m_isPhongLoc, on);
    if(on) std::cout << "Phong ON\n";
    else std::cout << "Phong OFF\n";
    this->update();
}

void Viewer::setMagLinear(bool on) {
    m_program->bind();
    if(on) {
        std::cout << "Linear ON" << endl;

        s_texture->setMagnificationFilter(QOpenGLTexture::Linear);
    } else {
        std::cout << "Nearest ON" << endl;

        s_texture->setMagnificationFilter(QOpenGLTexture::Nearest);
    }

    this->update();
}

void Viewer::setMinLinear(bool on) {
    m_program->bind();
    if(on) {
        std::cout << "Linear MipMap Linear ON" << endl;

        s_texture->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
    } else {
        std::cout << "Nearest ON" << endl;

        s_texture->setMinificationFilter(QOpenGLTexture::Nearest);
    }

    this->update();
}

void Viewer::toggleNormalMaps(bool state) {
    m_program->bind();
    m_program->setUniformValue(m_useNormalMap, state);
}

void Viewer::deleteSelected() {
    m_program->bind();
    int pointCLocs[3] = {m_c1Loc, m_c2Loc, m_c3Loc};

    m_program->setUniformValue(pointCLocs[0], QColor(0,0,0));
    m_program->setUniformValue(pointCLocs[1], QColor(0,0,0));
    m_program->setUniformValue(pointCLocs[2], QColor(0,0,0));

    // Remove Shape from its parent
    if(selectedObj.shape == nullptr) return;
//    selectedObj.shape->getParent()->getChildren()->erase(
//                std::remove(selectedObj.shape->getParent()->getChildren()->begin(),
//                            selectedObj.shape->getParent()->getChildren()->end(),
//                            selection),
//                selectedObj.shape->getParent()->getChildren()->end());

    selectedObj.shape->getParent()->getParent()->getChildren()->erase(
        std::remove(selectedObj.shape->getParent()->getParent()->getChildren()->begin(),
                    selectedObj.shape->getParent()->getParent()->getChildren()->end(),
                    selectedObj.shape->getParent()),
        selectedObj.shape->getParent()->getParent()->getChildren()->end());

    selectedObj.shape = nullptr; // Rebind to "null"

    cubeSelected(false);
}

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();
}

void Viewer::animate(){
    clock_t time = clock();
    double dt = 1/0.006;//time - startTime;
    frame += dt * 0.006 * frame_mult; // 60fps
    startTime = time;
    update();
}

void Viewer::rotateSelectedObjXCCW() {
    rotateSelected(RotateDirection::X_CCW);
}
void Viewer::rotateSelectedObjXCW() {
    rotateSelected(RotateDirection::X_CW);
}
void Viewer::rotateSelectedObjYCCW() {
    rotateSelected(RotateDirection::Y_CCW);
}
void Viewer::rotateSelectedObjYCW() {
    rotateSelected(RotateDirection::Y_CW);
}
void Viewer::rotateSelectedObjZCCW() {
    rotateSelected(RotateDirection::Z_CCW);
}
void Viewer::rotateSelectedObjZCW() {
    rotateSelected(RotateDirection::Z_CW);
}

void Viewer::animationSpeedChange(int val) {
    frame_mult = fmax(double(val) / 10, 0.05);
}

void Viewer::rotateSelected(RotateDirection dir) {
    if(selectedObj.shape == nullptr || selectedObj.shape->getParent() == nullptr) return;

    switch(dir) {
        case X_CCW:
            selectedObj.shape->getParent()->transform.rotate(90, 1, 0, 0);
            break;
        case X_CW:
            selectedObj.shape->getParent()->transform.rotate(-90, 1, 0, 0);
            break;
        case Y_CCW:
            selectedObj.shape->getParent()->transform.rotate(90, 0, 1, 0);
            break;
        case Y_CW:
            selectedObj.shape->getParent()->transform.rotate(-90, 0, 1, 0);
            break;
        case Z_CCW:
            selectedObj.shape->getParent()->transform.rotate(90, 0, 0, 1);
            break;
        case Z_CW:
            selectedObj.shape->getParent()->transform.rotate(-90, 0, 0, 1);
            break;
    }
}

PickedGeom Viewer::pickGeom(int x, int y){

	makeCurrent();

	m_program->bind();
	m_program->setUniformValue(m_isPickingModeLoc, true);

	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
	glCullFace(GL_BACK);

	isPickingActivated = true;

	QMap<QRgb, QMatrix4x4> mapColorToPosition;
    QMap<QRgb, Shape*> mapColorToShape;
	QColor c;
	QRgb startColor = 1; // alpha must be 100%, glReadPixels doesn't resturn alpha
	unsigned char pixelData[3];

	// Give each cube a color, let each cube manage their face-color assignment
	//std::cout << "Iterating through " << root.getChildren()->size() << " items"<<endl;

	for(int i = 0; i<root.getChildren()->size(); 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));
		}

		c.setRgb(startColor);
		currentCube->setColor(c);
		if(currentCube) for (int j = 0; j<3; j++)
		{
			for(int i = -1; i<=1; i+=2)
			{
				QMatrix4x4 currentTransform = QMatrix4x4(current->transform);
				int components[3] = {0, 0, 0};
				components[j] = i;
				//std::cout << "Setting " << startColor << " to {" << components[0] << ", " << components[0] << ", " << components[1] << ", " << components[2] << "}\n";
				currentTransform.translate(components[1], -components[2], -components[0]);
				mapColorToPosition.insert(startColor, currentTransform);
                mapColorToShape.insert(startColor, currentCube);
				startColor++;
			}
		}
	}

	root.accept(*this);
	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

	// for debugging purposes
	/*unsigned char pdata[512][512][4];
	glReadPixels(x-256, y-256, 512, 512, GL_RGBA, GL_UNSIGNED_BYTE, pdata);
	QImage *im = new QImage(512, 512, QImage::Format_RGB32);
	for(int i = 0; i< 512; i++){
		for(int j = 0; j< 512; j++){
			QColor* pickedColor = new QColor();

			pickedColor->setRgb(pdata[512-j][i][0], pdata[512-j][i][1], pdata[512-j][i][2]);
			if(i==256 && j == 256) pickedColor->setRgba(0xFFFFFFFF);
			if(i==256 && j == 256)
				std::cout<<"--- Color under cursor: " << (int)pdata[j][i][0] <<" "<< (int)pdata[j][i][1] <<" "<< (int)pdata[j][i][2] <<" "<< (int)pdata[j][i][3] << endl;
			pickedColor->setAlpha(255);
			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], pixelData[3]);
	unsigned int pickedInt = pickedColor->rgb() % 0x1000000; // rgb returns ARGB. go figure.
	std::cout << "Picked " << pickedInt << endl;

	QMatrix4x4 pickedPosition = mapColorToPosition.value(pickedInt, identityMatrix);
    Shape* pickedShape = mapColorToShape.value(pickedInt);

	mapColorToPosition.clear();
	m_program->setUniformValue(m_isPickingModeLoc, false);
	isPickingActivated = false;
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    doneCurrent();

    PickedGeom result = PickedGeom();

    result.position = pickedPosition;
    result.shape = pickedShape;

    return result;
}
