#include <unordered_set>
#include <iostream>
#include <Ogre.h>
#include <BulletCollision/CollisionShapes/btHeightfieldTerrainShape.h>
#include <OgreTerrain.h>
#include <OgreTerrainGroup.h>
#include <OgrePageManager.h>
#include <OgreTerrainPagedWorldSection.h>
#include <OgrePage.h>
#include <OgreTerrainPaging.h>
#include <OgreFileSystemLayer.h>
#include <nlohmann/json.hpp>

#include "GameData.h"
#include "Components.h"
#include "CharacterModule.h"
#include "SunModule.h"
#include "PhysicsModule.h"
#include "StaticGeometryModule.h"
#include "TerrainModule.h"
#include <tracy/Tracy.hpp>

#define TERRAIN_SIZE 65
#define TERRAIN_WORLD_SIZE 500.0f
#define ENDLESS_TERRAIN_FILE_PREFIX Ogre::String("EndlessWorldTerrain")
#define ENDLESS_TERRAIN_FILE_SUFFIX Ogre::String("dat")

#define ENDLESS_PAGING

// max range for a int16
#define ENDLESS_PAGE_MIN_X (-0x7FFF)
#define ENDLESS_PAGE_MIN_Y (-0x7FFF)
#define ENDLESS_PAGE_MAX_X 0x7FFF
#define ENDLESS_PAGE_MAX_Y 0x7FFF
namespace ECS
{
class DummyPageProvider;
/*  Components */
struct TerrainPrivate {
	DummyPageProvider *mDummyPageProvider;
	Ogre::Timer mSunUpdate;
};

#define BRUSH_SIZE 64
struct HeightData {
	Ogre::Image img;
	Ogre::Image img_brushes;
	Ogre::Image img_noise;
	static HeightData *singleton;
	HeightData()
	{
        ZoneScoped;
		img.load(
			"world_map.png",
			Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
		img_brushes.load(
			"brushes.png",
			Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
		img_noise.load(
			"terrain.png",
			Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
	}
	static HeightData *get_singleton()
	{
        ZoneScoped;
        if (!singleton)
			singleton = new HeightData();
		return singleton;
	}
	float get_brush_height(int id, int x, int y)
	{
        ZoneScoped;
        int m = 0;
		switch (id) {
		case 0:
			m = 0;
			break;
		case 1:
			m = BRUSH_SIZE;
			break;
		default:
			OgreAssert(false, "bad brush id");
			break;
		}
		return img_brushes.getColourAt(x, y + m, 0).r;
	}
	float get_world_x(int x)
	{
		return (float)(x - (int)img.getWidth() / 2) * BRUSH_SIZE;
	}
	float get_world_y(int y)
	{
		return (float)(y - (int)img.getHeight() / 2) * BRUSH_SIZE;
	}
	int get_img_x(float world_x)
	{
		float world_img_x = world_x + (float)img.getWidth() *
						      (float)BRUSH_SIZE / 2.0f;
		int ret = (world_img_x + BRUSH_SIZE - 1) / BRUSH_SIZE;
		// ret = Ogre::Math::Clamp(ret, 0, (int)img.getWidth() - 1);
		return ret;
	}
	int get_img_y(float world_z)
	{
		float world_img_y = world_z + (float)img.getHeight() *
						      (float)BRUSH_SIZE / 2.0f;
		int ret = (world_img_y + BRUSH_SIZE - 1) / BRUSH_SIZE;
		// ret = Ogre::Math::Clamp(ret, 0, (int)img.getHeight() - 1);
		return ret;
	}
	void update_heightmap(const Ogre::Image &heightmap)
	{
		img = heightmap;
	}
	void save_heightmap()
	{
        ZoneScoped;
        Ogre::String group =
			Ogre::ResourceGroupManager::getSingleton()
				.findGroupContainingResource("world_map.png");
		Ogre::FileInfoListPtr fileInfoList(
			Ogre::ResourceGroupManager::getSingleton()
				.findResourceFileInfo(group, "world_map.png"));
		OgreAssert(fileInfoList->size() == 1,
			   "worpd_map.png should be there and only once");
		Ogre::String path = fileInfoList->at(0).archive->getName() +
				    "/" + "world_map.png";
		Ogre::FileSystemLayer::removeFile(path);
		img.save(path);
	}
	float get_base_height(long world_x, long world_y)
	{
        float height = 0.0f;
		int world_img_x =
			world_x + (int)img.getWidth() * BRUSH_SIZE / 2;
		int world_img_y =
			world_y + (int)img.getHeight() * BRUSH_SIZE / 2;
		Ogre::ColourValue color, colorb1, colorb2;
		// float d;
		int div = 1;
		int map_img_x = world_img_x / (BRUSH_SIZE) / div;
		int map_img_y = world_img_y / (BRUSH_SIZE) / div;
		int brush_img_x = (world_img_x / div) % BRUSH_SIZE;
		int brush_img_y = (world_img_y / div) % BRUSH_SIZE;
		if (world_img_x < 0 ||
		    world_img_x >= img.getWidth() * BRUSH_SIZE ||
		    world_img_y < 0 ||
		    world_img_y >= img.getWidth() * BRUSH_SIZE) {
			height = 0.0f;
			goto out;
		}
		color = img.getColourAt(map_img_x, map_img_y, 0);
		colorb1 = img_brushes.getColourAt(brush_img_x,
						  brush_img_y + BRUSH_SIZE, 0);
		colorb2 = img_brushes.getColourAt(brush_img_x, brush_img_y, 0);
		// d = Ogre::Math::saturate(color.r - 0.05f);
		height = color.r;
out:
		return height;
	}
	float get_noise_height(long world_x, long world_y)
	{
        int h;
		Ogre::Vector2 noisePoint;

		struct noise_types {
			Ogre::Vector2 noiseOfft;
			float noiseMul;
			float noiseBias;
			float noiseAmp;
		};
		static struct noise_types noise_pass[] = {
			{ { -100.0f, 70.0f }, 10.2f, -0.55f, 5.0f },
			{ { -130.0f, 55.0f }, 5.35f, -0.55f, 1.0f }
		};
		static float noise_values[] = { 0.0f, 0.0f };
		for (h = 0; h < (int)sizeof(noise_values) /
					(int)sizeof(noise_values[0]);
		     h++) {
			noisePoint = (Ogre::Vector2(world_x, world_y) +
				      noise_pass[h].noiseOfft) *
				     noise_pass[h].noiseMul;
			int noise_x =
				(int)(noisePoint.x + img_noise.getWidth() / 2) %
				img_noise.getWidth();
			int noise_y = (int)(noisePoint.y +
					    img_noise.getHeight() / 2) %
				      img_noise.getHeight();
			Ogre::ColourValue noise_color =
				img_noise.getColourAt(noise_x, noise_y, 0);
			noise_values[h] =
				(noise_color.r + noise_pass[h].noiseBias) *
				noise_pass[h].noiseAmp;
		}
		return noise_values[0] + noise_values[1];
	}
	float get_height(Ogre::TerrainGroup *terrainGroup, long world_x,
			 long world_y)
	{
        long grid_center_x = img.getWidth() * BRUSH_SIZE / 2;
		long grid_center_y = img.getHeight() * BRUSH_SIZE / 2;
		long world_grid_x = world_x + grid_center_x;
		long world_grid_y = world_y + grid_center_y;
		float amplitude = 150.0f;
		if (world_grid_x < 0 || world_grid_y < 0) {
#if 0
			std::cout << "world: " << world_x << " " << world_y
				  << " ";
			std::cout << "grid: " << world_grid_x << " ";
			std::cout << world_grid_y << std::endl;
#endif
			return -amplitude;
		}
		OgreAssert(world_grid_x >= 0, "bad world x");
		OgreAssert(world_grid_y >= 0, "bad world y");
		long cell = 8;
		long b1 = (world_grid_x / cell) * cell + cell / 2;
		long b2 = (world_grid_y / cell) * cell + cell / 2;
		long o1 = Ogre::Math::Abs(world_grid_x - b1);
		long o2 = Ogre::Math::Abs(world_grid_y - b2);
		// std::cout << "h: " << o1 << " " << o2 << std::endl;
		// std::cout << "h: " << (o1 * o1 + o2 * o2) << std::endl;
		float hbase = 1.0 / (1.0f + 0.02f * (o1 * o1 + o2 * o2));
		float h1 = hbase * 0.5f + 0.5f;
		float h2 = (1.0f - hbase) * 0.5f;
		float mheight =
			(Ogre::Math::lerp(h2, h1,
					  get_base_height(world_x, world_y)) -
			 0.51f) *
			amplitude;
		float height = mheight;
		if (mheight > 0.5f)
			height += 2.0f + get_noise_height(world_x, world_y);
		else if (mheight < -0.5f)
			height -= 2.0f + get_noise_height(world_x, world_y);
		// OgreAssert(false, "height");
		return height;
	}
};
HeightData *HeightData::singleton = nullptr;

class FlatTerrainDefiner
	: public Ogre::TerrainPagedWorldSection::TerrainDefiner,
	  public Ogre::FrameListener {
	Ogre::SceneManager *mScnMgr;
	// Ogre::Bullet::DynamicsWorld *mWorld;
	struct gen_collider {
		Ogre::TerrainGroup *group;
		long x;
		long y;
	};
	std::deque<struct gen_collider> collider_queue;
	std::deque<struct gen_collider> colliderRemove_queue;

public:
	FlatTerrainDefiner(Ogre::SceneManager *
				   scm /*, Ogre::Bullet::DynamicsWorld *world */)
		: Ogre::TerrainPagedWorldSection::TerrainDefiner()
		, Ogre::FrameListener()
		, mScnMgr(scm)
	// , mWorld(world)
	{
		Ogre::Root::getSingleton().addFrameListener(this);
	}

private:
	std::mutex mtx;

public:
	void createTerrainChunk(Ogre::TerrainGroup *terrainGroup, long x,
				long y)
	{
        ZoneScoped;
		Ogre::Terrain *terrain = terrainGroup->getTerrain(x, y);
		float minH = terrain->getMinHeight();
		float maxH = terrain->getMaxHeight();
		uint16_t terrainSize = terrainGroup->getTerrainSize();
		OgreAssert(terrain && terrain->getHeightData() &&
				   terrain->isLoaded(),
			   "invalid terrain supplied");
		uint16_t size = terrain->getSize();
		float *heightData = terrain->getHeightData();
		Ogre::SceneNode *node = terrain->_getRootSceneNode();
		float worldSize = terrain->getWorldSize();
		float scaled = worldSize / (size - 1);
		Ogre::Vector3 bodyPosition = terrain->getPosition();
		// bodyPosition.y += (maxH + minH) / 2.0f;
		bodyPosition.x += worldSize / 2.0f;
		bodyPosition.z += worldSize / 2.0f;
		Ogre::Vector3 offset =
			node->_getDerivedPosition() - bodyPosition;
		flecs::entity e = PhysicsModule::createTerrainChunkBody(
			node, heightData, offset,
			Ogre::Vector3(scaled, 1, scaled), size);
		node->getUserObjectBindings().setUserAny("_collider", e);
	}
	void define(Ogre::TerrainGroup *terrainGroup, long x, long y) override
	{
        ZoneScoped;
        std::lock_guard<std::mutex> guard(mtx);
		uint16_t terrainSize = terrainGroup->getTerrainSize();
		float *heightMap = OGRE_ALLOC_T(float, terrainSize *terrainSize,
						MEMCATEGORY_GEOMETRY);
		//	float *heightMapCollider = OGRE_ALLOC_T(
		//		float, terrainSize *terrainSize, MEMCATEGORY_GEOMETRY);
		//		Ogre::Vector2 worldOrigin =
		//			Ogre::Vector2(img.getWidth(), img.getHeight()) * 0.5f;
		float chunk = 128.0f;
		Ogre::Vector2 revisedValuePoint;
		Ogre::Vector3 worldPos;
		terrainGroup->convertTerrainSlotToWorldPosition(x, y,
								&worldPos);
		for (int i = 0; i < terrainSize; i++)
			for (int j = 0; j < terrainSize; j++) {
				long world_x = (long)(worldPos.x + j -
						      (terrainSize - 1) / 2);
				long world_y = (long)(worldPos.z + i -
						      (terrainSize - 1) / 2);
				float height = 0.0f;
				height +=
					HeightData::get_singleton()->get_height(
						terrainGroup, world_x, world_y);

				// height = -2.0f;
				heightMap[i * terrainSize + j] = height;
				//				heightMapCollider[(terrainSize - i - 1) *
				//							  terrainSize +
				//						  j] = height;
			}
		terrainGroup->defineTerrain(x, y, heightMap);
		Ogre::LogManager::getSingleton().logError(
			"defined terrain at " +
			Ogre::StringConverter::toString(x) + " " +
			Ogre::StringConverter::toString(y));
		//		collider_queue.push_back(
		//			{ terrainGroup, x, y, heightMapCollider });
		delete[] heightMap;
		collider_queue.push_back({ terrainGroup, x, y });
	}
	bool frameStarted(const Ogre::FrameEvent &evt) override
	{
        ZoneScoped;
        (void)evt;
		update();
		return true;
	}
	void update()
	{
        ZoneScoped;
        std::lock_guard<std::mutex> guard(mtx);
		static bool created = false;
		while (!collider_queue.empty()) {
			Ogre::TerrainGroup *group =
				collider_queue.front().group;
			if (group->isDerivedDataUpdateInProgress())
				break;
			long x = collider_queue.front().x;
			long y = collider_queue.front().y;
            // std::cout << x << " " << y << " "
            //	  << collider_queue.size() << std::endl;
			Ogre::Terrain *terrain = group->getTerrain(x, y);
			Ogre::Vector3 worldPos;
			group->convertTerrainSlotToWorldPosition(x, y,
								 &worldPos);
#if 0
            std::cout << "terrain: " << terrain;
            if (terrain)
                std::cout
                    << terrain->getHeightData() << " "
                    << terrain->isLoaded() << " "
                    << terrain->isDerivedDataUpdateInProgress()
                    << std::endl;
#endif
			if (terrain && terrain->getHeightData() &&
			    terrain->isLoaded() &&
			    !terrain->isDerivedDataUpdateInProgress()) {
				Ogre::LogManager::getSingleton().logError(
					"can create collider for " +
					Ogre::StringConverter::toString(x) +
					" " +
					Ogre::StringConverter::toString(y));
				// float minH = terrain->getMinHeight();
				// float maxH = terrain->getMaxHeight();
				int size = terrain->getSize();
				float worldSize = terrain->getWorldSize();
				{
					createTerrainChunk(group, x, y);
				}
#if 0
				if (true) {
					btRigidBody *body =
						mWorld->addTerrainRigidBody(
							group, x, y, 2,
							0x7ffffffd & (~16));
					OgreAssert(
						body,
						"Could not create RigidBody");
					Ogre::LogManager::getSingleton().logError(
						"created rigid body " +
						Ogre::StringConverter::toString(
							Ogre::Bullet::convert(
								body->getWorldTransform()
									.getOrigin())));
					Ogre::LogManager::getSingleton().logError(
						"minHeight " +
						Ogre::StringConverter::toString(
							minH));
					Ogre::LogManager::getSingleton().logError(
						"maxHeight " +
						Ogre::StringConverter::toString(
							maxH));
					Ogre::LogManager::getSingleton().logError(
						"size " +
						Ogre::StringConverter::toString(
							size));
					Ogre::LogManager::getSingleton().logError(
						"world size " +
						Ogre::StringConverter::toString(
							worldSize));
					Ogre::LogManager::getSingleton().logError(
						"created collider for " +
						Ogre::StringConverter::toString(
							x) +
						" " +
						Ogre::StringConverter::toString(
							y));
					created = true;
				}
#endif
				// FIXME: create entities and components instead
#if 0
				Ogre::SceneNode *items =
					terrain->_getRootSceneNode()
						->createChildSceneNode();
				for (int i = 0; i < ECS::get<PlacementObjects>()
							    .altar_items.size();
				     i++) {
					const struct PlacementObjects::item &item =
						ECS::get<PlacementObjects>()
							.altar_items[i];
					Ogre::Entity *ent =
						group->getSceneManager()
							->createEntity(
								item.entity);
					Ogre::SceneNode *what =
						items->createChildSceneNode();
					what->attachObject(ent);
					what->setOrientation(item.rotation);
					what->setPosition(item.position);
				}
#endif
				/* Spawn items */
				StaticGeometryModule::addGeometryForSlot(x, y);
				collider_queue.pop_front();

			} else {
				/* Terrain data not ready maybe move to next terrain */
				gen_collider m = collider_queue.front();
				collider_queue.pop_front();
				collider_queue.push_back(m);
				break; // allow system to move on
			}
		}
		if (collider_queue.empty() &&
		    !ECS::get<Terrain>().mTerrainReady) {
			ECS::get_mut<Terrain>().mTerrainReady = true;
			ECS::modified<Terrain>();
		}
	}
};
class DummyPageProvider : public Ogre::PageProvider {
public:
	DummyPageProvider(/* btDynamicsWorld *world */)
		: Ogre::PageProvider()
	{
	}
	bool prepareProceduralPage(Ogre::Page *page,
				   Ogre::PagedWorldSection *section)
	{
		return true;
	}
	bool loadProceduralPage(Ogre::Page *page,
				Ogre::PagedWorldSection *section)
	{
		return true;
	}
	bool unloadProceduralPage(Ogre::Page *page,
				  Ogre::PagedWorldSection *section)
	{
        ZoneScoped;
        long x, y;
		ECS::get<Terrain>().mTerrainGroup->unpackIndex(page->CHUNK_ID,
							       &x, &y);
		StaticGeometryModule::removeGeometryForSlot(x, y);
		return true;
	}
	bool unprepareProceduralPage(Ogre::Page *page,
				     Ogre::PagedWorldSection *section)
	{
		return true;
	}
};
TerrainModule::TerrainModule(flecs::world &ecs)
{
    ZoneScoped;
    struct CanSetPlayerPosition {};
	ecs.module<TerrainModule>();
	ecs.component<CanSetPlayerPosition>().add(flecs::Singleton);
	ecs.component<Terrain>().add(flecs::Singleton);
	ecs.component<TerrainPrivate>().add(flecs::Singleton);
	ecs.component<PlacementObjects>();
	ecs.component<TerrainReady>().add(flecs::Singleton);
	ecs.import <CharacterModule>();
	ecs.import <SunModule>();
	ecs.import <StaticGeometryModule>();
	ecs.set<TerrainPrivate>({ nullptr, {} });
	ecs.system<const EngineData, const Camera, const Sun, Terrain,
		   TerrainPrivate>("SetupUpdateTerrain")
		.kind(flecs::OnUpdate)
		.each([](const EngineData &eng, const Camera &camera,
			 const Sun &sun, Terrain &terrain,
			 TerrainPrivate &priv) {
            ZoneScoped;
            if (!terrain.mTerrainGroup && sun.mSun && eng.mScnMgr) {
				std::cout << "Terrain setup\n";
				if (!priv.mDummyPageProvider)
					priv.mDummyPageProvider =
						new DummyPageProvider(
							/* eng.mWorld
								->getBtWorld() */);
				terrain.mTerrainGlobals =
					OGRE_NEW Ogre::TerrainGlobalOptions();

				OgreAssert(terrain.mTerrainGlobals,
					   "Failed to allocate global options");

				terrain.mTerrainGroup =
					OGRE_NEW Ogre::TerrainGroup(
						eng.mScnMgr,
						Ogre::Terrain::ALIGN_X_Z,
						TERRAIN_SIZE,
						TERRAIN_WORLD_SIZE);
				terrain.mTerrainGroup->setFilenameConvention(
					ENDLESS_TERRAIN_FILE_PREFIX,
					ENDLESS_TERRAIN_FILE_SUFFIX);
				terrain.mTerrainGroup->setOrigin(
					terrain.mTerrainPos);
				// Configure global
				terrain.mTerrainGlobals->setMaxPixelError(1);
				// testing composite map
				// mTerrainGlobals->setCompositeMapDistance(30);
				terrain.mTerrainGlobals->setCompositeMapDistance(
					300);
				//mTerrainGlobals->setUseRayBoxDistanceCalculation(true);
				terrain.mTerrainGlobals
					->getDefaultMaterialGenerator()
					->setLightmapEnabled(false);

				terrain.mTerrainGlobals->setCompositeMapAmbient(
					eng.mScnMgr->getAmbientLight());
				terrain.mTerrainGlobals->setCompositeMapDiffuse(
					sun.mSun->getDiffuseColour());
				terrain.mTerrainGlobals->setLightMapDirection(
					sun.mSun->getDerivedDirection());

				// Configure default import settings for if we use imported image
				Ogre::Terrain::ImportData &defaultimp =
					terrain.mTerrainGroup
						->getDefaultImportSettings();
				defaultimp.terrainSize = TERRAIN_SIZE;
				defaultimp.worldSize = TERRAIN_WORLD_SIZE;
				defaultimp.inputScale = 1.0f;
				// defaultimp.minBatchSize = 33;
				defaultimp.minBatchSize = 5;
				defaultimp.maxBatchSize = 65;
				Ogre::Image combined;
				combined.loadTwoImagesAsRGBA(
					"Ground23_col.jpg", "Ground23_spec.png",
					"General");
				Ogre::TextureManager::getSingleton().loadImage(
					"Ground23_diffspec", "General",
					combined);
				defaultimp.layerList.resize(1);

				defaultimp.layerList[0].worldSize = 60;
				defaultimp.layerList[0].textureNames.push_back(
					"Ground23_diffspec");
				// Paging setup
				terrain.mPageManager =
					OGRE_NEW Ogre::PageManager();
				// Since we're not loading any pages from .page files, we need a way just
				// to say we've loaded them without them actually being loaded
				terrain.mPageManager->setPageProvider(
					priv.mDummyPageProvider);
				terrain.mPageManager->addCamera(camera.mCamera);
				terrain.mPageManager->setDebugDisplayLevel(0);
				terrain.mTerrainPaging =
					OGRE_NEW Ogre::TerrainPaging(
						terrain.mPageManager);
				terrain.mPagedWorld =
					terrain.mPageManager->createWorld();
#if 0
				terrain.mTerrainGroup->setAutoUpdateLod(
					Ogre::TerrainAutoUpdateLodFactory::
						getAutoUpdateLod(
							Ogre::BY_DISTANCE));
#endif

				terrain.mTerrainPagedWorldSection =
					terrain.mTerrainPaging
						->createWorldSection(
							terrain.mPagedWorld,
							terrain.mTerrainGroup,
							300, 500,
							ENDLESS_PAGE_MIN_X,
							ENDLESS_PAGE_MIN_Y,
							ENDLESS_PAGE_MAX_X,
							ENDLESS_PAGE_MAX_Y);
				terrain.definer = OGRE_NEW FlatTerrainDefiner(
					eng.mScnMgr /*, eng.mWorld */);

				terrain.mTerrainPagedWorldSection->setDefiner(
					terrain.definer);

				terrain.mTerrainGroup->freeTemporaryResources();
				std::cout << "Terrain setup done\n";
				ECS::get().set<PlacementObjects>({});
				terrain.mTerrainGroup->loadAllTerrains(true);
			}
			if (sun.mSun &&
			    priv.mSunUpdate.getMilliseconds() > 1000) {
				terrain.mTerrainGlobals->setCompositeMapAmbient(
					eng.mScnMgr->getAmbientLight());
				terrain.mTerrainGlobals->setCompositeMapDiffuse(
					sun.mSun->getDiffuseColour());
				terrain.mTerrainGlobals->setLightMapDirection(
					sun.mSun->getDerivedDirection());
				std::cout << "sun pitch: "
					  << sun.mSunNode->getOrientation()
						     .getPitch()
					  << "\n";
				priv.mSunUpdate.reset();
				//terrain.mTerrainGroup->autoUpdateLodAll()
			}
		});
	ecs.system<const ECS::Camera, const Terrain>("UpdateTerrainStatus")
		.kind(flecs::OnUpdate)
		.without<TerrainReady>()
		.each([](const ECS::Camera &cam, const Terrain &terrain) {
            ZoneScoped;
            std::cout << "mTerrainReady: " << terrain.mTerrainReady
				  << "\n";
			if (cam.mCameraNode && terrain.mTerrainReady) {
				long x, y;
				Ogre::Vector3 pos =
					cam.mCameraNode->getPosition();
				terrain.mTerrainGroup
					->convertWorldPositionToTerrainSlot(
						pos, &x, &y);
				if (terrain.mTerrainGroup->getTerrain(x, y) &&
				    terrain.mTerrainGroup->getTerrain(x, y)
					    ->isLoaded())
					ECS::get().add<TerrainReady>();
			}
		});
	ecs.system<const Terrain, PlacementObjects>("UpdatePlacementObjects")
		.kind(flecs::OnUpdate)
		.each([](const Terrain &terrain, PlacementObjects &placement) {
            ZoneScoped;
            if (placement.altar_items.size() == 0) {
				struct PlacementObjects::item item;
				int i, j;
				int worldSize = terrain.mTerrainGroup
							->getTerrainWorldSize();
				uint16_t terrainSize =
					terrain.mTerrainGroup->getTerrainSize();
				item.entity = "altar.glb";
				item.rotation = Ogre::Quaternion(0, 0, 0, 1);
				item.position = Ogre::Vector3(0, 0, 0);
				float height =
					HeightData::get_singleton()->get_height(
						terrain.mTerrainGroup, 0, 0);
				item.position.y = height;
				placement.altar_items.push_back(item);
				for (i = -64000; i < 64000; i += 2000)
					for (j = -64000; j < 64000; j += 2000) {
						if (i == 0 && j == 0)
							continue;
						Ogre::Vector3 position(i, 0, j);
						long xslot, yslot;
						terrain.mTerrainGroup
							->convertWorldPositionToTerrainSlot(
								position,
								&xslot, &yslot);
						Ogre::Vector3 slotpos;
						terrain.mTerrainGroup
							->convertTerrainSlotToWorldPosition(
								xslot, yslot,
								&slotpos);
						Ogre::Vector3 offset =
							(position - slotpos) *
							terrainSize / worldSize;
						height =
							HeightData::get_singleton()
								->get_height(
									terrain.mTerrainGroup,
									(long)position
										.x,
									(long)position
										.z);
#if 0
						height =
							terrain.mTerrainGroup
								->getHeightAtWorldPosition(
									position);
#endif
						if (height > -9.0f)
							continue;
#ifdef VDEBUG
						std::cout << "worldSize: "
							  << worldSize - 1
							  << std::endl;
						std::cout << "height: " << i
							  << " " << j << " "
							  << height
							  << std::endl;
#endif
						item.entity = "altar.glb";
						item.rotation =
							Ogre::Quaternion(0, 0,
									 0, 1);
						position.y = height;
						item.position = position;
						placement.altar_items.push_back(
							item);
					}
#ifdef VDEBUG
				for (i = 0; i < placement.altar_items.size();
				     i++) {
					std::cout << "placement: " << i << " "
						  << placement.altar_items[i]
							     .position
						  << std::endl;
				}
#endif
				ECS::get().add<CanSetPlayerPosition>();
			}
		});
#if 0
	ecs.system<const Terrain>("SetPlayerPosition")
		.kind(flecs::OnUpdate)
		.with<CanSetPlayerPosition>()
		.each([this](const Terrain &terrain) {
            flecs::entity player = ECS::get<CharacterM;
			if (!player.is_valid())
				return;
			if (!player.has<CharacterLocation>())
				return;
			if (!player.has<CharacterBase>())
				return;
			if (!player.has<Player>())
				return;
			CharacterLocation &loc =
				player.get_mut<CharacterLocation>();
			const CharacterBase &ch = player.get<CharacterBase>();
			if (!ch.mBodyNode) {
				std::cout << "no player yet";
				return;
			}
			float height =
				get_height(terrain.mTerrainGroup, loc.position);
			loc.position.y = height + 0.0f;
			ch.mBodyNode->setPosition(loc.position);
			ch.mBodyNode->setOrientation(Ogre::Quaternion());
			player.modified<CharacterLocation>();
			ECS::get().remove<CanSetPlayerPosition>();
		});
#endif
	ecs.system<const Terrain>("UpdateTerrainGroup")
		.kind(flecs::OnUpdate)
		.interval(2.0f)
		.each([](const Terrain &terrain) {
            ZoneScoped;
            if (!terrain.mTerrainGroup
				     ->isDerivedDataUpdateInProgress())
				terrain.mTerrainGroup->update(false);
		});
}
float TerrainModule::get_height(Ogre::TerrainGroup *group,
				const Ogre::Vector3 &position)
{
	int worldSize = group->getTerrainWorldSize();
	uint16_t terrainSize = group->getTerrainSize();
	long xslot, yslot;
	group->convertWorldPositionToTerrainSlot(position, &xslot, &yslot);
	Ogre::Vector3 slotpos;
	group->convertTerrainSlotToWorldPosition(xslot, yslot, &slotpos);
	Ogre::Vector3 offset = (position - slotpos) * terrainSize / worldSize;
	float height = HeightData::get_singleton()->get_height(
		group, (long)position.x, (long)position.z);
	return height;
}
float TerrainModule::get_world_x(int x)
{
	return HeightData::get_singleton()->get_world_x(x);
}
float TerrainModule::get_world_y(int y)
{
	return HeightData::get_singleton()->get_world_y(y);
}
int TerrainModule::get_img_x(float world_x)
{
	return HeightData::get_singleton()->get_img_x(world_x);
}
int TerrainModule::get_img_y(float world_z)
{
	return HeightData::get_singleton()->get_img_y(world_z);
}
void TerrainModule::update_heightmap(const Ogre::Image &heightmap)
{
	return HeightData::get_singleton()->update_heightmap(heightmap);
}
void TerrainModule::save_heightmap()
{
	HeightData::get_singleton()->save_heightmap();
}
void TerrainModule::defineTerrain(long x, long y)
{
	ECS::get<Terrain>().definer->define(ECS::get<Terrain>().mTerrainGroup,
					    x, y);
}
}
