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

#include <Util.h>

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

#include <fstream>
#include <iostream>

Game::Game(bool useImgui_in)
	: config()
	, score(font)
	, failSound(failSoundBuffer)
	, winSound(winSoundBuffer)
	, bgMusic(bgMusicBuffer)
	, eatSound(eatSoundBuffer)
	, moveSound(moveSoundBuffer)
	, useImgui(useImgui_in)
{
	if (!parseConfigFile())
	{
		std::cout << "ERROR: Could not parse config file\n";
		return;
	}

	window.create(sf::VideoMode(config.resolution), "project-snake", sf::Style::Resize | sf::Style::Close);

	if (!ImGui::SFML::Init(window))
	{
		std::cout << "ERROR: Could not open window\n";
		return;
	}

	config.headGridStartPos.x = Random::get(0u, config.gridCount - 1ull);
	config.headGridStartPos.y = Random::get(std::min(config.gridCount - 1ull, 3ull), config.gridCount - 1ull);

	gameBoundary.setFillColor(sf::Color::Transparent);
	gameBoundary.setOutlineColor(config.boundaryColor.sfml());
	setNewBounds();

	player.body.reserve(config.gridCount * config.gridCount);
	player.body.emplace_back(config.headGridStartPos, config.playerHeadColor);

	fruit.respawn(player.body, config.gridCount);
	fruit.color = config.fruitColor;

	if (!font.openFromFile("assets/fonts/ChakraPetch-Regular.ttf"))
	{
		std::cerr << "ERROR: Failed to load font!\n";
		return;
	}

	if (!failSoundBuffer.loadFromFile("assets/sounds/lose.ogg"))
	{
		std::cerr << "ERROR: Failed to load lose.ogg! Check file path.\n";
		return;
	}

	if (!winSoundBuffer.loadFromFile("assets/sounds/win.ogg"))
	{
		std::cerr << "ERROR: Failed to load lose.ogg! Check file path.\n";
		return;
	}

	if (!bgMusicBuffer.loadFromFile("assets/sounds/bgMusic.wav"))
	{
		std::cerr << "ERROR: Failed to load lose.ogg! Check file path.\n";
		return;
	}

	if (!eatSoundBuffer.loadFromFile("assets/sounds/eat.wav"))
	{
		std::cerr << "ERROR: Failed to load lose.ogg! Check file path.\n";
		return;
	}

	if (!moveSoundBuffer.loadFromFile("assets/sounds/move.wav"))
	{
		std::cerr << "ERROR: Failed to load lose.ogg! Check file path.\n";
		return;
	}

	failSound.setVolume(config.volume);
	winSound.setVolume(config.volume);
	bgMusic.setVolume(config.volume);
	eatSound.setVolume(config.volume);
	moveSound.setVolume(config.volume);

	bgMusic.setLooping(true);
	bgMusic.play();

	//uncomment to quickly test player movement
	/* 
	player.body.emplace_back(sf::Vector2i{config.headGridStartPos.x - 1, config.headGridStartPos.y});
	player.body.emplace_back(sf::Vector2i{config.headGridStartPos.x - 2, config.headGridStartPos.y});
	player.body.emplace_back(sf::Vector2i{config.headGridStartPos.x - 3, config.headGridStartPos.y});
	player.body.emplace_back(sf::Vector2i{config.headGridStartPos.x - 4, config.headGridStartPos.y});
 	 */

	tempRect.setSize(sf::Vector2f{config.nodeSize, config.nodeSize});
	tempRect.setOutlineThickness((config.nodeSize / 10.f) * -1);
	tempRect.setOutlineColor(sf::Color::Black);

	score.setPosition({10, static_cast<float>(window.getSize().y - 40)});
	score.setOutlineColor(sf::Color::Black);
	score.setOutlineThickness(-1.f);

	window.setFramerateLimit(config.framerate);
}

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 >> config.framerate;
			continue;
		}

		if (inputBuff == "tickRate")
		{
			configFile >> config.tickRate;
			config.tickRateBuffer = config.tickRate;
			continue;
		}

		if (inputBuff == "playerHeadColor")
		{
			configFile >> config.playerHeadColor.r >> config.playerHeadColor.g >> config.playerHeadColor.b;
			continue;
		}

		if (inputBuff == "playerBodyColor")
		{
			configFile >> config.playerBodyColor.r >> config.playerBodyColor.g >> config.playerBodyColor.b;
			continue;
		}

		if (inputBuff == "gridCount")
		{
			configFile >> config.gridCount;
			config.gridCountBuffer = static_cast<int>(config.gridCount);
			continue;
		}

		if (inputBuff == "resolution")
		{
			configFile >> config.resolution.x >> config.resolution.y;
			continue;
		}

		if (inputBuff == "boundaryColor")
		{
			configFile >> config.boundaryColor.r >> config.boundaryColor.g >> config.boundaryColor.b;
		}

		if (inputBuff == "fruitColor")
		{
			configFile >> config.fruitColor.r >> config.fruitColor.g >> config.fruitColor.b;
		}
	}

	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::Up:
				case sf::Keyboard::Scan::W:
					player.up = true;
					break;

				case sf::Keyboard::Scan::Down:
				case sf::Keyboard::Scan::S:
					player.down = 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;

				case sf::Keyboard::Scan::Up:
				case sf::Keyboard::Scan::W:
					player.up = false;
					break;

				case sf::Keyboard::Scan::Down:
				case sf::Keyboard::Scan::S:
					player.down = false;
					break;

				default:
					break;
			}
		}

		if (const auto* windowResized = event->getIf<sf::Event::Resized>())
		{
			setNewBounds();
		}
	}
}

void Game::collision()
{
	const size_t limit = player.body.size();
	for (size_t i = 1; i < limit; i++)
	{
		if (player.head().gridPos == player.body.at(i).gridPos)
		{
			state = GameState::lose;
			failSound.play();
			return;
		}
	}

	if (player.head().gridPos == fruit.gridPos)
	{
		player.body.emplace_back(player.body.back().previousGridPos, config.playerBodyColor);
		if (player.body.size() != (config.gridCount * config.gridCount))
		{
			eatSound.play();
			player.score += 100;
			fruit.respawn(player.body, config.gridCount);
		} 
		else 
		{
			state = GameState::win;
			winSound.play();
		}
	}

}

void Game::movement()
{
	if (player.left and !player.right and (player.facing == Direction::up or player.facing == Direction::down))
	{
		player.inputBuffer = Direction::left;
	}

	if (player.right and !player.left and (player.facing == Direction::up or player.facing == Direction::down))
	{
		player.inputBuffer = Direction::right;
	}

	if (player.up and !player.down and (player.facing == Direction::left or player.facing == Direction::right))
	{
		player.inputBuffer = Direction::up;
	}

	if (player.down and !player.up and (player.facing == Direction::left or player.facing == Direction::right))
	{
		player.inputBuffer = Direction::down;
	}

	if (frameCount % (config.framerate / config.tickRate) != 0) return;

	moveSound.play();

	SnakeNode& head = player.head();
	switch (player.inputBuffer)
	{
        case Direction::up:
			if (head.gridPos.y == 0) 
			{
				state = GameState::lose;
				failSound.play();
				return;
			}

			head.previousGridPos = head.gridPos;
			head.gridPos.y -= 1;
			player.facing = Direction::up;
			// std::cout << "up\n";
			break;

        case Direction::down:
			if (head.gridPos.y == config.gridCount - 1)
			{
				state = GameState::lose;
				failSound.play();
				return;
			}

			head.previousGridPos = head.gridPos;
			head.gridPos.y += 1;
			player.facing = Direction::down;
			// std::cout << "down\n";
			break;

        case Direction::left:
			if (head.gridPos.x == 0) 
			{
				state = GameState::lose;
				failSound.play();
				return;
			}

			head.previousGridPos = head.gridPos;
			head.gridPos.x -= 1;
			player.facing = Direction::left;
			// std::cout << "left\n";
			break;

        case Direction::right:
			if (head.gridPos.x == config.gridCount -1)
			{
				state = GameState::lose;
				failSound.play();
				return;
			}

			head.previousGridPos = head.gridPos;
			head.gridPos.x += 1;
			player.facing = Direction::right;
			// std::cout << "right\n";
			break;
	}

	const size_t limit = player.body.size();
	for (size_t i = 1; i < limit; i++)
	{
		player.body[i].previousGridPos = player.body[i].gridPos;
		player.body[i].gridPos = player.body[i-1].previousGridPos;
	}

}

void Game::imgui()
{
	ImGui::SetNextWindowCollapsed(false, ImGuiCond_Once);
	ImGui::SetNextWindowSize({369,374}, ImGuiCond_Once);
	ImGui::SetNextWindowPos({26,29}, ImGuiCond_Once);
	if(!ImGui::Begin("menu"))
	{
		ImGui::End();
		return;
	}
	ImGui::Text("Controls:");
	ImGui::Indent();
	ImGui::Text("W/A/S/D or arrow keys: move");
	ImGui::Text("R: reset game");
	ImGui::Text("Esc: close game");
	ImGui::Unindent();

	if (ImGui::SliderFloat("Game Volume", &config.volume, 0, 100, "%.1f"))
	{
		failSound.setVolume(config.volume);
		winSound.setVolume(config.volume);
		bgMusic.setVolume(config.volume);
		eatSound.setVolume(config.volume);
		moveSound.setVolume(config.volume);
	}

	if (ImGui::ColorEdit3("Snake head color", config.playerHeadColor.imgui()))
	{
		player.head().color = config.playerHeadColor;
	}

	if (ImGui::ColorEdit3("Snake body color", config.playerBodyColor.imgui()))
	{
		for (size_t i = 1; i < player.body.size(); i++)
		{
			player.body[i].color = config.playerBodyColor;
		}
	}

	if (ImGui::ColorEdit3("Boundary color", config.boundaryColor.imgui()))
	{
		gameBoundary.setOutlineColor(config.boundaryColor.sfml());
	}

	if (ImGui::ColorEdit3("Fruit color", config.fruitColor.imgui()))
	{
		fruit.color = config.fruitColor;
	}

	ImGui::Text("Game configuration: (press 'reload game' to apply)");
	ImGui::SliderInt("Moves per second", &config.tickRateBuffer, 1, 10);
	ImGui::SliderInt("Grid size", &config.gridCountBuffer, 2, 20);
	if (ImGui::Button("reload game"))
	{
		config.tickRate = config.tickRateBuffer;
		config.gridCount = config.gridCountBuffer;
		resetGame();
	}

	ImGui::End();
}

void Game::render()
{
		window.clear();

		for (auto& node : player.body)
		{
			tempRect.setFillColor(node.color.sfml());
			tempRect.setPosition(node.windowPos(gameBoundary.getPosition(), config.nodeSize));
			window.draw(tempRect);
		}

		tempRect.setFillColor(fruit.color.sfml());
		tempRect.setPosition(fruit.windowPos(gameBoundary.getPosition(), config.nodeSize));
		window.draw(tempRect);

		window.draw(gameBoundary);

		score.setString("Score: " + std::to_string(player.score));

		window.draw(score);

		ImGui::SFML::Render(window);

		window.display();
}

void Game::soundSystem()
{
	if (state != GameState::playing)
	{
		bgMusic.stop();
		eatSound.stop();
		moveSound.stop();
	}

}

void Game::resetGame()
{
	failSound.stop();
	winSound.stop();

	setNewBounds();

	config.headGridStartPos.x = Random::get(0ull, config.gridCount - 1ull);
	config.headGridStartPos.y = Random::get(std::min(config.gridCount - 1ull, 3ull), config.gridCount - 1ull);

	player.body.clear();
	player.body.emplace_back(config.headGridStartPos, config.playerHeadColor);
	player.facing = Direction::up;
	player.inputBuffer = {};
	player.score = 0;

	fruit.respawn(player.body, config.gridCount);

	frameCount = 0;

	state = GameState::playing;
	bgMusic.play();
}

void Game::failState()
{
	if (failSound.getStatus() == sf::Sound::Status::Stopped)
	{
		resetGame();
		return;
	}
}

void Game::winState()
{
	if (winSound.getStatus() == sf::Sound::Status::Stopped)
	{
		resetGame();
		return;
	}
}

void Game::setNewBounds()
{
	const sf::Vector2f newSize(window.getSize());
	const float minDimension = std::min(newSize.x, newSize.y);

	const sf::FloatRect visibleArea({0.f, 0.f} , newSize);
	window.setView(sf::View(visibleArea));

	// how big each node is
	// use integer division to cut off remainder
	config.nodeSize =  minDimension / (float)config.gridCount;
	const float boundaryThickness = config.nodeSize * 0.05f * -1;

	tempRect.setSize(sf::Vector2f{config.nodeSize, config.nodeSize});
	tempRect.setOutlineThickness((config.nodeSize / 10.f) * -1);

	const float boundarySize = (float)config.gridCount * config.nodeSize;

	// Center the grid in the window
	const float boundaryPosX = (newSize.x - boundarySize) / 2.f;
	const float boundaryPosY = (newSize.y - boundarySize) / 2.f;

	gameBoundary.setSize({boundarySize, boundarySize});
	gameBoundary.setPosition({boundaryPosX, boundaryPosY});
	gameBoundary.setOutlineThickness(boundaryThickness);

	score.setPosition({10, newSize.y - 40});
}



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

		frameCount++;

		input();

		if (state == GameState::playing)
		{
			movement();

			collision();

			soundSystem();
		}
		else if (state == GameState::lose)
		{
			failState();
		}
		else if (state == GameState::win)
		{
			winState();
		}

		imgui();

		render();

	}

	ImGui::SFML::Shutdown();
}