#include "system/ParticleSystem.h"
#include "system/PhysicsSystem.h"
#include "system/RenderSystem.h"
#include <cmath>
#include <chrono>
#include <gtest/gtest.h>

#define private public
#define protected public

#include <crepe/ComponentManager.h>
#include <crepe/api/Event.h>
#include <crepe/api/EventManager.h>
#include <crepe/api/GameObject.h>
#include <crepe/api/Rigidbody.h>
#include <crepe/api/Script.h>
#include <crepe/api/Transform.h>
#include <crepe/api/ParticleEmitter.h>
#include <crepe/system/CollisionSystem.h>
#include <crepe/system/ScriptSystem.h>
#include <crepe/types.h>
#include <crepe/util/Log.h>

using namespace std;
using namespace std::chrono_literals;
using namespace crepe;
using namespace testing;


/*
List of test cases with component settings/details
1. Minimal test creates gameobject without additonal components
2. Minimal 'Complex' gameobject. Has dynamic body without bounce and no collision handler
3. Minimal 'Complex' gameobject. Same as test 2 but with particle emitter
*/

class TestScript : public Script {
	bool oncollision(const CollisionEvent& test) {
		Log::logf("Box {} script on_collision()", test.info.first_collider.game_object_id);
		return true;
	}
	void init() {
		subscribe<CollisionEvent>([this](const CollisionEvent& ev) -> bool {
			return this->oncollision(ev);
		});
	}
	void update() {
		// Retrieve component from the same GameObject this script is on
	}
};

class Profiling : public Test {
public:
	// Config for test
	// Minimum amount to let test pass
	const int min_gameobject_count = 100;
	// Maximum amount to stop test
	const int max_gameobject_count = 200;
	// Amount of times a test runs to calculate average
	const int average = 5;
	// Maximum duration to stop test
	const std::chrono::microseconds duration = 16000us;
	

	ComponentManager mgr;
	// Add system used for profling tests 
	CollisionSystem collision_sys{mgr};
	PhysicsSystem physics_sys{mgr};
	ParticleSystem particle_sys{mgr};
	RenderSystem render_sys{mgr};
	ScriptSystem script_sys{mgr};

	// Test data
	std::map<std::string, std::chrono::microseconds> timings;
	int game_object_count = 0;
	std::chrono::microseconds total_time = 0us;

	
	void SetUp() override {
		
		GameObject do_not_use = mgr.new_object("DO_NOT_USE","",{0,0});
		do_not_use.add_component<Camera>(Color::WHITE);
		// initialize systems here:
		//calls init
		script_sys.update();
		//creates window
		render_sys.update();
	}

	// Helper function to time an update call and store its duration
	template <typename Func>
	std::chrono::microseconds time_function(const std::string& name, Func&& func) {
		auto start = std::chrono::steady_clock::now();
		func();
		auto end = std::chrono::steady_clock::now();
		std::chrono::microseconds duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
		timings[name] += duration;
		return duration;
	}

	// Run and profile all systems, return the total time in milliseconds
	std::chrono::microseconds run_all_systems() {
		std::chrono::microseconds total_microseconds = 0us;
		total_microseconds += time_function("PhysicsSystem", [&]() { physics_sys.update(); });
		total_microseconds += time_function("CollisionSystem", [&]() { collision_sys.update(); });
		total_microseconds += time_function("ParticleSystem", [&]() { particle_sys.update(); });
		total_microseconds += time_function("RenderSystem", [&]() { render_sys.update(); });
		return total_microseconds;
	}

	// Print timings of all functions
	void log_timings() const {
		std::stringstream ss;
        ss << "\nFunction timings:\n";
        for (const auto& [name, duration] : timings) {
            ss << name << " took " << duration.count() / 1000.0 / average << " ms (" << duration.count() / average << " µs).\n";
        }
        ss << "Total time: " << this->total_time.count() / 1000.0 / average << " ms (" << this->total_time.count() / average << " µs)\n";
        ss << "Amount of gameobjects: " << game_object_count << "\n";
        GTEST_LOG_(INFO) << ss.str();
	}

	void clear_timings() {
		for (auto& [key, value] : timings) {
    	value = std::chrono::microseconds(0);
		}
	}
};

TEST_F(Profiling, Profiling_1) {
	while (this->total_time/this->average < this->duration) {
		

		{
			//define gameobject used for testing
			GameObject gameobject = mgr.new_object("gameobject","",{0,0});
		}

		this->game_object_count++;

		this->total_time = 0us;
		clear_timings();

		for (int amount = 0; amount < this->average; amount++) {
			this->total_time += run_all_systems();
		}

		if(this->game_object_count >= this->max_gameobject_count) break;
	}
	log_timings();
	EXPECT_GE(this->game_object_count, this->min_gameobject_count);
}

TEST_F(Profiling, Profiling_2) {
	while (this->total_time/this->average < this->duration) {
	
		{
			//define gameobject used for testing
			GameObject gameobject = mgr.new_object("gameobject","",{static_cast<float>(game_object_count*2),0});
			gameobject.add_component<Rigidbody>(Rigidbody::Data{
			.body_type = Rigidbody::BodyType::STATIC,
			.use_gravity = false,
			});
			gameobject.add_component<BoxCollider>(vec2{0, 0}, 1, 1);
			gameobject.add_component<BehaviorScript>().set_script<TestScript>();
			Color color(0, 0, 0, 0);
			gameobject.add_component<Sprite>(
			make_shared<Texture>("asset/texture/green_square.png"), color,
			FlipSettings{true, true});
		}

		this->game_object_count++;

		this->total_time = 0us;
		clear_timings();
		for (int amount = 0; amount < this->average; amount++) {
			this->total_time += run_all_systems();
		}

		if(this->game_object_count >= this->max_gameobject_count) break;
	}
	log_timings();
	EXPECT_GE(this->game_object_count, this->min_gameobject_count);
}

TEST_F(Profiling, Profiling_3) {
	while (this->total_time/this->average < this->duration) {
	
		{
			//define gameobject used for testing
			GameObject gameobject = mgr.new_object("gameobject","",{static_cast<float>(game_object_count*2),0});
			gameobject.add_component<Rigidbody>(Rigidbody::Data{
			.body_type = Rigidbody::BodyType::STATIC,
			.use_gravity = false,
			});
			gameobject.add_component<BoxCollider>(vec2{0, 0}, 1, 1);
			gameobject.add_component<BehaviorScript>().set_script<TestScript>();
			Color color(0, 0, 0, 0);
			gameobject.add_component<Sprite>(
			make_shared<Texture>("asset/texture/green_square.png"), color,
			FlipSettings{true, true});
			Sprite & test_sprite = gameobject.add_component<Sprite>(
			make_shared<Texture>("asset/texture/img.png"), color, FlipSettings{false, false});
			auto & test = gameobject.add_component<ParticleEmitter>(ParticleEmitter::Data{
			.max_particles = 10,
			.emission_rate = 100,
			.end_lifespan = 100000,
			.boundary{
				.width = 1000,
				.height = 1000,
				.offset = vec2{0, 0},
				.reset_on_exit = false,
			},
			.sprite = test_sprite,
	});
		}
		render_sys.update();
		this->game_object_count++;
		
		this->total_time = 0us;
		clear_timings();
		for (int amount = 0; amount < this->average; amount++) {
			this->total_time += run_all_systems();
		}

		if(this->game_object_count >= this->max_gameobject_count) break;
	}
	log_timings();
	EXPECT_GE(this->game_object_count, this->min_gameobject_count);
}