
#ifndef X_TERRAIN_H_
#define X_TARRAIN_H_
#include <unordered_set>
#include <iostream>
#include <Ogre.h>
#include <OgreTerrain.h>
#include <OgreTerrainGroup.h>
#include <OgrePageManager.h>
#include <OgreTerrainPagedWorldSection.h>
#include <OgrePage.h>
#include <OgreTerrainPaging.h>

class TerrainSetup {
	Ogre::SceneManager *mScnMgr;

	std::unique_ptr<Ogre::Bullet::DynamicsWorld> mDynWorld;
	std::unique_ptr<Ogre::Bullet::DebugDrawer> mDbgDraw;
	Ogre::TerrainGlobalOptions *mTerrainGlobals;
	Ogre::TerrainGroup *mTerrainGroup;
	Ogre::TerrainPaging *mTerrainPaging;
	Ogre::PageManager *mPageManager;
	Ogre::PagedWorld *mPagedWorld;
	Ogre::TerrainPagedWorldSection *mTerrainPagedWorldSection;
	bool mLodStatus;

	class DummyPageProvider : public Ogre::PageProvider {
	public:
		DummyPageProvider(btDynamicsWorld *world)
			: Ogre::PageProvider()
			, mBtWorld(world)
		{
		}
		std::unordered_map<Ogre::PageID, btRigidBody *> body;
		btDynamicsWorld *mBtWorld;
		bool prepareProceduralPage(Ogre::Page *page,
					   Ogre::PagedWorldSection *section);
		bool loadProceduralPage(Ogre::Page *page,
					Ogre::PagedWorldSection *section)
		{
			return true;
		}
		bool unloadProceduralPage(Ogre::Page *page,
					  Ogre::PagedWorldSection *section)
		{
			return true;
		}
		bool unprepareProceduralPage(Ogre::Page *page,
					     Ogre::PagedWorldSection *section)
		{
			return true;
		}
	};
	DummyPageProvider mDummyPageProvider;

	bool mFly;
	bool mParallaxOcclusion;
	Ogre::Real mFallVelocity;
	enum Mode {
		MODE_NORMAL = 0,
		MODE_EDIT_HEIGHT = 1,
		MODE_EDIT_BLEND = 2,
		MODE_COUNT = 3
	};
	enum ShadowMode {
		SHADOWS_NONE = 0,
		SHADOWS_COLOUR = 1,
		SHADOWS_DEPTH = 2,
		SHADOWS_COUNT = 3
	};
	Mode mMode;
	ShadowMode mShadowMode;
	Ogre::uint8 mLayerEdit;
	Ogre::Real mBrushSizeTerrainSpace;
	Ogre::SceneNode *mEditNode;
	Ogre::Entity *mEditMarker;
	Ogre::Real mHeightUpdateCountDown;
	Ogre::Real mHeightUpdateRate;
	Ogre::Vector3 mTerrainPos;
	/*
	OgreBites::SelectMenu *mEditMenu;
	OgreBites::SelectMenu *mShadowsMenu;
	OgreBites::CheckBox *mFlyBox;
	OgreBites::CheckBox *mParallaxOcclusionBox;
	OgreBites::Label *mInfoLabel = nullptr;
    */
	bool mTerrainsImported;
	Ogre::ShadowCameraSetupPtr mPSSMSetup;

	typedef std::list<Ogre::Entity *> EntityList;
	EntityList mHouseList;

public:
	TerrainSetup(btDynamicsWorld *world)
		: mScnMgr(nullptr)
		, mTerrainPos(Ogre::Vector3(0, 0, 0))
		, mDummyPageProvider(world)
	{
	}
	void defineTerrain(long x, long y, bool flat = false)
	{
		// if a file is available, use it
		// if not, generate file from import

		// Usually in a real project you'll know whether the compact terrain data is
		// available or not; I'm doing it this way to save distribution size

		if (flat) {
			mTerrainGroup->defineTerrain(x, y, 0.0f);
			return;
		}

		//! [define]
		Ogre::String filename = mTerrainGroup->generateFilename(x, y);
		if (Ogre::ResourceGroupManager::getSingleton().resourceExists(
			    mTerrainGroup->getResourceGroup(), filename)) {
			mTerrainGroup->defineTerrain(x, y, filename);
		} else {
			Ogre::Image img;
			getTerrainImage(x % 2 != 0, y % 2 != 0, img);
			mTerrainGroup->defineTerrain(x, y, &img);
			mTerrainsImported = true;
		}
		//! [define]
	}

	void getTerrainImage(bool flipX, bool flipY, Ogre::Image &img)
	{
		//! [heightmap]
		img.load("terrain.png", mTerrainGroup->getResourceGroup());
		if (flipX)
			img.flipAroundY();
		if (flipY)
			img.flipAroundX();
		//! [heightmap]
	}

	void initBlendMaps(Ogre::Terrain *terrain)
	{
		//! [blendmap]
		using namespace Ogre;
		TerrainLayerBlendMap *blendMap0 = terrain->getLayerBlendMap(1);
		TerrainLayerBlendMap *blendMap1 = terrain->getLayerBlendMap(2);
		float minHeight0 = 20;
		float fadeDist0 = 15;
		float minHeight1 = 70;
		float fadeDist1 = 15;
		float *pBlend0 = blendMap0->getBlendPointer();
		float *pBlend1 = blendMap1->getBlendPointer();
		for (uint16 y = 0; y < terrain->getLayerBlendMapSize(); ++y) {
			for (uint16 x = 0; x < terrain->getLayerBlendMapSize();
			     ++x) {
				Real tx, ty;

				blendMap0->convertImageToTerrainSpace(x, y, &tx,
								      &ty);
				float height =
					terrain->getHeightAtTerrainPosition(tx,
									    ty);

				*pBlend0++ = Math::saturate(
					(height - minHeight0) / fadeDist0);
				*pBlend1++ = Math::saturate(
					(height - minHeight1) / fadeDist1);
			}
		}
		blendMap0->dirty();
		blendMap1->dirty();
		blendMap0->update();
		blendMap1->update();
		//! [blendmap]
		// set up a colour map
		/*
          if (!terrain->getGlobalColourMapEnabled())
          {
          terrain->setGlobalColourMapEnabled(true);
          Image colourMap;
          colourMap.load("testcolourmap.jpg", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
          terrain->getGlobalColourMap()->loadImage(colourMap);
          }
        */
	}

	void configureTerrainDefaults(Ogre::Light *l);
#if 0
	void addTextureShadowDebugOverlay(Ogre::TrayLocation loc, size_t num)
	{
		for (size_t i = 0; i < num; ++i) {
			Ogre::TexturePtr shadowTex = m_app->getSceneManager()->getShadowTexture(i);
			addTextureDebugOverlay(loc, shadowTex, i);
		}
	}
#endif

#if 0
	void changeShadows()
	{
		configureShadows(mShadowMode != SHADOWS_NONE,
				 mShadowMode == SHADOWS_DEPTH);
	}


	void configureShadows(bool enabled, bool depthShadows)
	{
		auto matProfile =
			static_cast<Ogre::TerrainMaterialGeneratorA::SM2Profile *>(
				mTerrainGlobals->getDefaultMaterialGenerator()
					->getActiveProfile());
		matProfile->setReceiveDynamicShadowsEnabled(
			false); // force regen if colour/ depth shadows change
		matProfile->setReceiveDynamicShadowsEnabled(enabled);
		matProfile->setReceiveDynamicShadowsLowLod(
			SHADOWS_IN_LOW_LOD_MATERIAL);

		Ogre::RTShader::RenderState *schemRenderState =
			mShaderGenerator->getRenderState(MSN_SHADERGEN);
		if (auto srs = schemRenderState->getSubRenderState(
			    RTShader::SRS_SHADOW_MAPPING)) {
			schemRenderState->removeSubRenderState(srs);
		}

		if (enabled) {
			// General scene setup
			mSceneMgr->setShadowTechnique(
				SHADOWTYPE_TEXTURE_ADDITIVE_INTEGRATED);
			mSceneMgr->setShadowFarDistance(3000);

			// 3 textures per directional light (PSSM)
			mSceneMgr->setShadowTextureCountPerLightType(
				Ogre::Light::LT_DIRECTIONAL, 3);

			if (!mPSSMSetup) {
				// shadow camera setup
				PSSMShadowCameraSetup *pssmSetup =
					new PSSMShadowCameraSetup();
				pssmSetup->setSplitPadding(
					mCamera->getNearClipDistance() * 2);
				pssmSetup->calculateSplitPoints(
					3, mCamera->getNearClipDistance(),
					mSceneMgr->getShadowFarDistance());
				pssmSetup->setOptimalAdjustFactor(0, 2);
				pssmSetup->setOptimalAdjustFactor(1, 1);
				pssmSetup->setOptimalAdjustFactor(2, 0.5);

				mPSSMSetup.reset(pssmSetup);
			}
			mSceneMgr->setShadowCameraSetup(mPSSMSetup);

			if (depthShadows) {
				mSceneMgr->setShadowTextureCount(3);
				mSceneMgr->setShadowTextureConfig(0, 2048, 2048,
								  PF_DEPTH16);
				mSceneMgr->setShadowTextureConfig(1, 1024, 1024,
								  PF_DEPTH16);
				mSceneMgr->setShadowTextureConfig(2, 1024, 1024,
								  PF_DEPTH16);
				mSceneMgr->setShadowTextureSelfShadow(true);
				mSceneMgr->setShadowCasterRenderBackFaces(true);

				auto subRenderState =
					mShaderGenerator->createSubRenderState(
						RTShader::SRS_SHADOW_MAPPING);
				subRenderState->setParameter(
					"split_points",
					static_cast<PSSMShadowCameraSetup *>(
						mPSSMSetup.get())
						->getSplitPoints());
				schemRenderState->addTemplateSubRenderState(
					subRenderState);
			} else {
				mSceneMgr->setShadowTextureCount(3);
				mSceneMgr->setShadowTextureConfig(0, 2048, 2048,
								  PF_X8B8G8R8);
				mSceneMgr->setShadowTextureConfig(1, 1024, 1024,
								  PF_X8B8G8R8);
				mSceneMgr->setShadowTextureConfig(2, 1024, 1024,
								  PF_X8B8G8R8);
				mSceneMgr->setShadowTextureSelfShadow(false);
				mSceneMgr->setShadowCasterRenderBackFaces(
					false);
			}

			matProfile->setReceiveDynamicShadowsPSSM(
				static_cast<PSSMShadowCameraSetup *>(
					mPSSMSetup.get()));

			// addTextureShadowDebugOverlay(TL_RIGHT, 3);
		} else {
			mSceneMgr->setShadowTechnique(SHADOWTYPE_NONE);
		}

		// Update parallax occlusion
		matProfile->setLayerParallaxOcclusionMappingEnabled(
			mParallaxOcclusion);

		mShaderGenerator->invalidateScheme(MSN_SHADERGEN);
	}
#endif

#if 0

	void setupControls()
	{
		mTrayMgr->showCursor();

		// make room for the controls
		mTrayMgr->showLogo(TL_TOPRIGHT);
		mTrayMgr->showFrameStats(TL_TOPRIGHT);
		mTrayMgr->toggleAdvancedFrameStats();

		//! [infolabel_create]
		mInfoLabel = mTrayMgr->createLabel(TL_TOP, "TInfo", "", 350);
		//! [infolabel_create]

		mEditMenu = mTrayMgr->createLongSelectMenu(
			TL_BOTTOM, "EditMode", "Edit Mode", 370, 250, 3);
		mEditMenu->addItem("None");
		mEditMenu->addItem("Elevation");
		mEditMenu->addItem("Blend");
		mEditMenu->selectItem(0); // no edit mode

		mFlyBox = mTrayMgr->createCheckBox(TL_BOTTOM, "Fly", "Fly");
		mFlyBox->setChecked(false, false);

		mParallaxOcclusionBox =
			mTrayMgr->createCheckBox(TL_BOTTOM, "ParallaxOcclusion",
						 "Parallax Occlusion Mapping");
		mParallaxOcclusionBox->setChecked(false, false);

		mShadowsMenu = mTrayMgr->createLongSelectMenu(
			TL_BOTTOM, "Shadows", "Shadows", 370, 250, 3);
		mShadowsMenu->addItem("None");
		mShadowsMenu->addItem("Colour Shadows");
		mShadowsMenu->addItem("Depth Shadows");
		mShadowsMenu->selectItem(0); // no edit mode

		// a friendly reminder
		StringVector names;
		names.push_back("Help");
		mTrayMgr->createParamsPanel(TL_TOPLEFT, "Help", 100, names)
			->setParamValue(0, "H/F1");
	}
#endif

	void setupTerrain(Ogre::Camera *camera, Ogre::Light *sun,
			  Ogre::Bullet::DynamicsWorld *dynamicsWorld,
			  Ogre::Bullet::DebugDrawer *dbgDraw);
	float get_height(const Ogre::Vector3 &pos)
	{
		return mTerrainGroup->getHeightAtWorldPosition(pos);
	}
	std::unordered_set<long> col_terrains;
	std::unordered_map<long, btRigidBody *> col_bodies;
#if 0
	btRigidBody *createHeightfieldShape(int size, float *data,
					    const Ogre::Real &minHeight,
					    const Ogre::Real &maxHeight,
					    const Ogre::Vector3 &position,
					    const Ogre::Real &scale,
					    btDynamicsWorld *world)
	{
		Ogre::SceneNode *terrainNode =
			mScnMgr->getRootSceneNode()->createChildSceneNode(
				position, Ogre::Quaternion::IDENTITY);
		// Convert height data in a format suitable for the physics engine
		float *terrainHeights = new float[size * size];
		assert(terrainHeights != 0);

		for (int i = 0; i < size; i++) {
			memcpy(terrainHeights + size * i,
			       data + size * (size - i - 1),
			       sizeof(float) * size);
		}

		btScalar heightScale = 1.f;

		btVector3 localScaling(scale, heightScale, scale);

		btHeightfieldTerrainShape *terrainShape =
			new btHeightfieldTerrainShape(
				size, size, terrainHeights, 1 /*ignore*/,
				minHeight, maxHeight, 1, PHY_FLOAT, true);
		terrainShape->setUseDiamondSubdivision(true);
		terrainShape->setLocalScaling(localScaling);

		//Create Rigid Body using 0 mass so it is static
		btRigidBody *body = new btRigidBody(
			0, new Ogre::Bullet::RigidBodyState(terrainNode),
			terrainShape);
		body->setFriction(0.8f);
		body->setHitFraction(0.8f);
		body->setRestitution(0.6f);
		body->getWorldTransform().setOrigin(btVector3(
			position.x, position.y + (maxHeight - minHeight) / 2,
			position.z));
		body->getWorldTransform().setRotation(
			Ogre::Bullet::convert(Ogre::Quaternion::IDENTITY));
		body->setCollisionFlags(body->getCollisionFlags() |
					btCollisionObject::CF_STATIC_OBJECT);

		world->addRigidBody(body);
		return body;
	}
#endif
	bool isLoadedAt(const Ogre::Vector3 &position) const
	{
		long x, y;
		mTerrainGroup->convertWorldPositionToTerrainSlot(position, &x,
								 &y);
		if (mTerrainGroup->getTerrain(x, y))
			return mTerrainGroup->getTerrain(x, y)->isLoaded();
		return false;
	}
};
#endif
