#include "OGL.h"
#include "Camera.h"
#include "utils.h"

#include <iostream>
#include <stdexcept>

bool wiredFlag = false;
double lastX = 0.0, lastY = 0.0;

void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
	const float turnStep = 0.1f;  // in radians
	const float moveStep = 0.1f;

	if (key == GLFW_KEY_M && action == GLFW_PRESS) {
		glPolygonMode(GL_FRONT_AND_BACK, wiredFlag ? GL_FILL : GL_LINE);
		wiredFlag = !wiredFlag;
	}

	if(action == GLFW_PRESS || action == GLFW_REPEAT) {
		switch (key) {
			case GLFW_KEY_LEFT:
				Camera::instance()->turnLeft(turnStep);
				break;
			case GLFW_KEY_RIGHT:
				Camera::instance()->turnRight(turnStep);
				break;
			case GLFW_KEY_UP:
				Camera::instance()->turnUp(turnStep);
				break;
			case GLFW_KEY_DOWN:
				Camera::instance()->turnDown(turnStep);
				break;
			case GLFW_KEY_PAGE_UP:
				Camera::instance()->rotateCW(turnStep);
				break;
			case GLFW_KEY_PAGE_DOWN:
				Camera::instance()->rotateCCW(turnStep);
				break;
			case GLFW_KEY_W:
				Camera::instance()->moveToCenter(moveStep);
				break;
			case GLFW_KEY_S:
				Camera::instance()->moveFromCenter(moveStep);
				break;
		}
	}
}

void cursorPositionCallback(GLFWwindow* window, double xpos, double ypos) {
	if(lastX != 0 || lastY != 0) {
		float dx = (float)(xpos - lastX);
		float dy = (float)(ypos - lastY);
		static const float coeff = 0.01f;
		if(glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) {
			Camera::instance()->mouseMoveAroundCenter(coeff*dx, coeff*dy);
//			for(IGLObject* obj: OGL::instance()->_glObjects) {
//				obj->rotate(glm::vec3(coeff*dy, 0.f, coeff*dx));
//				obj->update();
//			}
		}
	}

	lastX = xpos;
	lastY = ypos;
}

void scrollCallback(GLFWwindow* window, double xoffset, double yoffset) {
	if(yoffset > 0) {
		Camera::instance()->moveToCenter(0.1f);
	} else {
		Camera::instance()->moveFromCenter(0.1f);
	}
}

void glfwErr(int code, const char* description) {
    std::cout << "GLFW error - code: " << code << ", description: " << description << std::endl;
}


OGL::OGL() {

}

OGL::~OGL() {
	glfwTerminate();
}

OGL *OGL::instance() {
	static OGL app;
	return &app;
}

void OGL::updateFpsCounter() const {
	static double previous_seconds = glfwGetTime ();
	static int frame_count;
	double current_seconds = glfwGetTime ();
	double elapsed_seconds = current_seconds - previous_seconds;
	if (elapsed_seconds > 0.25) {
		previous_seconds = current_seconds;
		double fps = (double)frame_count / elapsed_seconds;
		char tmp[128];
		sprintf (tmp, "opengl @ fps: %.2f", fps);
		glfwSetWindowTitle (_window, tmp);
		frame_count = 0;
	}
	frame_count++;
}

void OGL::init() {
    std::cout << glfwGetVersionString() << std::endl;
	if(glfwInit() == GLFW_FALSE) {
        throw new std::runtime_error("GLFW initialization error");
    }
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
	glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
	glfwWindowHint (GLFW_SAMPLES, 4);
    glfwSetErrorCallback(glfwErr);

	_window = glfwCreateWindow(1024, 1024, "OpenGL", nullptr, nullptr); // Windowed
	glfwMakeContextCurrent(_window);
	glfwSetKeyCallback(_window, keyCallback);
	glfwSetCursorPosCallback(_window, cursorPositionCallback);
	glfwSetScrollCallback(_window, scrollCallback);

	glewExperimental = GL_TRUE;
	GLenum err = glewInit();
	if(err != GLEW_OK) {
		std::string errMsg = std::string("GLEW init error: ") + (char*)glewGetErrorString(err);
		throw new std::runtime_error(errMsg);
	}

	// even successful call to glewInit() may cause setting global error GL_INVALID_ENUM,
	// so we need clear it in order to use glGetError() later
	// https://www.opengl.org/wiki/OpenGL_Loading_Library#GLEW_.28OpenGL_Extension_Wrangler.29
	utils::clearGLError();

	glEnable(GL_DEPTH_TEST);
	glClearDepth(1.0);
	glDepthFunc(GL_LEQUAL);
}

void OGL::run() {
	while(!glfwWindowShouldClose(_window)) {
		updateFpsCounter();
		glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		for(IGLObject* obj: _glObjects) {
			//obj->scale(glm::vec3(1.0f, 1.0f, 1.0f));
			obj->update();
			obj->draw();
		}

		glfwPollEvents();
		glfwSwapBuffers(_window);
	}
}

void OGL::addObject(IGLObject *object) {
	_glObjects.push_back(object);
}

ShaderProgram *OGL::getCurShaderProgram() {
	return _curShaderProgram;
}

void OGL::setCurShaderProgram(ShaderProgram *sp) {
	_curShaderProgram = sp;
	_curShaderProgram->use();
}
