#include "api/BoxCollider.h"
#include "manager/Mediator.h"
#include <cmath>
#include <cstddef>
#include <gtest/gtest.h>

#define private public
#define protected public

#include <crepe/api/Event.h>
#include <crepe/api/GameObject.h>
#include <crepe/api/Rigidbody.h>
#include <crepe/api/Script.h>
#include <crepe/api/Transform.h>
#include <crepe/manager/ComponentManager.h>
#include <crepe/manager/EventManager.h>
#include <crepe/manager/Mediator.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 CollisionHandler : public Script {
public:
	int box_id;
	function<void(const CollisionEvent & ev)> test_fn = [](const CollisionEvent & ev) {};

	CollisionHandler(int box_id) { this->box_id = box_id; }

	bool on_collision(const CollisionEvent & ev) {
		//Log::logf("Box {} script on_collision()", box_id);
		test_fn(ev);
		return true;
	}

	void init() {
		subscribe<CollisionEvent>(
			[this](const CollisionEvent & ev) -> bool { return this->on_collision(ev); });
	}
	void update() {
		// Retrieve component from the same GameObject this script is on
	}
};

class CollisionTest : public Test {
public:
	Mediator m;
	EventManager event_mgr{m};
	ComponentManager mgr{m};
	CollisionSystem collision_sys{m};
	ScriptSystem script_sys{m};
	LoopTimerManager loop_timer{m};

	GameObject world = mgr.new_object("world", "", {50, 50});
	GameObject game_object1 = mgr.new_object("object1", "", {50, 50});
	GameObject game_object2 = mgr.new_object("object2", "", {50, 30});

	CollisionHandler * script_object1_ref = nullptr;
	CollisionHandler * script_object2_ref = nullptr;

	void SetUp() override {
		world.add_component<Rigidbody>(Rigidbody::Data{
			// TODO: remove unrelated properties:
			.body_type = Rigidbody::BodyType::STATIC,
			.offset = {0, 0},
		});
		// Create a box with an inner size of 10x10 units
		world.add_component<BoxCollider>(vec2{0, -100}, vec2{100, 100}); // Top
		world.add_component<BoxCollider>(vec2{0, 100}, vec2{100, 100}); // Bottom
		world.add_component<BoxCollider>(vec2{-100, 0}, vec2{100, 100}); // Left
		world.add_component<BoxCollider>(vec2{100, 0}, vec2{100, 100}); // right

		game_object1.add_component<Rigidbody>(Rigidbody::Data{
			.mass = 1,
			.gravity_scale = 0.01,
			.body_type = Rigidbody::BodyType::DYNAMIC,
			.linear_velocity = {0, 0},
			.constraints = {0, 0, 0},
			.elastisity_coefficient = 1,
			.offset = {0, 0},
			.collision_layers = {0},
		});
		game_object1.add_component<BoxCollider>(vec2{0, 0}, vec2{10, 10});
		BehaviorScript & script_object1
			= game_object1.add_component<BehaviorScript>().set_script<CollisionHandler>(1);
		script_object1_ref = static_cast<CollisionHandler *>(script_object1.script.get());
		ASSERT_NE(script_object1_ref, nullptr);

		game_object2.add_component<Rigidbody>(Rigidbody::Data{
			.mass = 1,
			.gravity_scale = 0.01,
			.body_type = Rigidbody::BodyType::DYNAMIC,
			.linear_velocity = {0, 0},
			.constraints = {0, 0, 0},
			.elastisity_coefficient = 1,
			.offset = {0, 0},
			.collision_layers = {0},
		});
		game_object2.add_component<BoxCollider>(vec2{0, 0}, vec2{10, 10});
		BehaviorScript & script_object2
			= game_object2.add_component<BehaviorScript>().set_script<CollisionHandler>(2);
		script_object2_ref = static_cast<CollisionHandler *>(script_object2.script.get());
		ASSERT_NE(script_object2_ref, nullptr);

		// Ensure Script::init() is called on all BehaviorScript instances
		script_sys.update();
	}
};

TEST_F(CollisionTest, collision_example) {
	bool collision_happend = false;
	script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		collision_happend = true;
		EXPECT_EQ(ev.info.this_collider.game_object_id, 1);
	};
	script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		collision_happend = true;
		EXPECT_EQ(ev.info.this_collider.game_object_id, 2);
	};
	EXPECT_FALSE(collision_happend);
	collision_sys.update();
	EXPECT_FALSE(collision_happend);
}

TEST_F(CollisionTest, collision_box_box_dynamic_both_no_velocity) {
	bool collision_happend = false;
	script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		collision_happend = true;
		EXPECT_EQ(ev.info.this_collider.game_object_id, 1);
		EXPECT_EQ(ev.info.resolution.x, 10);
		EXPECT_EQ(ev.info.resolution.y, 10);
		EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::BOTH);
	};
	script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		collision_happend = true;
		EXPECT_EQ(ev.info.this_collider.game_object_id, 2);
		EXPECT_EQ(ev.info.resolution.x, 10);
		EXPECT_EQ(ev.info.resolution.y, 10);
		EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::BOTH);
	};
	EXPECT_FALSE(collision_happend);
	Transform & tf = this->mgr.get_components_by_id<Transform>(1).front().get();
	tf.position = {50, 30};
	collision_sys.update();
	EXPECT_TRUE(collision_happend);
}

TEST_F(CollisionTest, collision_box_box_dynamic_x_direction_no_velocity) {
	bool collision_happend = false;
	script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		collision_happend = true;
		EXPECT_EQ(ev.info.this_collider.game_object_id, 1);
		EXPECT_EQ(ev.info.resolution.x, -5);
		EXPECT_EQ(ev.info.resolution.y, 0);
		EXPECT_EQ(ev.info.resolution_direction,
				  crepe::CollisionSystem::Direction::X_DIRECTION);
	};
	script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		collision_happend = true;
		EXPECT_EQ(ev.info.this_collider.game_object_id, 2);
		EXPECT_EQ(ev.info.resolution.x, 5);
		EXPECT_EQ(ev.info.resolution.y, 0);
		EXPECT_EQ(ev.info.resolution_direction,
				  crepe::CollisionSystem::Direction::X_DIRECTION);
	};
	EXPECT_FALSE(collision_happend);
	Transform & tf = this->mgr.get_components_by_id<Transform>(1).front().get();
	tf.position = {45, 30};
	collision_sys.update();
	EXPECT_TRUE(collision_happend);
}

TEST_F(CollisionTest, collision_box_box_dynamic_y_direction_no_velocity) {
	bool collision_happend = false;
	script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		collision_happend = true;
		EXPECT_EQ(ev.info.this_collider.game_object_id, 1);
		EXPECT_EQ(ev.info.resolution.x, 0);
		EXPECT_EQ(ev.info.resolution.y, -5);
		EXPECT_EQ(ev.info.resolution_direction,
				  crepe::CollisionSystem::Direction::Y_DIRECTION);
	};
	script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		collision_happend = true;
		EXPECT_EQ(ev.info.this_collider.game_object_id, 2);
		EXPECT_EQ(ev.info.resolution.x, 0);
		EXPECT_EQ(ev.info.resolution.y, 5);
		EXPECT_EQ(ev.info.resolution_direction,
				  crepe::CollisionSystem::Direction::Y_DIRECTION);
	};
	EXPECT_FALSE(collision_happend);
	Transform & tf = this->mgr.get_components_by_id<Transform>(1).front().get();
	tf.position = {50, 25};
	collision_sys.update();
	EXPECT_TRUE(collision_happend);
}

TEST_F(CollisionTest, collision_box_box_dynamic_both) {
	bool collision_happend = false;
	script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		collision_happend = true;
		EXPECT_EQ(ev.info.this_collider.game_object_id, 1);
		EXPECT_EQ(ev.info.resolution.x, 10);
		EXPECT_EQ(ev.info.resolution.y, 10);
		EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::BOTH);
	};
	script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		collision_happend = true;
		EXPECT_EQ(ev.info.this_collider.game_object_id, 2);
		EXPECT_EQ(ev.info.resolution.x, 10);
		EXPECT_EQ(ev.info.resolution.y, 10);
		EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::BOTH);
	};
	EXPECT_FALSE(collision_happend);
	Transform & tf = this->mgr.get_components_by_id<Transform>(1).front().get();
	tf.position = {50, 30};
	Rigidbody & rg1 = this->mgr.get_components_by_id<Rigidbody>(1).front().get();
	rg1.data.linear_velocity = {10, 10};
	Rigidbody & rg2 = this->mgr.get_components_by_id<Rigidbody>(2).front().get();
	rg2.data.linear_velocity = {10, 10};
	collision_sys.update();
	EXPECT_TRUE(collision_happend);
}

TEST_F(CollisionTest, collision_box_box_dynamic_x_direction) {
	bool collision_happend = false;
	script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		collision_happend = true;
		EXPECT_EQ(ev.info.this_collider.game_object_id, 1);
		EXPECT_EQ(ev.info.resolution.x, -5);
		EXPECT_EQ(ev.info.resolution.y, -5);
		EXPECT_EQ(ev.info.resolution_direction,
				  crepe::CollisionSystem::Direction::X_DIRECTION);
	};
	script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		collision_happend = true;
		EXPECT_EQ(ev.info.this_collider.game_object_id, 2);
		EXPECT_EQ(ev.info.resolution.x, 5);
		EXPECT_EQ(ev.info.resolution.y, 5);
		EXPECT_EQ(ev.info.resolution_direction,
				  crepe::CollisionSystem::Direction::X_DIRECTION);
	};
	EXPECT_FALSE(collision_happend);
	Transform & tf = this->mgr.get_components_by_id<Transform>(1).front().get();
	tf.position = {45, 30};
	Rigidbody & rg1 = this->mgr.get_components_by_id<Rigidbody>(1).front().get();
	rg1.data.linear_velocity = {10, 10};
	Rigidbody & rg2 = this->mgr.get_components_by_id<Rigidbody>(2).front().get();
	rg2.data.linear_velocity = {10, 10};
	collision_sys.update();
	EXPECT_TRUE(collision_happend);
}

TEST_F(CollisionTest, collision_box_box_dynamic_y_direction) {
	bool collision_happend = false;
	script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		collision_happend = true;
		EXPECT_EQ(ev.info.this_collider.game_object_id, 1);
		EXPECT_EQ(ev.info.resolution.x, -5);
		EXPECT_EQ(ev.info.resolution.y, -5);
		EXPECT_EQ(ev.info.resolution_direction,
				  crepe::CollisionSystem::Direction::Y_DIRECTION);
	};
	script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		collision_happend = true;
		EXPECT_EQ(ev.info.this_collider.game_object_id, 2);
		EXPECT_EQ(ev.info.resolution.x, 5);
		EXPECT_EQ(ev.info.resolution.y, 5);
		EXPECT_EQ(ev.info.resolution_direction,
				  crepe::CollisionSystem::Direction::Y_DIRECTION);
	};
	EXPECT_FALSE(collision_happend);
	Transform & tf = this->mgr.get_components_by_id<Transform>(1).front().get();
	tf.position = {50, 25};
	Rigidbody & rg1 = this->mgr.get_components_by_id<Rigidbody>(1).front().get();
	rg1.data.linear_velocity = {10, 10};
	Rigidbody & rg2 = this->mgr.get_components_by_id<Rigidbody>(2).front().get();
	rg2.data.linear_velocity = {10, 10};
	collision_sys.update();
	EXPECT_TRUE(collision_happend);
}

TEST_F(CollisionTest, collision_box_box_static_both) {
	bool collision_happend = false;
	script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		collision_happend = true;
		EXPECT_EQ(ev.info.this_collider.game_object_id, 1);
		EXPECT_EQ(ev.info.resolution.x, 10);
		EXPECT_EQ(ev.info.resolution.y, 10);
		EXPECT_EQ(ev.info.resolution_direction, crepe::CollisionSystem::Direction::BOTH);
	};
	script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		// is static should not be called
		FAIL();
	};
	EXPECT_FALSE(collision_happend);
	Transform & tf = this->mgr.get_components_by_id<Transform>(1).front().get();
	tf.position = {50, 30};
	Rigidbody & rg2 = this->mgr.get_components_by_id<Rigidbody>(2).front().get();
	rg2.data.body_type = crepe::Rigidbody::BodyType::STATIC;
	collision_sys.update();
	EXPECT_TRUE(collision_happend);
}

TEST_F(CollisionTest, collision_box_box_static_x_direction) {
	bool collision_happend = false;
	script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		collision_happend = true;
		EXPECT_EQ(ev.info.this_collider.game_object_id, 1);
		EXPECT_EQ(ev.info.resolution.x, -5);
		EXPECT_EQ(ev.info.resolution.y, -5);
		EXPECT_EQ(ev.info.resolution_direction,
				  crepe::CollisionSystem::Direction::X_DIRECTION);
	};
	script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		// is static should not be called
		FAIL();
	};
	EXPECT_FALSE(collision_happend);
	Transform & tf = this->mgr.get_components_by_id<Transform>(1).front().get();
	tf.position = {45, 30};
	Rigidbody & rg1 = this->mgr.get_components_by_id<Rigidbody>(1).front().get();
	rg1.data.linear_velocity = {10, 10};
	Rigidbody & rg2 = this->mgr.get_components_by_id<Rigidbody>(2).front().get();
	rg2.data.body_type = crepe::Rigidbody::BodyType::STATIC;
	collision_sys.update();
	EXPECT_TRUE(collision_happend);
}

TEST_F(CollisionTest, collision_box_box_static_y_direction) {
	bool collision_happend = false;
	script_object1_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		collision_happend = true;
		EXPECT_EQ(ev.info.this_collider.game_object_id, 1);
		EXPECT_EQ(ev.info.resolution.x, -5);
		EXPECT_EQ(ev.info.resolution.y, -5);
		EXPECT_EQ(ev.info.resolution_direction,
				  crepe::CollisionSystem::Direction::Y_DIRECTION);
	};
	script_object2_ref->test_fn = [&collision_happend](const CollisionEvent & ev) {
		// is static should not be called
		FAIL();
	};
	EXPECT_FALSE(collision_happend);
	Transform & tf = this->mgr.get_components_by_id<Transform>(1).front().get();
	tf.position = {50, 25};
	Rigidbody & rg1 = this->mgr.get_components_by_id<Rigidbody>(1).front().get();
	rg1.data.linear_velocity = {10, 10};
	Rigidbody & rg2 = this->mgr.get_components_by_id<Rigidbody>(2).front().get();
	rg2.data.body_type = crepe::Rigidbody::BodyType::STATIC;
	collision_sys.update();
	EXPECT_TRUE(collision_happend);
}

TEST_F(CollisionTest, collision_box_box_static_multiple) { //todo check visually
	bool collision_happend = false;
	float offset_value = 0;
	float resolution = 0;
	script_object1_ref->test_fn = [&](const CollisionEvent & ev) {
		collision_happend = true;
		EXPECT_EQ(ev.info.this_collider.game_object_id, 1);
		EXPECT_EQ(ev.info.this_collider.offset.x, offset_value);
		EXPECT_EQ(ev.info.resolution.x, resolution);
	};
	script_object2_ref->test_fn = [&](const CollisionEvent & ev) {
		// is static should not be called
		FAIL();
	};
	EXPECT_FALSE(collision_happend);
	Transform & tf = this->mgr.get_components_by_id<Transform>(1).front().get();
	tf.position = {45, 30};
	Rigidbody & rg1 = this->mgr.get_components_by_id<Rigidbody>(1).front().get();
	rg1.data.linear_velocity = {10, 10};
	Rigidbody & rg2 = this->mgr.get_components_by_id<Rigidbody>(2).front().get();
	rg2.data.body_type = crepe::Rigidbody::BodyType::STATIC;
	BoxCollider & bxc = this->mgr.get_components_by_id<BoxCollider>(1).front().get();
	bxc.offset = {5, 0};
	this->game_object1.add_component<BoxCollider>(vec2{-5, 0}, vec2{10, 10});
	offset_value = 5;
	resolution = 10;
	collision_sys.update();
	offset_value = -5;
	resolution = 10;
	tf.position = {55, 30};
	collision_sys.update();
	EXPECT_TRUE(collision_happend);
}