#include <iostream>
#include <fstream>
#include <algorithm>
#include <boost/filesystem.hpp>

#include "Model.h"
#include "Physics.h"
#include "PhysicsBase.h"

#include "EntityFactory.h"
#include "AsteroidsEvents.h"
#include "AsteroidEntity.h"

#include <assert.h>

namespace asteroids {

static Model* ModelInstance = NULL;

/*
 * Inherited Module functions
 */
int Model::OnInit (int argc, char* argv[]) {
	int result = Engine::ModelBase::OnInit (argc, argv);

	Engine::LogMessage ("Model Initialization!");

	ModelInstance = this;

	mGameState = GameStatePaused;
	mLastGameState = GameStatePaused;

	/// \TODO use <optional> or similar for initialization of mCurrentLevelIndex
	mCurrentLevelIndex = 99999;
	mLevelName = "";

	if (InitLevelList() == 0)
		Engine::LogError ("No levels found!");

	// First we reset the highscore list
	mHighscoreList.clear();
	// then we try to load values from the file
	LoadHighscoreList();

	// if we have less than the usual number of entries we add default values
	if (mHighscoreList.size() < 10) {
		AddHighscoreEntry ("Imperator",      1000000);
		AddHighscoreEntry ("Darth Vader",     800000);
		AddHighscoreEntry ("Luke Skywalker",  600000);
		AddHighscoreEntry ("Han Solo",        400000);
		AddHighscoreEntry ("Princess Leia",   200000);
		AddHighscoreEntry ("C3PO",            100000);
		AddHighscoreEntry ("R2-D2",            50000);
		AddHighscoreEntry ("Chewy",            10000);
		AddHighscoreEntry ("Mr. Ewok",          5000);
		AddHighscoreEntry ("Jabba the Hutt",    1000);
	}

	// Reset the newest highscore entry index which may be used for highlighting
	// the newest entry.
	mNewestHighscoreEntryIndex = std::numeric_limits<unsigned int>::max();

	// initialize event handlers and register them
	Engine::RegisterListener (this, EventGameOver);
	Engine::RegisterListener (this, EventShipExplode);

	mPlayerName = "Player";

	Engine::PlayMusic (Engine::GetResourceFullPath("/data/sounds/intro_music.ogg"));
	
	Engine::RegisterListener (this, EventAccelerateStart);
	Engine::RegisterListener (this, EventAccelerateStop);

	return result;
}

bool Model::OnReceiveEvent (const Engine::EventBasePtr &event) {
	switch (event->mEventType) {
		case EventAccelerateStart:
			Engine::PlaySoundLoop(Engine::GetResourceFullPath("/data/sounds/thrust.wav"), -1);
			break;
		case EventAccelerateStop:
			Engine::HaltSoundLoop(Engine::GetResourceFullPath("/data/sounds/thrust.wav"));
			break;
		case EventShipExplode:
			OnShipExplode();
			break;
		case EventGameOver:
			return OnGameOver();
			break;

		default: Engine::LogWarning ("Received Event with type %d but don't know what to do with it!", event->mEventType);
						 break;
	}

	return false;
}

/*
 * Module specific functions
 */
void Model::Process () {
	if (mLastGameState == mGameState) {
		if (mGameState == GameStateRunning) {
			Engine::ModelBase::Process();
		}
		return;
	}

	// when we are here we know that something has changed so we need to take
	// some action.
	Engine::LogDebug ("Switching from %s->%s", GetStringGameState(mLastGameState), GetStringGameState(mGameState));

	// ... and we have to set the last game state to the current gamestate
	// otherwise we end up in an infinite loop of performing the switching
	// action.
	mLastGameState = mGameState;
}

unsigned int Model::InitLevelList () {
	std::string level_dir_name = Engine::GetResourceFullPath("/data/levels/");
	Engine::LogDebug ("Searching for levels in %s", level_dir_name.c_str());

	mLevelList.clear();

	boost::filesystem::path level_dir(level_dir_name);

	if (!boost::filesystem::exists(level_dir)) {
		Engine::LogError ("Could not init level list: \todo %s does not exist!");
	}

	if (!boost::filesystem::is_directory(level_dir)) {
		Engine::LogError ("Could not init level list: \todo %s is not a directory!");
	}

	boost::filesystem::directory_iterator end_iter;
	for (boost::filesystem::directory_iterator dir_iter(level_dir);
			dir_iter != end_iter;
			++dir_iter) {
		if (boost::filesystem::is_regular_file (dir_iter->status())) {
			std::string level_relative_path (level_dir_name);
			level_relative_path += dir_iter->path().filename();
			mLevelList.push_back (level_relative_path);
			Engine::LogDebug ("Found level %s", mLevelList[mLevelList.size()-1].c_str());
		}
	}

	std::sort(mLevelList.begin(), mLevelList.end());

	return mLevelList.size();
}

void Model::LoadHighscoreList () {
	Engine::LogDebug ("Loading highscore file");
	boost::filesystem::path highscore_file(Engine::GetUserDirFullPath("/highscore.dat"));

	// if the file does not exist, we create it and write standard values into
	// it.
	if (!boost::filesystem::exists(highscore_file))
		return;

	if (!boost::filesystem::is_regular_file(highscore_file)) {
		Engine::LogError ("Could not load highscore file: %s is not a regular file!", highscore_file.filename().c_str());
	}

	std::ifstream score_stream (highscore_file.filename().c_str());
	while (!score_stream.eof()) {
		std::string name;
		unsigned int points;

		std::string line;
		getline (score_stream, line);

		std::string::size_type delimiter = line.find ('\t');
		if (delimiter == std::string::npos)
			break;

		name = line.substr(0, delimiter);

		std::istringstream points_stream(line.substr(delimiter + 1, line.size()));
		points_stream >> points;

		Engine::LogDebug ("Read Highscore Entry Name: %s Points: %d", name.c_str(), points);
		AddHighscoreEntry (name, points);
	}
}

void Model::SaveHighscoreList () {
	std::ofstream highscore_file (Engine::GetUserDirFullPath("/highscore.dat").c_str());

	std::list<HighscoreEntry>::iterator iter = mHighscoreList.begin();

	while (iter != mHighscoreList.end()) {
		highscore_file << iter->name << "\t" << iter->points << std::endl;
		iter++;
	}

	highscore_file.close();
}

bool highscore_cmp (Model::HighscoreEntry a, Model::HighscoreEntry b) {
	return a.points > b.points;
}

/** \brief Addes an entry to the highscore list and takes care that the list stays valid
 *
 * \TODO Re-think usage of mNewestHighscoreEntryIndex variable in this function
 */
unsigned int Model::AddHighscoreEntry(const std::string &name, const unsigned int points) {
	HighscoreEntry entry;
	entry.name = name;
	entry.points = points;

	unsigned int counter = 0;
	std::list<HighscoreEntry>::iterator iter = mHighscoreList.begin();
	while (iter != mHighscoreList.end()) {
		if (points >= iter->points) {
			mHighscoreList.insert (iter, entry);
			mNewestHighscoreEntryIndex = counter;
			break;
		}
		iter++;
		counter ++;
	}

	if (mHighscoreList.size() < 10) {
		mHighscoreList.push_back(entry);
		mNewestHighscoreEntryIndex = mHighscoreList.size();
		return mHighscoreList.size();
	}

	while (mHighscoreList.size() > 10) {
		mHighscoreList.pop_back();
	}

	if (counter < 10)
		return counter;

	// if we have all 10 entries then we can save
	// the highscore
	SaveHighscoreList();

	mNewestHighscoreEntryIndex = 99999;

	return 99999;
}

int Model::DoLoadLevel (const char* filename) {
	Engine::LogMessage ("Loading level from %s", filename);
	std::fstream level_file (filename, std::ios::in);
	
	if (!level_file) {
		Engine::LogError ("Unable to open file %s for reading!", filename);
		exit (-1);
	}

	ClearEntities();
	mAsteroids.clear();

	std::string entity_type_str;

	int entity_count = 0;

	while (level_file >> entity_type_str) {
		if (entity_type_str[0] == '#') {
			getline (level_file, entity_type_str);
			Engine::LogDebug ("Read Comment: %s", entity_type_str.c_str());
			continue;
		}

		GameEntityType entity_type = GameEntityTypeUnknown;

		if (entity_type_str == "GameEntityTypeShip")
			entity_type = GameEntityTypeShip;
		else if (entity_type_str == "GameEntityTypeAsteroid")
			entity_type = GameEntityTypeAsteroid;
		else {
			Engine::LogError ("Unknown Entity type: %s", entity_type_str.c_str());
			exit (-1);
		}

		Engine::EntityBase* entity = CreateEntity (entity_type);

		bool is_player;
		level_file >> is_player;

		if (is_player) {
			mPlayerEntityId = entity->mId;
			Engine::LogDebug ("Entity with id '%d' is player", entity->mId);
		}

		level_file >> entity->mPhysicState->mPosition[0];
		level_file >> entity->mPhysicState->mPosition[1];
		level_file >> entity->mPhysicState->mPosition[2];

		level_file >> entity->mPhysicState->mOrientation[0];
		level_file >> entity->mPhysicState->mOrientation[1];
		level_file >> entity->mPhysicState->mOrientation[2];

		level_file >> entity->mPhysicState->mVelocity[0];
		level_file >> entity->mPhysicState->mVelocity[1];
		level_file >> entity->mPhysicState->mVelocity[2];

		level_file >> entity->mPhysicState->mAngleVelocity;

		entity_count ++;
	}

	level_file.close();

	Engine::LogDebug ("%d Entities loaded!", mEntities.size());

	return 0;
}

int Model::DoSaveLevel (const char* filename) {
	Engine::LogMessage ("Saving level to %s", filename);
	std::fstream level_file (filename, std::ios::out);
	
	if (!level_file) {
		Engine::LogError ("Unable to open file %s for writing!", filename);
		exit (-1);
	}

	level_file << "# Format" << std::endl;
	level_file << "# <Type> <player?> <xpos> <ypos> <zpos> <zrot> <yrot> <xrot> <xvel> <yvel> <zvel> <rotvel>" << std::endl;

	std::map<unsigned int, Engine::EntityBase*>::iterator iter = mEntities.begin();
	unsigned int player_id = GetPlayerEntityId();

	for (iter = mEntities.begin(); iter != mEntities.end(); iter++) {
		Engine::EntityBase* game_entity = iter->second;

		level_file << GetStringGameEntityType((GameEntityType)game_entity->mType) << "\t"
			// this stores the player id
			<< (game_entity->mId == player_id) << "\t"
			<< game_entity->mPhysicState->mPosition[0] << "\t"
			<< game_entity->mPhysicState->mPosition[1] << "\t"
			<< game_entity->mPhysicState->mPosition[2] << "\t"
			<< game_entity->mPhysicState->mOrientation[0] << "\t"
			<< game_entity->mPhysicState->mOrientation[1] << "\t"
			<< game_entity->mPhysicState->mOrientation[2] << "\t"
			<< game_entity->mPhysicState->mVelocity[0] << "\t"
			<< game_entity->mPhysicState->mVelocity[1] << "\t"
			<< game_entity->mPhysicState->mVelocity[2] << "\t"
			<< game_entity->mPhysicState->mAngleVelocity << "\t"
			<< std::endl;
	}
	
	level_file.close();
	return 0;
}

void Model::ReloadLevel () {
	Engine::LogDebug ("Reloading level %d", mCurrentLevelIndex + 1);

	if (mCurrentLevelIndex < 0 || mCurrentLevelIndex >= mLevelList.size())
		Engine::LogError ("Invalid level index: %u", mCurrentLevelIndex);

	DoLoadLevel(mLevelList[mCurrentLevelIndex].c_str());
}

void Model::ProceedToNextLevel () {
	Engine::LogDebug ("Proceeding to next level %d", mCurrentLevelIndex + 1);
	mCurrentLevelIndex++;

	if (mCurrentLevelIndex == mLevelList.size()) {
		Engine::EventBasePtr gameover_event (new Engine::EventBase());
		gameover_event->mEventType = EventGameOver;
		QueueEvent (gameover_event);
	}	else {
		DoLoadLevel(mLevelList[mCurrentLevelIndex].c_str());
	}
}

void Model::SetGameState (const unsigned int &state) {
	mLastGameState = mGameState;
	mGameState = state;
}

bool Model::OnGameOver() {
	Engine::LogMessage ("Points = %d lowest = %d", mPoints, mHighscoreList.back().points );

	if (mPoints > mHighscoreList.back().points) {
		Engine::LogMessage ("New Highscore!");
		AddHighscoreEntry (mPlayerName, mPoints);
	}

	return false;
};

void Model::OnNewGame() {
	ClearEntities();

	mNewestHighscoreEntryIndex = std::numeric_limits<unsigned int>::max();
	mPlayerLives = 3;
	mCurrentLevelIndex = 0;
	mPoints = 0;

	DoLoadLevel (mLevelList[mCurrentLevelIndex].c_str());
}

void Model::OnShipExplode () {
	mPlayerLives --;

	if (mPlayerLives == 0) {
		Engine::EventBasePtr gameover_event (new Engine::EventBase());
		gameover_event->mEventType = EventGameOver;
		QueueEvent (gameover_event);
	} else {
		Engine::EventBasePtr playerdied_event (new Engine::EventBase());
		playerdied_event->mEventType = EventPlayerDied;
		QueueEvent (playerdied_event);
	}
}

void Model::OnCreateEntity (const int type, const unsigned int id) {
	GameEntityType entity_type = (GameEntityType) type;

	if (entity_type == GameEntityTypeAsteroid) {
		mAsteroids.push_back (id);
	}
}

void Model::OnKillEntity (const Engine::EntityBase *entity) {
	GameEntityType entity_type = (GameEntityType) entity->mType;

	if (entity_type == GameEntityTypeAsteroid) {
		Engine::PlaySound(Engine::GetResourceFullPath("/data/sounds/rock_destroyed.wav"));

		unsigned int i;
		const AsteroidEntity *asteroid = static_cast<const AsteroidEntity*>(entity);

		if (GetPlayerEntityId() != Engine::NullEntityId)
			mPoints += 150 + asteroid->mSubAsteroidsCount * 75;

		for (i = 0; i < mAsteroids.size(); i++) {
			if (mAsteroids.at(i) == entity->mId) {
				std::vector<unsigned int>::iterator entity_iter = mAsteroids.begin() + i;
				mAsteroids.erase (entity_iter);
				break;
			}
		}

		if (mAsteroids.size() == 0) {
			Engine::EventBasePtr level_complete_event (new Engine::EventBase());
			level_complete_event->mEventType = EventLevelComplete;
			QueueEvent (level_complete_event);
		}
	}
}

float Model::GetWorldWidth () {
	return static_cast<Physics*>(mPhysics)->GetWorldWidth();
}

float Model::GetWorldHeight () {
	return static_cast<Physics*>(mPhysics)->GetWorldHeight();
}

}


