#include <Game.h>
#include <vector>

#include <Util.h>

#include <SFML/Graphics.hpp>
#include <imgui-SFML.h>
#include <imgui.h>

#include <algorithm>
#include <fstream>
#include <iostream>

Ball::Ball(sf::Vector2f position_in, sf::Vector2f vel_in, sf::SoundBuffer& collideSound_in, sf::SoundBuffer& brickBreakSound_in, float volume)
	: collideSound(collideSound_in)
	, brickBreakSound(brickBreakSound_in)
	, pos(position_in)
	, velocity(vel_in) 
{ 
	collideSound.setVolume(volume);
	brickBreakSound.setVolume(volume);
}

Brick::Brick(sf::Vector2f position_in)
	: pos(position_in)
{ }

Player::Player(sf::Vector2f position_in)
	: pos(position_in)
{ }


Game::Game(bool useImgui_in)
	: window({sf::VideoMode({ 1920u, 1080u }), "project-breakout"})
	, font("assets/fonts/ChakraPetch-Regular.ttf")
	, lives(font)
	, score(font)
	, currentMult(font)
	, collideSoundBuffer("assets/sounds/hit2.wav")
	, brickBreakSoundBuffer("assets/sounds/hit1.wav")
	, failSoundBuffer("assets/sounds/fail.wav")
	, failSound(failSoundBuffer)
	, volume(10)
	, useImgui(useImgui_in)
{ 
	
	if (!ImGui::SFML::Init(window)) return;
	
	if (!parseConfigFile()) return;

	lives.setPosition({10, static_cast<float>(window.getSize().y - 40)});
	score.setPosition({10, static_cast<float>(window.getSize().y - 80)});
	currentMult.setPosition({10, static_cast<float>(window.getSize().y - 120)});

	window.setFramerateLimit(framerate);

	failSound.setVolume(volume);

	player.pos = playerStartPos;
	playerHalfSize = playerSize / 2.f;
	brickHalfSize = brickSize / 2.f;

	tempCircle.setOrigin({ballRadius, ballRadius});
	ballMaxSpeed /= numPhysicsUpdates;

	bricks.reserve(totalBricks);
	specialBricks.reserve(totalSpecialBricks);
	balls.reserve(10);
	ballsToAdd.reserve(10);

	setupLevel();

	ballsToAdd.emplace_back(playerStartPos.x, playerStartPos.y - 50);
}

void Game::setupLevel()
{
	const int specialBrickInterval = totalBricks / totalSpecialBricks;

	constexpr int xSpacing = 20;
	constexpr int ySpacing = 20;
	const int xOffset = (int)brickSize.x + xSpacing;
	const int yOffset = (int)brickSize.y + ySpacing;
	const int numRows = window.getSize().x / xOffset;
	const int numColumns = (window.getSize().y - 300)/ yOffset;
	int row{};
	int column{};

	for (int i = 0; i < totalBricks; i++)
	{
		if (row >= numRows)
		{
			row = 0;
			column++;
		}

		if (column >= numColumns)
			break;

		if (i % specialBrickInterval == 0) 
		{
			specialBricks.emplace_back(sf::Vector2f{(row * xOffset) + xSpacing + brickHalfSize.x, (column * yOffset) + ySpacing + brickHalfSize.y});
		}
		else 
		{
			bricks.emplace_back(sf::Vector2f{(row * xOffset) + xSpacing + brickHalfSize.x, (column * yOffset) + ySpacing + brickHalfSize.y});
		}
		row++;
	}
}

bool Game::parseConfigFile()
{
	std::ifstream configFile{"config/game-config.txt"};

	if(!configFile)
	{
		std::cerr << "ERROR: CANNOT OPEN 'game-config.txt'\n";
		return false;
	}

	std::string inputBuff;
	while(configFile >> inputBuff)
	{
		if (inputBuff == "framerate") 
			configFile >> framerate;

		if (inputBuff == "ballRadius") 
			configFile >> ballRadius;

		if (inputBuff == "playerSpeed") 
			configFile >> playerSpeed;

		if (inputBuff == "brickCount") 
			configFile >> totalBricks;

		if (inputBuff == "specialBrickCount") 
			configFile >> totalSpecialBricks;

		if (inputBuff == "brickSize") 
			configFile >> brickSize.x >> brickSize.y;

		if (inputBuff == "playerSize") 
			configFile >> playerSize.x >> playerSize.y;

		if (inputBuff == "ballColor") 
			configFile >> ballColor.r >> ballColor.g >> ballColor.b;

		if (inputBuff == "playerStartPos") 
			configFile >> playerStartPos.x >> playerStartPos.y;

		if (inputBuff == "brickColor") 
			configFile >> brickColor.r >> brickColor.g >> brickColor.b;

		if (inputBuff == "playerColor") 
			configFile >> playerColor.r >> playerColor.g >> playerColor.b;

		if (inputBuff == "specialBrickColor") 
			configFile >> specialBrickColor.r >> specialBrickColor.g >> specialBrickColor.b;

		if (inputBuff == "ballSpeed")
			configFile >> ballMaxSpeed;
	}

	if (totalSpecialBricks > totalBricks)
		totalSpecialBricks = totalBricks;

	return true;
}

void Game::input()
{
	while (const std::optional event = window.pollEvent())
	{
		ImGui::SFML::ProcessEvent(window, *event);

		if (event->is<sf::Event::Closed>())
		{
			window.close();
		}

		if (const auto* keyPressed = event->getIf<sf::Event::KeyPressed>())
		{
			switch (keyPressed->scancode) 
			{
				case sf::Keyboard::Scan::Escape:
					window.close();
					break;
				
				case sf::Keyboard::Scan::Left:
				case sf::Keyboard::Scan::A:
					player.left = true;
					break;

				case sf::Keyboard::Scan::Right:
				case sf::Keyboard::Scan::D:
					player.right = true;
					break;

				case sf::Keyboard::Scan::R:
					resetGame();
					break;
					
				default:
					break;
			}
		}

		if (const auto* keyReleased = event->getIf<sf::Event::KeyReleased>())
		{
			switch (keyReleased->scancode) 
			{
				case sf::Keyboard::Scan::Left:
				case sf::Keyboard::Scan::A:
					player.left = false;
					break;

				case sf::Keyboard::Scan::Right:
				case sf::Keyboard::Scan::D:
					player.right = false;
					break;

				default:
					break;
			}
		}

		// if (const auto* MouseButtonPressed = event->getIf<sf::Event::MouseButtonPressed>())
		// {
		// 	switch (MouseButtonPressed->button) 
		// 	{
		// 		case sf::Mouse::Button::Left:
		// 			//specialBricks.emplace_back((sf::Vector2f)MouseButtonPressed->position);
		// 			break;

		// 		default:
		// 			break;
		// 	}

		// }
	}	
}

void Game::collision()
{
	
	checkBallCollision();
	
	//player wall collide
	const sf:: FloatRect playerBounds = {player.pos, playerHalfSize};
	if (playerBounds.position.x + playerBounds.size.x > window.getSize().x)
	{
		player.pos.x = window.getSize().x - playerHalfSize.x;
	}
	
	if (playerBounds.position.x < playerHalfSize.x)
	{
		player.pos.x = playerHalfSize.x;
	}
}

void Game::checkBallCollision()
{
	for (auto& ball : balls)	
	{
		ball.collided = false;
		ball.brokeBrick = false;
		const sf::FloatRect ballBounds = {ball.pos, {ballRadius, ballRadius}};
		const sf::FloatRect previousBallBounds = {ball.previousPos, {ballRadius, ballRadius}};

		for (auto& brick : bricks)
		{
			const sf::FloatRect brickBounds = {brick.pos, brickHalfSize};
			const auto intersect = getOverlap(ballBounds, brickBounds);
			const auto previousIntersect = getOverlap(previousBallBounds, brickBounds);

			// coming from x directon
			if (intersect.x > 0 && previousIntersect.x <= 0 && intersect.y > 0)
			{
				ball.collided = true;
				ball.brokeBrick = true;
				brick.alive = false;
				ball.velocity.x *= -1;

				if (ball.pos.x < brick.pos.x)
				{// from the left
					ball.pos.x -= intersect.x;
				}
				else 
				{// from the right
					ball.pos.x += intersect.x;
				}
			}

			// coming from y direction
			if (intersect.y > 0 && previousIntersect.y <= 0 && intersect.x > 0)
			{
				ball.collided = true;
				ball.brokeBrick = true;
				brick.alive = false;
				ball.velocity.y *= -1;

				if (ball.pos.y < brick.pos.y)
				{// from the top
					ball.pos.y -= intersect.y;
				}
				else 
				{// from the bottom
					ball.pos.y += intersect.y;
				}
			}
		}

		for (auto& brick : specialBricks)
		{
			const sf::FloatRect brickBounds = {brick.pos, brickHalfSize};
			const auto intersect = getOverlap(ballBounds, brickBounds);
			const auto previousIntersect = getOverlap(previousBallBounds, brickBounds);
			bool spawnExtraBall{false};

			// coming from x direction
			if (intersect.x > 0 && previousIntersect.x <= 0 && intersect.y > 0)
			{
				ball.collided = true;
				ball.brokeBrick = true;
				brick.alive = false;
				spawnExtraBall = true;
				ball.velocity.x *= -1;

				if (ball.pos.x < brick.pos.x)
				{// from the left
					ball.pos.x -= intersect.x;
				}
				else 
				{// from the right
					ball.pos.x += intersect.x;
				}
			}

			// coming from y direction
			if (intersect.y > 0 && previousIntersect.y <= 0 && intersect.x > 0)
			{
				ball.collided = true;
				ball.brokeBrick = true;
				brick.alive = false;
				spawnExtraBall = true;
				ball.velocity.y *= -1;

				if (ball.pos.y < brick.pos.y)
				{// from the top
					ball.pos.y -= intersect.y;
				}
				else 
				{// from the bottom
					ball.pos.y += intersect.y;
				}
			}

			if (spawnExtraBall)
			{
				ballsToAdd.emplace_back(ball.pos);
			}
		}

		//wall collide
		if (ballBounds.position.y + ballBounds.size.y > window.getSize().y)
		{
			ball.alive = false;
		}

		if (ball.pos.x - ballRadius <= 0)
		{
			ball.collided = true;
			ball.pos.x = ballRadius;
			ball.velocity.x *= -1;
		}

		if (ball.pos.x + ballRadius >= window.getSize().x)
		{
			ball.collided = true;
			ball.pos.x = window.getSize().x - ballRadius;
			ball.velocity.x *= -1;
		}

		if (ball.pos.y - ballRadius < 0)
		{
			ball.collided = true;
			ball.pos.y = ballRadius;
			ball.velocity.y *= -1;
		}

		// player collide
		const sf:: FloatRect playerBounds = {player.pos, playerHalfSize};
		const sf:: FloatRect previousPlayerBounds = {player.previousPos, playerHalfSize};
		
		const auto intersect = getOverlap(ballBounds, playerBounds);
		const auto previousIntersect = getOverlap(previousBallBounds, previousPlayerBounds);

		// coming from x direction
		if (intersect.x > 0 && previousIntersect.x <= 0 && intersect.y > 0)
		{
			ball.collided = true;
			ball.velocity.x *= -1;
			if (ball.pos.x < player.pos.x)
			{// from the left
				ball.pos.x -= intersect.x;
			}
			else 
			{// from the right
				ball.pos.x += intersect.x;
			}
		}

		// coming from y direction
		if (intersect.y > 0 && previousIntersect.y <= 0 && intersect.x > 0)
		{
			ball.collided = true;
			ball.velocity.y *= -1;
			if (ball.pos.y < player.pos.y)
			{// from the top
				ball.pos.y -= intersect.y;
			}
			else if (ball.pos.y > player.pos.y)
			{// from the bottom
				ball.pos.y += intersect.y;
			}
		}

	}
}

void Game::updateEntities()
{
	balls.erase(std::remove_if(balls.begin(), balls.end(), [](const Ball& ball){ return !ball.alive; }), balls.end());
	bricks.erase(std::remove_if(bricks.begin(), bricks.end(), [](const Brick& brick){ return !brick.alive; }), bricks.end());
	specialBricks.erase(std::remove_if(specialBricks.begin(), specialBricks.end(), [](const Brick& brick){ return !brick.alive; }), specialBricks.end());

	const auto limit = ballsToAdd.size();

	for (size_t i = 0; i < limit; i++)
	{
		balls.emplace_back(ballsToAdd[i], velocityInRandomDir(ballMaxSpeed), collideSoundBuffer, brickBreakSoundBuffer, volume);
		ballsToAdd[i] = {0,0};
	}

	ballsToAdd.clear();
}

void Game::checkEndGame()
{
	if (balls.empty())
	{
		player.lives--;
		ballsToAdd.emplace_back(sf::Vector2f{player.pos.x, player.pos.y - 50});
	}


	if (player.lives <= 0)
	{
		resetGame();
	}
}

void Game::movement()
{
	for (auto& ball : balls)
	{
		ball.previousPos = ball.pos;
		ball.pos += ball.velocity;
	}

	const int dir = player.right - player.left;

	player.previousPos = player.pos;
	player.pos.x += (playerSpeed / numPhysicsUpdates) * dir;
}

void Game::imgui()
{
	ImGui::SetNextWindowCollapsed(false, ImGuiCond_Once);
	ImGui::SetNextWindowSize({490,375}, ImGuiCond_Once);
	ImGui::SetNextWindowPos({26,29}, ImGuiCond_Once);
	if(!ImGui::Begin("menu"))
	{
		ImGui::End();
		return;
	}
	ImGui::Text("Controls:");
	ImGui::Indent();
	ImGui::Text("A/D or arrow keys: move left and right");
	ImGui::Text("R: reset game");
	ImGui::Unindent();

	if (ImGui::SliderFloat("Game Volume", &volume, 0, 100, "%.1f"))
	{
		for (auto& ball : balls)
		{
			ball.collideSound.setVolume(volume);
			ball.brickBreakSound.setVolume(volume);
			failSound.setVolume(volume);
		}
	}

	ImGui::Text("Level config:");
	if(ImGui::InputInt("Total bricks", &totalBricks))
	{
		if (totalBricks < 0)
			totalBricks = 0;

		if (totalSpecialBricks > totalBricks)
			totalSpecialBricks = totalBricks;
	}

	if(ImGui::InputInt("Special bricks", &totalSpecialBricks))
	{
		if (totalSpecialBricks < 0)
			totalSpecialBricks = 0;

		if (totalSpecialBricks > totalBricks)
			totalSpecialBricks = totalBricks;
	}

	if(ImGui::Button("Reload Level"))
	{
		resetGame();
	}

	ImGui::Text("Game config:");
	ImGui::SliderFloat("Player Speed", &playerSpeed, 1.f, 50.f);
	ImGui::SliderFloat("Ball Speed", &ballMaxSpeed, 1.f, 50.f);
	if (ImGui::SliderFloat("Player Length", &playerSize.x, 1.f, 600.f))
	{
		playerHalfSize.x = playerSize.x / 2;
	}

	ImGui::ColorEdit3("Player Color", playerColor.imgui());
	ImGui::ColorEdit3("Ball Color", ballColor.imgui());
	ImGui::ColorEdit3("Brick Color", brickColor.imgui());
	ImGui::ColorEdit3("Special Brick Color", specialBrickColor.imgui());  

	ImGui::End();
}

void Game::render()
{
		window.clear();
		
		lives.setString("Lives: " + std::to_string(player.lives));
		score.setString("Score: " + std::to_string(player.score));
		currentMult.setString("Mult: x" + std::to_string(player.mult));

		window.draw(lives);
		window.draw(score);
		window.draw(currentMult);

		tempRect.setOrigin(brickHalfSize);
		tempRect.setSize(brickSize);
		for (auto& brick : bricks)
		{
			tempRect.setPosition(brick.pos);
			tempRect.setFillColor(brickColor.sfml());
			window.draw(tempRect);
		}
		
		for (auto& brick : specialBricks)
		{
			tempRect.setPosition(brick.pos);
			tempRect.setFillColor(specialBrickColor.sfml());
			window.draw(tempRect);
		}
		
		tempRect.setOrigin(playerHalfSize);
		tempRect.setSize(playerSize);
		tempRect.setPosition(player.pos);
		tempRect.setFillColor(playerColor.sfml());
		window.draw(tempRect);

		tempCircle.setRadius(ballRadius);
		for (auto& ball : balls)
		{
			tempCircle.setPosition(ball.pos);
			tempCircle.setFillColor(ballColor.sfml());
			window.draw(tempCircle);
		}

		ImGui::SFML::Render(window);
		
		window.display();
}

void Game::soundSystem()
{
	for (auto& ball : balls)
	{
		if (ball.collided)
		{
			ball.collideSound.play();
		}
		
		if (ball.brokeBrick)
		{
			ball.brickBreakSound.play();
		}
		
		if (!ball.alive)
		{
			failSound.play();
		}
	}
}

void Game::scoreSystem()
{
	player.mult = Math::max<int>(balls.size(), 1); 
	for (auto& brick : bricks)
	{
		if (!brick.alive)
		{
			player.score += 100 * player.mult;
		}
	}

	for (auto& brick : specialBricks)
	{
		if (!brick.alive)
		{
			player.score += 100 * player.mult;
		}
	}
}

void Game::resetGame()
{
	bricks.clear();
	specialBricks.clear();
	balls.clear();
	ballsToAdd.clear();

	setupLevel();
		
	player.lives = 3;
	player.score = 0; 

	ballsToAdd.emplace_back(sf::Vector2f{player.pos.x, player.pos.y - 50});
}

void Game::run()
{
	while(window.isOpen())
	{		
		ImGui::SFML::Update(window, clock.restart());
		
		input();

		for (size_t i = 0; i <= numPhysicsUpdates; i++)
		{
			updateEntities();

			movement();
			
			collision();
			
			soundSystem();

			scoreSystem();
		}
		
		checkEndGame();

		if (useImgui)
		{
			imgui();
		}

		render();
		
	}

	ImGui::SFML::Shutdown();
}