#include <flecs.h>
#include <iostream>

#include <Ogre.h>
#include <OgreApplicationContext.h>
#include <OgreOverlaySystem.h>
#include <OgreOverlayManager.h>
#include <OgreImGuiOverlay.h>
#include <OgreImGuiInputListener.h>
#include <OgreTrays.h>
#include <OgreTimer.h>
#include <OgreMeshLodGenerator.h>

// #include "water/water.h"
#include "GameData.h"
#include "Components.h"
#include "CharacterModule.h"
#include "TerrainModule.h"
#include "GUIModuleCommon.h"
#include "AppModule.h"
#include "EditorGizmoModule.h"
#include "EditorInputModule.h"
#include "PhysicsModule.h"
#include "physics.h"
#include "sound.h"
#include <tracy/Tracy.hpp>

class App;
class SkyRenderer : public Ogre::SceneManager::Listener {
protected:
	Ogre::SceneManager *mSceneManager;
	virtual void _updateRenderQueue(Ogre::RenderQueue *queue) = 0;

public:
	enum BoxPlane {
		BP_FRONT = 0,
		BP_BACK = 1,
		BP_LEFT = 2,
		BP_RIGHT = 3,
		BP_UP = 4,
		BP_DOWN = 5
	};

	SkyRenderer(Ogre::SceneManager *owner)
		: mSceneManager(owner)
		, mSceneNode(0)
		, mEnabled(false)
	{
	}

	virtual ~SkyRenderer()
	{
		setEnabled(false);
		if (mSceneNode)
			mSceneManager->destroySceneNode(mSceneNode);
	}

	Ogre::SceneNode *mSceneNode;
	bool mEnabled;

	void setEnabled(bool enable)
	{
		if (enable == mEnabled)
			return;
		mEnabled = enable;
		enable ? mSceneManager->addListener(this) :
			 mSceneManager->removeListener(this);
	}
	void
	postFindVisibleObjects(Ogre::SceneManager *source,
			       Ogre::SceneManager::IlluminationRenderStage irs,
			       Ogre::Viewport *vp) override
	{
		// Queue skies, if viewport seems it
		if (!vp->getSkiesEnabled() ||
		    irs == Ogre::SceneManager::IRS_RENDER_TO_TEXTURE)
			return;

		if (!mEnabled || !mSceneNode)
			return;

		// Update nodes
		// Translate the box by the camera position (constant distance)
		mSceneNode->setPosition(vp->getCamera()->getDerivedPosition());
		_updateRenderQueue(source->getRenderQueue());
	}
};
class SkyBoxRenderer : public SkyRenderer {
	std::unique_ptr<Ogre::ManualObject> mSkyBoxObj;

	Ogre::Quaternion mSkyBoxOrientation;
	void _updateRenderQueue(Ogre::RenderQueue *queue) override
	{
		if (mSkyBoxObj->isVisible()) {
			mSkyBoxObj->_updateRenderQueue(queue);
		}
	}

public:
	SkyBoxRenderer(Ogre::SceneManager *owner)
		: SkyRenderer(owner)
	{
	}
	Ogre::SceneManager::SkyBoxGenParameters mSkyBoxGenParameters;
	void create(const Ogre::String &materialName, Ogre::Real distance,
		    uint8_t renderQueue, const Ogre::Quaternion &orientation,
		    const Ogre::String &groupName)
	{
		Ogre::MaterialPtr m =
			Ogre::MaterialManager::getSingleton().getByName(
				materialName, groupName);
		OgreAssert(m, "Sky box material '" + materialName +
				      "' not found.");
		// Ensure loaded
		m->load();

		bool valid = m->getBestTechnique() &&
			     m->getBestTechnique()->getNumPasses();
		OgreAssert(valid, "Bad material" + materialName);

		// Create node
		mSceneNode = mSceneManager->createSceneNode();

		// Create object
		mSkyBoxObj = std::make_unique<Ogre::ManualObject>("SkyBox");
		mSkyBoxObj->setCastShadows(false);
		mSceneNode->attachObject(mSkyBoxObj.get());

		mSkyBoxObj->setRenderQueueGroup(renderQueue);
		mSkyBoxObj->begin(materialName,
				  Ogre::RenderOperation::OT_TRIANGLE_STRIP,
				  groupName);

		// rendering cube, only using 14 vertices
		const Ogre::Vector3 cube_strip[14] = {
			{ -1.f, 1.f, 1.f }, // Front-top-left
			{ 1.f, 1.f, 1.f }, // Front-top-right
			{ -1.f, -1.f, 1.f }, // Front-bottom-left
			{ 1.f, -1.f, 1.f }, // Front-bottom-right
			{ 1.f, -1.f, -1.f }, // Back-bottom-right
			{ 1.f, 1.f, 1.f }, // Front-top-right
			{ 1.f, 1.f, -1.f }, // Back-top-right
			{ -1.f, 1.f, 1.f }, // Front-top-left
			{ -1.f, 1.f, -1.f }, // Back-top-left
			{ -1.f, -1.f, 1.f }, // Front-bottom-left
			{ -1.f, -1.f, -1.f }, // Back-bottom-left
			{ 1.f, -1.f, -1.f }, // Back-bottom-right
			{ -1.f, 1.f, -1.f }, // Back-top-left
			{ 1.f, 1.f, -1.f } // Back-top-right
		};

		for (const auto &vtx : cube_strip) {
			mSkyBoxObj->position(orientation * (vtx * distance));
			// Note UVs mirrored front/back
			mSkyBoxObj->textureCoord(vtx.normalisedCopy() *
						 Ogre::Vector3(1, 1, -1));
		}

		mSkyBoxObj->end();
		mSkyBoxGenParameters.skyBoxDistance = distance;
	}
};
class App;
class KeyboardListener : public OgreBites::InputListener {
	App *mApp;

	uint32_t control;
	ECS::Vector2 mouse, mouse_abs;
	float wheel_y;
	bool mouse_moved, wheel_moved;
	float mInitDelay;

public:
	Ogre::Timer fps_timer;
	bool fast;
	KeyboardListener(App *app)
		: OgreBites::InputListener()
		, mApp(app)
		, fast(false)
		, control(0)
		, mouse({ 0, 0 })
		, mouse_abs({ 0, 0 })
		, wheel_y(0.0f)
		, mouse_moved(false)
		, wheel_moved(false)
		, mInitDelay(1.0)
	{
	}
	bool isGuiEnabled()
	{
		if (!ECS::get().has<ECS::GUI>())
			return false;
		return (ECS::get().get<ECS::GUI>().enabled);
	}
	void setGuiEnabled(bool value)
	{
		if (!ECS::get().has<ECS::GUI>())
			return;
		ECS::get().get_mut<ECS::GUI>().enabled = value;
		ECS::get().modified<ECS::GUI>();
	}
	bool keyPressed(const OgreBites::KeyboardEvent &evt) override
	{
		bool updated = false;
#if 0
		if (isGuiEnabled())
			return false;
		if (evt.keysym.sym == OgreBites::SDLK_ESCAPE) {
			OgreAssert(ECS::get().has<ECS::GUI>(), "");
			setGuiEnabled(true);
			if (ECS::get().has<ECS::GUI>())
				ECS::get<ECS::GUI>().setWindowGrab(false);
			return true;
		}
#endif
		OgreBites::Keycode key = evt.keysym.sym;
		if (key == 'w')
			control |= 1;
		else if (key == 'a')
			control |= 2;
		else if (key == 's')
			control |= 4;
		else if (key == 'd')
			control |= 8;
		else if (key == OgreBites::SDLK_LSHIFT)
			control |= 16;
		else if (key == 'e')
			control |= 32;
		else if (key == 'f')
			control |= 64;
		if (key == 'w' || key == 'a' || key == 's' || key == 'd' ||
		    key == 'e' || key == OgreBites::SDLK_LSHIFT)
			return true;
		return false;
	}
	bool keyReleased(const OgreBites::KeyboardEvent &evt) override
	{
		OgreBites::Keycode key = evt.keysym.sym;
#if 0
		if (isGuiEnabled())
			return false;
#endif
		if (key == 'w')
			control &= ~1;
		else if (key == 'a')
			control &= ~2;
		else if (key == 's')
			control &= ~4;
		else if (key == 'd')
			control &= ~8;
		else if (key == OgreBites::SDLK_LSHIFT)
			control &= ~16;
		if (key == 'w' || key == 'a' || key == 's' || key == 'd' ||
		    key == OgreBites::SDLK_LSHIFT)
			return true;
		return false;
	}
	bool mouseMoved(const OgreBites::MouseMotionEvent &evt) override
	{
		mouse.x = evt.xrel;
		mouse.y = evt.yrel;
		mouse_abs.x = evt.x;
		mouse_abs.y = evt.y;
		mouse_moved = true;
		/* no special mouse handling */
		return true;
	}
	bool mouseWheelRolled(const OgreBites::MouseWheelEvent &evt) override
	{
		/* no special mouse wheel handling */
		wheel_y = evt.y;
		wheel_moved = true;
		return true;
	}
	bool mousePressed(const OgreBites::MouseButtonEvent &evt) override
	{
		std::cout << "Mouse press " << (int)evt.button << " "
			  << (int)evt.clicks << "\n";
		if ((int)evt.button == 1)
			control |= 256;
		else
			control &= ~256;
		if ((int)evt.button == 3)
			control |= 512;
		else
			control &= ~512;
		return false;
	}
	bool mouseReleased(const OgreBites::MouseButtonEvent &evt) override
	{
		std::cout << "Mouse release " << (int)evt.button << " "
			  << (int)evt.clicks << "\n";
		if ((int)evt.button == 1)
			control &= ~256;
		if ((int)evt.button == 3)
			control &= ~512;
		return false;
	}
	void frameRendered(const Ogre::FrameEvent &evt) override;
};

struct SceneData {
	Ogre::SceneManager *mScnMgr;
	Ogre::Camera *mCamera;
	Ogre::SceneNode *mCameraNode, *mCameraPivot, *mCameraGoal;
	Ogre::Real mPivotPitch;
	SceneData()
		: mScnMgr(nullptr)
		, mCamera(nullptr)
		, mCameraNode(nullptr)
		, mCameraPivot(nullptr)
		, mCameraGoal(nullptr)
	{
	}
	void setup(Ogre::Root *root, Ogre::OverlaySystem *overlaySystem)
	{
		Ogre::SceneManager *scnMgr = root->createSceneManager();
		mScnMgr = scnMgr;
		mScnMgr->addRenderQueueListener(overlaySystem);
	}
	void initCamera(const Ogre::String &cameraName)
	{
		mCameraNode = mScnMgr->getRootSceneNode()->createChildSceneNode(
			cameraName + "CameraNode");
		mCameraNode->setPosition(0, 2, 3);
		mCameraNode->lookAt(Ogre::Vector3(0, 1, -1),
				    Ogre::Node::TS_PARENT);

		// create the camera
		mCamera = mScnMgr->createCamera(cameraName);
		mCamera->setNearClipDistance(0.05f);
		mCamera->setAutoAspectRatio(true);
		mCameraNode->attachObject(mCamera);
		mCameraPivot =
			mScnMgr->getRootSceneNode()->createChildSceneNode(
				cameraName + "FPSCameraPivot");
		mCameraGoal = mCameraPivot->createChildSceneNode(
			cameraName + "FPSCameraGoal", Ogre::Vector3(0, 2, 3));
		mCameraNode->setPosition(mCameraPivot->getPosition() +
					 mCameraGoal->getPosition());
		mCameraPivot->setFixedYawAxis(true);
		mCameraGoal->setFixedYawAxis(true);
		mCameraNode->setFixedYawAxis(true);
		// our model is quite small, so reduce the clipping planes
		mCamera->setNearClipDistance(0.1f);
		mCamera->setFarClipDistance(800);
		mPivotPitch = 0;
	}
	void setupRTSS()
	{
		Ogre::RTShader::ShaderGenerator *shadergen =
			Ogre::RTShader::ShaderGenerator::getSingletonPtr();
		shadergen->addSceneManager(mScnMgr);
	}
	Ogre::SceneManager *getSceneManager()
	{
		return mScnMgr;
	}
};

class App : public OgreBites::ApplicationContext {
	Ogre::Viewport *mViewport;
	SkyBoxRenderer *sky;
	bool mGrab;
	KeyboardListener mKbd;
	SceneData mEditorNormalScene;

public:
	App()
		: OgreBites::ApplicationContext("ChoroGame")
		, mKbd(this)
		, mGrab(false)
	{
	}
	virtual ~App()
	{
	}
	void setup() override
	{
		OgreBites::ApplicationContext::setup();
		Ogre::Root *root = getRoot();
		mEditorNormalScene.setup(root, getOverlaySystem());
	}
	bool isWindowGrab()
	{
		return mGrab;
	}
	void locateResources() override
	{
        Ogre::ResourceGroupManager::getSingleton().createResourceGroup(
            "Characters", true);
        Ogre::ResourceGroupManager::getSingleton().createResourceGroup(
			"Water", true);
		Ogre::ResourceGroupManager::getSingleton().createResourceGroup(
			"LuaScripts", false);
		Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
			"./lua-scripts", "FileSystem", "LuaScripts", true,
			true);
        Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
            "./characters/male", "FileSystem", "Characters", false,
            true);
        Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
            "./characters/female", "FileSystem", "Characters",
            false, true);
        OgreBites::ApplicationContext::locateResources();
	}
	void loadResources() override
	{
	}
	void setWindowGrab(bool grab = true)
	{
		mGrab = grab;
		ApplicationContextBase::setWindowGrab(grab);
	}

	void initCamera()
	{
		// and tell it to render into the main window
		mEditorNormalScene.initCamera("fps_camera");
		mViewport = getRenderWindow()->addViewport(
			mEditorNormalScene.mCamera);
	}
	void configure()
	{
		std::cout << "Startup"
			  << "\n";
		initApp();
		std::cout << "Set up RTSS"
			  << "\n";
		Ogre::Root *root = getRoot();
		mEditorNormalScene.setupRTSS();
		setWindowGrab(false);
		std::cout << "Init camera"
			  << "\n";
		initCamera();
		Ogre::ResourceGroupManager::getSingleton()
			.initialiseAllResourceGroups();
		std::cout << "Setup input"
			  << "\n";
		setupInput();
		std::cout << "Create content"
			  << "\n";
		createContent();
		std::cout << "Setup done"
			  << "\n";
		Ogre::LogManager::getSingleton().setMinLogLevel(
			Ogre::LML_TRIVIAL);
	}
	Ogre::Timer mTerrainUpd;
	// TODO: implement rough water level calculation
	float getWaterLevel(const Ogre::Vector3 &position)
	{
		return 0.0f;
	}
	void updateWorld(float delta)
	{
		if (!ECS::get().has<ECS::GUI>())
			return;
		/* Update window grab */
		ECS::GUI &gui = ECS::get().get_mut<ECS::GUI>();
		if (gui.grabChanged) {
			gui.grab = false;
			setWindowGrab(gui.grab);
			gui.grabChanged = false;
			ECS::get().modified<ECS::GUI>();
		}
		ECS::update(delta);
	}
	class InputListenerChainFlexible : public OgreBites::InputListener {
	protected:
		std::vector<OgreBites::InputListener *> mListenerChain;

	public:
		InputListenerChainFlexible()
		{
		}
		InputListenerChainFlexible(std::vector<InputListener *> chain)
			: mListenerChain(chain)
		{
		}

		void add(OgreBites::InputListener *listener)
		{
			mListenerChain.push_back(listener);
		}
		void erase(OgreBites::InputListener *listener)
		{
			mListenerChain.erase(std::find(mListenerChain.begin(),
						       mListenerChain.end(),
						       listener));
		}
		bool empty() const
		{
			return mListenerChain.empty();
		}

		InputListenerChainFlexible &
		operator=(const InputListenerChainFlexible &o)
		{
			mListenerChain = o.mListenerChain;
			return *this;
		}

		void frameRendered(const Ogre::FrameEvent &evt) override
		{
			for (auto listener : mListenerChain)
				listener->frameRendered(evt);
		}

		bool keyPressed(const OgreBites::KeyboardEvent &evt) override
		{
			for (auto listner : mListenerChain) {
				if (listner->keyPressed(evt))
					return true;
			}
			return false;
		}
		bool keyReleased(const OgreBites::KeyboardEvent &evt) override
		{
			for (auto listner : mListenerChain) {
				if (listner->keyReleased(evt))
					return true;
			}
			return false;
		}
		bool touchMoved(const OgreBites::TouchFingerEvent &evt) override
		{
			for (auto listner : mListenerChain) {
				if (listner->touchMoved(evt))
					return true;
			}
			return false;
		}
		bool
		touchPressed(const OgreBites::TouchFingerEvent &evt) override
		{
			for (auto listner : mListenerChain) {
				if (listner->touchPressed(evt))
					return true;
			}
			return false;
		}
		bool
		touchReleased(const OgreBites::TouchFingerEvent &evt) override
		{
			for (auto listner : mListenerChain) {
				if (listner->touchReleased(evt))
					return true;
			}
			return false;
		}
		bool mouseMoved(const OgreBites::MouseMotionEvent &evt) override
		{
			for (auto listner : mListenerChain) {
				if (listner->mouseMoved(evt))
					return true;
			}
			return false;
		}
		bool
		mouseWheelRolled(const OgreBites::MouseWheelEvent &evt) override
		{
			for (auto listner : mListenerChain) {
				if (listner->mouseWheelRolled(evt))
					return true;
			}
			return false;
		}
		bool
		mousePressed(const OgreBites::MouseButtonEvent &evt) override
		{
			for (auto listner : mListenerChain) {
				if (listner->mousePressed(evt))
					return true;
			}
			return false;
		}
		bool
		mouseReleased(const OgreBites::MouseButtonEvent &evt) override
		{
			for (auto listner : mListenerChain) {
				if (listner->mouseReleased(evt))
					return true;
			}
			return false;
		}
		bool textInput(const OgreBites::TextInputEvent &evt) override
		{
			for (auto listner : mListenerChain) {
				if (listner->textInput(evt))
					return true;
			}
			return false;
		}
	};
	flecs::entity input_update;
	flecs::entity find_wait_gui;
	void setupInput()
	{
	}
	bool switchWindow = false;
	JoltPhysicsWrapper *mJolt;
	void createContent()
	{
		int i;
		mJolt = new JoltPhysicsWrapper(
			mEditorNormalScene.getSceneManager(),
			mEditorNormalScene.mCameraNode);
		sky = new SkyBoxRenderer(mEditorNormalScene.getSceneManager());
		bool drawFirst = true;
		uint8_t renderQueue = drawFirst ?
					      Ogre::RENDER_QUEUE_SKIES_EARLY :
					      Ogre::RENDER_QUEUE_SKIES_LATE;
		sky->create("Skybox/Dynamic", 450, renderQueue,
			    Ogre::Quaternion::IDENTITY,
			    Ogre::ResourceGroupManager::
				    AUTODETECT_RESOURCE_GROUP_NAME);
		sky->setEnabled(true);

		Ogre::MaterialPtr m =
			Ogre::MaterialManager::getSingleton().getByName(
				"Skybox/Dynamic", "General");
		OgreAssert(m, "Sky box material not found.");
		m->load();
		ECS::setupEditor(
			mEditorNormalScene.mScnMgr,
			/*mDynWorld.get(), */ mEditorNormalScene.mCameraNode,
			mEditorNormalScene.mCamera, getRenderWindow());
		ECS::get().import <ECS::PhysicsModule>();
		ECS::get().import <ECS::EditorGizmoModule>();
		ECS::get().import <ECS::EditorInputModule>();
		ECS::Physics &ph = ECS::get().ensure<ECS::Physics>();
		ph.physics = mJolt;
		ECS::modified<ECS::Physics>();
		ECS::get().set<ECS::RenderWindow>(
			{ getRenderWindow(), getDisplayDPI() });
		ECS::get()
			.observer<ECS::GUI>("UpdateGrab")
			.event(flecs::OnSet)
			.each([this](ECS::GUI &gui) {
				if (gui.grabChanged)
					setWindowGrab(gui.grab);
				std::cout << "grab: " << gui.grab << "\n";
				std::cout << "GUI enabled: " << gui.enabled
					  << "\n";
			});
		ECS::get()
			.observer<ECS::App>("UpdateInputListener")
			.event(flecs::OnSet)
			.each([this](ECS::App &app) {
				if (app.mInput)
					removeInputListener(app.mInput);
				delete app.mInput;
				app.mInput =
					OGRE_NEW OgreBites::InputListenerChain(
						app.listeners);
				addInputListener(app.mInput);
			});
		ECS::get().set<ECS::App>(
			{ initialiseImGui(),
			  nullptr,
			  { getImGuiInputListener(), &mKbd } });
		ECS::get().add<ECS::EditorDebugMaterial>();
		std::shared_ptr<Ogre::ManualObject> manualObj(
			mEditorNormalScene.mScnMgr->createManualObject(
				"EditorGizmo"));
		Ogre::SceneNode *gizmoNode =
			mEditorNormalScene.mScnMgr->getRootSceneNode()
				->createChildSceneNode("EditorGizmoNode");
		gizmoNode->attachObject(manualObj.get());
		manualObj->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
		ECS::get().set<ECS::EditorGizmo>({ manualObj, gizmoNode });
	}
	bool get_gui_active()
	{
		return ECS::get().get<ECS::GUI>().enabled;
	}
	void set_gui_active(bool active)
	{
		ECS::get().get_mut<ECS::GUI>().enabled = active;
		ECS::get().modified<ECS::GUI>();
	}
	void enableDbgDraw(bool enable)
	{
		ECS::get_mut<ECS::EngineData>().enableDbgDraw = enable;
		ECS::modified<ECS::EngineData>();
	}
	bool isEnabledDbgDraw() const
	{
		return ECS::get<ECS::EngineData>().enableDbgDraw;
	}
};
void KeyboardListener::frameRendered(const Ogre::FrameEvent &evt)
{
    {
        ZoneScopedN("frameRendered");
        if (fps_timer.getMilliseconds() > 1000.0f) {
            std::cout << "FPS: "
                  << mApp->getRenderWindow()
                         ->getStatistics()
                         .lastFPS
                  << " ";
            std::cout << "Draw calls: "
                  << mApp->getRenderWindow()
                         ->getStatistics()
                         .batchCount
                  << " ";
            fps_timer.reset();
            std::cout << "Drops: "
                  << mApp->getRenderWindow()
                         ->getStatistics()
                         .vBlankMissCount
                  << "\n";
            fps_timer.reset();
        }
        /* for editor we always update world */
        /* TODO: implement pause */
        mApp->updateWorld(evt.timeSinceLastFrame);
        if (mInitDelay >= 0.0f)
            mInitDelay -= evt.timeSinceLastFrame;

        if (!isGuiEnabled() && ECS::get().has<ECS::Input>()) {
            ECS::Input &input = ECS::get().get_mut<ECS::Input>();
            input.control = control;
            input.mouse = mouse;
            input.mouse_abs = mouse_abs;
            mouse.x = 0;
            mouse.y = 0;
            input.wheel_y = wheel_y;
            wheel_y = 0;
            input.mouse_moved = mouse_moved;
            input.wheel_moved = wheel_moved;
            ECS::modified<ECS::Input>();
        } else {
            ECS::Input &input = ECS::get().get_mut<ECS::Input>();
            input.control = control;
            input.mouse = mouse;
            input.mouse_abs = mouse_abs;
            mouse.x = 0;
            mouse.y = 0;
            input.wheel_y = wheel_y;
            wheel_y = 0;
            input.mouse_moved = mouse_moved;
            input.wheel_moved = wheel_moved;
            ECS::modified<ECS::Input>();
        }
	}
    FrameMark;
}

int main()
{
	App ctx;
	ctx.configure();
	// ctx.enableDbgDraw(false);
	ctx.getRoot()->startRendering();
	ctx.setWindowGrab(false);
	ctx.closeApp();
	return 0;
}
