#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

#include <Ogre.h>
#include <OgrePageManager.h>
#include <OgreTerrainMaterialGeneratorA.h>
#include <OgreTerrainQuadTreeNode.h>
#include <OgreMaterialManager.h>
#include <OgreTerrainAutoUpdateLod.h>
#include <OgreTerrainPagedWorldSection.h>
#include <OgreMath.h>
#include <OgreLogManager.h>
#include "terrain.h"

#define ENDLESS_TERRAIN_FILE_PREFIX Ogre::String("EndlessWorldTerrain")
#define ENDLESS_TERRAIN_FILE_SUFFIX Ogre::String("dat")
#define TERRAIN_WORLD_SIZE 4000.0f
// #define TERRAIN_SIZE 513
#define TERRAIN_SIZE 129
// #define HOLD_LOD_DISTANCE 3000.0
#define USE_PERLIN_DEFINER 0
template <typename T> T CLAMP(T value, T low, T high)
{
	return (value < low) ? low : ((value > high) ? high : value);
}

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;
	Ogre::Image img, img_noise, img_brushes;

public:
	FlatTerrainDefiner(Ogre::SceneManager *scm,
			   Ogre::Bullet::DynamicsWorld *world)
		: Ogre::TerrainPagedWorldSection::TerrainDefiner()
		, Ogre::FrameListener()
		, mScnMgr(scm)
		, mWorld(world)
	{
		Ogre::Root::getSingleton().addFrameListener(this);
		img.load(
			"world_map.png",
			Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
		img_noise.load(
			"terrain.png",
			Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
		img_brushes.load(
			"brushes.png",
			Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
	}

private:
#if 0
	float get_terrain_height(const Ogre::Vector2 &worldOffset, int x, int y)
	{
		revisedValuePoint = (worldOffset + Ogre::Vector2(x, y));
		int img_x = revisedValuePoint.x + img.getWidth() / 2;
		int img_y = revisedValuePoint.y + img.getHeight() / 2;
		float height = 0.0f;
		if (img_x < 0 || img_x >= img.getWidth() || img_y < 64 ||
		    img_y >= img.getWidth())
			height = -150.0f;
		else {
			Ogre::ColourValue color(0, 0, 0, 1);
			int m, n;
			color = img.getColourAt(img_x, img_y, 0);
			float mheight = (color.r - 0.55f) * 120.0f;
			height = mheight;
		}
		return height;
	}
#endif
	float get_noise_height(const Ogre::Vector2 &worldOffset, int x, int 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 = (worldOffset + Ogre::Vector2(x, 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];
	}
#define BRUSH_SIZE 64
	float get_brush_height(int id, int x, int y)
	{
		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_base_height(const Ogre::Vector2 &worldOffset, int x, int y)
	{
		float height = 0.0f;
		int world_x = worldOffset.x + x;
		int world_y = worldOffset.y + y;
		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 map_img_x = world_img_x / (BRUSH_SIZE);
		int map_img_y = world_img_y / (BRUSH_SIZE);
		int brush_img_x = world_img_x % BRUSH_SIZE;
		int brush_img_y = world_img_y % 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 = -1.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;
	}

public:
	void define(Ogre::TerrainGroup *terrainGroup, long x, long y) override
	{
		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 worldOffset(Ogre::Real(x * (terrainSize - 1)),
					  Ogre::Real(y * (terrainSize - 1)));
		Ogre::Vector2 worldOrigin =
			Ogre::Vector2(img.getWidth(), img.getHeight()) * 0.5f;
		float chunk = 128.0f;
		Ogre::Vector2 revisedValuePoint;
		for (int i = 0; i < terrainSize; i++)
			for (int j = 0; j < terrainSize; j++) {
				float brush_height0 =
					get_brush_height(0, j % BRUSH_SIZE,
							 i % BRUSH_SIZE) -
					0.55f;
				float brush_height1 =
					get_brush_height(1, j % BRUSH_SIZE,
							 i % BRUSH_SIZE) -
					0.55f;
				float mheight =
					Ogre::Math::lerp(
						brush_height1, brush_height0,
						get_base_height(worldOffset, j,
								i)) *
					120.0f;
				float height = mheight;
				if (mheight > 0.5f)
					height += 2.0f +
						  get_noise_height(worldOffset,
								   j, i);
				else if (mheight < -0.5f)
					height -= 2.0f +
						  get_noise_height(worldOffset,
								   j, i);

				// 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
	{
		(void)evt;
		update();
		return true;
	}
	void update()
	{
		static bool created = false;
		std::deque<struct gen_collider> output;
		while (!collider_queue.empty()) {
			Ogre::TerrainGroup *group =
				collider_queue.front().group;
			long x = collider_queue.front().x;
			long y = collider_queue.front().y;
			Ogre::Terrain *terrain = group->getTerrain(x, y);
			Ogre::Vector3 worldPos;
			group->convertTerrainSlotToWorldPosition(x, y,
								 &worldPos);
			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();
				if (!created || true) {
					btRigidBody *body =
						mWorld->addTerrainRigidBody(
							group, x, y, 2,
							0x7ffffffd & (~16));
					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;
				}
				collider_queue.pop_front();
			} else {
				output.push_back(collider_queue.front());
				collider_queue.pop_front();
			}
		}
		collider_queue = output;
	}
};

void TerrainSetup::setupTerrain(Ogre::Camera *camera,
				Ogre::Bullet::DynamicsWorld *dynamicsWorld,
				Ogre::Bullet::DebugDrawer *dbgDraw)
{
	Ogre::Light *sun = ECS::get<ECS::Sun>().mSun;
	mDynWorld.reset(dynamicsWorld);
	mDbgDraw.reset(dbgDraw);
	mScnMgr = camera->getSceneManager();
	mTerrainGlobals = OGRE_NEW Ogre::TerrainGlobalOptions();

#if 0
	mCameraMan->setTopSpeed(100);

	setDragLook(true);

#endif
#if 0
#if OGRE_PLATFORM != OGRE_PLATFORM_ANDROID
	Ogre::MaterialManager::getSingleton().setDefaultTextureFiltering(
		Ogre::TFO_ANISOTROPIC);
	Ogre::MaterialManager::getSingleton().setDefaultAnisotropy(8);
#endif
#endif

	//	mScnMgr->setFog(Ogre::FOG_LINEAR, Ogre::ColourValue(0.7, 0.7, 0.8), 0,
	//			4000, 10000);

	Ogre::LogManager::getSingleton().setMinLogLevel(Ogre::LML_TRIVIAL);

	mTerrainGroup =
		OGRE_NEW Ogre::TerrainGroup(mScnMgr, Ogre::Terrain::ALIGN_X_Z,
					    TERRAIN_SIZE, TERRAIN_WORLD_SIZE);
	mTerrainGroup->setFilenameConvention(ENDLESS_TERRAIN_FILE_PREFIX,
					     ENDLESS_TERRAIN_FILE_SUFFIX);
	mTerrainGroup->setOrigin(mTerrainPos);
	//	mTerrainGroup->setAutoUpdateLod(
	//		Ogre::TerrainAutoUpdateLodFactory::getAutoUpdateLod(
	//			Ogre::BY_DISTANCE));

	configureTerrainDefaults(sun);

	// Paging setup
	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
	mPageManager->setPageProvider(&mDummyPageProvider);
	mPageManager->addCamera(camera);
	mPageManager->setDebugDisplayLevel(0);
	mTerrainPaging = OGRE_NEW Ogre::TerrainPaging(mPageManager);
	mPagedWorld = mPageManager->createWorld();
	mTerrainPagedWorldSection = mTerrainPaging->createWorldSection(
		mPagedWorld, mTerrainGroup, 300, 800, ENDLESS_PAGE_MIN_X,
		ENDLESS_PAGE_MIN_Y, ENDLESS_PAGE_MAX_X, ENDLESS_PAGE_MAX_Y);

#if USE_PERLIN_DEFINER == 1
	mPerlinNoiseTerrainGenerator = OGRE_NEW PerlinNoiseTerrainGenerator;
	mTerrainPagedWorldSection->setDefiner(mPerlinNoiseTerrainGenerator);
#else
	mTerrainPagedWorldSection->setDefiner(OGRE_NEW FlatTerrainDefiner(
		camera->getSceneManager(), dynamicsWorld));
#endif

	mTerrainGroup->freeTemporaryResources();

	//	mSceneMgr->setSkyBox(true, "Examples/CloudyNoonSkyBox");

	// setup LOD info overlay
	//	mLodInfoOverlay =
	//		OverlayManager::getSingleton().create("LODInfoOverlay");

	//	mLodInfoOverlay->setZOrder(10);
	//	mLodInfoOverlayContainer =
	//		(OverlayContainer *)OverlayManager::getSingleton()
	//			.createOverlayElement("Panel", "LODInfoOverlayPanel");
	//	mLodInfoOverlayContainer->setDimensions(1.0, 1.0);
	//	mLodInfoOverlayContainer->setPosition(0.0, 0.0);

	//	mLodInfoOverlay->add2D(mLodInfoOverlayContainer);
	//	mLodInfoOverlay->show();

	//	setupControls();
}

void TerrainSetup::configureTerrainDefaults(Ogre::Light *l)
{
	// Configure global
	mTerrainGlobals->setMaxPixelError(0);
	// testing composite map
	// mTerrainGlobals->setCompositeMapDistance(30);
	mTerrainGlobals->setCompositeMapDistance(500);
	//mTerrainGlobals->setUseRayBoxDistanceCalculation(true);
	mTerrainGlobals->getDefaultMaterialGenerator()->setLightmapEnabled(
		false);

	mTerrainGlobals->setCompositeMapAmbient(mScnMgr->getAmbientLight());
	mTerrainGlobals->setCompositeMapDiffuse(l->getDiffuseColour());
	mTerrainGlobals->setLightMapDirection(l->getDerivedDirection());

	// Configure default import settings for if we use imported image
	Ogre::Terrain::ImportData &defaultimp =
		mTerrainGroup->getDefaultImportSettings();
	defaultimp.terrainSize = TERRAIN_SIZE;
	defaultimp.worldSize = TERRAIN_WORLD_SIZE;
	defaultimp.inputScale = 1.0f;
	defaultimp.minBatchSize = 33;
	defaultimp.maxBatchSize = 65;
#if 0
	// textures
	defaultimp.layerList.resize(1);
	defaultimp.layerList[0].worldSize = 200;
	defaultimp.layerList[0].textureNames.push_back("Ground37_diffspec.dds");
	defaultimp.layerList[0].textureNames.push_back(
		"Ground37_normheight.dds");
#endif
	Ogre::Image combined;
	combined.loadTwoImagesAsRGBA("Ground23_col.jpg", "Ground23_spec.png",
				     "General");
	Ogre::TextureManager::getSingleton().loadImage("Ground23_diffspec",
						       "General", combined);
	//! [tex_from_src]

	//! [textures]
	defaultimp.layerList.resize(1);
#if 0
	defaultimp.layerList[0].worldSize = 200;
	defaultimp.layerList[0].textureNames.push_back("Ground37_diffspec.dds");
	defaultimp.layerList[0].textureNames.push_back(
		"Ground37_normheight.dds");
#endif
#if 0
	defaultimp.layerList[0].worldSize = 200;
	defaultimp.layerList[0].textureNames.push_back(
		"Ground23_diffspec"); // loaded from memory
	defaultimp.layerList[0].textureNames.push_back(
		"Ground23_normheight.dds");
#endif
	defaultimp.layerList[0].worldSize = 60;
	defaultimp.layerList[0].textureNames.push_back("Ground23_diffspec");
	//	defaultimp.layerList[0].textureNames.push_back("Rock20_normheight.png");
	//! [textures]
}

bool TerrainSetup::DummyPageProvider::prepareProceduralPage(
	Ogre::Page *page, Ogre::PagedWorldSection *section)
{
	return true;
}