#include "ProceduralMaterialSystem.hpp"
#include "../components/ProceduralMaterial.hpp"
#include "../components/ProceduralTexture.hpp"
#include <OgreMaterialManager.h>
#include <OgreTechnique.h>
#include <OgrePass.h>
#include <OgreTextureUnitState.h>
#include <OgreLogManager.h>
#include <OgreRTShaderSystem.h>

ProceduralMaterialSystem::ProceduralMaterialSystem(flecs::world &world,
						   Ogre::SceneManager *sceneMgr)
	: m_world(world)
	, m_sceneMgr(sceneMgr)
	, m_query(world.query<ProceduralMaterialComponent>())
{
}

ProceduralMaterialSystem::~ProceduralMaterialSystem() = default;

void ProceduralMaterialSystem::initialize()
{
	if (m_initialized)
		return;
	m_initialized = true;

	Ogre::LogManager::getSingleton().logMessage(
		"ProceduralMaterialSystem initialized");
}

void ProceduralMaterialSystem::update()
{
	if (!m_initialized)
		return;

	m_query.each([&](flecs::entity entity,
			 ProceduralMaterialComponent &component) {
		bool needsRecreate = component.dirty || !component.created;

		// Check if texture has changed (new version)
		if (!needsRecreate &&
		    component.diffuseTextureEntity.is_alive() &&
		    component.diffuseTextureEntity
			    .has<ProceduralTextureComponent>()) {
			const auto &tex =
				component.diffuseTextureEntity
					.get<ProceduralTextureComponent>();
			if (tex.version != component.textureVersion) {
				needsRecreate = true;
			}
		}

		if (needsRecreate) {
			createMaterial(entity, component);
		}
	});
}

void ProceduralMaterialSystem::createMaterial(
	flecs::entity entity, ProceduralMaterialComponent &component)
{
	try {
		// Ensure persistent ID is assigned.
		if (component.materialId.empty()) {
			component.materialId = generatePersistentId();
		}

		// Generate unique material name if not set
		if (component.materialName.empty()) {
			component.materialName =
				"ProceduralMat_" + component.materialId;
		}

		Ogre::MaterialManager &matMgr =
			Ogre::MaterialManager::getSingleton();

		// Re-use the existing material object when possible.
		// Destroying and recreating breaks cached MaterialPtr
		// references held by Ogre::SubEntity instances that use
		// this material.
		if (!component.ogreMaterial) {
			if (matMgr.resourceExists(component.materialName)) {
				component.ogreMaterial =
					matMgr.getByName(component.materialName);
			} else {
				component.ogreMaterial = matMgr.create(
					component.materialName,
					Ogre::ResourceGroupManager::
						DEFAULT_RESOURCE_GROUP_NAME);
			}
		}

		// Clear existing technique and rebuild passes so that every
		// SubEntity::mMaterialPtr (which points to this same object)
		// sees the updated state.
		Ogre::Technique *technique =
			component.ogreMaterial->getTechnique(0);
		technique->removeAllPasses();
		Ogre::Pass *pass = technique->createPass();

		// Set material colors
		pass->setAmbient(component.ambient[0], component.ambient[1],
				 component.ambient[2]);
		pass->setDiffuse(component.diffuse[0], component.diffuse[1],
				 component.diffuse[2], 1.0f);
		pass->setSpecular(component.specular[0], component.specular[1],
				  component.specular[2], 1.0f);
		pass->setShininess(component.shininess);

		// Cull back-facing geometry
		pass->setCullingMode(Ogre::CULL_CLOCKWISE);

		// Add diffuse texture if we have a reference to a ProceduralTexture entity
		unsigned int currentTexVersion = 0;
		if (component.diffuseTextureEntity.is_alive() &&
		    component.diffuseTextureEntity
			    .has<ProceduralTextureComponent>()) {
			const auto &textureComp =
				component.diffuseTextureEntity
					.get<ProceduralTextureComponent>();
			currentTexVersion = textureComp.version;
			if (textureComp.generated &&
			    !textureComp.textureName.empty()) {
				Ogre::TextureUnitState *texUnit =
					pass->createTextureUnitState();
				texUnit->setTextureName(
					textureComp.textureName);
				texUnit->setTextureAddressingMode(
					Ogre::TAM_CLAMP);
			}
		}

		// Re-generate RTSS shaders for the updated material
		Ogre::RTShader::ShaderGenerator *shaderGen =
			Ogre::RTShader::ShaderGenerator::getSingletonPtr();
		if (shaderGen && component.ogreMaterial) {
			shaderGen->removeAllShaderBasedTechniques(
				*component.ogreMaterial);
			shaderGen->createShaderBasedTechnique(
				*component.ogreMaterial,
				Ogre::MaterialManager::DEFAULT_SCHEME_NAME,
				Ogre::RTShader::ShaderGenerator::
					DEFAULT_SCHEME_NAME);
			shaderGen->validateMaterial(
				Ogre::RTShader::ShaderGenerator::
					DEFAULT_SCHEME_NAME,
				component.materialName);
		}

		component.created = true;
		component.dirty = false;
		component.textureVersion = currentTexVersion;

		Ogre::LogManager::getSingleton().logMessage(
			"ProceduralMaterial: Updated '" +
			component.materialName + "'");

	} catch (const Ogre::Exception &e) {
		Ogre::LogManager::getSingleton().logMessage(
			"ProceduralMaterial ERROR: " + e.getDescription());
		component.dirty = true; // Will retry
	}
}

void ProceduralMaterialSystem::destroyMaterial(
	ProceduralMaterialComponent &component)
{
	if (component.ogreMaterial) {
		try {
			// Remove RTSS shader techniques before destroying the
			// material, otherwise the shader generator keeps stale
			// cache entries keyed by material name.
			Ogre::RTShader::ShaderGenerator *shaderGen =
				Ogre::RTShader::ShaderGenerator::getSingletonPtr();
			if (shaderGen) {
				shaderGen->removeAllShaderBasedTechniques(
					*component.ogreMaterial);
			}
			Ogre::MaterialManager::getSingleton().remove(
				component.ogreMaterial);
		} catch (...) {
			// Ignore errors during cleanup
		}
		component.ogreMaterial.reset();
	}
}

void ProceduralMaterialSystem::recreateMaterial(flecs::entity entity)
{
	if (!entity.is_alive() || !entity.has<ProceduralMaterialComponent>()) {
		return;
	}

	entity.get_mut<ProceduralMaterialComponent>().markDirty();
}

std::string ProceduralMaterialSystem::getMaterialName(flecs::entity entity)
{
	if (!entity.is_alive() || !entity.has<ProceduralMaterialComponent>()) {
		return "";
	}

	return entity.get<ProceduralMaterialComponent>().materialName;
}

bool ProceduralMaterialSystem::materialExists(const std::string &name)
{
	return Ogre::MaterialManager::getSingleton().resourceExists(name);
}
