#include "Ogre.h"
#include "OgreKeyEvent.h"
#include "OgreEventListeners.h"
#include "OgreStringConverter.h"
#include "OgreException.h"

#include "GameFrameListener.h"
#include "Player.h"
#include "Building.h"

using namespace Ogre;
using namespace DinastyOnline::Network;

/// Auxiliar function for translating Ogre clicks to CEGUI
CEGUI::MouseButton convertOgreButtonToCegui(int buttonID) {
	switch (buttonID)
	{
	case MouseEvent::BUTTON0_MASK:
		return CEGUI::LeftButton;
	case MouseEvent::BUTTON1_MASK:
		return CEGUI::RightButton;
	case MouseEvent::BUTTON2_MASK:
		return CEGUI::MiddleButton;
	case MouseEvent::BUTTON3_MASK:
		return CEGUI::X1Button;
	default:
		return CEGUI::LeftButton;
	}
}

namespace DinastyOnline {
	namespace Graphics {

		/// Frame Stadistics and Debug text
		void GameFrameListener::updateStats(void) {
			static String currFps = "Current FPS: ";
			static String avgFps = "Average FPS: ";
			static String bestFps = "Best FPS: ";
			static String worstFps = "Worst FPS: ";
			static String tris = "Triangle Count: ";

			// update stats when necessary
			try {
				OverlayElement* guiAvg = OverlayManager::getSingleton().getOverlayElement("Core/AverageFps");
				OverlayElement* guiCurr = OverlayManager::getSingleton().getOverlayElement("Core/CurrFps");
				OverlayElement* guiBest = OverlayManager::getSingleton().getOverlayElement("Core/BestFps");
				OverlayElement* guiWorst = OverlayManager::getSingleton().getOverlayElement("Core/WorstFps");

				const RenderTarget::FrameStats& stats = mWindow->getStatistics();

				guiAvg->setCaption(avgFps + StringConverter::toString(stats.avgFPS));
				guiCurr->setCaption(currFps + StringConverter::toString(stats.lastFPS));
				guiBest->setCaption(bestFps + StringConverter::toString(stats.bestFPS)
					+" "+StringConverter::toString(stats.bestFrameTime)+" ms");
				guiWorst->setCaption(worstFps + StringConverter::toString(stats.worstFPS)
					+" "+StringConverter::toString(stats.worstFrameTime)+" ms");

				OverlayElement* guiTris = OverlayManager::getSingleton().getOverlayElement("Core/NumTris");
				guiTris->setCaption(tris + StringConverter::toString(stats.triangleCount));

				OverlayElement* guiDbg = OverlayManager::getSingleton().getOverlayElement("Core/DebugText");
				guiDbg->setCaption(mWindow->getDebugText());
			}
			catch(...)
			{
				// ignore
			}
		}

		GameFrameListener::GameFrameListener(RenderWindow* win, Camera* cam, SceneManager *sceneManager, CEGUI::Renderer *renderer, std::deque<Vector3> &walk) : mGUIRenderer(renderer), mWalkList( walk ) {
			mSceneMgr = sceneManager;
			mCamera = cam;
			mWindow = win;
			mWindow->setDebugText(">> Here goes debug text <<");

			// Create RaySceneQuery
			mRaySceneQuery = mSceneMgr->createRayQuery( Ray() );
			mLMouseDown = false;
			mRMouseDown = false;

			// Load visual Debug elements
			mDebugOverlay = OverlayManager::getSingleton().getByName("Core/DebugOverlay");

			// Set default values for variables
			world = NULL;
			mRotateSpeed = 36;
			mMoveSpeed = 75;
			mWalkSpeed = 75.0f;
			mWalkDirection = Vector3::ZERO;
			mCount = 0;
			mDirection = Vector3::ZERO;

			// Continue rendering
			mContinue = true;

			// Create event processor
			mEventProcessor = new EventProcessor();
			mEventProcessor->initialise(win);
			mEventProcessor->startProcessingEvents();
			mEventProcessor->addKeyListener(this);
			// Register this so that we get mouse events.
			mEventProcessor->addMouseListener( this );
			mEventProcessor->addMouseMotionListener( this );

			// We start with menu interface on
			menuInteraction = true;
			gameStarted = false;

			showDebugOverlay(true);
		}

		GameFrameListener::~GameFrameListener()	{
			delete mRaySceneQuery;
			delete mEventProcessor;
		}

		/// Main menu closed and scene created, we can init
		void GameFrameListener::startGame(GameWorldClient *world) {
			// Network world
			this->world = world;
			
			// No menus in screen
			menuInteraction = false;

			// Scene has been created, so we can interact with elements
			mCamNode = mCamera->getParentSceneNode( )->getParentSceneNode( );

			// Set idle animation to our player
			Entity *mEntity = mSceneMgr->getEntity("Player");
			mAnimationState = mEntity->getAnimationState( "Idle" );
			mAnimationState->setLoop( true );
			mAnimationState->setEnabled( true );

			// Game has been started
			gameStarted = true;
		}

		/// When a CEGUI menu appears, the framelistener has to operate in a different way
		void GameFrameListener::setMenuInteraction(bool interaction) {
			menuInteraction = interaction;
		}

		/// Show debug info
		void GameFrameListener::showDebugOverlay(bool show)
		{
			if (mDebugOverlay)
			{
				if (show)
				{
					mDebugOverlay->show();
				}
				else
				{
					mDebugOverlay->hide();
				}
			}
		}

		/// Update movements
		bool GameFrameListener::frameStarted(const FrameEvent& evt) {
			if(mWindow->isClosed()) {
				return false;
			}

			if (gameStarted) {
				// Move our camera
				mCamNode->translate( mCamNode->getOrientation() * mDirection * evt.timeSinceLastFrame );

				// Control Terrain limit, do not let the camera be under the terrain

				// Setup the scene query, from camera position... topdown.
				Vector3 camPos = mCamNode->getPosition( );
				Ray cameraRay( Vector3(camPos.x, 5000.0f, camPos.z), Vector3::NEGATIVE_UNIT_Y );
				mRaySceneQuery->setRay( cameraRay );
				mRaySceneQuery->setSortByDistance( false );

				// Perform the scene query
				RaySceneQueryResult &result = mRaySceneQuery->execute();
				RaySceneQueryResult::iterator itr = result.begin( );

				// Get the results, set the camera height
				for ( itr = result.begin( ); itr != result.end(); itr++ ) {
					if ( itr->worldFragment ) {
					Real terrainHeight = itr->worldFragment->singleIntersection.y;
					if ((terrainHeight + 10.0f) > camPos.y)
						mCamNode->setPosition( camPos.x, terrainHeight + 10.0f, camPos.z );
					}
					break;
				}

				if (world->clientPlayer) {
					if (world->clientPlayer->isMoving() )	{
						mAnimationState = mSceneMgr->getEntity("Player")->getAnimationState( "Walk" );
						mAnimationState->setLoop( true );
						mAnimationState->setEnabled( true );
					} else {
						mAnimationState = mSceneMgr->getEntity("Player")->getAnimationState( "Idle" );
						mAnimationState->setLoop( true );
						mAnimationState->setEnabled( true );
					}
					// Update animation
					mAnimationState->addTime( evt.timeSinceLastFrame );
					
					rotateRobot();
					Position pos = world->clientPlayer->getPosition();
					mSceneMgr->getSceneNode("PlayerNode")->setPosition( Vector3(pos.x, 0, pos.y));
					
					// Be sure our player is above the terrain
					// Setup the scene query
					SceneNode *sNode = mSceneMgr->getEntity("Player")->getParentSceneNode( );
					Vector3 sPos = sNode->getPosition( );
					Ray nodeRay( Vector3(sPos.x, 5000.0f, sPos.z), Vector3::NEGATIVE_UNIT_Y );
					mRaySceneQuery->setRay( nodeRay );
	
					// Perform the scene query
					result = mRaySceneQuery->execute();
					itr = result.begin( );
	
					// Get the results, set the camera height
					for ( itr = result.begin( ); itr != result.end(); itr++ ) {
						if ( itr->worldFragment ) {
							Real terrainHeight = itr->worldFragment->singleIntersection.y;
							if ((terrainHeight + 0.0f) > sPos.y) {
								sNode->setPosition( sPos.x, terrainHeight + 0.0f, sPos.z );
								//TNL::logprintf("Moved player above terrain (%i)", terrainHeight);
							}
							break;
						}
					}
					
					// Draw the rest of the objects
					// TODO: Robot orientation
					// TODO: Delete disconnected players
					Vector<GameObject *> objects = world->getGameObjects();
					for (TNL::S32 i = 0; i < objects.size(); i++) {
						if (objects[i]->getObjectTypeMask() & BuildingType) {
							Building *b = (Building *)objects[i];
							String bName = String((b->getName()).getString());
							SceneNode *node;
							try {
								node = mSceneMgr->getSceneNode( bName + "Node");
								Position renderPos = b->getPosition();
								node->setPosition( Vector3(renderPos.x, 0, renderPos.y) );
							} catch (Exception& e) {
								Entity *ent = mSceneMgr->createEntity( bName, "sphere.mesh" );
								node = mSceneMgr->getRootSceneNode()->createChildSceneNode( bName + "Node" );
								node->attachObject( ent );
								Position renderPos = b->getPosition();
								node->setPosition( Vector3(renderPos.x, 100, renderPos.y) );
							}

						} else if (objects[i]->getObjectTypeMask() & PlayerType) {
							
							Player *p = (Player *)objects[i];
							if (! p->isMyPlayer()) {
								String pName = String((p->getPlayerName()).getString());
								SceneNode *node;
								try {
									node = mSceneMgr->getSceneNode( pName + "Node");
									Position renderPos = p->getPosition();
									node->setPosition( Vector3(renderPos.x, 0, renderPos.y) );
									
								} catch (Exception& e) {
									TNL::logprintf((p->getPlayerName()).getString());
									Entity *ent = mSceneMgr->createEntity( pName, "robot.mesh" );
									node = mSceneMgr->getRootSceneNode()->createChildSceneNode( pName + "Node" );
									node->attachObject( ent );
									Position renderPos = p->getPosition();
									node->setPosition( Vector3(renderPos.x, 0, renderPos.y) );
									
								}
								
								
								if (p->isMoving() )	{
									mAnimationState = mSceneMgr->getEntity(pName)->getAnimationState( "Walk" );
									mAnimationState->setLoop( true );
									mAnimationState->setEnabled( true );
								} else {
									mAnimationState = mSceneMgr->getEntity(pName)->getAnimationState( "Idle" );
									mAnimationState->setLoop( true );
									mAnimationState->setEnabled( true );
								}
								// Update animation
								mAnimationState->addTime( evt.timeSinceLastFrame );
								
								// Be sure the player is above the terrain
								// Setup the scene query
								Vector3 nodePos = node->getPosition( );
								Ray nodeRay( Vector3(nodePos.x, 5000.0f, nodePos.z), Vector3::NEGATIVE_UNIT_Y );
								mRaySceneQuery->setRay( nodeRay );
				
								// Perform the scene query
								result = mRaySceneQuery->execute();
								itr = result.begin( );
				
								// Get the results, set the camera height
								for ( itr = result.begin( ); itr != result.end(); itr++ ) {
									if ( itr->worldFragment ) {
										Real terrainHeight = itr->worldFragment->singleIntersection.y;
										if ((terrainHeight + 0.0f) > nodePos.y) {
											node->setPosition( nodePos.x, terrainHeight + 0.0f, nodePos.z );
											//TNL::logprintf("Moved player above terrain (%i)", terrainHeight);
										}
										break;
									}
								}
							}
						}
					}
				}
				
			}

			return mContinue;
		}

		bool GameFrameListener::frameEnded(const FrameEvent& evt) {
			updateStats();
			if (world != NULL) {
				world->idle();
			}
			return true;
		}

		void GameFrameListener::requestShutdown(void) {
				mContinue = false;
		}

		void GameFrameListener::keyClicked(KeyEvent* e) { }
		void GameFrameListener::keyPressed(KeyEvent* e) {
			// Abort if ESCAPE has been pressed
			if (e->getKey( ) == KC_ESCAPE) {
				mContinue = false;
				return;
			}

			// If game has not been started or there is menu interaction
			if (!gameStarted || menuInteraction) {
				// Pass info to CEGUI
				CEGUI::System::getSingleton().injectKeyDown(e->getKey());
				CEGUI::System::getSingleton().injectChar(e->getKeyChar());
				return;
			}

			switch ( e->getKey( ) )	{
				case KC_1:
					mCamera->getParentSceneNode()->detachObject( mCamera );
					mCamNode = mSceneMgr->getSceneNode( "CamNode1" );
					mSceneMgr->getSceneNode( "PitchNode1" )->attachObject( mCamera );
					break;

				case KC_2:
					mCamera->getParentSceneNode()->detachObject( mCamera );
					mCamNode = mSceneMgr->getSceneNode( "CamNode2" );
					mSceneMgr->getSceneNode( "PitchNode2" )->attachObject( mCamera );
					break;
				case KC_UP:
				case KC_W:
					mDirection.z -= mMoveSpeed;
					break;

				case KC_DOWN:
				case KC_S:
					mDirection.z += mMoveSpeed;
					break;

				case KC_LEFT:
				case KC_A:
					mDirection.x -= mMoveSpeed;
					break;

				case KC_RIGHT:
				case KC_D:
					mDirection.x += mMoveSpeed;
					break;

				case KC_PGDOWN:
				case KC_E:
					mDirection.y -= mMoveSpeed;
					break;

				case KC_PGUP:
				case KC_Q:
					mDirection.y += mMoveSpeed;
					break;
			}
		}

		void GameFrameListener::keyReleased(KeyEvent* e) {
			// If game has not been started or there is menu interaction
			if (!gameStarted || menuInteraction) {
				CEGUI::System::getSingleton().injectKeyUp(e->getKey());
				return;
			}

			switch ( e->getKey( ) )	{
				case KC_UP:
				case KC_W:
					mDirection.z += mMoveSpeed;
					break;

				case KC_DOWN:
				case KC_S:
					mDirection.z -= mMoveSpeed;
					break;

				case KC_LEFT:
				case KC_A:
					mDirection.x += mMoveSpeed;
					break;

				case KC_RIGHT:
				case KC_D:
					mDirection.x -= mMoveSpeed;
					break;

				case KC_PGDOWN:
				case KC_E:
					mDirection.y += mMoveSpeed;
					break;

				case KC_PGUP:
				case KC_Q:
					mDirection.y -= mMoveSpeed;
					break;
			} // switch
		}

		/// Mouse interaction
		void GameFrameListener::mouseClicked(MouseEvent* e) {}
		void GameFrameListener::mouseEntered(MouseEvent* e) {}
		void GameFrameListener::mouseExited(MouseEvent* e) {}
		void GameFrameListener::mousePressed(MouseEvent* e) {
			// If game has not been started or there is menu interaction
			if (!gameStarted || menuInteraction) {
				CEGUI::System::getSingleton().injectMouseButtonDown(
					convertOgreButtonToCegui(e->getButtonID()));
				e->consume();
				return;
			}

			if (e->getButtonID() & InputEvent::BUTTON0_MASK) {
				// Left mouse button down

				// We want to move our player to the clicked place
				// Find the place through a Ray scene query

				// Setup the ray scene query
				Real x = CEGUI::MouseCursor::getSingleton().getPosition().d_x;
				Real y = CEGUI::MouseCursor::getSingleton().getPosition().d_y;
				x = x / mWindow->getWidth();
				y = y / mWindow->getHeight();
				Ray mouseRay = mCamera->getCameraToViewportRay(x,y);
				mRaySceneQuery->setRay( mouseRay );

				// Execute query
				RaySceneQueryResult &result = mRaySceneQuery->execute();
				RaySceneQueryResult::iterator itr = result.begin( );
				mRaySceneQuery->setSortByDistance( false );

				// Get results
				for ( itr = result.begin( ); itr != result.end(); itr++ ) {
					if ( itr->worldFragment ) {
						// Send new position to server
						Position p;
						p.x = (int)(itr->worldFragment->singleIntersection[0]);
						p.y = (int)(itr->worldFragment->singleIntersection[2]);
						world->moveMyPlayerTo(p);
						
						// Point to go
						mWalkList.push_back(itr->worldFragment->singleIntersection);

						// Update variables
						rotateRobot();

						/*
						// Place an element in that point
						char name[16];
						sprintf( name, "Robot%d", mCount++ );

						Entity *ent = mSceneMgr->createEntity( name, "robot.mesh" );
						SceneNode *mCurrentObject = mSceneMgr->getRootSceneNode( )->createChildSceneNode( String(name) + "Node", itr->worldFragment->singleIntersection );
						mCurrentObject->attachObject( ent );
						mCurrentObject->setScale( 0.5f, 0.5f, 0.5f );*/

						break;
					}
				}

				mLMouseDown = true;

			} else if (e->getButtonID() & InputEvent::BUTTON1_MASK) {
				// Right mouse button down
				// Hide cursor, if user move the mouse, the camera will yaw/pitch
				CEGUI::MouseCursor::getSingleton().hide( );
				mRMouseDown = true;
			}
		}

		void GameFrameListener::mouseReleased(MouseEvent* e) {
			// If game has not been started or there is menu interaction
			if (!gameStarted || menuInteraction) {
				CEGUI::System::getSingleton().injectMouseButtonUp(
						convertOgreButtonToCegui(e->getButtonID()));
				return;
			}

			// Left mouse button up
			if (e->getButtonID() & InputEvent::BUTTON0_MASK)
			{
				mLMouseDown = false;
			} else if (e->getButtonID() & InputEvent::BUTTON1_MASK) {
				// Right mouse button up
				CEGUI::MouseCursor::getSingleton().show( );
				mRMouseDown = false;
			}
		}

		void GameFrameListener::mouseMoved (MouseEvent *e) {
			// Update CEGUI mouse pointer with the mouse motion
			CEGUI::System::getSingleton().injectMouseMove(e->getRelX() * mGUIRenderer->getWidth(), e->getRelY() * mGUIRenderer->getHeight());

		}

		// This is when the mouse is clicked, held and dragged.
		void GameFrameListener::mouseDragged (MouseEvent *e) {
			if ( mRMouseDown ) {
				// Right click
				mCamNode->yaw( Degree(-e->getRelX( ) * mRotateSpeed) );
				mCamNode->getChild( 0 )->pitch( Degree(-e->getRelY() * mRotateSpeed) );
			} else {
				// Update CEGUI with the mouse motion
				mouseMoved(e);
			}
		}

		/// Rotate the robot with the correct orientation
		bool GameFrameListener::rotateRobot() {			
			if ( mWalkList.empty() ) {
                return false;
            }

            mDestination = mWalkList.front( );  // this gets the front of the deque
            mWalkList.pop_front( );             // this removes the front of the deque

			SceneNode *mNode = mSceneMgr->getSceneNode("PlayerNode");
			mWalkDirection = mDestination - mNode->getPosition( );
			mDistance = mWalkDirection.normalise( );

			// Robot's Orientation
			Vector3 src = mNode->getOrientation( ) * Vector3::UNIT_X;

			// As we are above a terrain, we do not want to orient the robot with X also, because
			// it can start rotating without sense:
			Vector3 mWalkDirectionNoX = Vector3(mWalkDirection[0], 0, mWalkDirection[2]);

			if ( (1.0f + src.dotProduct( mWalkDirectionNoX )) < 0.0001f ) {
				mNode->yaw( Degree(180) );	// If 180, Quaternion does a division by 0, so just use yaw in this case
			} else {
				Ogre::Quaternion quat = src.getRotationTo( mWalkDirectionNoX );
				mNode->rotate( quat );
			}

			return true;
		}
	}
}
