#include "Game.hpp"

#include <iostream>
using namespace std;

#include "Global.hpp"
#include "Entity.hpp"
#include "Exception.hpp"
#include "Paddle.hpp"
#include "Block.hpp"
#include "Extra.hpp"
#include "Ball.hpp"
#include "ResourcesManager.hpp"
#include "ConfigFile.hpp"
#include "Helper.hpp"
#include "glm/gtc/type_ptr.hpp"
#include "glm/gtc/matrix_transform.hpp"
#include "Input.hpp"

using namespace boost;

Game::Game(ConfigFile& config, int width, int height):
m_Config(config),
m_Water(*this),gameOver(true),
m_score(0),
m_balls(m_Config.GetValue<int>("start_balls")),
m_actualLevel(m_Config.GetValue<int>("start_level")),
m_max_current_balls(m_Config.GetValue<int>("max_current_balls")),
difficulty(1),
levelManager(LevelManager(this)),
cameraMode(0),
m_TextManager({width, height}, glm::uvec2(512, 512)),
m_Volume(config.GetValue<float>("snd_volume")),
rnd(std::random_device()()),
m_MouseCaptured(false),
m_GameState(GameState::startup),
m_NewLevelCounter(0.f),
m_MouseSensitivity(config.GetValue<float>("mouse_sensitivity")/100.f)
{
	m_SkySphere.Load(ResourceDir+"SkySphere/SkySphere.obj");
	m_World.Load(ResourceDir+"Oasis/terrain.obj");
	m_Trees.Load(ResourceDir+"Oasis/trees.obj");

	//Load some common models:
	ResMan.GetModel(ResourceDir+"Block/Block.obj");
	ResMan.GetModel(ResourceDir+"ExplosionBlock/Dynamite.obj");
	ResMan.GetModel(ResourceDir+"Extra01/Extra.obj");
	ResMan.GetModel(ResourceDir+"ExtraBlock/Barrel.obj");
	ResMan.GetModel(ResourceDir+"LauncherBlock/LauncherBlock.obj");
	ResMan.GetModel(ResourceDir+"Paddle/Paddle.obj");

	m_TextManager.AddFont(ResourceDir+"Fonts/SatellaRegular-ZVVaz.ttf", 26);
	m_TextManager.AddFont(ResourceDir+"Fonts/SatellaRegular-ZVVaz.ttf", 42);
	m_TextLevel = m_TextManager.NewText("Level: 1", glm::vec2(10, 10));
	m_TextBalls = m_TextManager.NewText("Balls: 1", glm::vec2(10, 40));
	m_TextScore = m_TextManager.NewText("Points: 1", glm::vec2(10, 70));
	m_TextLevel->SetColor(glm::vec3(0, 0, 0.2));
	m_TextBalls->SetColor(glm::vec3(0, 0, 0.2));
	m_TextScore->SetColor(glm::vec3(0, 0, 0.2));

	m_TextStartup = m_TextManager.NewText("Click to start game", glm::vec2(), 42);
	CenterText(m_TextStartup, glm::vec2(width, height));
	m_TextStartup->SetVisible(true);

	m_TextLaunch = m_TextManager.NewText("Click again to launch ball", glm::vec2(), 42);
	CenterText(m_TextLaunch, glm::vec2(width, height));
	m_TextLaunch->SetVisible(false);

	m_TextLose = m_TextManager.NewText("Game Over", glm::vec2(), 42);
	CenterText(m_TextLose, glm::vec2(width, height));
	m_TextLose->SetVisible(false);

	AddSound("music");
	auto& next_song = m_Sounds["music"][0];
	m_BgMusic =  make_unique<SoundController>(next_song->PlayControlled(true, false, PlayOptions(true, m_Volume, true)));

	auto bg_ambient = m_SoundManager.LoadSound(ResourceDir+"Sounds/"+m_Config.GetValue<string>("snd_ambient"));
	bg_ambient.Play(PlayOptions(true, m_Volume, true));

	AddSound("hit");
	AddSound("paddle");
	AddSound("explosion");
	AddSound("win");
	AddSound("wall");
	AddSound("launch");
	AddSound("block_launch_ball");
	AddSound("block_launch_extra");
	AddSound("lose");
	AddSound("extra_score");
	AddSound("extra_ball");
}


Game::~Game()
{
	m_EntityManager.clearEntities();

	for(ParticleSystem* ps : m_ParticleSystems)
	{
		delete ps;
	}
}

void Game::DrawScene()
{
	switch(cameraMode%3)
	{
	case 0:
		CameraSetup_Above();
		break;
	case 1:
		CameraSetup_Behind();
		break;
	case 2:
		CameraSetup_Free();
		break;
	}

	//------------------Lighting------------------
	
	Vector3f LightColor(255/255.0f, 240/255.0f, 240/255.0f);
	glEnable(GL_LIGHTING);
	glShadeModel(GL_SMOOTH);
	
	GLfloat ambientLight[] = {LightColor.x/8,	LightColor.y/8,	LightColor.z/8,	1.0f};
	GLfloat diffuseLight[] = {LightColor.x,		LightColor.y,	LightColor.z,	1.0f};

	glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight);
	glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight);
	glEnable(GL_LIGHT0);

	glEnable(GL_COLOR_MATERIAL);
	glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);

	float SunDistance=2;
	GLfloat SunPosition[] = {0.4f, -0.8f, 1.0f, 0.0f};
	glLightfv(GL_LIGHT0, GL_POSITION, SunPosition);
	//_____________________________________________

	//Texture rendering
	m_Water.RenderTexture();
	m_Water.Render();

	//scene rendering
	glPushMatrix();
	RenderEntities();
	glPopMatrix();
	

	m_TextManager.RenderAll();

	//m_EntityManager.renderCollisionModels();
}

void Game::RenderEntities()
{
	glPushMatrix();
	glDisable(GL_LIGHTING);
	glScalef(150, 150, 150);
	m_SkySphere.RenderModel();
	glEnable(GL_LIGHTING);
	glPopMatrix();

	m_EntityManager.renderEntities();
		

	//render world
	m_World.RenderModel();

	glEnable(GL_BLEND);//trees need alpha blending
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	glAlphaFunc(GL_GREATER, 0.1f);
	glEnable(GL_ALPHA_TEST);

	m_Trees.RenderModel();

	glDisable(GL_ALPHA_TEST);
	glDisable(GL_BLEND);

	//Render Particles
	for(ParticleSystem* ps : m_ParticleSystems)
	{
		ps->onRender();
	}
}

void Game::CenterText(Text* t, glm::vec2 screen_size)
{
	t->SetScreenPosition((screen_size-t->GetSize())/2.f);
}

bool Game::isOver()
{
	return gameOver;
}

void Game::start()
{
	gameOver = false;
	loadLevel(m_actualLevel);
}

bool isGameElement(Entity* ent)
{
	Block* block=dynamic_cast<Block*>(ent);
	if(block)
	{
		if(block->isDestructable())
			return true;
	}
	if(dynamic_cast<Extra*>(ent))
	{
		return true;
	}
	return false;
}

bool Game::allBlocksDestroyed()
{
	return m_EntityManager.FindEntities(isGameElement).size()==0;
}

void Game::loadLevel(int level)
{
	/*
		* clear actual entity-vector of entityManager.
		* this has to be done because usually WallBlocks, Balls and Paddles remain in the list after finishing a level
	*/
	m_EntityManager.clearEntities();

	//fill entity-vector of entityManager with new entities
	m_EntityManager.setEntities(levelManager.getEntities(m_actualLevel));
}

void Game::incrementScore(int n)
{
	m_score += n;
}

bool Game::onUpdate(float time, void* window)
{
	//check if we have lost the game
	auto is_ball = [](Entity* ent){
		return dynamic_cast<Ball*>(ent)!=nullptr;
	};


	if(Input::GetKeyClicked(Input::KEY_ESCAPE))
	{
		if(m_MouseCaptured)
		{
			glfwSetInputMode(static_cast<GLFWwindow*>(window), GLFW_CURSOR, GLFW_CURSOR_NORMAL);
			m_MouseCaptured = false;
			m_GameState = GameState::paused;
		}
		else
			return true;
	}
	if(Input::GetMouseButtonClicked(Input::MOUSE_BUTTON_1) && !m_MouseCaptured)
	{
		glfwSetInputMode(static_cast<GLFWwindow*>(window), GLFW_CURSOR, GLFW_CURSOR_DISABLED);
		m_MouseCaptured = true;
		if(m_GameState == GameState::startup)
		{
			m_GameState = GameState::initial_capture;
			m_TextStartup->SetVisible(false);
			m_TextLaunch->SetVisible(true);
		}
		else
		{
			m_TextLaunch->SetVisible(false); // maybe player pressed ESC before launching first ball
			m_GameState = GameState::running;
		}
		return false;// avoid handling the click event again
	}

	if(m_GameState == GameState::initial_capture || m_GameState == GameState::running)
	{
		InputStruct Input;
		Input.leftMButton  = Input::GetMouseButtonClicked(Input::MOUSE_BUTTON_LEFT);
		Input.rightMButton = Input::GetMouseButtonClicked(Input::MOUSE_BUTTON_RIGHT);
		double x, y;
		Input::GetCursorRel(&x, &y);
		Input.relMouseX = x * m_MouseSensitivity;
		Input.relMouseY = y * m_MouseSensitivity;

		m_EntityManager.onInput(Input);
	}

	//updateParticles:
	for(std::list<ParticleSystem*>::iterator It=m_ParticleSystems.begin(); It!=m_ParticleSystems.end(); )
	{
		(*It)->onTick(time);
		if((*It)->IsDead())
		{
			delete *It;
			It=m_ParticleSystems.erase(It);
		}
		else
			++It;
	}

	m_Water.onTick(time);

	if(m_GameState == GameState::initial_capture || m_GameState == GameState::running)
	{
		m_EntityManager.onTick(time);
	}
	if(m_GameState == GameState::initial_capture)
	{
		if(m_EntityManager.FindEntities(is_ball).size() > 0)
		{
			m_GameState = GameState::running;
			m_TextLaunch->SetVisible(false);
		}
	}


	if(Input::GetKeyClicked(Input::KEY_N))
	{
		m_EntityManager.clearEntities();
	}

	//check if we go to the next level
	if(allBlocksDestroyed())
	{
		m_BgMusic->FadeOut(1.5);
		auto& win_sound = m_Sounds["win"][m_actualLevel % levelManager.getNumLevels()];
		m_WinSound = make_unique<SoundController>(win_sound->PlayControlled(true, false, PlayOptions(false, m_Volume)));

		m_actualLevel++;
		m_balls += 2;  //2 new balls :)
		// count balls

		auto isBall = [](Entity* ent)
		{
			if(dynamic_cast<Ball*>(ent))
				return true;
			return false;
		};
		m_balls += m_EntityManager.FindEntities(isBall).size();

		loadLevel(m_actualLevel);
	}
	// start next musci after win sound ended
	if((m_WinSound && !m_WinSound->IsPlaying()) && !m_BgMusic->IsPlaying())
	{
		// start next bg music
		auto& next_song = m_Sounds["music"][m_actualLevel % levelManager.getNumLevels()];
		m_BgMusic =  make_unique<SoundController>(next_song->PlayControlled(true, false, PlayOptions(true, m_Volume, true)));
	}

	if(m_balls==0 && m_EntityManager.FindEntities(is_ball).size()==0)
	{
		gameOver=true;
		m_TextLose->SetVisible(true);
	}

	// update onscreen text:

	m_TextLevel->SetText("Level: "+std::to_string(getLevel()+1));
	m_TextBalls->SetText("Balls: "+std::to_string(getBalls()));
	m_TextScore->SetText("Score: "+std::to_string(getScore()));

	return false;
}



//camera helper function
bool FindPaddle(Entity* Ent)
{
	if(dynamic_cast<Paddle*>(Ent))
		return true;
	return false;
}



void Game::CameraSetup_Above()
{
	if(!m_EntityManager.FindEntity(FindPaddle))
		THROW(Exception() << ex_message("No paddle found"));

	auto trans = glm::translate(glm::mat4(), glm::vec3(0, -6, 0));
	//glTranslatef(0, -6, 0);
	
	Vector3f Position, LookAt(0, 12, -12);

	//find the paddle
	Position=m_EntityManager.FindEntity(FindPaddle)->getPosition();
	Position.x*=-0.8f;
	Position.z+=8;
	Position.y-=16;

	auto look = glm::lookAt(glm::vec3(Position.x, Position.y, Position.z), glm::vec3(LookAt.x, LookAt.y, LookAt.z), glm::vec3(0, 0, 1));

	auto matrix = trans * look;
	glMatrixMode(GL_MODELVIEW);
	glLoadMatrixf(glm::value_ptr(matrix));
}

void Game::CameraSetup_Behind()
{
	if(!m_EntityManager.FindEntity(FindPaddle))
		THROW(Exception() << ex_message("No paddle found"));

	Vector3f Position(0, -12, 10);
	Vector3f LookAt(m_EntityManager.FindEntity(FindPaddle)->getPosition());
	LookAt.y=12;

	auto look = glm::lookAt(glm::vec3(Position.x, Position.y, Position.z), glm::vec3(LookAt.x, LookAt.y, LookAt.z), glm::vec3(0, 0, 1));
	glMatrixMode(GL_MODELVIEW);
	glLoadMatrixf(glm::value_ptr(look));
}


void Game::CameraSetup_Free()
{
	glMatrixMode(GL_MODELVIEW);
	//glTranslatef(0, -6, -24);

	auto look = glm::lookAt(glm::vec3(0, -7, 20), glm::vec3(0, 4, 0), glm::vec3(0, 0, 1));
	glLoadMatrixf(glm::value_ptr(look));

}

void Game::SetScreenSize(int width, int height)
{
	m_TextManager.SetScreenSize({width, height});
	CenterText(m_TextStartup, {width, height});
	CenterText(m_TextLaunch, {width, height});
	CenterText(m_TextLose, {width, height});
}

void Game::AddSound(string name)
{
	vector<unique_ptr<SoundFile>> sounds;
	if(FileIsOpenable(ResourceDir+"Sounds/"+m_Config.GetValue<string>("snd_"+name))) // load a single sound
	{
		sounds.push_back(
			make_unique<SoundFile>(
				m_SoundManager.LoadSound(ResourceDir+"Sounds/"+m_Config.GetValue<string>("snd_"+name))));
	}
	else // load a variety of sounds
	{
		int i = 1;
		auto snd_fn = [&](int i){ return ResourceDir+"Sounds/"+m_Config.GetValue<string>("snd_"+name)+to_string(i)+".ogg";};
		while(FileIsOpenable(snd_fn(i)))
		{
			sounds.push_back(
				make_unique<SoundFile>(
					m_SoundManager.LoadSound(snd_fn(i))));
			i += 1;
		}
		if(sounds.empty())
			throw Exception() << ex_message("No sounds loaded: "+name);

	}
	m_Sounds[name] = move(sounds);
}

void Game::PlaySound(string name, int number)
{
	if(m_Sounds.find(name) == m_Sounds.end())
		throw Exception() << ex_message("Sound not found: "+name);
	auto& sounds = m_Sounds[name];

	if(number == -1)
	{
		auto dist = uniform_int_distribution<>(0, sounds.size()-1);
		number = dist(rnd);
	}
	if(number >= sounds.size())
		throw Exception() << ex_message("Invalid sound number: "+name+"  "+std::to_string(number));

	sounds[number]->Play({false, m_Volume, false});
}
