#include "gameplay/scene.h"
#include "gameplay/camera.h"
#include "gameplay/gameactor.h"
#include "gameplay/weapons/weapons.h"
#include "gameplay/weapons/bulletmanager.h"
#include "gameplay/map.h"
#include "gameplay/physics.h"
#include "gameplay/ai.h"

#include "graphics/sprite.h"
#include "graphics/animation.h"

#include "utility/component.h"
#include "utility/xmlloader.h"
#include "utility/resourcemanager.h"
#include "utility/events.h"
#include "utility/raycaster.h"
#include "utility/debugdraw.h"

#include <memory>
#include <tracy/Tracy.hpp>

#include <execution>

// Scene xml files, should contain a node called <player> that holds the sprite location
/*
like this:
<player sprite="sprites/player2Atlas.png" frameSize=64.0>
	<x=5/>
	<y=6/>
</player>
*/

Scene::Scene(SceneType sceneType, std::shared_ptr<ResourceManager> resources, std::weak_ptr<EventManager> globalEvents)
 : type(sceneType), resourceManager(resources), globalEventManager(globalEvents)
{
	camera = std::make_shared<Camera>(800.f, 600.f);
	physicsEngine = std::make_shared<PhysicsEngine>();
	eventManager = std::make_shared<EventManager>();
}

void Scene::init()
{
	physicsEngine->hookEventManager(eventManager);
	//if (sceneType == SCENE_SHOOTER)
	loadDebugShooterScene();
}

// This function is full of hardcoded values and test sprites, NOT for use with final product
void Scene::loadDebugShooterScene()
{
	hookSceneEvents();
	sceneData = resourceManager->loadScene("000");
	if (!sceneData)
		return;
	EntityData playerData = sceneData->entities[0];
	auto mapData = sceneData->map;
	auto playerShader = resourceManager->loadShader("GL_player", "shaders/GL_player.vert", "shaders/GL_player.frag");
	auto bubbleShader = resourceManager->loadShader("GL_bubble", "shaders/GL_bubble.vert", "shaders/GL_bubble.frag");
	auto weaponShader = resourceManager->loadShader("GL_pistol", "shaders/GL_pistol.vert", "shaders/GL_pistol.frag");

	// creating map from scene
	auto tileShader = resourceManager->loadShader("GL_tile", "shaders/GL_tile.vert", "shaders/GL_tile.frag");
	map = std::make_shared<Map>(mapData, tileShader, resourceManager);

	for (EntityData entityData : sceneData->entities)
	{
		auto entity = std::make_shared<GameActor>(this, playerShader);
		// Directional is the kind of sprite sheet we are using, in this case for directional, I have the sprite sheet handle the rotations
		// instead of just rotating the object, this makes it look quite a bit better from the end user perspective.
		if (entityData.animated)
		{
			auto entityAnimation = resourceManager->loadAnimationSet(entityData.graphic, entity->getEntityID());
			// because we don't want to have the engine rotate the object based on the entities rotation, 
			// we set the this value to false so we no longer rotate the object.
			if (entityAnimation->getDirectional())
				entity->setRotatable(false);
			entity->addComponent(std::make_unique<AnimationComponent>(entityAnimation, eventManager));
		}
		else
		{
			auto entitySprite = resourceManager->loadSpriteStatic(entityData.graphic);
			entity->addComponent(std::make_unique<SpriteComponent>(entitySprite));
		}
		auto defaultWeapon = resourceManager->loadWeapon("pistolGun", weaponShader, bubbleShader);
		auto entityWeapon = resourceManager->loadWeapon(entityData.weapon, weaponShader, bubbleShader);

		entity->pickupWeapon(defaultWeapon);
		entity->pickupWeapon(entityWeapon);
		entity->setPosition(glm::vec3(entityData.x * mapData->tileSize, entityData.y * mapData->tileSize, 0.f));
		entity->setScale(glm::vec3(mapData->tileSize, mapData->tileSize, 1.f));

		entity->addPhysicsComponent(
			physicsEngine->createObject(entity->getEntityID(), 
				entity->getPosition(), 
				49.0, 
				PhysicsComponent::Collider::Shape::Circle, 
				glm::vec3(mapData->tileSize / 2))
		);

		if (entityData.isPlayer)
		{
			player = entity;
			camera->setTarget(entity.get());
			entity->setLocalPosition(camera->worldToLocal(entity->getPosition()));
		}
		else
		{
			// attach ai
			if (!entityData.script.empty())
			{
				auto behaviourScript = resourceManager->loadAIScript(entityData.script);
				auto rayCaster = std::make_shared<Raycaster>(40.f, 300.f, map->getCollisionMap(), mapData->tileSize);
				auto ai = std::make_shared<AI>(entity, rayCaster);
				ai->setTarget(player);
				ai->attachBehaviourScript(behaviourScript);
				entity->addComponent(std::make_unique<AIComponent>(ai));
			}
		}
		entities.emplace(entity->getEntityID(), entity);
	}

	physicsEngine->loadCollisionMap(map->getCollisionMap(), mapData->tileSize);

	// Setup map and other entities...
}

std::shared_ptr<GameActor> Scene::getPlayer() const
{
	return (!player) ? nullptr : player;
}

void Scene::update(double deltaTime)
{
	ZoneScoped;
	for (const auto& [id, e] : entities)
	{
		e->update(deltaTime);
		if (camera->getTarget() == e.get())
			e->setLocalPosition(camera->worldToLocal(e->getPosition()));
	}
	physicsEngine->update(deltaTime);
	camera->update(deltaTime);
}

void Scene::render(std::shared_ptr<Renderer> renderer)
{
	ZoneScoped;
	renderer->clear();

	renderer->setProjAndViewMatrix(camera->getProjectionMatrix(), camera->getViewMatrix());
	renderer->addDrawable(RenderLayer::Map, map);
	//map->draw();
	for (auto& [id, e] : entities)
	{
		//e->draw();
		renderer->addDrawable(RenderLayer::GameObjects, e);
		if (e->getHeldWeapon()) {
			renderer->addDrawable(RenderLayer::GameObjects, e->getHeldWeapon());
			for (const auto& bullet : e->getHeldWeapon()->getBulletManager()->getBullets()) {
				renderer->addDrawable(RenderLayer::GameObjects, bullet);
			}
		}
	}

	renderer->render();
	DebugDrawer::getInstance().draw(camera->getProjectionMatrix() * camera->getViewMatrix());
}

void Scene::unloadScene()
{
	//xmlLoader.reset();
}

void Scene::hookSceneEvents()
{
	std::weak_ptr<Scene> weakSelf = shared_from_this();
	eventManager->subscribe("OnBulletCollide", [weakSelf](std::shared_ptr<Event> e) {
		if (auto self = weakSelf.lock()) {
			auto collideEvent = std::static_pointer_cast<BulletCollideEvent>(e);
			GameActor* shooter = self->getGameActorByID(collideEvent->ownerID);
			GameActor* target  = self->getGameActorByID(collideEvent->victimID);
			if (shooter && target)
				if (auto& weapon = shooter->getHeldWeapon())
					weapon->onHitCallback(target, collideEvent->bullet.get(), collideEvent->normal);
		}
	});
}

GameActor* Scene::getGameActorByID(const unsigned int ID)
{
	auto iterator = entities.find(ID);
	if (iterator == entities.end())
		return nullptr;
	return iterator->second.get();
}

std::span<std::weak_ptr<GameActor>> Scene::getAllEntities() const 
{
	entityCache.clear();
	for (const auto& [_, entity] : entities) {
		entityCache.push_back(entity);
	}
	return entityCache;
}