#include "TerrainSystem.hpp"
#include "../components/Terrain.hpp"
#include "../components/Transform.hpp"
#include "../physics/physics.h"

#include <OgreTerrain.h>
#include <OgreTerrainGroup.h>
#include <OgreTerrainPaging.h>
#include <OgreTerrainPagedWorldSection.h>
#include <OgrePageManager.h>
#include <OgrePage.h>
#include <OgrePagedWorld.h>
#include <OgreTerrainMaterialGeneratorA.h>
#include <OgreMaterial.h>
#include <OgreLogManager.h>
#include <OgreImage.h>

#include <OgreRTShaderSystem.h>
#include <OgreShaderRenderState.h>
#include <OgreShaderSubRenderState.h>

#include <Jolt/Jolt.h>
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
#include <Jolt/Physics/Body/BodyCreationSettings.h>
#include <Jolt/Physics/Collision/RayCast.h>
#include <Jolt/Physics/Collision/CastResult.h>
#include <Jolt/Physics/PhysicsSystem.h>

#include <cmath>
#include <fstream>
#include <filesystem>
#include <sys/stat.h>

TerrainSystem *TerrainSystem::s_instance = nullptr;

/* ------------------------------------------------------------------ */

TerrainSystem::TerrainSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
			     Ogre::Camera *camera, JoltPhysicsWrapper *physics)
	: m_world(world)
	, m_sceneMgr(sceneMgr)
	, m_camera(camera)
	, m_physics(physics)
	, m_terrainQuery(world.query<TerrainComponent, TransformComponent>())
{
	s_instance = this;
	m_commandQueue.setTerrainSystem(this);
}

TerrainSystem::~TerrainSystem()
{
	s_instance = nullptr;
	deactivate();
}

/* ------------------------------------------------------------------ */
/* TerrainBodyDrawFilter                                               */
/* ------------------------------------------------------------------ */

bool TerrainSystem::TerrainBodyDrawFilter::ShouldDraw(
	const JPH::Body &inBody) const
{
	if (!showTerrain)
		return m_terrainIds.find(inBody.GetID()) == m_terrainIds.end();
	return true;
}

/* ------------------------------------------------------------------ */
/* Procedural height helper                                            */
/* ------------------------------------------------------------------ */

static float proceduralHeight(long worldX, long worldZ)
{
	float h = 0.0f;

	h += 15.0f * sinf((float)worldX * 0.005f) *
	     cosf((float)worldZ * 0.005f);
	h += 5.0f * sinf((float)worldX * 0.02f + 1.3f) *
	     cosf((float)worldZ * 0.02f + 0.7f);
	h += 2.0f * sinf((float)worldX * 0.05f + 2.1f) *
	     sinf((float)worldZ * 0.05f + 0.3f);

	return h;
}

/* ------------------------------------------------------------------ */
/* DummyPageProvider                                                   */
/* ------------------------------------------------------------------ */

bool TerrainSystem::DummyPageProvider::unloadProceduralPage(
	Ogre::Page *page, Ogre::PagedWorldSection *)
{
	if (!owner || !owner->mTerrainGroup)
		return true;

	long x, y;
	owner->mTerrainGroup->unpackIndex(page->getID(), &x, &y);
	Ogre::LogManager::getSingleton().logMessage(
		"Terrain: unloading page (" +
		Ogre::StringConverter::toString(x) + ", " +
		Ogre::StringConverter::toString(y) + ")");
	owner->queueColliderRemove(x, y);
	return true;
}

/* ------------------------------------------------------------------ */
/* CustomTerrainDefiner                                                */
/* ------------------------------------------------------------------ */

void TerrainSystem::CustomTerrainDefiner::define(Ogre::TerrainGroup *group,
						 long x, long y)
{
	Ogre::uint16 terrainSize = group->getTerrainSize();
	float *heightMap = OGRE_ALLOC_T(float, terrainSize *terrainSize,
					Ogre::MEMCATEGORY_GEOMETRY);

	m_sys->fillPageHeightData(group, x, y, heightMap);
	group->defineTerrain(x, y, heightMap);

	Ogre::LogManager::getSingleton().logMessage(
		"Terrain: defined page (" + Ogre::StringConverter::toString(x) +
		", " + Ogre::StringConverter::toString(y) + ")");

	OGRE_FREE(heightMap, Ogre::MEMCATEGORY_GEOMETRY);
}

/* ------------------------------------------------------------------ */
/* buildPageCollider — zigzag JPH::MeshShape                          */
/* ------------------------------------------------------------------ */

JPH::ShapeRefC TerrainSystem::buildPageCollider(Ogre::Terrain *terrain)
{
	uint16_t size = terrain->getSize();
	float *heightData = terrain->getHeightData();
	Ogre::Real worldSize = terrain->getWorldSize();
	float scale = worldSize / (float)(size - 1);

	JPH::TriangleList triangles;
	triangles.reserve((size - 1) * (size - 1) * 2);

	for (int z = 0; z < size - 1; ++z) {
		for (int x = 0; x < size - 1; ++x) {
			float vx0 = (float)x * scale - worldSize * 0.5f;
			float vz0 = worldSize * 0.5f - (float)z * scale;
			float vx1 = (float)(x + 1) * scale - worldSize * 0.5f;
			float vz1 = worldSize * 0.5f - (float)(z + 1) * scale;

			float h00 = heightData[z * size + x];
			float h10 = heightData[z * size + x + 1];
			float h01 = heightData[(z + 1) * size + x];
			float h11 = heightData[(z + 1) * size + x + 1];

			JPH::Float3 v00(vx0, h00, vz0);
			JPH::Float3 v10(vx1, h10, vz0);
			JPH::Float3 v01(vx0, h01, vz1);
			JPH::Float3 v11(vx1, h11, vz1);

			if ((z & 1) == 0) {
				triangles.push_back(
					JPH::Triangle(v00, v11, v01, 0));
				triangles.push_back(
					JPH::Triangle(v00, v10, v11, 0));
			} else {
				triangles.push_back(
					JPH::Triangle(v00, v10, v01, 0));
				triangles.push_back(
					JPH::Triangle(v10, v11, v01, 0));
			}
		}
	}

	JPH::MeshShapeSettings settings(triangles);
	auto result = settings.Create();
	if (result.HasError()) {
		Ogre::LogManager::getSingleton().logMessage(
			"Terrain: MeshShape creation failed: " +
			Ogre::String(result.GetError().c_str()));
		return nullptr;
	}
	return result.Get();
}

/* ------------------------------------------------------------------ */
/* Collider queue helpers                                              */
/* ------------------------------------------------------------------ */

void TerrainSystem::queueColliderCreate(long x, long y)
{
	if (!mTerrainGroup)
		return;

	uint64_t key = mTerrainGroup->packIndex(x, y);
	if (mColliders.find(key) != mColliders.end())
		return;
	if (!mPendingColliderPages.insert(key).second)
		return;

	std::lock_guard<std::mutex> lock(m_colliderQueueMutex);
	mColliderCreateQueue.push_back({ x, y });
}

void TerrainSystem::queueColliderRemove(long x, long y)
{
	uint64_t key = mTerrainGroup->packIndex(x, y);
	auto it = mColliders.find(key);
	if (it != mColliders.end())
		it->second.pendingRemove = true;
}

void TerrainSystem::processColliderCreates()
{
	if (!m_physics || !mTerrainGroup)
		return;

	std::lock_guard<std::mutex> lock(m_colliderQueueMutex);

	const size_t initial = mColliderCreateQueue.size();
	for (size_t i = 0; i < initial && !mColliderCreateQueue.empty(); ++i) {
		auto [x, y] = mColliderCreateQueue.front();
		mColliderCreateQueue.pop_front();

		uint64_t key = mTerrainGroup->packIndex(x, y);
		if (mColliders.find(key) != mColliders.end()) {
			mPendingColliderPages.erase(key);
			continue;
		}

		Ogre::Terrain *terrain = mTerrainGroup->getTerrain(x, y);
		if (!terrain || !terrain->isLoaded()) {
			mColliderCreateQueue.push_back({ x, y });
			continue;
		}

		JPH::ShapeRefC shape = buildPageCollider(terrain);
		if (!shape) {
			Ogre::LogManager::getSingleton().logMessage(
				"Terrain: collider create - shape build failed (" +
				Ogre::StringConverter::toString(x) + "," +
				Ogre::StringConverter::toString(y) + ")");
			continue;
		}

		Ogre::Vector3 pagePos =
			terrain->_getRootSceneNode()->_getDerivedPosition();

		JPH::BodyCreationSettings bodySettings(
			shape.GetPtr(),
			JPH::RVec3(pagePos.x, pagePos.y, pagePos.z),
			JPH::Quat::sIdentity(), JPH::EMotionType::Static,
			Layers::NON_MOVING);
		JPH::BodyID bodyId = m_physics->createBody(bodySettings);
		if (bodyId.IsInvalid()) {
			Ogre::LogManager::getSingleton().logMessage(
				"Terrain: collider create - body creation failed (" +
				Ogre::StringConverter::toString(x) + "," +
				Ogre::StringConverter::toString(y) + ")");
			continue;
		}

		m_physics->addBody(bodyId, JPH::EActivation::DontActivate);

		mColliders[key] = { x, y, bodyId, false };
		mPendingColliderPages.erase(key);
		mBodyDrawFilter.addTerrainBody(bodyId);

		Ogre::LogManager::getSingleton().logMessage(
			"Terrain: collider created for page (" +
			Ogre::StringConverter::toString(x) + ", " +
			Ogre::StringConverter::toString(y) + ")");
	}
}

void TerrainSystem::processColliderRemoves()
{
	if (!m_physics || !mTerrainGroup)
		return;

	std::vector<uint64_t> toRemove;
	for (auto &pair : mColliders) {
		if (!pair.second.pendingRemove)
			continue;

		uint64_t key = pair.first;
		long x = pair.second.pageX;
		long y = pair.second.pageY;

		Ogre::Terrain *terrain = mTerrainGroup->getTerrain(x, y);
		if (terrain && terrain->isLoaded() &&
		    terrain->isDerivedDataUpdateInProgress())
			continue;

		mBodyDrawFilter.removeTerrainBody(pair.second.bodyId);
		m_physics->removeBody(pair.second.bodyId);
		m_physics->destroyBody(pair.second.bodyId);
		toRemove.push_back(key);

		Ogre::LogManager::getSingleton().logMessage(
			"Terrain: collider removed for page (" +
			Ogre::StringConverter::toString(x) + ", " +
			Ogre::StringConverter::toString(y) + ")");
	}

	for (auto key : toRemove)
		mColliders.erase(key);
}

void TerrainSystem::removeAllColliders()
{
	if (!m_physics)
		return;

	for (auto &pair : mColliders) {
		mBodyDrawFilter.removeTerrainBody(pair.second.bodyId);
		m_physics->removeBody(pair.second.bodyId);
		m_physics->destroyBody(pair.second.bodyId);
	}
	mColliders.clear();
	mPendingColliderPages.clear();

	std::lock_guard<std::mutex> lock(m_colliderQueueMutex);
	mColliderCreateQueue.clear();
	mBodyDrawFilter.clear();
}

/* ------------------------------------------------------------------ */
/* Page collider polling                                               */
/* ------------------------------------------------------------------ */

void TerrainSystem::ensurePageColliders()
{
	if (!m_physics || !mTerrainGroup)
		return;

	for (const auto &kv : mTerrainGroup->getTerrainSlots()) {
		Ogre::TerrainGroup::TerrainSlot *slot = kv.second;
		if (!slot || !slot->instance || !slot->instance->isLoaded())
			continue;

		uint64_t key = mTerrainGroup->packIndex(slot->x, slot->y);
		if (mColliders.find(key) != mColliders.end())
			continue;

		queueColliderCreate(slot->x, slot->y);
	}
}

/* ------------------------------------------------------------------ */
/* Heightmap helpers                                                   */
/* ------------------------------------------------------------------ */

std::string TerrainSystem::getHeightmapPath(const TerrainComponent &tc) const
{
	return "heightmaps/" + Ogre::StringConverter::toString(tc.terrainId) +
	       "/" + tc.heightmapFile;
}

bool TerrainSystem::loadHeightmap(const std::string &path)
{
	std::ifstream file(path, std::ios::binary);
	if (!file) {
		Ogre::LogManager::getSingleton().logMessage(
			"Terrain: failed to open heightmap: " + path);
		return false;
	}

	uint32_t res = 0;
	file.read(reinterpret_cast<char *>(&res), sizeof(res));
	if (res < 2 || res > 8192) {
		Ogre::LogManager::getSingleton().logMessage(
			"Terrain: invalid heightmap resolution: " +
			Ogre::StringConverter::toString(res));
		return false;
	}

	m_heightmapRes = (int)res;
	m_heightData.resize(res * res);
	file.read(reinterpret_cast<char *>(m_heightData.data()),
		  res * res * sizeof(float));
	m_heightmapLoaded = true;
	Ogre::LogManager::getSingleton().logMessage(
		"Terrain: loaded heightmap " + path + " (" +
		Ogre::StringConverter::toString(res) + "x" +
		Ogre::StringConverter::toString(res) + ")");
	return true;
}

bool TerrainSystem::saveHeightmap(const std::string &path)
{
	if (!m_heightmapLoaded)
		return false;

	std::filesystem::path p(path);
	std::filesystem::create_directories(p.parent_path());

	std::ofstream file(path, std::ios::binary);
	if (!file) {
		Ogre::LogManager::getSingleton().logMessage(
			"Terrain: failed to create heightmap file: " + path);
		return false;
	}

	uint32_t res = (uint32_t)m_heightmapRes;
	file.write(reinterpret_cast<const char *>(&res), sizeof(res));
	file.write(reinterpret_cast<const char *>(m_heightData.data()),
		   m_heightmapRes * m_heightmapRes * sizeof(float));
	Ogre::LogManager::getSingleton().logMessage(
		"Terrain: saved heightmap " + path + " (" +
		Ogre::StringConverter::toString(res) + "x" +
		Ogre::StringConverter::toString(res) + ")");
	return true;
}

bool TerrainSystem::loadSceneHeightmap(const TerrainComponent &tc)
{
	return loadHeightmap(getHeightmapPath(tc));
}

bool TerrainSystem::saveSceneHeightmap(const TerrainComponent &tc)
{
	return saveHeightmap(getHeightmapPath(tc));
}

/* ------------------------------------------------------------------ */
/* Blend map save/load (M3)                                            */
/* ------------------------------------------------------------------ */

bool TerrainSystem::saveBlendMaps(const std::string &dirPath) const
{
	if (!mTerrainGroup || !m_active)
		return false;

	std::filesystem::create_directories(dirPath);

	auto &slots = mTerrainGroup->getTerrainSlots();
	int savedPages = 0;

	for (const auto &kv : slots) {
		Ogre::TerrainGroup::TerrainSlot *slot = kv.second;
		if (!slot || !slot->instance || !slot->instance->isLoaded())
			continue;

		Ogre::Terrain *t = slot->instance;
		int numLayers = (int)t->getLayerCount();
		if (numLayers < 2)
			continue; /* No blend maps (only base layer). */

		int bmSize = (int)t->getLayerBlendMapSize();

		/* Each blendable layer (1..numLayers-1) gets its own file. */
		for (int l = 1; l < numLayers; ++l) {
			Ogre::TerrainLayerBlendMap *bm =
				t->getLayerBlendMap((Ogre::uint8)l);
			if (!bm)
				continue;

			float *data = bm->getBlendPointer();
			if (!data)
				continue;

			std::string fname =
				dirPath + "/page_" +
				Ogre::StringConverter::toString(slot->x) + "_" +
				Ogre::StringConverter::toString(slot->y) +
				"_l" + Ogre::StringConverter::toString(l) +
				".bin";

			std::ofstream file(fname, std::ios::binary);
			if (!file)
				continue;

			/* Header: blendMapSize (uint32), layerIndex (uint8). */
			uint32_t sz = (uint32_t)bmSize;
			uint8_t li = (uint8_t)l;
			file.write(reinterpret_cast<const char *>(&sz),
				   sizeof(sz));
			file.write(reinterpret_cast<const char *>(&li),
				   sizeof(li));

			file.write(reinterpret_cast<const char *>(data),
				   bmSize * bmSize * sizeof(float));
			savedPages++;
		}
	}

	Ogre::LogManager::getSingleton().logMessage(
		"Terrain: saved " +
		Ogre::StringConverter::toString(savedPages) +
		" blend map layers to " + dirPath);
	return true;
}

bool TerrainSystem::loadBlendMaps(const std::string &dirPath)
{
	if (!mTerrainGroup || !m_active)
		return false;

	auto &slots = mTerrainGroup->getTerrainSlots();
	int loadedPages = 0;

	for (const auto &kv : slots) {
		Ogre::TerrainGroup::TerrainSlot *slot = kv.second;
		if (!slot || !slot->instance || !slot->instance->isLoaded())
			continue;

		Ogre::Terrain *t = slot->instance;
		int numLayers = (int)t->getLayerCount();
		if (numLayers < 2)
			continue;

		int bmSize = (int)t->getLayerBlendMapSize();

		for (int l = 1; l < numLayers; ++l) {
			std::string fname =
				dirPath + "/page_" +
				Ogre::StringConverter::toString(slot->x) + "_" +
				Ogre::StringConverter::toString(slot->y) +
				"_l" + Ogre::StringConverter::toString(l) +
				".bin";

			std::ifstream file(fname, std::ios::binary);
			if (!file)
				continue;

			uint32_t fileSz = 0;
			uint8_t fileLayer = 0;
			file.read(reinterpret_cast<char *>(&fileSz),
				  sizeof(fileSz));
			file.read(reinterpret_cast<char *>(&fileLayer),
				  sizeof(fileLayer));

			if ((int)fileSz != bmSize || (int)fileLayer != l) {
				Ogre::LogManager::getSingleton().logMessage(
					"Terrain: blend map mismatch in " +
					fname + " (expected " +
					Ogre::StringConverter::toString(
						bmSize) +
					"x" +
					Ogre::StringConverter::toString(
						bmSize) +
					" layer " +
					Ogre::StringConverter::toString(l) +
					")");
				continue;
			}

			Ogre::TerrainLayerBlendMap *bm =
				t->getLayerBlendMap((Ogre::uint8)l);
			if (!bm)
				continue;

			float *data = bm->getBlendPointer();
			if (!data)
				continue;

			file.read(reinterpret_cast<char *>(data),
				  bmSize * bmSize * sizeof(float));
			bm->dirty();
			bm->update();
			loadedPages++;
		}
	}

	if (loadedPages > 0) {
		Ogre::LogManager::getSingleton().logMessage(
			"Terrain: loaded " +
			Ogre::StringConverter::toString(loadedPages) +
			" blend map layers from " + dirPath);
	}
	return loadedPages > 0;
}

bool TerrainSystem::saveSceneBlendMaps(const TerrainComponent &tc) const
{
	return saveBlendMaps(getHeightmapPath(tc) + "_blendmaps");
}

bool TerrainSystem::loadSceneBlendMaps(const TerrainComponent &tc)
{
	return loadBlendMaps(getHeightmapPath(tc) + "_blendmaps");
}

void TerrainSystem::ensureHeightmapLoaded(TerrainComponent &tc)
{
	if (m_heightmapLoaded)
		return;

	if (loadSceneHeightmap(tc))
		return;

	m_heightmapRes = tc.heightmapSize;
	m_heightData.resize(m_heightmapRes * m_heightmapRes);

	for (int z = 0; z < m_heightmapRes; ++z) {
		for (int x = 0; x < m_heightmapRes; ++x) {
			const float fx = (float)x / (float)(m_heightmapRes - 1);
			const float fz = (float)z / (float)(m_heightmapRes - 1);
			const float wx = m_heightmapWorldMinX +
					 fx * m_heightmapWorldSize;
			const float wz = m_heightmapWorldMinZ +
					 fz * m_heightmapWorldSize;
			m_heightData[z * m_heightmapRes + x] =
				proceduralHeight((long)wx, (long)wz);
		}
	}
	m_heightmapLoaded = true;
}

void TerrainSystem::fillPageHeightData(Ogre::TerrainGroup *group, long x,
				       long y, float *heightMap)
{
	const Ogre::uint16 terrainSize = group->getTerrainSize();
	const Ogre::Real worldSize = group->getTerrainWorldSize();

	Ogre::Vector3 worldPos;
	group->convertTerrainSlotToWorldPosition(x, y, &worldPos);
	const Ogre::Real step = worldSize / Ogre::Real(terrainSize - 1);

	for (int j = 0; j < terrainSize; ++j) {
		for (int i = 0; i < terrainSize; ++i) {
			const long wx =
				(long)(worldPos.x + (Ogre::Real)i * step);
			const long wz =
				(long)(worldPos.z + (Ogre::Real)j * step);
			heightMap[j * terrainSize + i] = sampleHeightAt(wx, wz);
		}
	}
}

float TerrainSystem::sampleHeightAt(long worldX, long worldZ) const
{
	std::lock_guard<std::mutex> lock(m_heightmapMutex);
	return sampleHeightAtLocked(worldX, worldZ);
}

float TerrainSystem::sampleHeightAtLocked(long worldX, long worldZ) const
{
	if (!m_heightmapLoaded || m_heightData.empty())
		return proceduralHeight(worldX, worldZ);

	float fx = ((float)worldX - m_heightmapWorldMinX) /
		   m_heightmapWorldSize * (float)m_heightmapRes;
	float fz = ((float)worldZ - m_heightmapWorldMinZ) /
		   m_heightmapWorldSize * (float)m_heightmapRes;

	int x0 = (int)floorf(fx);
	int z0 = (int)floorf(fz);
	int x1 = x0 + 1;
	int z1 = z0 + 1;

	int r = m_heightmapRes;
	x0 = std::max(0, std::min(x0, r - 1));
	x1 = std::max(0, std::min(x1, r - 1));
	z0 = std::max(0, std::min(z0, r - 1));
	z1 = std::max(0, std::min(z1, r - 1));

	float tx = fx - (float)x0;
	float tz = fz - (float)z0;

	float h00 = m_heightData[z0 * r + x0];
	float h10 = m_heightData[z0 * r + x1];
	float h01 = m_heightData[z1 * r + x0];
	float h11 = m_heightData[z1 * r + x1];

	return (1.0f - tx) * (1.0f - tz) * h00 + tx * (1.0f - tz) * h10 +
	       (1.0f - tx) * tz * h01 + tx * tz * h11;
}

void TerrainSystem::setHeightAt(long worldX, long worldZ, float value)
{
	std::lock_guard<std::mutex> lock(m_heightmapMutex);
	if (!m_heightmapLoaded)
		return;

	int x = (int)(((float)worldX - m_heightmapWorldMinX) /
		      m_heightmapWorldSize * (float)m_heightmapRes);
	int z = (int)(((float)worldZ - m_heightmapWorldMinZ) /
		      m_heightmapWorldSize * (float)m_heightmapRes);
	if (x < 0 || x >= m_heightmapRes || z < 0 || z >= m_heightmapRes)
		return;
	m_heightData[z * m_heightmapRes + x] = value;
}

long TerrainSystem::worldToPage(float worldCoord, float worldSize) const
{
	return (long)floorf(worldCoord / worldSize);
}

void TerrainSystem::markPageDirty(long pageX, long pageY)
{
	if (mTerrainGroup)
		m_dirtyPages.insert(mTerrainGroup->packIndex(pageX, pageY));
}

void TerrainSystem::updatePageGeometry(long pageX, long pageY)
{
	if (!mTerrainGroup)
		return;

	Ogre::Terrain *terrain = mTerrainGroup->getTerrain(pageX, pageY);
	if (!terrain || !terrain->isLoaded())
		return;

	uint16_t size = terrain->getSize();
	float *temp =
		OGRE_ALLOC_T(float, size *size, Ogre::MEMCATEGORY_GEOMETRY);
	fillPageHeightData(mTerrainGroup, pageX, pageY, temp);

	float *dest = terrain->getHeightData();
	for (int i = 0; i < size * size; ++i)
		dest[i] = temp[i];
	OGRE_FREE(temp, Ogre::MEMCATEGORY_GEOMETRY);

	terrain->dirty();
	terrain->update(true);
}

void TerrainSystem::rebuildDirtyPages()
{
	if (!mTerrainGroup || m_dirtyPages.empty())
		return;

	/* When sculpt previews are active, only update ManualObjects.
	 * Terrain pages are unloaded — don't touch colliders or try to
	 * update terrain geometry. */
	if (m_sculptPreviewsActive) {
		Ogre::LogManager::getSingleton().logMessage(
			"Terrain: updating " +
			Ogre::StringConverter::toString(
				(int)m_dirtyPages.size()) +
			" dirty sculpt preview pages");
		updateSculptDirtyPages();
		return;
	}

	Ogre::LogManager::getSingleton().logMessage(
		"Terrain: rebuilding " +
		Ogre::StringConverter::toString((int)m_dirtyPages.size()) +
		" dirty pages");

	for (uint64_t key : m_dirtyPages) {
		long x, y;
		mTerrainGroup->unpackIndex(key, &x, &y);

		auto it = mColliders.find(key);
		if (it != mColliders.end() && !it->second.bodyId.IsInvalid()) {
			mBodyDrawFilter.removeTerrainBody(it->second.bodyId);
			m_physics->removeBody(it->second.bodyId);
			m_physics->destroyBody(it->second.bodyId);
			mColliders.erase(it);
		}

		updatePageGeometry(x, y);
		queueColliderCreate(x, y);
	}
	m_dirtyPages.clear();
}

void TerrainSystem::processDeferredReloads()
{
	if (!mTerrainGroup || m_reloadQueue.empty())
		return;

	for (uint64_t key : m_reloadQueue) {
		long x, y;
		mTerrainGroup->unpackIndex(key, &x, &y);
		updatePageGeometry(x, y);
		queueColliderCreate(x, y);
	}
	m_reloadQueue.clear();
}

/* ------------------------------------------------------------------ */
/* activate                                                            */
/* ------------------------------------------------------------------ */

void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform)
{
	Ogre::LogManager::getSingleton().logMessage(
		"TerrainSystem: activating terrain...");

	deactivate();

	/* Paging range and heightmap world area must be known before the
	 * heightmap is generated or sampled. */
	m_pageMinX = -1;
	m_pageMinY = -1;
	m_pageMaxX = 1;
	m_pageMaxY = 1;
	m_heightmapWorldMinX = (float)m_pageMinX * tc.worldSize;
	m_heightmapWorldMinZ = (float)m_pageMinY * tc.worldSize;
	m_heightmapWorldSize =
		(float)(m_pageMaxX - m_pageMinX + 1) * tc.worldSize;

	ensureHeightmapLoaded(tc);

	/* Reuse TerrainGlobalOptions if it survived a previous
	 * deactivate/reactivate cycle.  Old terrain instances
	 * (intentionally leaked) still reference the singleton
	 * during rendering, so we must not delete it. */
	if (!mTerrainGlobals) {
		mTerrainGlobals = OGRE_NEW Ogre::TerrainGlobalOptions();
	}
	mTerrainGlobals->setMaxPixelError(tc.maxPixelError);
	mTerrainGlobals->setCompositeMapDistance(tc.compositeMapDistance);
	mTerrainGlobals->setCompositeMapAmbient(m_sceneMgr->getAmbientLight());

	Ogre::TerrainMaterialGeneratorPtr matGen(
		new Ogre::TerrainMaterialGeneratorA());
	mTerrainGlobals->setDefaultMaterialGenerator(matGen);

	mTerrainGroup = OGRE_NEW Ogre::TerrainGroup(m_sceneMgr,
						    Ogre::Terrain::ALIGN_X_Z,
						    (uint16_t)tc.terrainSize,
						    tc.worldSize);
	mTerrainGroup->setOrigin(xform.position);

	Ogre::Terrain::ImportData &defaultImp =
		mTerrainGroup->getDefaultImportSettings();
	defaultImp.terrainSize = (uint16_t)tc.terrainSize;
	defaultImp.worldSize = tc.worldSize;
	defaultImp.minBatchSize = (uint16_t)tc.minBatchSize;
	defaultImp.maxBatchSize = (uint16_t)tc.maxBatchSize;
	defaultImp.inputScale = 1.0f;
	defaultImp.layerDeclaration = matGen->getLayerDeclaration();

	if (tc.layers.empty()) {
		Ogre::Terrain::LayerInstance li;
		li.worldSize = 100.0f;
		li.textureNames.push_back("Ground23_col.jpg");
		li.textureNames.push_back("Ground23_normheight.dds");
		defaultImp.layerList.push_back(li);
	} else {
		for (auto &cl : tc.layers) {
			Ogre::Terrain::LayerInstance li;
			li.worldSize = cl.worldSize;
			li.textureNames.push_back(cl.diffuseTexture);
			li.textureNames.push_back(cl.normalTexture);
			defaultImp.layerList.push_back(li);
		}
	}

	/* Editor-friendly paging: a small fixed world section with procedural
	 * pages.  The DummyPageProvider lets us hook unload so colliders are
	 * removed safely, while CustomTerrainDefiner re-samples the heightmap
	 * when a page is (re)loaded. */
	mPageManager = OGRE_NEW Ogre::PageManager();
	mDummyPageProvider = std::make_unique<DummyPageProvider>();
	mDummyPageProvider->owner = this;
	mPageManager->setPageProvider(mDummyPageProvider.get());

	mTerrainPaging = OGRE_NEW Ogre::TerrainPaging(mPageManager);
	mPagedWorld = mPageManager->createWorld();

	mTerrainPagedWorldSection = mTerrainPaging->createWorldSection(
		mPagedWorld, mTerrainGroup, 300.0f, 500.0f, m_pageMinX,
		m_pageMinY, m_pageMaxX, m_pageMaxY);

	mTerrainDefiner = std::make_unique<CustomTerrainDefiner>(this);
	mTerrainPagedWorldSection->setDefiner(mTerrainDefiner.get());

	mTerrainGroup->freeTemporaryResources();

	/* Editor mode: define and synchronously load every page directly through
	 * the TerrainGroup.  This avoids leaving background WorkQueue tasks from
	 * the paging load chain, which otherwise can deadlock or hang shutdown.
	 * The paging objects are still set up so runtime game mode can attach a
	 * camera and use page unload hooks (DummyPageProvider) safely. */
	for (long py = m_pageMinY; py <= m_pageMaxY; ++py) {
		for (long px = m_pageMinX; px <= m_pageMaxX; ++px) {
			Ogre::uint16 terrainSize =
				mTerrainGroup->getTerrainSize();
			float *heightMap =
				OGRE_ALLOC_T(float, terrainSize *terrainSize,
					     Ogre::MEMCATEGORY_GEOMETRY);
			fillPageHeightData(mTerrainGroup, px, py, heightMap);
			mTerrainGroup->defineTerrain(px, py, heightMap);
			OGRE_FREE(heightMap, Ogre::MEMCATEGORY_GEOMETRY);
		}
	}
	mTerrainGroup->loadAllTerrains(true);

	m_active = true;

	/* Restore saved blend maps if they exist.  This must happen after
	 * m_active is set because loadBlendMaps early-outs when inactive. */
	loadSceneBlendMaps(tc);

	if (m_physics)
		m_physics->setBodyDrawFilter(&mBodyDrawFilter);

	Ogre::LogManager::getSingleton().logMessage(
		"TerrainSystem: activation complete");
}

/* ------------------------------------------------------------------ */
/* deactivate                                                          */
/* ------------------------------------------------------------------ */

void TerrainSystem::deactivate()
{
	if (!m_active && !mTerrainGroup)
		return;

	Ogre::LogManager::getSingleton().logMessage(
		"TerrainSystem: deactivating...");

	m_active = false;
	m_terrainEntityId = 0;
	m_dirtyPages.clear();
	m_reloadQueue.clear();
	m_sculptMode = false;
	m_paintMode = false;

	/* Clean up sculpt previews without trying to reload terrain
	 * pages (we're shutting down the whole terrain). */
	if (m_sculptPreviewsActive) {
		for (auto &kv : m_sculptPreviews) {
			auto &sp = kv.second;
			if (sp.manualObj) {
				if (sp.sceneNode)
					sp.sceneNode->detachObject(
						sp.manualObj);
				m_sceneMgr->destroyManualObject(sp.manualObj);
			}
			if (sp.sceneNode)
				m_sceneMgr->destroySceneNode(sp.sceneNode);
		}
		m_sculptPreviews.clear();
		m_sculptPreviewsActive = false;
	}

	/* Destroy brush decal. */
	if (m_brushDecal) {
		m_brushDecalNode->detachObject(m_brushDecal);
		m_sceneMgr->destroyManualObject(m_brushDecal);
		m_brushDecal = nullptr;
	}
	if (m_brushDecalNode) {
		m_sceneMgr->destroySceneNode(m_brushDecalNode);
		m_brushDecalNode = nullptr;
	}

	removeAllColliders();
	if (m_physics)
		m_physics->setBodyDrawFilter(nullptr);

	/* Stop paging from loading/unloading pages while we tear down. */
	if (mPageManager)
		mPageManager->setPagingOperationsEnabled(false);

	/* Remove the actual terrain page instances from the scene manager before
	 * we drop the group.  The group object itself is intentionally leaked (see
	 * below), but the terrain pages must be destroyed or they stay rendered
	 * and obscure the new terrain after reactivation. */
	if (mTerrainGroup)
		mTerrainGroup->removeAllTerrains();

	/* Release RTShader SubRenderState objects held by the terrain material
	 * generator before the RTSS is torn down.  The persistent instances live
	 * in the generator's main RenderState; remove them explicitly so the
	 * factories can be destroyed cleanly. */
	if (mTerrainGlobals) {
		Ogre::TerrainMaterialGeneratorPtr gen =
			mTerrainGlobals->getDefaultMaterialGenerator();
		if (gen) {
			auto *genA =
				dynamic_cast<Ogre::TerrainMaterialGeneratorA *>(
					gen.get());
			if (genA) {
				Ogre::RTShader::RenderState *rs =
					genA->getMainRenderState();
				if (rs) {
					Ogre::RTShader::SubRenderStateList srs =
						rs->getSubRenderStates();
					for (Ogre::RTShader::SubRenderState
						     *sub : srs)
						rs->removeSubRenderState(sub);
				}
			}
		}
	}

	mTerrainDefiner.reset();
	mTerrainPagedWorldSection = nullptr;
	mPagedWorld = nullptr;
	mDummyPageProvider.reset();

	/* The PagedWorld owns the TerrainPagedWorldSection, which in turn owns
	 * the TerrainGroup.  PageManager::~PageManager does not delete the worlds
	 * it manages, so destroying the PageManager here leaks the world/section
	 * and the group.  This is intentional: deleting the group can hang or
	 * crash on some GL drivers, and leaking it at shutdown is harmless. */
	OGRE_DELETE mTerrainPaging;
	mTerrainPaging = nullptr;
	OGRE_DELETE mPageManager;
	mPageManager = nullptr;
	mTerrainGroup = nullptr;

	if (mTerrainGlobals) {
		mTerrainGlobals->setDefaultMaterialGenerator(
			Ogre::TerrainMaterialGeneratorPtr());
	}

	Ogre::LogManager::getSingleton().logMessage(
		"TerrainSystem: deactivated");
}

/* ------------------------------------------------------------------ */
/* update                                                              */
/* ------------------------------------------------------------------ */

void TerrainSystem::update(float /*deltaTime*/)
{
	flecs::entity_t foundEntity = 0;

	m_terrainQuery.each([&](flecs::entity e, TerrainComponent &tc,
				TransformComponent &xform) {
		foundEntity = e.id();

		if (!tc.enabled && m_active) {
			deactivate();
			return;
		}
		if (tc.enabled && !m_active) {
			activate(tc, xform);
			m_terrainEntityId = e.id();
		}
	});

	if (m_active && foundEntity == 0) {
		deactivate();
		return;
	}

	rebuildDirtyPages();
	processDeferredReloads();

	ensurePageColliders();
	processColliderRemoves();
	processColliderCreates();

	mBodyDrawFilter.showTerrain = m_showTerrainColliders;
}

/* ------------------------------------------------------------------ */

float TerrainSystem::getHeightAt(const Ogre::Vector3 &worldPos) const
{
	if (!m_active)
		return 0.0f;

	/* In sculpt mode the Ogre terrain instances are unloaded;
	 * sample directly from the in-memory heightmap.  Convert
	 * visual world position to physical heightmap coords. */
	if (m_sculptPreviewsActive && m_heightmapLoaded)
		return sampleHeightAt((long)visualToPhysicalX(worldPos.x),
				      (long)visualToPhysicalZ(worldPos.z));

	if (mTerrainGroup)
		return mTerrainGroup->getHeightAtWorldPosition(worldPos);

	return 0.0f;
}

bool TerrainSystem::raycastTerrain(const Ogre::Ray &ray, float &outT,
				   Ogre::Vector3 &outNormal) const
{
	if (!m_active || !mTerrainGroup || !m_sceneMgr)
		return false;

	/* --- Sculpt mode: terrain pages are unloaded (no Jolt
	 * colliders).  Iteratively intersect the camera ray with
	 * the heightmap: start at Y=0, look up the terrain height
	 * at that XZ, then solve for the ray's intersection with a
	 * plane at that height, and repeat.  Converges in 2–4
	 * iterations even on steep slopes. --- */
	if (m_sculptPreviewsActive && m_heightmapLoaded) {
		const Ogre::Vector3 &origin = ray.getOrigin();
		const Ogre::Vector3 &dir = ray.getDirection();

		if (fabsf(dir.y) < 1e-6f)
			return false;

		/* Seed: start just in front of the camera, not
		 * from Y=0 (which fails when camera is below Y=0
		 * looking down).  The iterative refinement converges
		 * in 2-4 steps from any reasonable seed. */
		float t = 0.01f;
		Ogre::Vector3 pt = origin + dir * t;

		/* Safety: don't march past 100 km. */
		const float maxDist = 100000.0f;
		for (int iter = 0; iter < 16; ++iter) {
			float h = sampleHeightAt((long)pt.x, (long)pt.z);
			float tNew = (h - origin.y) / dir.y;
			if (tNew < 0.0f || tNew > maxDist)
				return false;
			Ogre::Vector3 ptNew = origin + dir * tNew;

			/* Converged? */
			if (fabsf(ptNew.x - pt.x) < 0.2f &&
			    fabsf(ptNew.z - pt.z) < 0.2f) {
				outT = tNew;
				/* Normal from heightmap gradient. */
				float eps = 1.0f;
				float dx = sampleHeightAt((long)(ptNew.x + eps),
							  (long)ptNew.z) -
					   h;
				float dz =
					sampleHeightAt((long)ptNew.x,
						       (long)(ptNew.z + eps)) -
					h;
				outNormal = Ogre::Vector3(-dx, eps * 2.0f, -dz);
				outNormal.normalise();
				return true;
			}
			pt = ptNew;
			t = tNew;
		}
		/* No convergence — use last result. */
		outT = t;
		outNormal = Ogre::Vector3::UNIT_Y;
		return true;
	}

	/* --- Primary: Jolt physics raycast against colliders --- */
	if (m_physics && !mColliders.empty()) {
		const JPH::RVec3 origin(ray.getOrigin().x, ray.getOrigin().y,
					ray.getOrigin().z);
		JPH::Vec3 dir(ray.getDirection().x, ray.getDirection().y,
			      ray.getDirection().z);
		dir = dir.Normalized();

		const JPH::RRayCast rayCast(origin, dir);

		JPH::PhysicsSystem *jphSys = m_physics->getPhysicsSystem();
		if (jphSys) {
			JPH::RayCastResult hit;
			if (jphSys->GetNarrowPhaseQuery().CastRay(
				    rayCast, hit, JPH::BroadPhaseLayerFilter(),
				    JPH::ObjectLayerFilter(),
				    JPH::BodyFilter())) {
				for (auto &pair : mColliders) {
					if (pair.second.bodyId == hit.mBodyID) {
						outT = hit.mFraction;
						outNormal =
							Ogre::Vector3::UNIT_Y;
						return true;
					}
				}
			}
		}
	}

	/* --- Fallback: Y=0 plane intersection for when Jolt
	 * colliders miss (camera far above/below or shallow
	 * angle).  Not used in sculpt mode which has its own
	 * heightmap-based path above. --- */
	if (m_sculptPreviewsActive)
		return false;

	{
		const Ogre::Vector3 &origin = ray.getOrigin();
		const Ogre::Vector3 &dir = ray.getDirection();
		if (fabsf(dir.y) < 1e-6f)
			return false;

		const float planeY = 0.0f;
		float t = (planeY - origin.y) / dir.y;
		if (t < 0.0f)
			return false;

		outT = t;
		outNormal = Ogre::Vector3::UNIT_Y;
		return true;
	}

	return false;
}

void TerrainSystem::setShowTerrainColliders(bool show)
{
	m_showTerrainColliders = show;
}

/* ------------------------------------------------------------------ */
/* snapCameraAboveTerrain                                              */
/* ------------------------------------------------------------------ */

void TerrainSystem::snapCameraAboveTerrain()
{
	if (!m_active || !m_sceneMgr)
		return;

	Ogre::SceneNode *targetNode =
		m_sceneMgr->getSceneNode("EditorCameraTarget");
	if (!targetNode)
		targetNode = m_sceneMgr->getSceneNode("EditorCameraTargetNode");
	if (!targetNode)
		return;

	Ogre::Vector3 pos = targetNode->getPosition();
	/* Sample centre plus 4 corner points to avoid
	 * clipping through terrain on steep slopes. */
	float r = 3.0f;
	float h = getHeightAt(pos);
	h = std::max(h, getHeightAt(Ogre::Vector3(pos.x - r, pos.y, pos.z)));
	h = std::max(h, getHeightAt(Ogre::Vector3(pos.x + r, pos.y, pos.z)));
	h = std::max(h, getHeightAt(Ogre::Vector3(pos.x, pos.y, pos.z - r)));
	h = std::max(h, getHeightAt(Ogre::Vector3(pos.x, pos.y, pos.z + r)));
	if (pos.y < h + 5.0f)
		targetNode->setPosition(pos.x, h + 5.0f, pos.z);
}

/* ------------------------------------------------------------------ */
/* applySculptBrush                                                    */
/* ------------------------------------------------------------------ */

void TerrainSystem::setSculptMode(bool v)
{
	if (v == m_sculptMode)
		return;

	/* Sculpt and paint are mutually exclusive. */
	if (v && m_paintMode)
		m_paintMode = false;

	m_sculptMode = v;
	if (v)
		beginSculptPreviews();
	else
		endSculptPreviews();
}

void TerrainSystem::setSculptTool(SculptTool t)
{
	m_sculptTool = t;
}

void TerrainSystem::applySculptBrush(const Ogre::Vector3 &worldPos)
{
	if (!m_heightmapLoaded || !m_active || !mTerrainGroup)
		return;

	Ogre::LogManager::getSingleton().logMessage(
		"Terrain: applySculptBrush at (" +
		Ogre::StringConverter::toString(worldPos.x) + ", " +
		Ogre::StringConverter::toString(worldPos.y) + ", " +
		Ogre::StringConverter::toString(worldPos.z) +
		") radius=" + Ogre::StringConverter::toString(m_sculptRadius));

	std::lock_guard<std::mutex> lock(m_heightmapMutex);

	float spacing = m_heightmapWorldSize / (float)(m_heightmapRes - 1);
	int rSamples = (int)(m_sculptRadius / spacing) + 1;
	int cx = (int)((worldPos.x - m_heightmapWorldMinX) /
		       m_heightmapWorldSize * (float)m_heightmapRes);
	int cz = (int)((worldPos.z - m_heightmapWorldMinZ) /
		       m_heightmapWorldSize * (float)m_heightmapRes);
	int x0 = std::max(0, cx - rSamples);
	int x1 = std::min(m_heightmapRes - 1, cx + rSamples);
	int z0 = std::max(0, cz - rSamples);
	int z1 = std::min(m_heightmapRes - 1, cz + rSamples);

	if (m_sculptTool == SculptTool::Smooth) {
		std::vector<std::pair<int, int> > s;
		for (int z = z0; z <= z1; ++z)
			for (int x = x0; x <= x1; ++x) {
				float dx = ((float)x - (float)cx) * spacing;
				float dz = ((float)z - (float)cz) * spacing;
				if (dx * dx + dz * dz <=
				    m_sculptRadius * m_sculptRadius)
					s.push_back({ x, z });
			}
		float avg = 0;
		for (auto &p : s)
			avg += m_heightData[p.second * m_heightmapRes + p.first];
		avg /= (float)s.size();
		for (auto &p : s) {
			float &h = m_heightData[p.second * m_heightmapRes +
						p.first];
			h = avg * m_sculptStrength +
			    h * (1.0f - m_sculptStrength);
		}
	} else if (m_sculptTool == SculptTool::Flatten) {
		float ch = m_heightData[cz * m_heightmapRes + cx];
		for (int z = z0; z <= z1; ++z)
			for (int x = x0; x <= x1; ++x) {
				float dx = ((float)x - (float)cx) * spacing;
				float dz = ((float)z - (float)cz) * spacing;
				if (dx * dx + dz * dz <=
				    m_sculptRadius * m_sculptRadius) {
					float &h =
						m_heightData[z * m_heightmapRes +
							     x];
					h = h * (1.0f - m_sculptStrength) +
					    ch * m_sculptStrength;
				}
			}
	} else {
		float sign = (m_sculptTool == SculptTool::Raise) ? 1.0f : -1.0f;
		for (int z = z0; z <= z1; ++z)
			for (int x = x0; x <= x1; ++x) {
				float dx = ((float)x - (float)cx) * spacing;
				float dz = ((float)z - (float)cz) * spacing;
				float d2 = dx * dx + dz * dz;
				if (d2 <= m_sculptRadius * m_sculptRadius) {
					float falloff =
						1.0f -
						sqrtf(d2) / m_sculptRadius;
					m_heightData[z * m_heightmapRes + x] +=
						sign * m_sculptStrength *
						falloff;
				}
			}
	}

	float ws = mTerrainGroup->getTerrainWorldSize();
	long px0 = worldToPage(worldPos.x - m_sculptRadius, ws);
	long px1 = worldToPage(worldPos.x + m_sculptRadius, ws);
	long pz0 = worldToPage(worldPos.z - m_sculptRadius, ws);
	long pz1 = worldToPage(worldPos.z + m_sculptRadius, ws);
	for (long pz = pz0; pz <= pz1; ++pz)
		for (long px = px0; px <= px1; ++px)
			markPageDirty(px, pz);
}

/* ------------------------------------------------------------------ */
/* applySplatBrush                                                     */
/* ------------------------------------------------------------------ */

void TerrainSystem::applySplatBrush(const Ogre::Vector3 &worldPos)
{
	if (!mTerrainGroup || !m_active) {
		Ogre::LogManager::getSingleton().logMessage(
			"Terrain: applySplatBrush early return - no group or not active");
		return;
	}

	/* Blend maps live on the terrain instance — must be loaded.
	 * In sculpt mode terrain is unloaded; skip. */
	if (m_sculptPreviewsActive) {
		Ogre::LogManager::getSingleton().logMessage(
			"Terrain: applySplatBrush early return - sculpt previews active");
		return;
	}

	int layerIdx = m_paintLayerIndex;
	if (layerIdx < 1) {
		Ogre::LogManager::getSingleton().logMessage(
			"Terrain: applySplatBrush early return - layerIdx < 1");
		return; /* layer 0 is base, no blend map */
	}

	Ogre::Real ws = mTerrainGroup->getTerrainWorldSize();
	if (ws <= 0.0f)
		return;

	/* Determine the terrain slot that contains the brush centre and the
	 * range of slots the brush radius touches.  Use Ogre's helper so the
	 * origin and alignment are handled exactly like the rest of the terrain
	 * system. */
	long centreX = 0, centreY = 0;
	mTerrainGroup->convertWorldPositionToTerrainSlot(worldPos, &centreX,
							 &centreY);

	long slotRadius = (long)ceilf(m_paintRadius / ws);

	for (long py = centreY - slotRadius; py <= centreY + slotRadius; ++py) {
		for (long px = centreX - slotRadius; px <= centreX + slotRadius;
		     ++px) {
			Ogre::Terrain *terrain =
				mTerrainGroup->getTerrain(px, py);
			if (!terrain || !terrain->isLoaded()) {
				Ogre::LogManager::getSingleton().logMessage(
					"Terrain: applySplatBrush skip page (" +
					Ogre::StringConverter::toString(px) +
					"," +
					Ogre::StringConverter::toString(py) +
					") not loaded");
				continue;
			}

			if (layerIdx >= (int)terrain->getLayerCount()) {
				Ogre::LogManager::getSingleton().logMessage(
					"Terrain: applySplatBrush skip page (" +
					Ogre::StringConverter::toString(px) +
					"," +
					Ogre::StringConverter::toString(py) +
					") layerIdx >= layerCount(" +
					Ogre::StringConverter::toString(
						(int)terrain->getLayerCount()) +
					")");
				continue;
			}

			/* getLayerBlendMap expects the actual layer index (1..n),
			 * not the internal blend-map array index. */
			Ogre::TerrainLayerBlendMap *blendMap =
				terrain->getLayerBlendMap(
					(Ogre::uint8)layerIdx);
			if (!blendMap) {
				Ogre::LogManager::getSingleton().logMessage(
					"Terrain: applySplatBrush skip page (" +
					Ogre::StringConverter::toString(px) +
					"," +
					Ogre::StringConverter::toString(py) +
					") blendMap null for layer " +
					Ogre::StringConverter::toString(
						layerIdx));
				continue;
			}

			/* Use the actual blend-map image dimensions (the GPU texture may
			 * be a different size than getLayerBlendMapSize() due to
			 * hardware padding / packing). */
			size_t maxX, maxY;
			blendMap->convertUVToImageSpace(1.0f, 1.0f, &maxX, &maxY);
			int bmW = (int)maxX + 1;
			int bmH = (int)maxY + 1;

			/* Convert the brush centre to blend-map UV using Ogre's helper.
			 * Values can be outside [0,1] when the centre is not inside this
			 * page (e.g. neighbouring pages touched by a large brush). */
			float brushU, brushV;
			blendMap->convertWorldToUVSpace(worldPos, &brushU,
							&brushV);

			/* Keep the unclamped float image coordinates so the distance
			 * falloff remains centred on the true brush position even when
			 * we clamp the affected pixel range to the page bounds. */
			float cxRaw = brushU * (float)(bmW - 1);
			float cyRaw = brushV * (float)(bmH - 1);

			/* Texel spacing in world units. */
			float spacingX = ws / (float)(bmW - 1);
			float spacingY = ws / (float)(bmH - 1);
			int rSamplesX = (int)(m_paintRadius / spacingX) + 1;
			int rSamplesY = (int)(m_paintRadius / spacingY) + 1;

			/* Pixel range potentially touched by the brush on this page. */
			int x0 = (int)floorf(cxRaw - (float)rSamplesX);
			int x1 = (int)ceilf(cxRaw + (float)rSamplesX);
			int y0 = (int)floorf(cyRaw - (float)rSamplesY);
			int y1 = (int)ceilf(cyRaw + (float)rSamplesY);

			/* Clamp to the page image bounds. */
			x0 = std::max(0, x0);
			x1 = std::min(bmW - 1, x1);
			y0 = std::max(0, y0);
			y1 = std::min(bmH - 1, y1);

			if (x0 > x1 || y0 > y1)
				continue;

			/* Integer brush centre clamped to bounds; used for debug
			 * readback and logging only. */
			int cxClamped = (int)std::max(0.0f,
					      std::min(cxRaw,
						       (float)(bmW - 1)));
			int cyClamped = (int)std::max(0.0f,
					      std::min(cyRaw,
						       (float)(bmH - 1)));

			bool painted = false;
			for (int y = y0; y <= y1; ++y) {
				for (int x = x0; x <= x1; ++x) {
					float dx = (x - cxRaw) * spacingX;
					float dy = (y - cyRaw) * spacingY;
					float d2 = dx * dx + dy * dy;
					if (d2 > m_paintRadius * m_paintRadius)
						continue;

					float falloff =
						1.0f -
						sqrtf(d2) / m_paintRadius;
					float cur = blendMap->getBlendValue(
						(Ogre::uint32)x,
						(Ogre::uint32)y);
					float delta = m_paintStrength * falloff;
					float val = std::max(
						0.0f,
						std::min(1.0f, cur + delta));
					blendMap->setBlendValue((Ogre::uint32)x,
								(Ogre::uint32)y,
								val);
					painted = true;
				}
			}

			if (painted) {
				/* dirtyRect uses half-open bounds. */
				blendMap->dirtyRect(
					Ogre::Rect((long)x0, (long)y0,
						   (long)x1 + 1,
						   (long)y1 + 1));
				float cpuBefore = blendMap->getBlendValue(
					(Ogre::uint32)cxClamped,
					(Ogre::uint32)cyClamped);
				blendMap->update();
				/* Update the composite map so the painted region is also
				 * visible at distance.  The blend-map texture itself has
				 * already been uploaded by update(). */
				terrain->updateCompositeMap();

				/* Debug: read back the GPU blend value at the centre pixel
				 * to verify the upload actually reached the GPU. */
				static int s_splatDebugCount = 0;
				if (s_splatDebugCount < 10) {
					try {
						std::pair<Ogre::uint8, Ogre::uint8> texIdx =
							terrain->getLayerBlendTextureIndex(
								(Ogre::uint8)layerIdx);
						Ogre::TexturePtr tex =
							terrain->getLayerBlendTexture(
								texIdx.first);
						if (tex) {
							Ogre::HardwarePixelBufferSharedPtr buf =
								tex->getBuffer();
							Ogre::PixelBox pb = buf->lock(
								Ogre::Box((Ogre::uint32)cxClamped,
									(Ogre::uint32)cyClamped,
									(Ogre::uint32)cxClamped + 1,
									(Ogre::uint32)cyClamped + 1),
								Ogre::HardwareBuffer::HBL_READ_ONLY);
							Ogre::PixelFormat fmt = buf->getFormat();
							int bpp = (int)Ogre::PixelUtil::getNumElemBytes(
								fmt);
							std::string vals;
							for (int i = 0; i < bpp && i < 4; ++i) {
								if (i > 0)
									vals += ",";
								vals += Ogre::StringConverter::toString(
									(int)((uint8_t *)pb.data)[i]);
							}
							buf->unlock();
							Ogre::LogManager::getSingleton().logMessage(
								"Terrain: blend debug cpuBefore=" +
								Ogre::StringConverter::toString(
									cpuBefore) +
								" gpuBytes(" + Ogre::StringConverter::toString(cxClamped) + "," + Ogre::StringConverter::toString(cyClamped) + ")=[" +
								vals + "] fmt=" +
								Ogre::PixelUtil::getFormatName(fmt) +
								" bpp=" +
								Ogre::StringConverter::toString(bpp));
						}
					} catch (const std::exception &e) {
						Ogre::LogManager::getSingleton().logMessage(
							"Terrain: blend debug readback failed: " +
							Ogre::String(e.what()));
					}
					++s_splatDebugCount;
				}

				Ogre::LogManager::getSingleton().logMessage(
					"Terrain: splat painted layer " +
					Ogre::StringConverter::toString(
						layerIdx) +
					" on page (" +
					Ogre::StringConverter::toString(px) +
					"," +
					Ogre::StringConverter::toString(py) +
					") at world(" +
					Ogre::StringConverter::toString(
						worldPos.x) +
					"," +
					Ogre::StringConverter::toString(
						worldPos.z) +
					") image(" +
					Ogre::StringConverter::toString(
						cxClamped) +
					"," +
					Ogre::StringConverter::toString(
						cyClamped) +
					")");
			}
		}
	}
}

/* ------------------------------------------------------------------ */
/* Sculpt preview material (M3)                                        */
/* ------------------------------------------------------------------ */

void TerrainSystem::ensureSculptPreviewMaterial()
{
	if (m_sculptPreviewMaterial)
		return;

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

	m_sculptPreviewMaterial = matMgr.create(
		"TerrainSculptPreview",
		Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);

	Ogre::Pass *pass =
		m_sculptPreviewMaterial->getTechnique(0)->createPass();
	pass->setDiffuse(0.55f, 0.50f, 0.40f, 1.0f); /* earthy brown */
	pass->setAmbient(0.35f, 0.30f, 0.25f);
	pass->setSpecular(0.1f, 0.1f, 0.1f, 1.0f);
	pass->setShininess(4.0f);
	pass->setCullingMode(Ogre::CULL_CLOCKWISE);

	/* Let RTSS inject per-pixel lighting shaders. */
	Ogre::RTShader::ShaderGenerator *shaderGen =
		Ogre::RTShader::ShaderGenerator::getSingletonPtr();
	if (shaderGen) {
		shaderGen->createShaderBasedTechnique(
			*m_sculptPreviewMaterial,
			Ogre::MaterialManager::DEFAULT_SCHEME_NAME,
			Ogre::RTShader::ShaderGenerator::DEFAULT_SCHEME_NAME);
		shaderGen->validateMaterial(
			Ogre::RTShader::ShaderGenerator::DEFAULT_SCHEME_NAME,
			"TerrainSculptPreview");
	}

	Ogre::LogManager::getSingleton().logMessage(
		"Terrain: created sculpt preview material");
}

/* ------------------------------------------------------------------ */
/* beginSculptPreviews                                                 */
/* ------------------------------------------------------------------ */

void TerrainSystem::beginSculptPreviews()
{
	if (!m_active || !mTerrainGroup || !m_sceneMgr)
		return;

	if (m_sculptPreviewsActive)
		endSculptPreviews();

	ensureSculptPreviewMaterial();

	/* 1. Disable paging so no new pages load/unload while sculpting. */
	if (mPageManager)
		mPageManager->setPagingOperationsEnabled(false);

	/* 2. Remember all loaded pages and unload them. */
	std::vector<std::pair<long, long> > loadedPages;
	for (const auto &kv : mTerrainGroup->getTerrainSlots()) {
		Ogre::TerrainGroup::TerrainSlot *slot = kv.second;
		if (slot && slot->instance && slot->instance->isLoaded())
			loadedPages.push_back({ slot->x, slot->y });
	}

	/* Unload terrain instances so they stop rendering. */
	for (auto [x, y] : loadedPages)
		mTerrainGroup->removeTerrain(x, y);

	/* 3. Create ManualObject + SceneNode per page. */
	for (auto [x, y] : loadedPages) {
		uint64_t key = mTerrainGroup->packIndex(x, y);

		/* SceneNode at page minimum corner (match terrain
		 * root node position). */
		Ogre::Vector3 worldPos;
		mTerrainGroup->convertTerrainSlotToWorldPosition(x, y,
								 &worldPos);

		Ogre::SceneNode *node =
			m_sceneMgr->getRootSceneNode()->createChildSceneNode(
				worldPos);

		Ogre::ManualObject *mo = m_sceneMgr->createManualObject();
		mo->setDynamic(true);
		node->attachObject(mo);

		m_sculptPreviews[key] = { mo, node, x, y };
		buildSculptPreview(x, y);
		mo->setVisible(true);
	}

	m_sculptPreviewsActive = true;

	Ogre::LogManager::getSingleton().logMessage(
		"Terrain: sculpt previews active (" +
		Ogre::StringConverter::toString((int)loadedPages.size()) +
		" pages)");
}

/* ------------------------------------------------------------------ */
/* buildSculptPreview                                                  */
/* ------------------------------------------------------------------ */

void TerrainSystem::buildSculptPreview(long pageX, long pageY)
{
	uint64_t key = mTerrainGroup->packIndex(pageX, pageY);
	auto it = m_sculptPreviews.find(key);
	if (it == m_sculptPreviews.end())
		return;

	Ogre::ManualObject *mo = it->second.manualObj;
	if (!mo)
		return;

	Ogre::LogManager::getSingleton().logMessage(
		"Terrain: building sculpt preview page (" +
		Ogre::StringConverter::toString(pageX) + ", " +
		Ogre::StringConverter::toString(pageY) + ")");

	const Ogre::uint16 size = mTerrainGroup->getTerrainSize();
	const Ogre::Real worldSize = mTerrainGroup->getTerrainWorldSize();
	const Ogre::Real step = worldSize / (Ogre::Real)(size - 1);

	/* Sample heights. */
	std::vector<float> heights(size * size);
	{
		Ogre::Vector3 worldPos;
		mTerrainGroup->convertTerrainSlotToWorldPosition(pageX, pageY,
								 &worldPos);
		for (int j = 0; j < size; ++j) {
			for (int i = 0; i < size; ++i) {
				long wx = (long)(worldPos.x +
						 (Ogre::Real)i * step);
				long wz = (long)(worldPos.z +
						 (Ogre::Real)j * step);
				heights[j * size + i] = sampleHeightAt(wx, wz);
			}
		}
	}

	/* Compute per-vertex normals via central differences. */
	std::vector<Ogre::Vector3> normals(size * size);
	for (int j = 0; j < size; ++j) {
		for (int i = 0; i < size; ++i) {
			float hl = (i > 0) ? heights[j * size + (i - 1)] :
					     heights[j * size + i];
			float hr = (i < size - 1) ?
					   heights[j * size + (i + 1)] :
					   heights[j * size + i];
			float hu = (j > 0) ? heights[(j - 1) * size + i] :
					     heights[j * size + i];
			float hd = (j < size - 1) ?
					   heights[(j + 1) * size + i] :
					   heights[j * size + i];
			Ogre::Vector3 n(-(hr - hl), 2.0f * step, -(hd - hu));
			n.normalise();
			normals[j * size + i] = n;
		}
	}

	/* Build ManualObject with zigzag triangulation matching Ogre
	 * terrain and Jolt collider layout. */
	mo->clear();
	mo->begin("TerrainSculptPreview",
		  Ogre::RenderOperation::OT_TRIANGLE_LIST);

	float halfSize = worldSize * 0.5f;
	for (int j = 0; j < size; ++j) {
		for (int i = 0; i < size; ++i) {
			float vx = (float)i * step - halfSize;
			float vz = halfSize - (float)j * step;
			float vy = heights[j * size + i];
			mo->position(vx, vy, vz);
			mo->normal(normals[j * size + i].x,
				   normals[j * size + i].y,
				   normals[j * size + i].z);
		}
	}

	for (int j = 0; j < size - 1; ++j) {
		for (int i = 0; i < size - 1; ++i) {
			int idx00 = j * size + i;
			int idx10 = j * size + i + 1;
			int idx01 = (j + 1) * size + i;
			int idx11 = (j + 1) * size + i + 1;
			if ((j & 1) == 0) {
				mo->triangle(idx00, idx11, idx01);
				mo->triangle(idx00, idx10, idx11);
			} else {
				mo->triangle(idx00, idx10, idx01);
				mo->triangle(idx10, idx11, idx01);
			}
		}
	}

	mo->end();
}

/* ------------------------------------------------------------------ */
/* updateSculptDirtyPages                                              */
/* ------------------------------------------------------------------ */

void TerrainSystem::updateSculptDirtyPages()
{
	if (!m_sculptPreviewsActive || m_dirtyPages.empty())
		return;

	for (uint64_t key : m_dirtyPages) {
		long x, y;
		mTerrainGroup->unpackIndex(key, &x, &y);
		buildSculptPreview(x, y);
	}
	m_dirtyPages.clear();
}

/* ------------------------------------------------------------------ */
/* endSculptPreviews                                                   */
/* ------------------------------------------------------------------ */

void TerrainSystem::endSculptPreviews()
{
	if (!m_sculptPreviewsActive || !mTerrainGroup)
		return;

	hideBrushDecal();

	/* 1. Destroy all ManualObjects and SceneNodes. */
	for (auto &kv : m_sculptPreviews) {
		auto &sp = kv.second;
		if (sp.manualObj) {
			if (sp.sceneNode)
				sp.sceneNode->detachObject(sp.manualObj);
			m_sceneMgr->destroyManualObject(sp.manualObj);
		}
		if (sp.sceneNode)
			m_sceneMgr->destroySceneNode(sp.sceneNode);
	}
	m_sculptPreviews.clear();

	/* 2. Reload terrain pages with updated heightmap data. */
	const Ogre::uint16 terrainSize = mTerrainGroup->getTerrainSize();
	for (long py = m_pageMinY; py <= m_pageMaxY; ++py) {
		for (long px = m_pageMinX; px <= m_pageMaxX; ++px) {
			float *heightMap =
				OGRE_ALLOC_T(float, terrainSize *terrainSize,
					     Ogre::MEMCATEGORY_GEOMETRY);
			fillPageHeightData(mTerrainGroup, px, py, heightMap);
			mTerrainGroup->defineTerrain(px, py, heightMap);
			OGRE_FREE(heightMap, Ogre::MEMCATEGORY_GEOMETRY);
		}
	}
	mTerrainGroup->loadAllTerrains(true);

	/* 3. Re-enable paging. */
	if (mPageManager)
		mPageManager->setPagingOperationsEnabled(true);

	m_sculptPreviewsActive = false;

	Ogre::LogManager::getSingleton().logMessage(
		"Terrain: sculpt previews destroyed, paging restored");
}

/* ------------------------------------------------------------------ */
/* Brush decal (M3)                                                    */
/* ------------------------------------------------------------------ */

void TerrainSystem::updateBrushDecal(const Ogre::Vector3 &worldPos,
				     const Ogre::Vector3 &normal, float radius)
{
	if (!m_sceneMgr)
		return;

	/* Lazy-create the decal. */
	if (!m_brushDecal) {
		m_brushDecalNode =
			m_sceneMgr->getRootSceneNode()->createChildSceneNode();

		m_brushDecal = m_sceneMgr->createManualObject();
		m_brushDecal->setDynamic(true);
		m_brushDecalNode->attachObject(m_brushDecal);

		/* Material: unlit, alpha-blended, vertex-coloured.
		 * RTSS reads the pass settings (lighting off,
		 * scene_blend, TVC_DIFFUSE) and generates a shader
		 * that passes vertex colours straight through with
		 * proper alpha blending. */
		Ogre::MaterialManager &matMgr =
			Ogre::MaterialManager::getSingleton();
		if (!matMgr.resourceExists("TerrainBrushDecal")) {
			auto mat = matMgr.create(
				"TerrainBrushDecal",
				Ogre::ResourceGroupManager::
					DEFAULT_RESOURCE_GROUP_NAME);
			Ogre::Pass *pass = mat->getTechnique(0)->createPass();
			pass->setLightingEnabled(false);
			pass->setSceneBlending(Ogre::SBT_TRANSPARENT_ALPHA);
			pass->setDepthWriteEnabled(false);
			pass->setDepthCheckEnabled(false);
			pass->setCullingMode(Ogre::CULL_NONE);
			/* Vertex colours provide the actual colour. */
			pass->setVertexColourTracking(Ogre::TVC_DIFFUSE |
						      Ogre::TVC_AMBIENT);
			pass->setDiffuse(1.0f, 1.0f, 1.0f, 1.0f);
			pass->setAmbient(1.0f, 1.0f, 1.0f);

			Ogre::RTShader::ShaderGenerator *shaderGen = Ogre::
				RTShader::ShaderGenerator::getSingletonPtr();
			if (shaderGen) {
				shaderGen->createShaderBasedTechnique(
					*mat,
					Ogre::MaterialManager::DEFAULT_SCHEME_NAME,
					Ogre::RTShader::ShaderGenerator::
						DEFAULT_SCHEME_NAME);
				shaderGen->validateMaterial(
					Ogre::RTShader::ShaderGenerator::
						DEFAULT_SCHEME_NAME,
					"TerrainBrushDecal");
			}
		}

		/* Section 1: semi-transparent circle (triangle fan). */
		const int segments = 32;
		m_brushDecal->begin("TerrainBrushDecal",
				    Ogre::RenderOperation::OT_TRIANGLE_FAN);
		/* Center — low alpha, marks the exact brush origin. */
		m_brushDecal->position(0, 0, 0);
		m_brushDecal->colour(0.0f, 0.8f, 0.0f, 0.10f);
		for (int i = 0; i <= segments; ++i) {
			float angle = 2.0f * M_PI * (float)i / (float)segments;
			m_brushDecal->position(cosf(angle), 0, sinf(angle));
			/* Edge — higher alpha, shows brush boundary. */
			m_brushDecal->colour(0.0f, 0.8f, 0.0f, 0.28f);
		}
		m_brushDecal->end();

		/* Section 2: centre cross (line list) so the
		 * operator can always see the exact hit point. */
		m_brushDecal->begin("TerrainBrushDecal",
				    Ogre::RenderOperation::OT_LINE_LIST);
		float cs = 0.12f; /* cross arm half-length */
		m_brushDecal->position(-cs, 0, 0);
		m_brushDecal->colour(1.0f, 1.0f, 1.0f, 0.85f);
		m_brushDecal->position(cs, 0, 0);
		m_brushDecal->colour(1.0f, 1.0f, 1.0f, 0.85f);
		m_brushDecal->position(0, 0, -cs);
		m_brushDecal->colour(1.0f, 1.0f, 1.0f, 0.85f);
		m_brushDecal->position(0, 0, cs);
		m_brushDecal->colour(1.0f, 1.0f, 1.0f, 0.85f);
		m_brushDecal->end();
	}

	/* Position on terrain surface with slight Y offset to
	 * avoid z-fighting. */
	m_brushDecalNode->setVisible(true);
	m_brushDecalNode->setPosition(worldPos.x, worldPos.y + 0.5f,
				      worldPos.z);

	/* Orient the decal to face the terrain normal. */
	m_brushDecalNode->setOrientation(
		Ogre::Vector3::UNIT_Y.getRotationTo(normal));

	/* Scale to match brush radius (the template is unit radius). */
	m_brushDecalNode->setScale(radius, 1.0f, radius);
}

void TerrainSystem::hideBrushDecal()
{
	if (m_brushDecalNode)
		m_brushDecalNode->setVisible(false);
}

/* ------------------------------------------------------------------ */
/* getTerrainWorldHalfSize                                              */
/* ------------------------------------------------------------------ */

float TerrainSystem::getTerrainWorldHalfSize() const
{
	if (!mTerrainGroup)
		return 1000.0f;
	return mTerrainGroup->getTerrainWorldSize() * 0.5f;
}

/* ------------------------------------------------------------------ */
/* destroyCollider                                                     */
/* ------------------------------------------------------------------ */

/* ------------------------------------------------------------------ */
/* physicalToVisualX                                                   */
/* ------------------------------------------------------------------ */

float TerrainSystem::physicalToVisualX(float physicalX) const
{
	if (!mTerrainGroup)
		return physicalX;
	/* For ALIGN_X_Z: Ogre terrain local X = i*step - halfSize.
	 * The heightmap physical X = pageMinX + i*step.
	 * Visual world X = pageMinX + i*step - halfSize = physicalX - halfSize.
	 * Same shift for all pages. */
	return physicalX - mTerrainGroup->getTerrainWorldSize() * 0.5f;
}

/* ------------------------------------------------------------------ */
/* physicalToVisualZ                                                   */
/* ------------------------------------------------------------------ */

float TerrainSystem::physicalToVisualZ(float physicalZ) const
{
	if (!mTerrainGroup)
		return physicalZ;
	Ogre::Real ws = mTerrainGroup->getTerrainWorldSize();
	float halfSize = ws * 0.5f;
	float pageZ = floorf(physicalZ / ws) * ws;
	return 2.0f * pageZ + halfSize - physicalZ;
}

/* ------------------------------------------------------------------ */
/* visualToPhysicalX                                                    */
/* ------------------------------------------------------------------ */

float TerrainSystem::visualToPhysicalX(float visualX) const
{
	if (!mTerrainGroup)
		return visualX;
	return visualX + mTerrainGroup->getTerrainWorldSize() * 0.5f;
}

/* ------------------------------------------------------------------ */
/* visualToPhysicalZ                                                    */
/* ------------------------------------------------------------------ */

float TerrainSystem::visualToPhysicalZ(float visualZ) const
{
	if (!mTerrainGroup)
		return visualZ;
	Ogre::Real ws = mTerrainGroup->getTerrainWorldSize();
	float halfSize = ws * 0.5f;
	/* Visual page: page 0 covers [-halfSize, halfSize). */
	float pageV = floorf((visualZ + halfSize) / ws) * ws;
	/* Physical Z such that the ManualObject displays it at visual Z. */
	return 2.0f * pageV + halfSize - visualZ;
}

void TerrainSystem::destroyCollider(uint64_t /*key*/,
				    TerrainCollider & /*collider*/)
{
	/* Legacy helper; colliders are removed directly via processColliderRemoves. */
}

/* ------------------------------------------------------------------ */
/* TerrainCommandQueue implementation (M3 tests)                      */
/* ------------------------------------------------------------------ */

void TerrainCommandQueue::executeCommandInternal(const TerrainCommand &cmd)
{
	if (!m_terrainSystem || !m_terrainSystem->isActive())
		return;

	/* Command worldPos is in visual world space.
	 * Brush methods expect physical heightmap coords. */
	Ogre::Vector3 physPos = cmd.worldPos;
	physPos.x = m_terrainSystem->visualToPhysicalX(cmd.worldPos.x);
	physPos.z = m_terrainSystem->visualToPhysicalZ(cmd.worldPos.z);

	switch (cmd.type) {
	case TerrainCommandType::Raise:
		m_terrainSystem->setSculptTool(
			TerrainSystem::SculptTool::Raise);
		m_terrainSystem->setSculptRadius(cmd.radius);
		m_terrainSystem->setSculptStrength(cmd.strength);
		m_terrainSystem->applySculptBrush(physPos);
		break;
	case TerrainCommandType::Lower:
		m_terrainSystem->setSculptTool(
			TerrainSystem::SculptTool::Lower);
		m_terrainSystem->setSculptRadius(cmd.radius);
		m_terrainSystem->setSculptStrength(cmd.strength);
		m_terrainSystem->applySculptBrush(physPos);
		break;
	case TerrainCommandType::Smooth:
		m_terrainSystem->setSculptTool(
			TerrainSystem::SculptTool::Smooth);
		m_terrainSystem->setSculptRadius(cmd.radius);
		m_terrainSystem->setSculptStrength(cmd.strength);
		m_terrainSystem->applySculptBrush(physPos);
		break;
	case TerrainCommandType::Flatten:
		m_terrainSystem->setSculptTool(
			TerrainSystem::SculptTool::Flatten);
		m_terrainSystem->setSculptRadius(cmd.radius);
		m_terrainSystem->setSculptStrength(cmd.strength);
		m_terrainSystem->applySculptBrush(physPos);
		break;
	case TerrainCommandType::SplatPaint:
		m_terrainSystem->setPaintRadius(cmd.radius);
		/* Use command strength as the brush delta; negative values
		 * erase, positive values paint. */
		m_terrainSystem->setPaintStrength(cmd.strength);
		m_terrainSystem->setPaintLayerIndex(cmd.layerIndex);
		/* SplatPaint uses visual world coords directly
		 * (blend-map texel mapping is visual-space). */
		m_terrainSystem->applySplatBrush(cmd.worldPos);
		break;
	}
}

void TerrainCommandQueue::executeAll()
{
	m_results.clear();
	for (auto &cmd : m_queue) {
		executeCommandInternal(cmd);
		cmd.executed = true;
		cmd.success = true;
		m_results.push_back(cmd);
	}
	m_queue.clear();
}
