// ---- Core Definitions ----
#define NOMINMAX
#include "core.h"

// ---- Asset Loading ----
#define TINYOBJLOADER_IMPLEMENTATION
#include "tiny_obj_loader.h"

// ---- Project-Specific Includes ----
#include "obj_loader.h"
#include "Camera.h"
#include "scene_manager.h"
#include "cursor_presets.h"
#include "point_cloud_loader.h"

// ---- GUI and Dialog ----
#include "imgui/imgui_incl.h"
#include <portable-file-dialogs.h>

// ---- Utility Libraries ----
#include <json.h>
#include <corecrt_math_defines.h>
#include <openLinks.h>


using namespace Engine;
using json = nlohmann::json;


// ---- Function Declarations ----
#pragma region Function Declarations
// ---- GLFW Callback Functions ----
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void mouse_button_callback(GLFWwindow* window, int button, int action, int mods);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void framebuffer_size_callback(GLFWwindow* window, int width, int height);

// ---- Rendering Functions ----
void renderEye(GLenum drawBuffer, const glm::mat4& projection, const glm::mat4& view, Shader* shader, ImGuiViewportP* viewport, ImGuiWindowFlags windowFlags);
void renderSphereCursor(const glm::mat4& projection, const glm::mat4& view);
void renderOrbitCenter(const glm::mat4& projection, const glm::mat4& view);
void renderGUI(bool isLeftEye, ImGuiViewportP* viewport, ImGuiWindowFlags windowFlags, Shader* shader);
void renderModels(Shader* shader);

void renderPointClouds(Shader* shader);

// ---- Update Functions ----
void updateCursorPosition(GLFWwindow* window, const glm::mat4& projection, const glm::mat4& view, Shader* shader);
void updateFragmentShaderUniforms(Shader* shader);
void updatePointLights();

PointCloud loadPointCloudFile(const std::string& filePath, size_t downsampleFactor = 1);


// ---- Utility Functions ----
float calculateLargestModelDimension();
void calculateMouseRay(float mouseX, float mouseY, glm::vec3& rayOrigin, glm::vec3& rayDirection, glm::vec3& rayNear, glm::vec3& rayFar, float aspect);
bool rayIntersectsModel(const glm::vec3& rayOrigin, const glm::vec3& rayDirection, const Engine::ObjModel& model, float& distance);

// ---- Scene Management Functions ----
void deleteSelectedModel();

// ---- Cleanup Functions ----
void cleanup(Shader* shader);
void terminateGLFW();
#pragma endregion


// ---- Global Variables ----
#pragma region Global Variables
// ---- Scene Management ----
Engine::Scene currentScene;
int currentModelIndex = -1;
std::string modelPath = "D:/OBJ/motorbike.obj";
static char modelPathBuffer[256] = ""; // Buffer for ImGui model path input

// ---- Camera Configuration ----
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = 1920.0f / 2.0;
float lastY = 1080.0f / 2.0;

// ---- Stereo Rendering Settings ----
float maxSeparation = 0.05f;   // Maximum stereo separation
float minSeparation = 0.0001f; // Minimum stereo separation

// The convergence will shift the zFokus but there is still some weirdness when going into negative
float minConvergence = -3.0f;  // Minimum convergence
float maxConvergence = 3.0f;   // Maximum convergence

// ---- GUI Settings ----
bool showGui = true;
bool showFPS = true;
bool showInfoWindow = false;

// ---- Scene Persistence ----
static char saveFilename[256] = "scene.json"; // Buffer for saving scene filename
static char loadFilename[256] = "scene.json"; // Buffer for loading scene filename
static std::string currentPresetName = "Default";
static bool isEditingPresetName = false;
static char editPresetNameBuffer[256] = "";

// ---- Input and Interaction ----
bool selectionMode = false;
bool isMovingModel = false;
bool isMouseCaptured = false;
bool leftMousePressed = false;   // Left mouse button state
bool rightMousePressed = false;  // Right mouse button state
bool middleMousePressed = false; // Middle mouse button state
double lastClickTime = 0.0;
const double doubleClickTime = 0.3; // 300 ms double-click threshold

// ---- Timing ----
float deltaTime = 0.0f;
float lastFrame = 0.0f;

// ---- Cursor Position ----
glm::vec3 g_cursorPos(0.0f);
bool g_cursorValid = false;

// ---- Window Configuration ----
int windowWidth = 1920;
int windowHeight = 1080;

// ---- Lighting ----
std::vector<PointLight> pointLights;
float zOffset = 0.5f;

// ---- Sphere Cursor ----
GLuint sphereVAO, sphereVBO, sphereEBO;
std::vector<float> sphereVertices;
std::vector<unsigned int> sphereIndices;
Shader* sphereShader = nullptr;

enum CursorScalingMode {
	CURSOR_NORMAL,
	CURSOR_FIXED,
	CURSOR_CONSTRAINED_DYNAMIC,
	CURSOR_LOGARITHMIC
};

CursorScalingMode currentCursorScalingMode = CURSOR_CONSTRAINED_DYNAMIC;
float fixedSphereRadius = 0.7f;
float minDiff = 0.01f;
float maxDiff = 0.1f;
float oldSphereRadius = fixedSphereRadius;
glm::vec4 cursorColor = glm::vec4(1.0f, 0.0f, 0.0f, 0.7f); // Initial color: semi-transparent red
float cursorTransparency = 1.0f;
bool showCursor = true;
float cursorEdgeSoftness = 0.8f;
float cursorCenterTransparency = 0.2f;

bool showInnerSphere = false;
glm::vec4 innerSphereColor = glm::vec4(0.0f, 1.0f, 0.0f, 1.0f); // Initial color: semi-transparent green
float innerSphereFactor = 0.1f;

// ---- Fragment Shader Cursor ----
struct FragmentShaderCursorSettings {
	float baseOuterRadius = 0.04f;
	float baseOuterBorderThickness = 0.005f;
	float baseInnerRadius = 0.004f;
	float baseInnerBorderThickness = 0.005f;
	glm::vec4 outerColor = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f);
	glm::vec4 innerColor = glm::vec4(1.0f, 1.0f, 1.0f, 0.5f);
};

FragmentShaderCursorSettings fragmentCursorSettings;

// ---- Cursor Visibility ----
bool showSphereCursor = false;
bool showFragmentCursor = true;

// ---- Orbit Settings ----
glm::vec3 capturedCursorPos;
bool orbitFollowsCursor = false;
bool showOrbitCenter = false;
glm::vec4 orbitCenterColor = glm::vec4(0.0f, 1.0f, 0.0f, 0.7f); // Initial color: semi-transparent green
float orbitCenterSphereRadius = 0.2f; // Fixed size for the orbit center sphere

// ---- Cursor Presets ----
struct CursorPreset {
	bool showSphereCursor;
	bool showFragmentCursor;
	float fragmentBaseInnerRadius;
	CursorScalingMode sphereScalingMode;
	float sphereFixedRadius;
	float sphereTransparency;
	bool showInnerSphere;
};
#pragma endregion


// ---- Sphere Cursor Functions
#pragma region Sphere Cursor Functions
Engine::CursorPreset createPresetFromCurrentSettings(const std::string& name) {
	Engine::CursorPreset preset;
	preset.name = name;
	preset.showSphereCursor = showSphereCursor;
	preset.showFragmentCursor = showFragmentCursor;
	preset.fragmentBaseInnerRadius = fragmentCursorSettings.baseInnerRadius;
	preset.sphereScalingMode = static_cast<int>(currentCursorScalingMode);
	preset.sphereFixedRadius = fixedSphereRadius;
	preset.sphereTransparency = cursorTransparency;
	preset.showInnerSphere = showInnerSphere;
	preset.cursorColor = cursorColor;
	preset.innerSphereColor = innerSphereColor;
	preset.innerSphereFactor = innerSphereFactor;
	preset.cursorEdgeSoftness = cursorEdgeSoftness;
	preset.cursorCenterTransparency = cursorCenterTransparency;
	return preset;
}

void applyPresetToGlobalSettings(const Engine::CursorPreset& preset) {
	showSphereCursor = preset.showSphereCursor;
	showFragmentCursor = preset.showFragmentCursor;
	fragmentCursorSettings.baseInnerRadius = preset.fragmentBaseInnerRadius;
	currentCursorScalingMode = static_cast<CursorScalingMode>(preset.sphereScalingMode);
	fixedSphereRadius = preset.sphereFixedRadius;
	cursorTransparency = preset.sphereTransparency;
	showInnerSphere = preset.showInnerSphere;
	cursorColor = preset.cursorColor;
	innerSphereColor = preset.innerSphereColor;
	innerSphereFactor = preset.innerSphereFactor;
	cursorEdgeSoftness = preset.cursorEdgeSoftness;
	cursorCenterTransparency = preset.cursorCenterTransparency;
}

// ---- Sphere Cursor Calculations ----
float calculateSphereRadius(const glm::vec3& cursorPosition, const glm::vec3& cameraPosition) {
	float distance = glm::distance(cursorPosition, cameraPosition);

	switch (currentCursorScalingMode) {
	case CURSOR_NORMAL:
		return fixedSphereRadius;

	case CURSOR_FIXED:
		return fixedSphereRadius * distance;

	case CURSOR_CONSTRAINED_DYNAMIC: {
		float distanceFactor = std::sqrt(distance);
		float defaultScreenSize = std::pow(fixedSphereRadius, 2) * distanceFactor;
		float minScreenSize = std::pow(fixedSphereRadius - minDiff, 2) * distanceFactor;
		float maxScreenSize = std::pow(fixedSphereRadius + maxDiff, 2) * distanceFactor;
		oldSphereRadius = glm::clamp(oldSphereRadius, minScreenSize, maxScreenSize);
		return oldSphereRadius;
	}

	case CURSOR_LOGARITHMIC:
		return fixedSphereRadius * (1.0f + std::log(distance));

	default:
		return fixedSphereRadius * distance;
	}
}

// ---- Sphere Mesh Generation ----
void generateSphereMesh(float radius, unsigned int rings, unsigned int sectors) {
	sphereVertices.clear();
	sphereIndices.clear();

	float const R = 1.0f / (float)(rings - 1);
	float const S = 1.0f / (float)(sectors - 1);

	for (unsigned int r = 0; r < rings; ++r) {
		for (unsigned int s = 0; s < sectors; ++s) {
			float const y = sin(-M_PI_2 + M_PI * r * R);
			float const x = cos(2 * M_PI * s * S) * sin(M_PI * r * R);
			float const z = sin(2 * M_PI * s * S) * sin(M_PI * r * R);

			sphereVertices.push_back(x * radius);
			sphereVertices.push_back(y * radius);
			sphereVertices.push_back(z * radius);

			sphereVertices.push_back(x);
			sphereVertices.push_back(y);
			sphereVertices.push_back(z);
		}
	}

	for (unsigned int r = 0; r < rings - 1; ++r) {
		for (unsigned int s = 0; s < sectors - 1; ++s) {
			sphereIndices.push_back(r * sectors + s);
			sphereIndices.push_back(r * sectors + (s + 1));
			sphereIndices.push_back((r + 1) * sectors + (s + 1));

			sphereIndices.push_back(r * sectors + s);
			sphereIndices.push_back((r + 1) * sectors + (s + 1));
			sphereIndices.push_back((r + 1) * sectors + s);
		}
	}
}

// ---- Sphere Cursor Setup ----
void setupSphereCursor() {
	generateSphereMesh(0.05f, 32, 32);

	glGenVertexArrays(1, &sphereVAO);
	glGenBuffers(1, &sphereVBO);
	glGenBuffers(1, &sphereEBO);

	glBindVertexArray(sphereVAO);

	glBindBuffer(GL_ARRAY_BUFFER, sphereVBO);
	glBufferData(GL_ARRAY_BUFFER, sphereVertices.size() * sizeof(float), sphereVertices.data(), GL_STATIC_DRAW);

	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, sphereEBO);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sphereIndices.size() * sizeof(unsigned int), sphereIndices.data(), GL_STATIC_DRAW);

	glEnableVertexAttribArray(0);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);

	glEnableVertexAttribArray(1);
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));

	glBindVertexArray(0);

	try {
		sphereShader = Engine::loadShader("sphereVertexShader.glsl", "sphereFragmentShader.glsl");
	}
	catch (const std::exception& e) {
		std::cerr << "Fatal error: " << e.what() << std::endl;
	}

	// Initialize uniforms for both shaders
	sphereShader->use();
	sphereShader->setMat4("projection", glm::mat4(1.0f));
	sphereShader->setMat4("view", glm::mat4(1.0f));
	sphereShader->setMat4("model", glm::mat4(1.0f));
	sphereShader->setVec3("viewPos", camera.Position);
}
#pragma endregion

#pragma region MarioLorenz
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Code by Mario Lorenz, TUC
////////////////////////////////////////////////////////////////////////////////////////////////////////////

// 
// Sample code for the construction of the projection matrix from l, r, b, t, n, f
// --> not needed, instead use glm::frustum()
// 
//glm::mat4 Frustum(GLfloat left, GLfloat right, GLfloat bottom, GLfloat top, GLfloat zNear, GLfloat zFar)
//{
//	glm::mat4 pm = glm::mat4(0);
//
//	pm[0][0] = (2.0f * zNear) / (right - left);
//	pm[0][1] = 0.0f;
//	pm[0][2] = 0.0f;
//	pm[0][3] = 0.0f;
//
//	pm[1][0] = 0.0f;
//	pm[1][1] = (2.0f * zNear) / (top - bottom);
//	pm[1][2] = 0.0f;
//	pm[1][3] = 0.0f;
//
//	pm[2][0] = (right + left) / (right - left);
//	pm[2][1] = (top + bottom) / (top - bottom);
//	pm[2][2] = (-zNear - zFar) / (zFar - zNear);
//	pm[2][3] = -1.0f;
//
//	pm[3][0] = 0.0f;
//	pm[3][1] = 0.0f;
//	pm[3][2] = (2.0f * zFar * zNear) / (zNear - zFar);
//	pm[3][3] = 0.0f;
//
//	return pm;
//}

void PerspectiveProjection(GLfloat* frustum, GLfloat dir,
	GLfloat fovy, GLfloat aspect,
	GLfloat znear, GLfloat zfar,
	GLfloat eyesep, GLfloat focaldist)
{
	// re-calculate the borders of the asymmectric stereo frustum at the near plane 
	// results in correct stereo viewing for all scenarios using the SAME stereo screen space
	// e.g. Powerwalls, CAVES, Pluraview
	// not correct for all HMDs, depends on optics
	// 
	// OUT: 
	// frustum: [left, right, bottom, top, near, far]
	// 
	// IN: 
	// dir: -1 (right eye), 0 (center), 1 (left eye)
	// fov_y, aspect, clip_near, clip_far, eyesep, focaldist
	// 
	// Note that the values of eyesep and focaldist depend on the scene dimension
	// as well on near and far

	// half the extent of the window
	GLfloat h_half = tanf(glm::radians(fovy / 2.0f));
	GLfloat w_half = h_half * aspect;

	// calculate the left and right frustum borders at focal plane
	frustum[0] = -w_half * focaldist - ((eyesep / 2.0f) * dir);
	frustum[1] = w_half * focaldist - ((eyesep / 2.0f) * dir);
	// project these onto the near plane
	frustum[0] = (frustum[0] / focaldist) * znear;
	frustum[1] = (frustum[1] / focaldist) * znear;

	// the remaining is the same as for Mono viewing
	// calculate bottom and top without shifting
	frustum[2] = -h_half * znear;
	frustum[3] = h_half * znear;
	// take over near and far
	frustum[4] = znear;
	frustum[5] = zfar;
}
#pragma endregion


int main() {
	// ---- Initialize GLFW ----
	if (!glfwInit()) {
		std::cout << "Failed to initialize GLFW" << std::endl;
		return -1;
	}

	// ---- Set OpenGL Version and Profile ----
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
	glfwWindowHint(GLFW_STEREO, GLFW_TRUE);  // Enable stereo hint

	// ---- Create GLFW Window ----
	GLFWwindow* window = glfwCreateWindow(windowWidth, windowHeight, "OpenGL Stereo Template", nullptr, nullptr);
	bool isStereoWindow = (window != nullptr);

	if (!isStereoWindow) {
		std::cout << "Failed to create stereo GLFW window, falling back to mono rendering." << std::endl;
		glfwWindowHint(GLFW_STEREO, GLFW_FALSE);
		window = glfwCreateWindow(windowWidth, windowHeight, "OpenGL Mono Template", nullptr, nullptr);

		if (window == nullptr) {
			std::cout << "Failed to create GLFW window" << std::endl;
			glfwTerminate();
			return -1;
		}
	}

	glfwMakeContextCurrent(window);
	Window::nativeWindow = window;

	// ---- Initialize GLAD ----
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
		std::cout << "Failed to initialize GLAD" << std::endl;
		glfwTerminate();
		return -1;
	}

	// ---- Set GLFW Callbacks ----
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
	glfwSetCursorPosCallback(window, mouse_callback);
	glfwSetScrollCallback(window, scroll_callback);
	glfwSetKeyCallback(window, key_callback);
	glfwSetMouseButtonCallback(window, mouse_button_callback);


	// ---- Initialize Shader ----
	Shader* shader = nullptr;
	try {
		shader = Engine::loadShader("vertexShader.glsl", "fragmentShader.glsl");
	}
	catch (std::exception& e) {
		std::cout << e.what() << std::endl;
		glfwTerminate();
		return -1;
	}

	// ---- Load Default cube ----
	Engine::ObjModel cube = Engine::createCube(glm::vec3(1.0f, 1.0f, 1.0f), 32.0f, 0.0f);
	cube.scale = glm::vec3(0.5f);
	cube.position = glm::vec3(0.0f, 0.0f, 0.0f);
	cube.emissive = 0.5f;
	currentScene.models.push_back(cube);
	currentModelIndex = 0;

	camera.centeringCompletedCallback = [&]() {
		if (orbitFollowsCursor) {
			camera.SetOrbitPointDirectly(capturedCursorPos);
			camera.StartOrbiting();
		}
		};

	// ---- Init Preset ----
	if (Engine::CursorPresetManager::getPresetNames().empty()) {
		// Create and save a default preset
		Engine::CursorPreset defaultPreset = createPresetFromCurrentSettings("Default");
		Engine::CursorPresetManager::savePreset("Default", defaultPreset);
	}
	currentPresetName = Engine::CursorPresetManager::getPresetNames().front();

	// ---- Calculate Largest Model Dimension ----
	float largestDimension = calculateLargestModelDimension();

	// ---- Initialize ImGui ----
	IMGUI_CHECKVERSION();
	ImGui::CreateContext();
	ImGuiIO& io = ImGui::GetIO(); (void)io;
	ImGui_ImplGlfw_InitForOpenGL(window, true);
	ImGui_ImplOpenGL3_Init("#version 330 core");
	ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)ImGui::GetMainViewport();
	ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus;
	ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange;

	// Setup ImGui style
	SetupImGuiStyle(true, 1.0f);
	strcpy_s(modelPathBuffer, modelPath.c_str());

	// ---- OpenGL Settings ----
	glEnable(GL_DEPTH_TEST);
	glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
	glfwSwapInterval(1); // Enable vsync

	setupSphereCursor();

	// ---- Main Loop ----
	while (!glfwWindowShouldClose(window)) {
		// ---- Per-frame Time Logic ----
		float currentFrame = glfwGetTime();
		deltaTime = currentFrame - lastFrame;
		lastFrame = currentFrame;

		if (deltaTime <= 0.0f) continue;

		// ---- Handle Input ----
		Input::handleKeyInput(camera, deltaTime);

		// ---- Update Camera Animation ----
		camera.UpdateAnimation(deltaTime);

		// ---- Set Projection Matrices ----
		glm::mat4 view = camera.GetViewMatrix();
		float aspectRatio = (float)windowWidth / (float)windowHeight;
		glm::mat4 projection = camera.GetProjectionMatrix(aspectRatio, currentScene.settings.nearPlane, currentScene.settings.farPlane);

		glm::mat4 leftProjection;
		glm::mat4 rightProjection;

		if (false == camera.useNewMethod) {
			// original code from benjamin except the render call of the GUI, 
			// which is removed from renderEye() to have the GUI on both eyes
			// new: wireframe render mode

			leftProjection = camera.offsetProjection(projection, currentScene.settings.separation / 2, abs(camera.Position.z) * currentScene.settings.convergence);
			rightProjection = camera.offsetProjection(projection, -currentScene.settings.separation / 2, abs(camera.Position.z) * currentScene.settings.convergence);

			// ---- Update Cursor Position ----
			updateCursorPosition(window, projection, view, shader);

			// ---- Set Common Shader Uniforms ----
			shader->use();
			shader->setVec3("viewPos", camera.Position);
			shader->setVec4("cursorPos", camera.IsOrbiting && orbitFollowsCursor && showSphereCursor ?
				glm::vec4(capturedCursorPos, g_cursorValid ? 1.0f : 0.0f) :
				glm::vec4(g_cursorPos, g_cursorValid ? 1.0f : 0.0f));
			updateFragmentShaderUniforms(shader);

			// Adjust camera speed
			float distanceToNearestObject = camera.getDistanceToNearestObject(camera, projection, view, currentScene.settings.farPlane, windowWidth, windowHeight);
			camera.AdjustMovementSpeed(distanceToNearestObject, largestDimension, currentScene.settings.farPlane);
			camera.isMoving = false;  // Reset the moving flag at the end of each frame

			if (camera.wireframe)
				glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
			else
				glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
			// glColorMask(GL_TRUE, GL_TRUE, GL_FALSE, GL_TRUE);

			// ---- Render for Left Eye ----
			renderEye(GL_BACK_LEFT, leftProjection, view, shader, viewport, windowFlags);

			// ---- Render for Right Eye (if in Stereo Mode) ----
			// glColorMask(GL_FALSE, GL_FALSE, GL_TRUE, GL_TRUE);
			if (isStereoWindow)
			{
				renderEye(GL_BACK_RIGHT, rightProjection, view, shader, viewport, windowFlags);
			}
			glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
			renderGUI(GL_TRUE, viewport, windowFlags, shader);
			glDrawBuffer(GL_BACK_LEFT);
			renderGUI(GL_TRUE, viewport, windowFlags, shader);
		}
		else {
#pragma region MarioLorenz
			////////////////////////////////////////////////////////////////////////////////////////////////////////////
			// Code by Mario Lorenz, TUC
			////////////////////////////////////////////////////////////////////////////////////////////////////////////

			// take over the parameters from the camera, scaling toroughly equalize to Benjamins camera 
			GLfloat focal_distance = currentScene.settings.convergence;
			GLfloat eye_separation = currentScene.settings.separation * 10;
			GLfloat clip_near = currentScene.settings.nearPlane + 0.1f;
			GLfloat clip_far = currentScene.settings.farPlane;

			// focal distance only makes sense between near and far
			if (focal_distance < 1.1 * clip_near) focal_distance = 1.1 * clip_near;
			if (focal_distance > clip_far) focal_distance = clip_far;

			// calculate the left frustum
			GLfloat frustum[6]; // [left, right, bottom, top, near, far]
			PerspectiveProjection(frustum, +1.0f, /*left eye*/
				camera.Zoom, aspectRatio,
				clip_near, clip_far,
				eye_separation, focal_distance
			);

			leftProjection = glm::frustum(
				frustum[0], frustum[1], frustum[2], frustum[3], frustum[4], frustum[5]
			);
			// using sample code --> leftProjection = Frustum(frustum[0], frustum[1], frustum[2], frustum[3], frustum[4], frustum[5]);

			PerspectiveProjection(frustum, -1.0f /*right eye*/,
				camera.Zoom, aspectRatio,
				clip_near, clip_far,
				eye_separation, focal_distance
			);

			rightProjection = glm::frustum(
				frustum[0], frustum[1], frustum[2], frustum[3], frustum[4], frustum[5]
			);
			// using sample code --> rightProjection = Frustum(frustum[0], frustum[1], frustum[2], frustum[3], frustum[4], frustum[5]);

			// ---- Update Cursor Position ----
			updateCursorPosition(window, projection, view, shader);

			// ---- Set Common Shader Uniforms ----
			shader->use();
			shader->setVec3("viewPos", camera.Position);
			shader->setVec4("cursorPos", camera.IsOrbiting && orbitFollowsCursor && showSphereCursor ?
				glm::vec4(capturedCursorPos, g_cursorValid ? 1.0f : 0.0f) :
				glm::vec4(g_cursorPos, g_cursorValid ? 1.0f : 0.0f));
			updateFragmentShaderUniforms(shader);

			// Adjust camera speed
			float distanceToNearestObject = camera.getDistanceToNearestObject(camera, projection, view, currentScene.settings.farPlane, windowWidth, windowHeight);
			camera.AdjustMovementSpeed(distanceToNearestObject, largestDimension, currentScene.settings.farPlane);
			camera.isMoving = false;  // Reset the moving flag at the end of each frame


			// ---- Render for Left Eye ----
			glm::vec3 pos = camera.Position;
			glm::vec3 right = camera.Right;
			glm::vec3 up = camera.Up;
			glm::vec3 front = camera.Front;
			glm::mat4 vm = glm::mat4(0);

			// VERY Important: WE HAVE TO MOVE THE CAMERA POSITION !!!
			// left eye postion
			pos = pos - (right * eye_separation / 2.0f);
			vm = glm::lookAt(pos, pos + front, up);

			if (camera.wireframe)
				glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
			else
				glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
			// glColorMask(GL_TRUE, GL_TRUE, GL_FALSE, GL_TRUE);
			renderEye(GL_BACK_LEFT, leftProjection, vm, shader, viewport, windowFlags);

			// ---- Render for Right Eye (if in Stereo Mode) ----
			//glColorMask(GL_FALSE, GL_FALSE, GL_TRUE, GL_TRUE);
			if (isStereoWindow)
			{
				// right eye position
				pos = pos + (right * eye_separation);
				vm = glm::lookAt(pos, pos + front, up);
				renderEye(GL_BACK_RIGHT, rightProjection, vm, shader, viewport, windowFlags);
			}
			glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
			renderGUI(GL_TRUE, viewport, windowFlags, shader);
			glDrawBuffer(GL_BACK_LEFT);
			renderGUI(GL_TRUE, viewport, windowFlags, shader);
#pragma endregion

		}

		// ---- Swap Buffers and Poll Events ----
		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	// ---- Cleanup ----
	cleanup(shader);

	return 0;
}


// ---- Initialization and Cleanup -----
#pragma region Initialization and Cleanup
void cleanup(Shader* shader) {
	delete shader;
	glDeleteVertexArrays(1, &sphereVAO);
	glDeleteBuffers(1, &sphereVBO);
	glDeleteBuffers(1, &sphereEBO);
	delete sphereShader;
	glfwTerminate();
	ImGui_ImplOpenGL3_Shutdown();
	ImGui_ImplGlfw_Shutdown();
	ImGui::DestroyContext();
}

void terminateGLFW() {
	glfwDestroyWindow(Window::nativeWindow);
}

float calculateLargestModelDimension() {
	if (currentScene.models.empty()) return 1.0f;

	glm::vec3 minBounds(std::numeric_limits<float>::max());
	glm::vec3 maxBounds(std::numeric_limits<float>::lowest());

	for (const auto& vertex : currentScene.models[0].vertices) {
		minBounds = glm::min(minBounds, vertex.position);
		maxBounds = glm::max(maxBounds, vertex.position);
	}

	glm::vec3 modelsize = maxBounds - minBounds;
	return glm::max(glm::max(modelsize.x, modelsize.y), modelsize.z);
}

#pragma endregion


// ---- Rendering ----
#pragma region Rendering
void renderEye(GLenum drawBuffer, const glm::mat4& projection, const glm::mat4& view, Shader* shader, ImGuiViewportP* viewport, ImGuiWindowFlags windowFlags) {
	// Set the draw buffer and clear color and depth buffers
	glDrawBuffer(drawBuffer);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	// Reset OpenGL state
	glUseProgram(0);
	glBindVertexArray(0);
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, 0);

	// Set up shader for this eye
	shader->use();
	shader->setMat4("projection", projection);
	shader->setMat4("view", view);
	shader->setVec3("viewPos", camera.Position);

	// Set cursor position based on orbiting state
	shader->setVec4("cursorPos", camera.IsOrbiting && orbitFollowsCursor && showSphereCursor ?
		glm::vec4(capturedCursorPos, g_cursorValid ? 1.0f : 0.0f) :
		glm::vec4(g_cursorPos, g_cursorValid ? 1.0f : 0.0f));
	updateFragmentShaderUniforms(shader);

	// Render scene elements
	renderModels(shader);
	renderPointClouds(shader);
	renderOrbitCenter(projection, view);
	renderSphereCursor(projection, view);

	// Render GUI
	//renderGUI(drawBuffer == GL_BACK_LEFT, viewport, windowFlags, shader);

	// Reset OpenGL state again
	glUseProgram(0);
	glBindVertexArray(0);
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, 0);
}

void renderSphereCursor(const glm::mat4& projection, const glm::mat4& view) {
	if (g_cursorValid && showSphereCursor) {
		// Enable depth testing and blending
		glEnable(GL_DEPTH_TEST);
		glDepthFunc(GL_LESS);
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

		// Set up sphere shader
		sphereShader->use();
		sphereShader->setMat4("projection", projection);
		sphereShader->setMat4("view", view);
		sphereShader->setVec3("viewPos", camera.Position);

		// Calculate cursor position and size
		glm::vec3 cursorRenderPos = camera.IsOrbiting && orbitFollowsCursor ? capturedCursorPos : g_cursorPos;
		float sphereRadius = calculateSphereRadius(cursorRenderPos, camera.Position);
		glm::mat4 model = glm::translate(glm::mat4(1.0f), cursorRenderPos);
		model = glm::scale(model, glm::vec3(sphereRadius));

		sphereShader->setMat4("model", model);
		sphereShader->setFloat("innerSphereFactor", innerSphereFactor);

		glBindVertexArray(sphereVAO);

		// Render inner sphere if enabled
		if (showInnerSphere) {
			glDepthMask(GL_FALSE);
			sphereShader->setBool("isInnerSphere", true);
			sphereShader->setVec4("sphereColor", innerSphereColor);
			sphereShader->setFloat("transparency", 1.0); // Full opacity for inner sphere
			glm::mat4 innerModel = glm::scale(model, glm::vec3(innerSphereFactor));
			sphereShader->setMat4("model", innerModel);
			glDrawElements(GL_TRIANGLES, sphereIndices.size(), GL_UNSIGNED_INT, 0);
		}

		// Render outer sphere
		glDepthMask(GL_TRUE);
		sphereShader->setBool("isInnerSphere", false);
		sphereShader->setVec4("sphereColor", cursorColor);
		sphereShader->setFloat("transparency", cursorTransparency);
		sphereShader->setFloat("edgeSoftness", cursorEdgeSoftness);
		sphereShader->setFloat("centerTransparencyFactor", cursorCenterTransparency);
		sphereShader->setMat4("model", model);
		glDrawElements(GL_TRIANGLES, sphereIndices.size(), GL_UNSIGNED_INT, 0);

		// Clean up
		glBindVertexArray(0);
		glUseProgram(0);
		glDisable(GL_BLEND);
	}
}

void renderOrbitCenter(const glm::mat4& projection, const glm::mat4& view) {
	if (!orbitFollowsCursor && showOrbitCenter && camera.IsOrbiting) {
		// Enable depth testing and blending
		glEnable(GL_DEPTH_TEST);
		glDepthFunc(GL_LESS);
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

		// Set up sphere shader for orbit center
		sphereShader->use();
		sphereShader->setMat4("projection", projection);
		sphereShader->setMat4("view", view);

		// Create model matrix for orbit center
		glm::mat4 model = glm::translate(glm::mat4(1.0f), camera.OrbitPoint);
		model = glm::scale(model, glm::vec3(orbitCenterSphereRadius));

		// Set shader uniforms
		sphereShader->setMat4("model", model);
		sphereShader->setVec3("viewPos", camera.Position);
		sphereShader->setVec4("sphereColor", orbitCenterColor);
		sphereShader->setFloat("transparency", 1.0f);
		sphereShader->setFloat("edgeSoftness", 0.0f);
		sphereShader->setFloat("centerTransparencyFactor", 0.0f);

		// Render orbit center sphere
		glBindVertexArray(sphereVAO);
		glDrawElements(GL_TRIANGLES, sphereIndices.size(), GL_UNSIGNED_INT, 0);
		glBindVertexArray(0);

		glDisable(GL_BLEND);
	}
}

void renderGUI(bool isLeftEye, ImGuiViewportP* viewport, ImGuiWindowFlags windowFlags, Shader* shader) {
	ImGui_ImplOpenGL3_NewFrame();
	ImGui_ImplGlfw_NewFrame();
	ImGui::NewFrame();

	if (showGui) {
		ImGui::BeginViewportSideBar("##ToolBar", viewport, ImGuiDir_Left, 630.0f, windowFlags | ImGuiWindowFlags_NoNavInputs);

		// Slider controls
		ImGui::Text("Camera Settings");
		ImGui::Separator();
		ImGui::SliderFloat("Separation", &currentScene.settings.separation, minSeparation, maxSeparation);

		ImGui::SliderFloat("Convergence / ZFocus", &currentScene.settings.convergence, minConvergence, maxConvergence);

		// FPS and Camera Settings
		ImGui::SliderFloat("Camera Speed Multiplier", &camera.speedFactor, 0.1f, 5.0f);
		ImGui::Checkbox("Show FPS", &showFPS);

		ImGui::SliderFloat("Near Plane", &currentScene.settings.nearPlane, 0.01f, 10.0f);
		ImGui::SliderFloat("Far Plane", &currentScene.settings.farPlane, 10.0f, 1000.0f);

		// focal dist cannot be outside near, far
		minConvergence = currentScene.settings.nearPlane;
		maxConvergence = currentScene.settings.farPlane;

		ImGui::Separator();

		if (ImGui::CollapsingHeader("Point Cloud Management")) {

			ImGui::Separator();
			if (ImGui::Button("Load Point Cloud")) {
				auto selection = pfd::open_file("Select a point cloud file", ".",
					{ "Point Cloud Files", "*.txt *.xyz *.ply", "All Files", "*" }).result();
				if (!selection.empty()) {
					PointCloud newPointCloud = loadPointCloudFile(selection[0], 1);
					currentScene.pointClouds.push_back(newPointCloud);
				}
			}

			for (size_t i = 0; i < currentScene.pointClouds.size(); i++) {
				auto& pointCloud = currentScene.pointClouds[i];
				if (ImGui::TreeNode(pointCloud.name.c_str())) {
					ImGui::DragFloat3("Position", glm::value_ptr(pointCloud.position), 0.1f);
					ImGui::DragFloat3("Rotation", glm::value_ptr(pointCloud.rotation), 1.0f, -360.0f, 360.0f);
					ImGui::DragFloat3("Scale", glm::value_ptr(pointCloud.scale), 0.01f, 0.01f, 100.0f);

					if (ImGui::Button("Delete")) {
						// Clean up OpenGL resources
						glDeleteVertexArrays(1, &pointCloud.vao);
						glDeleteBuffers(1, &pointCloud.vbo);

						// Remove from the vector
						currentScene.pointClouds.erase(currentScene.pointClouds.begin() + i);
						ImGui::TreePop();
						break;
					}

					ImGui::TreePop();
				}
			}
		}

		ImGui::Separator();

		// Model loading UI
		ImGui::Text("Model Loading");
		ImGui::InputText("Model Path", modelPathBuffer, IM_ARRAYSIZE(modelPathBuffer));
		if (ImGui::Button("Browse##Model")) {
			auto selection = pfd::open_file("Select a model file", ".",
				{ "OBJ Files", "*.obj", "All Files", "*" }).result();
			if (!selection.empty()) {
				strncpy_s(modelPathBuffer, selection[0].c_str(), sizeof(modelPathBuffer) - 1);
			}
		}
		ImGui::SameLine();
		if (ImGui::Button("Load Model")) {
			Engine::ObjModel newModel = Engine::loadObjFile(modelPathBuffer);
			currentScene.models.push_back(newModel);
			currentModelIndex = currentScene.models.size() - 1;
		}

		ImGui::Separator();

		// Model selection and manipulation
		if (!currentScene.models.empty()) {
			ImGui::Text("Model Manipulation");
			if (ImGui::BeginCombo("Select Model", currentScene.models[currentModelIndex].name.c_str())) {
				for (int i = 0; i < currentScene.models.size(); i++) {
					bool isSelected = (currentModelIndex == i);
					if (ImGui::Selectable(currentScene.models[i].name.c_str(), isSelected)) {
						currentModelIndex = i;
					}
					if (isSelected) {
						ImGui::SetItemDefaultFocus();
					}
				}
				ImGui::EndCombo();
			}

			if (currentModelIndex >= 0) {
				ImGui::DragFloat3("Position", glm::value_ptr(currentScene.models[currentModelIndex].position), 0.1f);
				ImGui::DragFloat3("Scale", glm::value_ptr(currentScene.models[currentModelIndex].scale), 0.1f);
				ImGui::DragFloat3("Rotation", glm::value_ptr(currentScene.models[currentModelIndex].rotation), 1.0f, -360.0f, 360.0f);

				ImGui::Text("Texture Loading");

				auto textureLoadingGUI = [&](const char* label, GLuint& textureID, std::string& texturePath, const char* uniformName) {
					char pathBuffer[256];
					strncpy_s(pathBuffer, texturePath.c_str(), sizeof(pathBuffer));
					pathBuffer[sizeof(pathBuffer) - 1] = '\0';

					ImGui::Text("%s", label);
					if (ImGui::InputText(("Path##" + std::string(label)).c_str(), pathBuffer, IM_ARRAYSIZE(pathBuffer))) {
						texturePath = pathBuffer;
					}
					if (ImGui::Button(("Browse##" + std::string(label)).c_str())) {
						auto selection = pfd::open_file("Select a texture file", ".",
							{ "Image Files", "*.png *.jpg *.jpeg *.bmp", "All Files", "*" }).result();
						if (!selection.empty()) {
							texturePath = selection[0];
						}
					}
					ImGui::SameLine();
					if (ImGui::Button(("Load##" + std::string(label)).c_str())) {
						GLuint newTexture = loadTextureFromFile(texturePath.c_str());
						if (newTexture != 0) {
							if (textureID != 0) {
								glDeleteTextures(1, &textureID);
							}
							textureID = newTexture;
							std::cout << label << " loaded successfully." << std::endl;
							currentScene.models[currentModelIndex].hasCustomTexture = true;
							shader->use();
							shader->setInt(uniformName, textureID);
						}
					}
					};

				textureLoadingGUI("Diffuse Texture", currentScene.models[currentModelIndex].texture,
					currentScene.models[currentModelIndex].diffuseTexturePath, "texture_diffuse1");

				// ---- There is currently a problem with displaying Normal Maps ----
				/*textureLoadingGUI("Normal Map", currentScene.models[currentModelIndex].normalMap,
					currentScene.models[currentModelIndex].normalTexturePath, "texture_normal1");*/

				textureLoadingGUI("Specular Map", currentScene.models[currentModelIndex].specularMap,
					currentScene.models[currentModelIndex].specularTexturePath, "texture_specular1");
				textureLoadingGUI("AO Map", currentScene.models[currentModelIndex].aoMap,
					currentScene.models[currentModelIndex].aoTexturePath, "texture_ao1");

				if (ImGui::Button("Delete Selected Model")) {
					deleteSelectedModel();
				}
			}
		}

		ImGui::Separator();

		// Adding and clearing cubes
		ImGui::Text("Cube Operations");
		if (ImGui::Button("Add Cube")) {
			ObjModel newCube = Engine::createCube(glm::vec3(1.0f, 1.0f, 1.0f), 10.0f, 0.0f);
			currentScene.models.push_back(newCube);
			currentModelIndex = currentScene.models.size() - 1;
		}
		ImGui::SameLine();
		if (ImGui::Button("Clear Cubes")) {
			currentScene.models.erase(
				std::remove_if(currentScene.models.begin(), currentScene.models.end(),
					[](const ObjModel& model) { return model.name == "Cube"; }),
				currentScene.models.end()
			);
			if (currentModelIndex >= currentScene.models.size()) {
				currentModelIndex = currentScene.models.empty() ? -1 : currentScene.models.size() - 1;
			}
		}

		ImGui::Separator();

		// Model properties
		if (currentModelIndex >= 0 && currentModelIndex < currentScene.models.size()) {
			auto& model = currentScene.models[currentModelIndex];
			ImGui::Text("Model Properties");
			ImGui::ColorEdit3("Model Color", glm::value_ptr(model.color));
			ImGui::SliderFloat("Model Shininess", &model.shininess, 1.0f, 90.0f);
			ImGui::SliderFloat("Model Emissive", &model.emissive, 0.0f, 1.0f);
		}

		ImGui::Separator();


		// Cursor Settings
		if (ImGui::CollapsingHeader("Cursor Settings")) {
			ImGui::Separator();

			// Preset management
			if (ImGui::BeginCombo("Cursor Preset", currentPresetName.c_str())) {
				std::vector<std::string> presetNames = Engine::CursorPresetManager::getPresetNames();

				// Add "New Preset" option
				if (ImGui::Selectable("New Preset")) {
					currentPresetName = "New Preset";
					isEditingPresetName = true;
					strcpy_s(editPresetNameBuffer, currentPresetName.c_str());
				}

				for (const auto& name : presetNames) {
					bool isSelected = (currentPresetName == name);
					if (ImGui::Selectable(name.c_str(), isSelected)) {
						currentPresetName = name;
						try {
							Engine::CursorPreset loadedPreset = Engine::CursorPresetManager::applyCursorPreset(name);
							applyPresetToGlobalSettings(loadedPreset);
						}
						catch (const std::exception& e) {
							std::cerr << "Error loading preset: " << e.what() << std::endl;
						}
					}
					if (isSelected) {
						ImGui::SetItemDefaultFocus();
					}
				}
				ImGui::EndCombo();
			}

			// Preset name editing
			if (isEditingPresetName) {
				ImGui::InputText("##EditPresetName", editPresetNameBuffer, IM_ARRAYSIZE(editPresetNameBuffer));
				ImGui::SameLine();
				if (ImGui::Button("Save")) {
					std::string newName = editPresetNameBuffer;
					if (!newName.empty()) {
						if (newName != currentPresetName) {
							// Rename existing preset
							if (currentPresetName != "New Preset") {
								Engine::CursorPreset oldPreset = Engine::CursorPresetManager::loadPreset(currentPresetName);
								oldPreset.name = newName;
								Engine::CursorPresetManager::savePreset(newName, oldPreset);
								Engine::CursorPresetManager::deletePreset(currentPresetName);
							}
							// Save new preset
							else {
								Engine::CursorPreset newPreset = createPresetFromCurrentSettings(newName);
								Engine::CursorPresetManager::savePreset(newName, newPreset);
							}
							currentPresetName = newName;
						}
						isEditingPresetName = false;
					}
				}
				ImGui::SameLine();
				if (ImGui::Button("Cancel")) {
					isEditingPresetName = false;
					if (currentPresetName == "New Preset") {
						currentPresetName = Engine::CursorPresetManager::getPresetNames().front();
					}
				}
			}
			else {
				if (ImGui::Button("Rename Preset")) {
					isEditingPresetName = true;
					strcpy_s(editPresetNameBuffer, currentPresetName.c_str());
				}
			}

			ImGui::SameLine();
			if (ImGui::Button("Update Preset")) {
				Engine::CursorPreset updatedPreset = createPresetFromCurrentSettings(currentPresetName);
				Engine::CursorPresetManager::savePreset(currentPresetName, updatedPreset);
			}

			ImGui::SameLine();
			if (ImGui::Button("Delete Preset")) {
				if (currentPresetName != "Default") {
					Engine::CursorPresetManager::deletePreset(currentPresetName);
					std::vector<std::string> remainingPresets = Engine::CursorPresetManager::getPresetNames();
					if (!remainingPresets.empty()) {
						currentPresetName = remainingPresets.front();
						Engine::CursorPreset loadedPreset = Engine::CursorPresetManager::applyCursorPreset(currentPresetName);
						applyPresetToGlobalSettings(loadedPreset);
					}
					else {
						currentPresetName = "Default";
						// Reset to default settings
						// You may want to create a function to set default cursor settings
						//setDefaultCursorSettings();
					}
				}
			}


			if (ImGui::TreeNode("3D Sphere Cursor")) {
				ImGui::Checkbox("Show 3D Sphere Cursor", &showSphereCursor);

				if (showSphereCursor) {
					ImGui::Checkbox("Orbit Follows Cursor", &orbitFollowsCursor);

					if (!orbitFollowsCursor) {
						ImGui::Checkbox("Show Orbit Center", &showOrbitCenter);
						if (showOrbitCenter) {
							float color[4] = { orbitCenterColor.r, orbitCenterColor.g, orbitCenterColor.b, orbitCenterColor.a };
							if (ImGui::ColorEdit4("Orbit Center Color", color)) {
								orbitCenterColor = glm::vec4(color[0], color[1], color[2], color[3]);
							}

							ImGui::SliderFloat("Orbit Center Size", &orbitCenterSphereRadius, 0.01f, 1.0f);
						}
					}

					const char* scalingModes[] = { "Normal", "Fixed", "Constrained Dynamic", "Logarithmic" };
					int currentMode = static_cast<int>(currentCursorScalingMode);
					if (ImGui::Combo("Cursor Scaling Mode", &currentMode, scalingModes, IM_ARRAYSIZE(scalingModes))) {
						currentCursorScalingMode = static_cast<CursorScalingMode>(currentMode);
					}

					ImGui::SliderFloat("Fixed Sphere Radius", &fixedSphereRadius, 0.01f, 3.0f);

					if (currentCursorScalingMode == CURSOR_CONSTRAINED_DYNAMIC) {
						ImGui::SliderFloat("Min Difference", &minDiff, 0.001f, 0.1f);
						ImGui::SliderFloat("Max Difference", &maxDiff, 0.01f, 1.0f);
					}

					float color[4] = { cursorColor.r, cursorColor.g, cursorColor.b, cursorColor.a };
					if (ImGui::ColorEdit4("Cursor Color", color)) {
						cursorColor = glm::vec4(color[0], color[1], color[2], color[3]);
					}

					ImGui::SliderFloat("Cursor Transparency", &cursorTransparency, 0.0f, 1.0f);
					ImGui::SliderFloat("Edge Softness", &cursorEdgeSoftness, 0.0f, 1.0f);
					ImGui::SliderFloat("Center Transparency", &cursorCenterTransparency, 0.0f, 1.0f);

					ImGui::Checkbox("Show Inner Sphere", &showInnerSphere);
					if (showInnerSphere) {
						float innerColor[4] = { innerSphereColor.r, innerSphereColor.g, innerSphereColor.b, innerSphereColor.a };
						if (ImGui::ColorEdit4("Inner Sphere Color", innerColor)) {
							innerSphereColor = glm::vec4(innerColor[0], innerColor[1], innerColor[2], innerColor[3]);
						}
						ImGui::SliderFloat("Inner Sphere Factor", &innerSphereFactor, 0.1f, 0.9f);
					}
				}

				ImGui::TreePop();
			}

			if (ImGui::TreeNode("Fragment Shader Cursor")) {
				ImGui::Checkbox("Show Fragment Shader Cursor", &showFragmentCursor);

				if (showFragmentCursor) {
					ImGui::SliderFloat("Outer Radius", &fragmentCursorSettings.baseOuterRadius, 0.0f, 0.2f);
					ImGui::SliderFloat("Outer Border Thickness", &fragmentCursorSettings.baseOuterBorderThickness, 0.01f, 0.05f);
					ImGui::SliderFloat("Inner Radius", &fragmentCursorSettings.baseInnerRadius, 0.0f, 0.1f);
					ImGui::SliderFloat("Inner Border Thickness", &fragmentCursorSettings.baseInnerBorderThickness, 0.01f, 0.05f);

					float outerColor[4] = { fragmentCursorSettings.outerColor.r, fragmentCursorSettings.outerColor.g, fragmentCursorSettings.outerColor.b, fragmentCursorSettings.outerColor.a };
					if (ImGui::ColorEdit4("Outer Color", outerColor)) {
						fragmentCursorSettings.outerColor = glm::vec4(outerColor[0], outerColor[1], outerColor[2], outerColor[3]);
					}

					float innerColor[4] = { fragmentCursorSettings.innerColor.r, fragmentCursorSettings.innerColor.g, fragmentCursorSettings.innerColor.b, fragmentCursorSettings.innerColor.a };
					if (ImGui::ColorEdit4("Inner Color", innerColor)) {
						fragmentCursorSettings.innerColor = glm::vec4(innerColor[0], innerColor[1], innerColor[2], innerColor[3]);
					}
				}

				ImGui::TreePop();
			}
		}

		ImGui::Separator();

		// Scene Management
		if (ImGui::CollapsingHeader("Scene Management")) {
			ImGui::InputText("Save Filename", saveFilename, IM_ARRAYSIZE(saveFilename));

			if (ImGui::Button("Browse##Save")) {
				auto destination = pfd::save_file("Select a file to save", ".",
					{ "JSON Files", "*.json", "All Files", "*" }).result();
				if (!destination.empty()) {
					strncpy_s(saveFilename, destination.c_str(), sizeof(saveFilename) - 1);
				}
			}
			ImGui::SameLine();
			if (ImGui::Button("Save Scene")) {
				Engine::saveScene(saveFilename, currentScene);
			}

			ImGui::InputText("Load Filename", loadFilename, IM_ARRAYSIZE(loadFilename));
			if (ImGui::Button("Browse##Load")) {
				auto selection = pfd::open_file("Select a file to load", ".",
					{ "JSON Files", "*.json", "All Files", "*" }).result();
				if (!selection.empty()) {
					strncpy_s(loadFilename, selection[0].c_str(), sizeof(loadFilename) - 1);
				}
			}
			ImGui::SameLine();
			if (ImGui::Button("Load Scene")) {
				currentScene = Engine::loadScene(loadFilename);
				currentModelIndex = currentScene.models.empty() ? -1 : 0;
			}
		}

		if (ImGui::Button("Info")) {
			showInfoWindow = !showInfoWindow;
		}

		if (showInfoWindow) {
			ImGui::SetNextWindowSize(ImVec2(400, 400), ImGuiCond_FirstUseEver);
			ImGui::Begin("Info", &showInfoWindow);

			ImGui::Text("Controls:");
			ImGui::BulletText("Left Mouse Button: Orbit camera / Select and move objects");
			ImGui::BulletText("Middle Mouse Button: Pan camera");
			ImGui::BulletText("Right Mouse Button: Rotate camera");
			ImGui::BulletText("Scroll Wheel: Zoom in/out");
			ImGui::BulletText("WASD: Move camera");
			ImGui::BulletText("Ctrl: Enter selection mode");
			ImGui::BulletText("Delete: Remove selected object");
			ImGui::BulletText("G: Toggle GUI visibility");

			ImGui::Separator();

			ImGui::Text("Libraries:");
			if (ImGui::TreeNode("OpenGL")) {
				ImGui::Text("Core graphics API");
				if (ImGui::Button("Website##OpenGL"))
					openURL("https://www.opengl.org/");
				ImGui::TreePop();
			}
			if (ImGui::TreeNode("GLFW")) {
				ImGui::Text("Window creation and management");
				if (ImGui::Button("GitHub##GLFW"))
					openURL("https://github.com/glfw/glfw");
				ImGui::TreePop();
			}
			if (ImGui::TreeNode("GLAD")) {
				ImGui::Text("OpenGL function loader");
				if (ImGui::Button("GitHub##GLAD"))
					openURL("https://github.com/Dav1dde/glad");
				ImGui::TreePop();
			}
			if (ImGui::TreeNode("GLM")) {
				ImGui::Text("Mathematics library for graphics");
				if (ImGui::Button("GitHub##GLM"))
					openURL("https://github.com/g-truc/glm");
				ImGui::TreePop();
			}
			if (ImGui::TreeNode("ImGui")) {
				ImGui::Text("User interface library");
				if (ImGui::Button("GitHub##ImGui"))
					openURL("https://github.com/ocornut/imgui");
				ImGui::TreePop();
			}
			if (ImGui::TreeNode("stb_image")) {
				ImGui::Text("Image loading library");
				if (ImGui::Button("GitHub##stb_image"))
					openURL("https://github.com/nothings/stb");
				ImGui::TreePop();
			}
			if (ImGui::TreeNode("tinyobjloader")) {
				ImGui::Text("Wavefront .obj file loader");
				if (ImGui::Button("GitHub##tinyobjloader"))
					openURL("https://github.com/tinyobjloader/tinyobjloader");
				ImGui::TreePop();
			}
			if (ImGui::TreeNode("json.hpp")) {
				ImGui::Text("JSON for Modern C++");
				if (ImGui::Button("GitHub##json.hpp"))
					openURL("https://github.com/nlohmann/json");
				ImGui::TreePop();
			}
			if (ImGui::TreeNode("portable-file-dialogs")) {
				ImGui::Text("Cross-platform file dialog library");
				if (ImGui::Button("GitHub##portable-file-dialogs"))
					openURL("https://github.com/samhocevar/portable-file-dialogs");
				ImGui::TreePop();
			}

			ImGui::End();
		}
		ImGui::End();
	}

	if (showFPS) {
		ImGui::SetNextWindowPos(ImVec2(windowWidth - 120, windowHeight - 60));
		ImGui::Begin("FPS Counter", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoBackground);
		ImGui::Text("FPS: %.1f", ImGui::GetIO().Framerate);
		ImGui::End();
	}

	ImGui::Render();
	ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
}

void renderModels(Shader* shader)
{
	updatePointLights();

	// Set point light uniforms
	for (int i = 0; i < pointLights.size() && i < MAX_LIGHTS; i++) {
		std::string lightName = "lights[" + std::to_string(i) + "]";
		shader->setVec3(lightName + ".position", pointLights[i].position);
		shader->setVec3(lightName + ".color", pointLights[i].color);
		shader->setFloat(lightName + ".intensity", pointLights[i].intensity);
	}
	shader->setInt("numLights", std::min((int)pointLights.size(), MAX_LIGHTS));

	for (int i = 0; i < currentScene.models.size(); i++) {
		const auto& model = currentScene.models[i];
		glm::mat4 modelMatrix = glm::mat4(1.0f);
		modelMatrix = glm::translate(modelMatrix, model.position);
		modelMatrix = glm::rotate(modelMatrix, glm::radians(model.rotation.x), glm::vec3(1, 0, 0));
		modelMatrix = glm::rotate(modelMatrix, glm::radians(model.rotation.y), glm::vec3(0, 1, 0));
		modelMatrix = glm::rotate(modelMatrix, glm::radians(model.rotation.z), glm::vec3(0, 0, 1));
		modelMatrix = glm::scale(modelMatrix, model.scale);

		shader->setMat4("model", modelMatrix);

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, model.texture);
		shader->setInt("texture_diffuse1", 0);
		shader->setFloat("hasTexture", model.hasCustomTexture ? 1.0f : 0.0f);
		shader->setVec3("objectColor", model.color);

		shader->setBool("isPointCloud", false);

		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, model.normalMap);
		shader->setInt("texture_normal1", 1);

		glActiveTexture(GL_TEXTURE2);
		glBindTexture(GL_TEXTURE_2D, model.specularMap);
		shader->setInt("texture_specular1", 2);

		glActiveTexture(GL_TEXTURE3);
		glBindTexture(GL_TEXTURE_2D, model.aoMap);
		shader->setInt("texture_ao1", 3);

		// Set flags for texture availability
		shader->setInt("hasNormalMap", model.normalMap != 0);
		shader->setInt("hasSpecularMap", model.specularMap != 0);
		shader->setInt("hasAOMap", model.aoMap != 0);

		// Set color and material properties
		shader->setFloat("shininess", model.shininess);
		shader->setFloat("emissive", model.emissive);

		// Set selection mode variables
		shader->setBool("selectionMode", selectionMode);
		shader->setBool("isSelected", selectionMode && (i == currentModelIndex));

		glBindVertexArray(model.vao);
		glDrawElements(GL_TRIANGLES, model.indices.size(), GL_UNSIGNED_INT, 0);
		glBindVertexArray(0);
	}
}

void renderPointClouds(Shader* shader) {
	for (const auto& pointCloud : currentScene.pointClouds) {
		glm::mat4 modelMatrix = glm::mat4(1.0f);
		modelMatrix = glm::translate(modelMatrix, pointCloud.position);
		modelMatrix = glm::rotate(modelMatrix, glm::radians(pointCloud.rotation.x), glm::vec3(1, 0, 0));
		modelMatrix = glm::rotate(modelMatrix, glm::radians(pointCloud.rotation.y), glm::vec3(0, 1, 0));
		modelMatrix = glm::rotate(modelMatrix, glm::radians(pointCloud.rotation.z), glm::vec3(0, 0, 1));
		modelMatrix = glm::scale(modelMatrix, pointCloud.scale);

		shader->setMat4("model", modelMatrix);
		shader->setBool("isPointCloud", true);

		glBindVertexArray(pointCloud.vao);
		glPointSize(5.0f); // Increase point size for better visibility
		glDrawArrays(GL_POINTS, 0, pointCloud.points.size());
		glBindVertexArray(0);
	}

	shader->setBool("isPointCloud", false);
}

#pragma endregion


// ---- Shader and Lighting ----
#pragma region Shader and Lighting
void updateFragmentShaderUniforms(Shader* shader) {
	// Set fragment shader uniforms for cursor rendering
	shader->setFloat("baseOuterRadius", showFragmentCursor ? fragmentCursorSettings.baseOuterRadius : 0.0f);
	shader->setFloat("baseOuterBorderThickness", showFragmentCursor ? fragmentCursorSettings.baseOuterBorderThickness : 0.0f);
	shader->setFloat("baseInnerRadius", showFragmentCursor ? fragmentCursorSettings.baseInnerRadius : 0.0f);
	shader->setFloat("baseInnerBorderThickness", showFragmentCursor ? fragmentCursorSettings.baseInnerBorderThickness : 0.0f);
	shader->setVec4("outerCursorColor", showFragmentCursor ? fragmentCursorSettings.outerColor : glm::vec4(0.0f));
	shader->setVec4("innerCursorColor", showFragmentCursor ? fragmentCursorSettings.innerColor : glm::vec4(0.0f));
	shader->setBool("showFragmentCursor", showFragmentCursor);
}

void updatePointLights() {
	pointLights.clear();
	for (const auto& model : currentScene.models) {
		if (model.emissive > 0.0f) {
			// Calculate rotation matrix for the model
			glm::mat4 rotX = glm::rotate(glm::mat4(1.0f), glm::radians(model.rotation.x), glm::vec3(1, 0, 0));
			glm::mat4 rotY = glm::rotate(glm::mat4(1.0f), glm::radians(model.rotation.y), glm::vec3(0, 1, 0));
			glm::mat4 rotZ = glm::rotate(glm::mat4(1.0f), glm::radians(model.rotation.z), glm::vec3(0, 0, 1));
			glm::mat4 rotationMatrix = rotZ * rotY * rotX;

			// Calculate the model's bounding box in world space
			glm::vec3 minBounds(std::numeric_limits<float>::max());
			glm::vec3 maxBounds(std::numeric_limits<float>::lowest());
			for (const auto& vertex : model.vertices) {
				glm::vec4 rotatedPos = rotationMatrix * glm::vec4(vertex.position, 1.0f);
				glm::vec3 worldPos = model.position + model.scale * glm::vec3(rotatedPos);
				minBounds = glm::min(minBounds, worldPos);
				maxBounds = glm::max(maxBounds, worldPos);
			}

			// Create multiple point lights distributed across the model's bounding box
			int numLightsPerDimension = 3; // Adjust this for more or fewer lights
			glm::vec3 step = (maxBounds - minBounds) / float(numLightsPerDimension - 1);
			for (int x = 0; x < numLightsPerDimension; ++x) {
				for (int y = 0; y < numLightsPerDimension; ++y) {
					for (int z = 0; z < numLightsPerDimension; ++z) {
						PointLight light;
						light.position = minBounds + glm::vec3(x * step.x, y * step.y, z * step.z);
						light.color = model.color;
						light.intensity = model.emissive / float(numLightsPerDimension * numLightsPerDimension * numLightsPerDimension);
						pointLights.push_back(light);
					}
				}
			}
		}
	}
}
#pragma endregion


PointCloud loadPointCloudFile(const std::string& filePath, size_t downsampleFactor) {
	return Engine::PointCloudLoader::loadPointCloudFile(filePath, downsampleFactor);
}


// ---- Cursor and Ray Casting ----
#pragma region Cursor and Ray Casting
void updateCursorPosition(GLFWwindow* window, const glm::mat4& projection, const glm::mat4& view, Shader* shader) {
	// Prepare for rendering
	glDrawBuffer(GL_BACK);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	shader->use();
	shader->setMat4("projection", projection);
	shader->setMat4("view", view);
	renderModels(shader);

	// Read depth at cursor position
	float depth = 0.0;
	glReadPixels(lastX, (float)windowHeight - lastY, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth);

	// Convert cursor position to world space
	glm::mat4 vpInv = glm::inverse(projection * view);
	glm::vec4 ndc = glm::vec4((lastX / (float)windowWidth) * 2.0 - 1.0, 1.0 - (lastY / (float)windowHeight) * 2.0, depth * 2.0 - 1.0, 1.0);
	auto worldPosH = vpInv * ndc;
	auto worldPos = worldPosH / worldPosH.w;
	auto isHit = depth != 1.0;

	static bool wasHit = false; // Keep track of previous hit state

	if (!((camera.IsOrbiting && orbitFollowsCursor) || camera.IsAnimating)) {
		if (isHit && (showSphereCursor || showFragmentCursor)) {
			g_cursorValid = true;
			g_cursorPos = glm::vec3(worldPos);

			if (!wasHit) {
				// Cursor just entered an object
				glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_HIDDEN);
			}
			wasHit = true;

		}
		else {
			g_cursorValid = false;

			if (wasHit) {
				// Cursor just left an object
				glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
			}

			wasHit = false;
		}
	}
}

void calculateMouseRay(float mouseX, float mouseY, glm::vec3& rayOrigin, glm::vec3& rayDirection, glm::vec3& rayNear, glm::vec3& rayFar, float aspect) {
	// Convert mouse position to normalized device coordinates
	float x = (2.0f * mouseX) / windowWidth - 1.0f;
	float y = 1.0f - (2.0f * mouseY) / windowHeight;

	// Calculate near and far points in clip space
	glm::vec4 rayNearClip = glm::vec4(x, y, -1.0, 1.0);
	glm::vec4 rayFarClip = glm::vec4(x, y, 1.0, 1.0);

	// Convert to eye space
	glm::mat4 invProj = glm::inverse(camera.GetProjectionMatrix(aspect, currentScene.settings.nearPlane, currentScene.settings.farPlane));
	glm::vec4 rayNearEye = invProj * rayNearClip;
	glm::vec4 rayFarEye = invProj * rayFarClip;

	// Normalize eye space coordinates
	rayNearEye /= rayNearEye.w;
	rayFarEye /= rayFarEye.w;

	// Convert to world space
	glm::mat4 invView = glm::inverse(camera.GetViewMatrix());
	glm::vec4 rayNearWorld = invView * rayNearEye;
	glm::vec4 rayFarWorld = invView * rayFarEye;

	rayNear = glm::vec3(rayNearWorld);
	rayFar = glm::vec3(rayFarWorld);

	rayOrigin = camera.Position;
	rayDirection = glm::normalize(rayFar - rayNear);
}

bool rayIntersectsModel(const glm::vec3& rayOrigin, const glm::vec3& rayDirection, const Engine::ObjModel& model, float& distance) {
	float closestDistance = std::numeric_limits<float>::max();
	bool intersected = false;

	// Transform ray to model space
	glm::mat4 modelMatrix = glm::translate(glm::mat4(1.0f), model.position) *
		glm::rotate(glm::mat4(1.0f), glm::radians(model.rotation.x), glm::vec3(1, 0, 0)) *
		glm::rotate(glm::mat4(1.0f), glm::radians(model.rotation.y), glm::vec3(0, 1, 0)) *
		glm::rotate(glm::mat4(1.0f), glm::radians(model.rotation.z), glm::vec3(0, 0, 1)) *
		glm::scale(glm::mat4(1.0f), model.scale);
	glm::mat4 invModelMatrix = glm::inverse(modelMatrix);
	glm::vec3 rayOriginModel = glm::vec3(invModelMatrix * glm::vec4(rayOrigin, 1.0f));
	glm::vec3 rayDirectionModel = glm::vec3(invModelMatrix * glm::vec4(rayDirection, 0.0f));

	// Iterate through all triangles in the model
	for (size_t i = 0; i < model.indices.size(); i += 3) {
		glm::vec3 v0 = model.vertices[model.indices[i]].position;
		glm::vec3 v1 = model.vertices[model.indices[i + 1]].position;
		glm::vec3 v2 = model.vertices[model.indices[i + 2]].position;

		// Möller–Trumbore intersection algorithm
		glm::vec3 edge1 = v1 - v0;
		glm::vec3 edge2 = v2 - v0;
		glm::vec3 h = glm::cross(rayDirectionModel, edge2);
		float a = glm::dot(edge1, h);

		if (a > -0.00001f && a < 0.00001f) continue; // Ray is parallel to triangle

		float f = 1.0f / a;
		glm::vec3 s = rayOriginModel - v0;
		float u = f * glm::dot(s, h);

		if (u < 0.0f || u > 1.0f) continue;

		glm::vec3 q = glm::cross(s, edge1);
		float v = f * glm::dot(rayDirectionModel, q);

		if (v < 0.0f || u + v > 1.0f) continue;

		float t = f * glm::dot(edge2, q);

		if (t > 0.00001f && t < closestDistance) {
			closestDistance = t;
			intersected = true;
		}
	}

	if (intersected) {
		// Transform the intersection point back to world space
		glm::vec3 intersectionPointModel = rayOriginModel + rayDirectionModel * closestDistance;
		glm::vec4 intersectionPointWorld = modelMatrix * glm::vec4(intersectionPointModel, 1.0f);
		distance = glm::distance(rayOrigin, glm::vec3(intersectionPointWorld));
		return true;
	}

	return false;
}
#pragma endregion



// ---- Model Management ----
#pragma region Model Management
void deleteSelectedModel() {
	if (currentModelIndex >= 0 && currentModelIndex < currentScene.models.size()) {
		currentScene.models.erase(currentScene.models.begin() + currentModelIndex);
		if (currentScene.models.empty()) {
			currentModelIndex = -1;
		}
		else {
			currentModelIndex = std::min(currentModelIndex, static_cast<int>(currentScene.models.size()) - 1);
		}
	}
}
#pragma endregion



// ---- Callbacks ----
#pragma region Callbacks
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	glViewport(0, 0, width, height);
	windowWidth = width;
	windowHeight = height;
}

void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) {
	if (!ImGui::GetIO().WantCaptureMouse) {
		camera.ProcessMouseScroll(yoffset);
	}
}

void mouse_button_callback(GLFWwindow* window, int button, int action, int mods) {
	if (ImGui::GetIO().WantCaptureMouse) {
		// Mouse is over ImGui UI, ensure Windows cursor is visible
		glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
		return;
	}

	if (button == GLFW_MOUSE_BUTTON_LEFT) {
		if (action == GLFW_PRESS) {
			// Handle double-click
			double currentTime = glfwGetTime();
			if (currentTime - lastClickTime < doubleClickTime) {
				if (g_cursorValid) {
					camera.StartCenteringAnimation(g_cursorPos);

					// Reset cursor position to center of window
					glfwSetCursorPos(window, windowWidth / 2, windowHeight / 2);
				}
			}
			lastClickTime = currentTime;

			if (!camera.IsAnimating && !camera.IsOrbiting) {
				leftMousePressed = true;

				if (g_cursorValid) {
					if (orbitFollowsCursor && showSphereCursor) {
						camera.StartCenteringAnimation(g_cursorPos);
						capturedCursorPos = g_cursorPos;

						// Capture the mouse
						glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
						isMouseCaptured = true;
					}
					else {
						// Set orbit point and start orbiting
						float cursorDepth = glm::length(g_cursorPos - camera.Position);
						glm::vec3 viewportCenter = camera.Position + camera.Front * cursorDepth;
						capturedCursorPos = viewportCenter;
						camera.SetOrbitPointDirectly(capturedCursorPos);
						camera.OrbitDistance = cursorDepth;
						camera.StartOrbiting();
					}
				}
				else {
					// Set default orbit point
					capturedCursorPos = camera.Position + camera.Front * camera.OrbitDistance;
					camera.SetOrbitPointDirectly(capturedCursorPos);
					camera.StartOrbiting();
				}
			}
			if (selectionMode) {
				glm::vec3 rayOrigin, rayDirection, rayNear, rayFar;
				calculateMouseRay(lastX, lastY, rayOrigin, rayDirection, rayNear, rayFar, (float)windowWidth / (float)windowHeight);

				float closestDistance = std::numeric_limits<float>::max();
				int closestModelIndex = -1;

				for (int i = 0; i < currentScene.models.size(); i++) {
					const auto& model = currentScene.models[i];
					float distance;
					if (rayIntersectsModel(rayOrigin, rayDirection, model, distance)) {
						if (distance < closestDistance) {
							closestDistance = distance;
							closestModelIndex = i;
						}
					}
				}

				if (closestModelIndex != -1) {
					currentModelIndex = closestModelIndex;
					isMovingModel = true;
				}
			}
		}
		else if (action == GLFW_RELEASE) {
			leftMousePressed = false;
			if (orbitFollowsCursor && showSphereCursor && camera.IsOrbiting) {
				// Release the mouse
				glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
				isMouseCaptured = false;

				// Reset cursor position to center of window
				glfwSetCursorPos(window, windowWidth / 2, windowHeight / 2);
			}
			camera.StopOrbiting();
			leftMousePressed = false;
			isMovingModel = false;

		}
	}
	else if (button == GLFW_MOUSE_BUTTON_MIDDLE) {
		if (action == GLFW_PRESS) {
			middleMousePressed = true;
			camera.StartPanning();
		}
		else if (action == GLFW_RELEASE) {
			middleMousePressed = false;
			camera.StopPanning();
		}

	}
	if (button == GLFW_MOUSE_BUTTON_RIGHT) {
		if (action == GLFW_PRESS) {
			rightMousePressed = true;

		}
		else if (action == GLFW_RELEASE) {
			rightMousePressed = false;
		}
	}
}

void mouse_callback(GLFWwindow* window, double xposIn, double yposIn) {
	if (ImGui::GetIO().WantCaptureMouse) {
		// Mouse is over ImGui UI, ensure Windows cursor is visible
		glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
		return;
	}
	float xpos = static_cast<float>(xposIn);
	float ypos = static_cast<float>(yposIn);

	if (isMouseCaptured) {
		// Calculate offset from center of window
		float xoffset = xpos - (windowWidth / 2);
		float yoffset = (windowHeight / 2) - ypos;  // Reversed since y-coordinates go from bottom to top

		// Reset cursor to center of window
		glfwSetCursorPos(window, windowWidth / 2, windowHeight / 2);

		// Use these offsets for camera movement
		if (camera.IsOrbiting && !camera.IsAnimating) {
			camera.ProcessMouseMovement(xoffset, yoffset);
			glm::vec3 directionToCamera = glm::normalize(camera.Position - camera.OrbitPoint);
			camera.Position = camera.OrbitPoint + directionToCamera * camera.OrbitDistance;
		}
	}
	else {
		float xoffset = (xpos - lastX);
		float yoffset = (lastY - ypos);

		lastX = xpos;
		lastY = ypos;

		if (isMovingModel && currentModelIndex != -1) {
			// Move selected model
			float distanceToModel = glm::distance(camera.Position, currentScene.models[currentModelIndex].position);
			float aspectRatio = static_cast<float>(windowWidth) / static_cast<float>(windowHeight);
			float normalizedXOffset = xoffset / static_cast<float>(windowWidth);
			float normalizedYOffset = yoffset / static_cast<float>(windowHeight);
			float baseSensitivity = 0.66f;
			float sensitivityFactor = (baseSensitivity * distanceToModel);
			normalizedXOffset *= aspectRatio;

			glm::vec3 right = glm::normalize(glm::cross(camera.Front, camera.Up));
			glm::vec3 up = glm::normalize(glm::cross(right, camera.Front));

			currentScene.models[currentModelIndex].position += right * normalizedXOffset * sensitivityFactor;
			currentScene.models[currentModelIndex].position += up * normalizedYOffset * sensitivityFactor;
		}
		else if (camera.IsOrbiting && !camera.IsAnimating) {
			// Handle camera orbiting
			camera.ProcessMouseMovement(xoffset, yoffset);
			glm::vec3 directionToCamera = glm::normalize(camera.Position - camera.OrbitPoint);
			camera.Position = camera.OrbitPoint + directionToCamera * camera.OrbitDistance;
		}
		else if ((leftMousePressed || rightMousePressed || middleMousePressed) && !camera.IsAnimating) {
			// Handle regular camera movement
			camera.ProcessMouseMovement(xoffset, yoffset);
		}
	}
}

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
	if (key == GLFW_KEY_G && action == GLFW_PRESS)
	{
		showGui = !showGui;
		std::cout << "GUI visibility toggled. showGui = " << (showGui ? "true" : "false") << std::endl;
	}

	// Handle Ctrl key
	if (key == GLFW_KEY_LEFT_CONTROL || key == GLFW_KEY_RIGHT_CONTROL)
	{
		if (action == GLFW_PRESS)
		{
			selectionMode = true;
			std::cout << "Entered selection mode" << std::endl;
		}
		else if (action == GLFW_RELEASE)
		{
			selectionMode = false;
			std::cout << "Exited selection mode" << std::endl;
		}
	}

	// Handle Delete key
	if (key == GLFW_KEY_DELETE && action == GLFW_PRESS)
	{
		deleteSelectedModel();
		std::cout << "Deleted selected model" << std::endl;
	}
	
	// added some keys to manipulate the stereo settings
	if (key == GLFW_KEY_LEFT /* && action == GLFW_PRESS*/) {
		currentScene.settings.separation -= 0.5f;
	}

	if (key == GLFW_KEY_RIGHT /* && action == GLFW_PRESS*/) {
		currentScene.settings.separation += 0.5f;
	}

	if (key == GLFW_KEY_DOWN /* && action == GLFW_PRESS*/) {
		currentScene.settings.convergence -= 0.5f;
	}

	if (key == GLFW_KEY_UP /* && action == GLFW_PRESS*/) {
		currentScene.settings.convergence += 0.5f;
	}



}
#pragma endregion