/*
 * Simple game engine example in C++
 * Fall 2024
 * For CS392:  Game Engine Design
 */
#define MAIN
#include "game.h"

#include "activation_area.h"
#include "projectiles.h"
#include "tile_floor.h"
#include "moving_platform.h"
#include "turret.h"
#include "geometric_objects.h"
#include "door.h"
#include "vehicle.h"
#include "maze.h"
#include "rbtree_set.h"

std::mutex grand_mutex;

float height = 2080 ;
float width = 3820 ;

/* Global section */
int time_resolution = 10;
int framecount = 0;
int loopcount = 0;

std::vector<gameobject*> objects;
projectile ice_balls;
wall_block spawned_blocks("cube.obj", "projectile.jpg", glm::vec3(2, 2, 2));
fragment brick_fragments;
maze_solver round_maze_solver;

class target : public loaded_object, public block_object {
	public:
		target(const char* model, const char* texture) : loaded_object(model, texture, glm::vec3(2, 2, 2)) {
			collision_check = true;
		}
		target() : loaded_object("monkey.obj", "brick.jpg", glm::vec3(15.0f, 10.0f, 15.0f)) {
			collision_check = true;
		}
		void hit_index(size_t index){
			brick_fragments.create_burst(100, locations[index], 0.01f);
			locations.erase(locations.begin() + index);
		}

};

ssize_t is_empty(glm::vec3 position, float distance){
	for(gameobject* o : objects) {
		ssize_t collide_index = o->collision_index(position, distance);
		if(collide_index != -1)
			return false;
	}
	return true;
}

void fire(bool burst = false){
	if(burst)
		ice_balls.add_projectile(player_position, player_heading, player_elevation, 0.4f, 10000.0f, 1.0f, true);
	else
		ice_balls.add_projectile(player_position, player_heading, player_elevation, 0.05f, 100000.0f, 1.0f, false);

}

void mouse_click_callback(GLFWwindow* window, int button, int action, int mods){
	if(button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_PRESS)
		fire();
	if(button == GLFW_MOUSE_BUTTON_RIGHT && action == GLFW_PRESS)
		fire(true);
	if(button == 2 && action == GLFW_PRESS){
		// Spawn a block
		glm::vec3 look_at_point = 2.0f * glm::vec3(roundf(player_position.x/2.0f), roundf(player_position.y/2.0f), roundf(player_position.z/2.0f)) ;
		look_at_point.x += 2.0f * roundf(4.0f * cosf(player_elevation) * sinf(player_heading));
		look_at_point.y += 2.0f * roundf(4.0f * sinf(player_elevation));
		look_at_point.z += 2.0f * roundf(4.0f * cosf(player_elevation) * cosf(player_heading));
		if(!is_empty(look_at_point, 0.0f)){
			look_at_point = 2.0f * glm::vec3(roundf(player_position.x/2.0f), roundf(player_position.y/2.0f), roundf(player_position.z/2.0f)) ;
			look_at_point.x += 2.0f * roundf(2.0f * cosf(player_elevation) * sinf(player_heading));
			look_at_point.y += 2.0f * roundf(2.0f * sinf(player_elevation));
			look_at_point.z += 2.0f * roundf(2.0f * cosf(player_elevation) * cosf(player_heading));
		}
		if(is_empty(look_at_point, 0.0f))
			spawned_blocks.locations.push_back(look_at_point);
	}
}

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods){
	if(GLFW_KEY_F3 == key && 1 == action)
		main_hud.visible = !main_hud.visible;
	if(GLFW_KEY_W == key && 1 == action){
		player_key_status.forward = 1;
	}	
	else if(GLFW_KEY_W == key && 0 == action){
		player_key_status.forward = 0;
	}	
	if(GLFW_KEY_S == key && 1 == action){
		player_key_status.backward = 1;
	}	
	else if(GLFW_KEY_S == key && 0 == action){
		player_key_status.backward = 0;
	}
	if(GLFW_KEY_E == key && 1 == action){
		// Spawn a block
		glm::vec3 look_at_point = glm::vec3(player_position.x, player_position.y, player_position.z) ;
		look_at_point.x += 6.0f * cosf(player_elevation) * sinf(player_heading);
		look_at_point.y += 6.0f * sinf(player_elevation);
		look_at_point.z += 6.0f * cosf(player_elevation) * cosf(player_heading);
		for(gameobject* o : objects) {
			ssize_t collide_index = o->collision_index(look_at_point, 20.0f);
			if(collide_index != -1)
				o->activate(collide_index);
		}
	}
	if(GLFW_KEY_A == key)
		player_key_status.left = action;
	if(GLFW_KEY_D == key)
		player_key_status.right = action;
	if(GLFW_KEY_SPACE == key && 1 == action){
		if(player_platform || player_position.y < 0.1f + player_height){
			player_fall_speed = 0.04f;
			player_position.y += 1.0f;
			player_platform = 0;
		}
	}
	if(GLFW_KEY_V == key && 0 == action)
		round_maze_solver.launch(-100);
}

int shutdown_engine = 0;
/* Must be called at a consistent rate */
void player_movement(){
	while(!shutdown_engine){
		//		grand_mutex.lock();
		auto start = std::chrono::system_clock::now();
		if(current_vehicle){
			current_vehicle->vehicle_move();

		} else {
			glm::vec3 step_to_point = player_position;
			if(player_key_status.forward){
				step_to_point += 0.1f * glm::vec3(sinf(player_heading), 0, cosf(player_heading));
			}
			if(player_key_status.backward){
				step_to_point += 0.1f * glm::vec3(-sinf(player_heading), 0, -cosf(player_heading));
			}
			if(player_key_status.left){
				step_to_point += 0.05f * glm::vec3(sinf(player_heading + M_PI/2), 0, cosf(player_heading + M_PI/2));
			}
			if(player_key_status.right){
				step_to_point += 0.05f * glm::vec3(-sinf(player_heading + M_PI/2), 0, -cosf(player_heading + M_PI/2));
			}
			for(gameobject* o : objects) {
				ssize_t collide_index = o->collision_index(step_to_point, 0.2f);
				if(collide_index != -1) {
					if(is_empty(glm::vec3(player_position.x, step_to_point.y, step_to_point.z), 0.2f)) {
						step_to_point.x = player_position.x;
						break;
					}
					else if(is_empty(glm::vec3(step_to_point.x, step_to_point.y, player_position.z), 0.2f)) {
						step_to_point.z = player_position.z;
						break;
					}
					else {
						step_to_point = player_position;
						break;
					}


				}
			}
			player_position = step_to_point;

			if(player_platform){
				if(!player_platform->is_on_idx(player_position, player_platform_index, player_height))
					player_platform = 0;
			} else {
				float floor_height = 0;
				for(gameobject* o : objects) {
					ssize_t ppi = o->is_on(player_position, player_height);
					if(ppi != -1) {
						player_platform_index = ppi;
						player_platform = o;	
						floor_height = player_platform->locations[player_platform_index].y + (player_platform->size.y / 2);
						player_fall_speed = 0;
						player_position.y = floor_height + player_height; 
					}
				}
				if(player_position.y - player_height > floor_height) {
					player_position.y += player_fall_speed;
					player_fall_speed -= GRAVITY;
				} else {
					player_fall_speed = 0;
					player_position.y = floor_height + player_height; 
				}
			}
		}
		main_hud.lprintf(2, "Player Position:  (%f, %f, %f)", player_position.x, player_position.y, player_position.z);
		//		grand_mutex.unlock();
		auto end = std::chrono::system_clock::now();
		//		double difference = std::chrono::duration_cast<std::chrono::milliseconds>(start - end).count();
		//		printf("Time difference:  %lf\n", difference);
		std::this_thread::sleep_for(std::chrono::microseconds(1000) - (start - end));
	}
}

void object_movement(){
	int loop_time;
	auto last_call = std::chrono::system_clock::now(); 
	while(!shutdown_engine){
		auto start_time = std::chrono::system_clock::now();
		loop_time = std::chrono::duration_cast<std::chrono::microseconds>(start_time - last_call).count();

		//		grand_mutex.lock();
		if(player_platform){
			glm::vec3 pltloc = player_platform->locations[player_platform_index];
			float floor_height = pltloc.y + (player_platform->size.y / 2);
			player_position.y = floor_height + player_height;
		}
		//		grand_mutex.unlock();
		for(gameobject* o : objects)
			o->move(loop_time); // move needs a parameter which indicates how long since we last called move (loop_time)
		auto end_time = std::chrono::system_clock::now();
		int cpu_time = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time).count();
		last_call = start_time;
		int sleep_time = 1000 - cpu_time;
		loopcount++;
		if(sleep_time > 100 )
			std::this_thread::sleep_for(std::chrono::microseconds(sleep_time));
	}
}

void animation(){
	while(!shutdown_engine){
		auto start = std::chrono::system_clock::now();
		for(gameobject* o : objects)
			o->animate();
		auto end = std::chrono::system_clock::now();
		//		double difference = std::chrono::duration_cast<std::chrono::milliseconds>(start - end).count();
		//		printf("Time difference:  %lf\n", difference);
		std::this_thread::sleep_for(std::chrono::microseconds(10000) - (start - end));
	}
}

void collision_detection(){
	while(!shutdown_engine){
		auto start = std::chrono::system_clock::now();
		ice_balls.data_mutex.lock();
		size_t nproj = ice_balls.locations.size();
		for(size_t proj_index = 0; proj_index < nproj; proj_index++){
			glm::vec3 l = ice_balls.locations[proj_index];
			for(auto o : objects){
				if(o->collision_check){
					ssize_t index = o->collision_index(l);
					if(index != -1) {
						o->hit_index(index);
						ice_balls.hit_index(proj_index);
						break;
					}
				}
			}
		}	
		ice_balls.data_mutex.unlock();
		auto end = std::chrono::system_clock::now();
		//		double difference = std::chrono::duration_cast<std::chrono::milliseconds>(start - end).count();
		//		printf("Time difference:  %lf\n", difference);
		std::this_thread::sleep_for(std::chrono::microseconds(1000) - (start - end));
	}
}
void pos_callback(GLFWwindow* window, double xpos, double ypos){
	double center_x = width/2;
	double diff_x = xpos - center_x;
	double center_y = height/2;
	double diff_y = ypos - center_y;
	glfwSetCursorPos(window, center_x, center_y);
	player_heading -= diff_x / 1000.0; // Is this too fast or slow?
	if((diff_y > 0 && player_elevation - diff_y/1000.0 > -M_PI/2) || (diff_y < 0 && player_elevation - diff_y/1000.0 < M_PI/2))
		player_elevation -= diff_y / 1000.0;
}

void resize(GLFWwindow*, int new_width, int new_height){
	width = new_width;
	height = new_height;
	printf("Window resized, now %f by %f\n", width, height);
	glViewport(0, 0, width, height);
}


target targets;
void bob(){
	static bool bob_happened = false;
	if(!bob_happened){
		bob_happened = true;
		targets.locations.push_back(glm::vec3(-10, 5, 10));
	}
};

turret t;
void activate_turret(){
	t.active = !t.active;
}

void debug_callback(GLenum, GLenum, GLuint, GLenum severity, GLsizei, const GLchar* message, const void*) {
	if(severity == GL_DEBUG_SEVERITY_HIGH)
		puts(RED(message).c_str());
	else if(severity == GL_DEBUG_SEVERITY_MEDIUM)
		puts(YELLOW(message).c_str());
	/* Only uncomment if you want a lot of messages */
	/*
		else
		puts(message);
		*/
}

int main(int argc, char** argv) {
	init_helpers();
	srandom((unsigned long)argv);
	if(!glfwInit()) {
		puts(RED("GLFW init error, terminating\n").c_str());
		return 1;
	}
	GLFWwindow* window = glfwCreateWindow(width, height, "Simple OpenGL 4.0+ Demo", 0, 0);
	if(!window){
		puts(RED("Window creation failed, terminating\n").c_str());
		return 1;
	}
	glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
	glfwMakeContextCurrent(window);
	glewInit();

	unsigned supported_threads = std::thread::hardware_concurrency();
	printf("Supported threads:  %u\n", supported_threads);

	/* Set up callbacks */
	glEnable(GL_DEBUG_OUTPUT);
	glDebugMessageCallback(debug_callback, 0);
	glfwSetKeyCallback(window, key_callback);
	glfwSetCursorPosCallback(window, pos_callback);
	glfwSetFramebufferSizeCallback(window, resize);
	glfwSetMouseButtonCallback(window, mouse_click_callback);

	/* Set starting point */
	player_position = glm::vec3(0, 10, 20);
	player_heading = M_PI;


	/* Level Loading (hardcoded at the moment) */
	objects.push_back(&brick_fragments);
	tile_floor fl;
	objects.push_back(&ice_balls);
	objects.push_back(&fl);

	target our_target;
	objects.push_back(&our_target);

	target carpet_torus("torus.obj", "carpet.jpg");
	objects.push_back(&carpet_torus);
	
	target chicken_cube("cube.obj", "JerseyGiant.jpg");
	objects.push_back(&chicken_cube);

	rbtree_set<target*> things;
	const char* textures[] = {"BarredRock.jpg", "brick.jpg", "carpet.jpg", "JerseyGiant.jpg", "nutsandbolts.jpg",  "projectile.jpg", "RhodeIslandRed.jpg", "carpet.jpg", "carpet.jpg"};
	for(auto tf : textures){
		target *sphere = new target("sphere.obj", tf);
		objects.push_back(sphere);
		things.insert(sphere);
	}

	things.insert(&our_target);
	things.insert(&carpet_torus);
	things.insert(&chicken_cube);
 	for(float x = 0;; x += 3) {
		target *next_thing = things.get_random();
		next_thing->locations.push_back(glm::vec3(x, 0, 0));
		things.delete_node(next_thing);
		if(things.empty())
			break;	
	}

	/* Initialize game objects */
	for(gameobject* o : objects){
		if(o->init()){
			puts(RED("Compile Failed, giving up!").c_str());
			return 1;
		}
	}

	main_hud.set_text("Hello World\nThis is line two\nAnd line three!");

	/* Start Other Threads */
	std::thread player_movement_thread(player_movement);
	std::thread object_movement_thread(object_movement);
	std::thread animation_thread(animation);
	std::thread collision_detection_thread(collision_detection);
	std::thread display_fps([](){
			while(!shutdown_engine){
			double loop_time = 1000.0 / loopcount;
			main_hud.lprintf(1, "FPS: %4d  Move: %lf", framecount, loop_time);
			framecount = 0;
			loopcount = 0;
			std::this_thread::sleep_for(std::chrono::seconds(1));
			}
			});


	glEnable(GL_DEPTH_TEST);
	while (!glfwWindowShouldClose(window)) {
		framecount++;
		glfwPollEvents();
		glClearColor(0, 0, 0, 1.0);
		glClear(GL_COLOR_BUFFER_BIT);
		glClear(GL_DEPTH_BUFFER_BIT);

		//		grand_mutex.lock();

		/* Where are we?  A:  player_position
		 * What are we looking at?
		 */

		glm::vec3 look_at_point = get_look_at_point();
		glm::mat4 view = glm::lookAt(player_position, look_at_point, glm::vec3(0, 1, 0));
		glm::mat4 projection = glm::perspective(45.0f, width / height, 0.1f, 10000.0f);
		glm::mat4 vp = projection * view;

		for(gameobject* o : objects)
			if(o->visible)
				o->draw(vp);
		//		grand_mutex.unlock();

		glfwSwapBuffers(window);
	}
	shutdown_engine = 1;
	player_movement_thread.join();
	// TODO:  join other threads
	glfwDestroyWindow(window);
	glfwTerminate();
	free_helpers();
}


