#include <chrono>
#include <cmath>
#include <crepe/api/Asset.h>
#include <crepe/manager/Mediator.h>
#include <crepe/manager/ResourceManager.h>
#include <crepe/system/ParticleSystem.h>
#include <crepe/system/PhysicsSystem.h>
#include <crepe/system/RenderSystem.h>
#include <gtest/gtest.h>

#define private public
#define protected public

#include <crepe/api/Event.h>
#include <crepe/api/GameObject.h>
#include <crepe/api/ParticleEmitter.h>
#include <crepe/api/Rigidbody.h>
#include <crepe/api/Script.h>
#include <crepe/api/Transform.h>
#include <crepe/facade/SDLContext.h>
#include <crepe/manager/ComponentManager.h>
#include <crepe/manager/EventManager.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;

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

class DISABLED_ProfilingTest : 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 = 3000;
	// Amount of times a test runs to calculate average
	const int average = 5;
	// Maximum duration to stop test
	const std::chrono::microseconds duration = 16000us;

	Mediator m;
	SDLContext sdl_context {m};
	ResourceManager resman {m};
	ComponentManager mgr {m};
	// Add system used for profling tests
	EventManager evmgr {m};
	LoopTimerManager loopmgr {m};
	CollisionSystem collision_sys {m};
	PhysicsSystem physics_sys {m};
	ParticleSystem particle_sys {m};
	RenderSystem render_sys {m};
	ScriptSystem script_sys {m};

	// 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>(
			ivec2 {1080, 720}, vec2 {2000, 2000},
			Camera::Data {
				.bg_color = Color::WHITE,
				.zoom = 1.0f,
			}
		);
		// initialize systems here:
		//calls init
		script_sys.fixed_update();
		//creates window
		render_sys.frame_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.fixed_update(); });
		total_microseconds
			+= time_function("CollisionSystem", [&]() { collision_sys.fixed_update(); });
		total_microseconds
			+= time_function("ParticleSystem", [&]() { particle_sys.fixed_update(); });
		total_microseconds
			+= time_function("RenderSystem", [&]() { render_sys.frame_update(); });
		return total_microseconds;
	}

	// Print timings of all functions
	void log_timings() const {
		std::string result = "\nFunction timings:\n";

		for (const auto & [name, duration] : timings) {
			result += name + " took " + std::to_string(duration.count() / 1000.0 / average)
					  + " ms (" + std::to_string(duration.count() / average) + " µs).\n";
		}

		result += "Total time: " + std::to_string(this->total_time.count() / 1000.0 / average)
				  + " ms (" + std::to_string(this->total_time.count() / average) + " µs)\n";

		result += "Amount of gameobjects: " + std::to_string(game_object_count) + "\n";

		GTEST_LOG_(INFO) << result;
	}

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

TEST_F(DISABLED_ProfilingTest, 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(DISABLED_ProfilingTest, 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 {
				.gravity_scale = 0.0,
				.body_type = Rigidbody::BodyType::STATIC,
			});
			gameobject.add_component<BoxCollider>(vec2 {0, 0}, vec2 {1, 1});

			gameobject.add_component<BehaviorScript>().set_script<TestScript>();
			Sprite & test_sprite = gameobject.add_component<Sprite>(
				Asset {"asset/texture/square.png"},
				Sprite::Data {
					.color = {0, 0, 0, 0},
					.flip = {.flip_x = false, .flip_y = false},
					.sorting_in_layer = 1,
					.order_in_layer = 1,
					.size = {.y = 500},
				}
			);
		}

		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(DISABLED_ProfilingTest, 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 {
				.gravity_scale = 0,
				.body_type = Rigidbody::BodyType::STATIC,
			});
			gameobject.add_component<BoxCollider>(vec2 {0, 0}, vec2 {1, 1});
			gameobject.add_component<BehaviorScript>().set_script<TestScript>();
			Sprite & test_sprite = gameobject.add_component<Sprite>(
				Asset {"asset/texture/square.png"},
				Sprite::Data {
					.color = {0, 0, 0, 0},
					.flip = {.flip_x = false, .flip_y = false},
					.sorting_in_layer = 1,
					.order_in_layer = 1,
					.size = {.y = 500},
				}
			);
			auto & test = gameobject.add_component<ParticleEmitter>(
				test_sprite,
				ParticleEmitter::Data {
					.max_particles = 10,
					.emission_rate = 100,
					.end_lifespan = 100000,
					.boundary {
						.width = 1000,
						.height = 1000,
						.offset = vec2 {0, 0},
						.reset_on_exit = false,
					},

				}
			);
		}
		render_sys.frame_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);
}