/*
 * Window.cpp
 *
 * Copyright (c) 2014-2015 SEACAVE
 *
 * Author(s):
 *
 *      cDc <cdc.seacave@gmail.com>
 *
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *
 * Additional Terms:
 *
 *      You are required to preserve legal notices and author attributions in
 *      that material or in the Appropriate Legal Notices displayed by works
 *      containing it.
 */

#include "Common.h"
#include "Window.h"

using namespace VIEWER;


// D E F I N E S ///////////////////////////////////////////////////


// S T R U C T S ///////////////////////////////////////////////////

Window::WindowsMap Window::g_mapWindows;

Window::Window()
	:
	window(NULL),
	pos(Eigen::Vector2d::Zero()),
	prevPos(Eigen::Vector2d::Zero())
{
}
Window::~Window()
{
	Release();
}

void Window::Release()
{
	if (IsValid()) {
		#ifdef _USE_NUKLEAR
		nk_glfw3_shutdown();
		#endif
		glfwDestroyWindow(window);
		window = NULL;
	}
	clbkOpenScene.reset();
	ReleaseClbk();
}

void Window::ReleaseClbk()
{
	clbkSaveScene.reset();
	clbkExportScene.reset();
	clbkCenterScene.reset();
	clbkRayScene.reset();
	clbkCompilePointCloud.reset();
	clbkCompileMesh.reset();
	clbkCompileBounds.reset();
	clbkTogleSceneBox.reset();
	clbkCropToBounds.reset();
}

bool Window::Init(const cv::Size& _size, LPCTSTR name)
{
	sizeScale = 1;
	size = _size;

	glfwDefaultWindowHints();
	glfwWindowHint(GLFW_VISIBLE, 0);
	window = glfwCreateWindow(size.width, size.height, name, NULL, NULL);
	if (!window)
		return false;
	glfwMakeContextCurrent(window);
	glfwSetFramebufferSizeCallback(window, Window::Resize);
	glfwSetKeyCallback(window, Window::Key);
	glfwSetMouseButtonCallback(window, Window::MouseButton);
	glfwSetCursorPosCallback(window, Window::MouseMove);
	glfwSetScrollCallback(window, Window::Scroll);
	glfwSetDropCallback(window, Window::Drop);
	g_mapWindows[window] = this;

	Reset();
	return true;
}
void Window::SetCamera(const Camera& cam)
{
	camera = cam;
	cv::Size _size;
	glfwGetFramebufferSize(window, &_size.width, &_size.height);
	Resize(_size);
}
void Window::SetName(LPCTSTR name)
{
	glfwSetWindowTitle(window, name);
}
void Window::SetVisible(bool v)
{
	if (v)
		glfwShowWindow(window);
	else
		glfwHideWindow(window);
}
bool Window::IsVisible() const
{
	return glfwGetWindowAttrib(window, GLFW_VISIBLE) != 0;
}
void Window::Reset(SPARSE _sparseType, unsigned _minViews)
{
	camera.Reset();
	inputType = INP_NA;
	sparseType = _sparseType;
	minViews = _minViews;
	pointSize = 2.f;
	cameraBlend = 0.5f;
	bRenderCameras = true;
	bRenderCameraTrajectory = true;
	bRenderImageVisibility = false;
	bRenderViews = true;
	bRenderSolid = true;
	bRenderTexture = true;
	bRenderBounds = false;
	selectionType = SEL_NA;
	selectionIdx = NO_IDX;
	if (clbkCompilePointCloud != NULL)
		clbkCompilePointCloud();
	if (clbkCompileMesh != NULL)
		clbkCompileMesh();
	glfwPostEmptyEvent();
}


void Window::CenterCamera(const Point3& pos)
{
	camera.center = pos;
	camera.dist *= 0.7;
}


void Window::UpdateView(const ImageArr& images, const MVS::ImageArr& sceneImagesMVS)
{
	if (camera.IsCameraViewMode()) {
		// enable camera view mode and apply current camera transform
		const Image& image = images[camera.currentCamID];
		const MVS::Camera& camera = sceneImagesMVS[image.idx].camera;
		UpdateView((const Matrix3x3::EMat)camera.R, camera.GetT());
	} else {
		// apply view point transform
		glMatrixMode(GL_MODELVIEW);
		const Eigen::Matrix4d trans(camera.GetLookAt());
		glLoadMatrixd((GLdouble*)trans.data());
	}
}

void Window::UpdateView(const Eigen::Matrix3d& R, const Eigen::Vector3d& t)
{
	glMatrixMode(GL_MODELVIEW);
	transform = gs_convert * TransW2L(R, t);
	glLoadMatrixd((GLdouble*)transform.data());
}

void Window::UpdateMousePosition(double xpos, double ypos)
{
	prevPos = pos;
	pos.x() = xpos;
	pos.y() = ypos;
	// normalize position to [-1:1] range
	const int w(camera.size.width);
	const int h(camera.size.height);
	pos.x() = (2.0 * pos.x() - w) / w;
	pos.y() = (h - 2.0 * pos.y()) / h;
}


void Window::GetFrame(Image8U3& image) const
{
	image.create(GetSize());
	glReadPixels(0, 0, image.width(), image.height(), GL_BGR_EXT, GL_UNSIGNED_BYTE, image.ptr());
	cv::flip(image, image, 0);
}


cv::Size Window::GetSize() const
{
	cv::Size _size;
	glfwGetWindowSize(window, &_size.width, &_size.height);
	return _size;
}
void Window::Resize(const cv::Size& _size)
{
	// detect scaled window
	sizeScale = (double)GetSize().width/_size.width;
	size = _size;
	// update resolution
	glfwMakeContextCurrent(window);
	glViewport(0, 0, size.width, size.height);
	camera.Resize(cv::Size(ROUND2INT(size.width*sizeScale), ROUND2INT(size.height*sizeScale)));
}
void Window::Resize(GLFWwindow* window, int width, int height)
{
	g_mapWindows[window]->Resize(cv::Size(width, height));
}

void Window::Key(int k, int /*scancode*/, int action, int mod)
{
	switch (k) {
	case GLFW_KEY_ESCAPE:
		if (action == GLFW_RELEASE)
			glfwSetWindowShouldClose(window, 1);
		break;
	case GLFW_KEY_DOWN:
		if (action == GLFW_RELEASE) {
			if (mod & GLFW_MOD_SHIFT) {
				if (minViews > 2) {
					minViews--;
					if (clbkCompilePointCloud != NULL)
						clbkCompilePointCloud();
				}
			} else {
				pointSize = MAXF(pointSize-0.5f, 0.5f);
			}
		}
		break;
	case GLFW_KEY_UP:
		if (action == GLFW_RELEASE) {
			if (mod & GLFW_MOD_SHIFT) {
				minViews++;
				if (clbkCompilePointCloud != NULL)
					clbkCompilePointCloud();
			} else {
				pointSize += 0.5f;
			}
		}
		break;
	case GLFW_KEY_LEFT:
		if (action != GLFW_RELEASE) {
			camera.prevCamID = camera.currentCamID;
			camera.currentCamID--;
			if (camera.currentCamID < NO_ID && camera.currentCamID >= camera.maxCamID)
				camera.currentCamID = camera.maxCamID-1;
		}
		break;
	case GLFW_KEY_RIGHT:
		if (action != GLFW_RELEASE) {
			camera.prevCamID = camera.currentCamID;
			camera.currentCamID++;
			if (camera.currentCamID >= camera.maxCamID)
				camera.currentCamID = NO_ID;
		}
		break;
	case GLFW_KEY_B:
		if (action == GLFW_RELEASE) {
			if (mod & GLFW_MOD_CONTROL) {
				if (clbkCropToBounds != NULL)
					clbkCropToBounds();
			} else if (mod & GLFW_MOD_SHIFT) {
				if (clbkTogleSceneBox != NULL)
					clbkTogleSceneBox();
			} else {
				if (clbkCompileBounds != NULL)
					clbkCompileBounds();
			}
		}
		break;
	case GLFW_KEY_C:
		if (action == GLFW_RELEASE) {
			if (mod & GLFW_MOD_SHIFT) {
				bRenderCameraTrajectory = !bRenderCameraTrajectory;
			} else if (mod & GLFW_MOD_CONTROL) {
				if (clbkCenterScene != NULL)
					clbkCenterScene();
			} else {
				bRenderCameras = !bRenderCameras;
			}
		}
		break;
	case GLFW_KEY_E:
		if (action == GLFW_RELEASE && clbkExportScene != NULL)
			clbkExportScene(NULL, NULL);
		break;
	case GLFW_KEY_P:
		switch (sparseType) {
		case SPR_POINTS: sparseType = SPR_LINES; break;
		case SPR_LINES: sparseType = SPR_ALL; break;
		case SPR_ALL: sparseType = SPR_POINTS; break;
		}
		if (clbkCompilePointCloud != NULL)
			clbkCompilePointCloud();
		break;
	case GLFW_KEY_R:
		if (action == GLFW_RELEASE)
			Reset();
		break;
	case GLFW_KEY_S:
		if (action == GLFW_RELEASE) {
			if (clbkSaveScene != NULL)
				clbkSaveScene(NULL, (mod & GLFW_MOD_SHIFT) != 0);
		}
		break;
	case GLFW_KEY_T:
		if (action == GLFW_RELEASE) {
			bRenderTexture = !bRenderTexture;
			if (clbkCompileMesh != NULL)
				clbkCompileMesh();
		}
		break;
	case GLFW_KEY_V:
		if (action == GLFW_RELEASE) {
			if (mod & GLFW_MOD_SHIFT) {
				bRenderImageVisibility = !bRenderImageVisibility;
			} else {
				bRenderViews = !bRenderViews;
			}
		}
		break;
	case GLFW_KEY_W:
		if (action == GLFW_RELEASE) {
			if (bRenderSolid) {
				bRenderSolid = false;
				glPolygonMode(GL_FRONT, GL_LINE);
			} else {
				bRenderSolid = true;
				glPolygonMode(GL_FRONT, GL_FILL);
			}
		}
		break;
	case GLFW_KEY_KP_SUBTRACT:
		if (action == GLFW_RELEASE) {
			if (mod & GLFW_MOD_CONTROL)
				camera.SetFOV(camera.fov-5.f);
			else if (mod & GLFW_MOD_SHIFT)
				camera.scaleF *= 0.9f;
			else
				cameraBlend = MAXF(cameraBlend-0.1f, 0.f);
		}
		break;
	case GLFW_KEY_KP_ADD:
		if (action == GLFW_RELEASE) {
			if (mod & GLFW_MOD_CONTROL)
				camera.SetFOV(camera.fov+5.f);
			else if (mod & GLFW_MOD_SHIFT)
				camera.scaleF *= 1.1111f;
			else
				cameraBlend = MINF(cameraBlend+0.1f, 1.f);
		}
		break;
	}
}
void Window::Key(GLFWwindow* window, int k, int scancode, int action, int mod)
{
	g_mapWindows[window]->Key(k, scancode, action, mod);
}

void Window::MouseButton(int button, int action, int /*mods*/)
{
	switch (button) {
	case GLFW_MOUSE_BUTTON_LEFT: {
		if (action == GLFW_PRESS) {
			inputType.set(INP_MOUSE_LEFT);
		} else
		if (action == GLFW_RELEASE) {
			inputType.unset(INP_MOUSE_LEFT);
			glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
		}
		if (clbkRayScene != NULL) {
			typedef Eigen::Matrix<double,4,4,Eigen::ColMajor> Mat4;
			Mat4 P, V;
			glGetDoublev(GL_MODELVIEW_MATRIX, V.data());
			glGetDoublev(GL_PROJECTION_MATRIX, P.data());
			// 4d Homogeneous Clip Coordinates
			const Eigen::Vector4d ray_clip(pos.x(), pos.y(), -1.0, 1.0);
			// 4d Eye (Camera) Coordinates
			Eigen::Vector4d ray_eye(P.inverse()*ray_clip);
			ray_eye.z() = -1.0;
			ray_eye.w() = 0.0;
			// 4d World Coordinates
			const Mat4 invV(V.inverse());
			ASSERT(ISEQUAL(invV(3,3),1.0));
			const Eigen::Vector3d start(invV.topRightCorner<3,1>());
			const Eigen::Vector4d ray_wor(invV*ray_eye);
			const Eigen::Vector3d dir(ray_wor.topRows<3>().normalized());
			clbkRayScene(Ray3d(start, dir), action);
		}
	} break;
	case GLFW_MOUSE_BUTTON_MIDDLE: {
		if (action == GLFW_PRESS) {
			inputType.set(INP_MOUSE_MIDDLE);
			glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
		} else
		if (action == GLFW_RELEASE) {
			inputType.unset(INP_MOUSE_MIDDLE);
			glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
		}
	} break;
	case GLFW_MOUSE_BUTTON_RIGHT: {
		if (action == GLFW_PRESS) {
			inputType.set(INP_MOUSE_RIGHT);
		} else
		if (action == GLFW_RELEASE) {
			inputType.unset(INP_MOUSE_RIGHT);
		}
	}
	}
}
void Window::MouseButton(GLFWwindow* window, int button, int action, int mods)
{
	g_mapWindows[window]->MouseButton(button, action, mods);
}

void Window::MouseMove(double xpos, double ypos)
{
	UpdateMousePosition(xpos, ypos);
	if (inputType.isSet(INP_MOUSE_LEFT)) {
		glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
		camera.Rotate(pos, prevPos);
	} else
	if (inputType.isSet(INP_MOUSE_MIDDLE)) {
		camera.Translate(pos, prevPos);
	}
}
void Window::MouseMove(GLFWwindow* window, double xpos, double ypos)
{
	g_mapWindows[window]->MouseMove(xpos, ypos);
}

void Window::Scroll(double /*xoffset*/, double yoffset)
{
	camera.dist *= (yoffset>0 ? POW(1.11,yoffset) : POW(0.9,-yoffset));
}
void Window::Scroll(GLFWwindow* window, double xoffset, double yoffset)
{
	g_mapWindows[window]->Scroll(xoffset, yoffset);
}

void Window::Drop(int count, const char** paths)
{
	if (clbkOpenScene && count > 0) {
		SetVisible(false);
		String fileName(paths[0]);
		Util::ensureUnifySlash(fileName);
		if (count > 1) {
			String geometryFileName(paths[1]);
			Util::ensureUnifySlash(geometryFileName);
			clbkOpenScene(fileName, geometryFileName);
		} else {
			clbkOpenScene(fileName, NULL);
		}
		SetVisible(true);
	}
}
void Window::Drop(GLFWwindow* window, int count, const char** paths)
{
	g_mapWindows[window]->Drop(count, paths);
}

bool Window::IsShiftKeyPressed() const
{
	return
		glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS ||
		glfwGetKey(window, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS;
}
bool Window::IsCtrlKeyPressed() const
{
	return
		glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS ||
		glfwGetKey(window, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS;
}
bool Window::IsAltKeyPressed() const
{
	return
		glfwGetKey(window, GLFW_KEY_LEFT_ALT) == GLFW_PRESS ||
		glfwGetKey(window, GLFW_KEY_RIGHT_ALT) == GLFW_PRESS;
}
/*----------------------------------------------------------------*/
