#include "Engine.hpp"
#include "Utils.hpp"
#include <SFML/Graphics/RenderWindow.hpp>
#include <SFML/System/Sleep.hpp>
#include <SFML/System/Time.hpp>
#include <SFML/Window/Event.hpp>
#include <iostream>
#include <random>

const char* colorNames[] = {"czerwony", "niebieski", "zolty", "zielony"};

Engine::Engine(): currentPlayerIndex(0) {
	if (!this->dicerollBuffer.openFromFile("res/audio/wardoctor17_diceroll.ogg")) {
		std::cerr << "Nie udalo sie zaladowac efektu dzwiekowego rzutu kostka.\n\n";
		this->dicerollLoaded = false;
	}
		
	if (!this->pawnmoveBuffer.openFromFile("res/audio/PeteBarry_pawnmove.ogg")) {
		std::cerr << "Nie udalo sie zaladowac efektu dzwiekowego ruchu pionkow.\n\n";
		this->pawnmoveLoaded = false;
	}

	if (!this->tadaBuffer.openFromFile("res/audio/jimhancock_tada.ogg")) {
		std::cerr << "Nie udalo sie zaladowac efektu dzwiekowego wygranej.\n\n";
		this->tadaLoaded = false;
	}

	if (!this->invalidBuffer.openFromFile("res/audio/timgormly_incorrect.ogg")) {
		std::cerr << "Nie udalo sie zaladowac efektu dzwiekowego niepoprawnego wyboru.\n\n";
		this->invalidLoaded = false;
	}
}

void Engine::addPlayer(const std::string& name, unsigned int seed, short color) {
	this->players.emplace_back(name, seed, color);
}

void Engine::initializeGame() {
	int totalPawnsPerPlayer = 4; // 4 pionki na gracza
	for (auto& player: players) {
		player.initializePawns(totalPawnsPerPlayer);
		short playerColor = player.getColor();
		for (int i = 0; i < 4; i++) {
			// 3. najmniej znaczący bit informuje o zajęciu pola
			// Ponadto przyjmujemy, że jeśli tylko 0 miejsce jest
			// zajęte, to możemy wyświetlić 
			this->board.fields[getPawnInitialPosition(playerColor, i)][i] = playerColor + 4;
		}
	}
}

void Engine::startGame(Board board) {
	this->board = board;
	initializeGame();
}

void Engine::nextTurn() {

	this->turnCounter++;
	std::cout << "-------------------- \n"
			  << this->turnCounter << "\n"
			  << "-------------------- \n";

	Player& currentPlayer = players[currentPlayerIndex];
	std::cout << "Ruch gracza " << currentPlayer.getName()
			  << " (" << colorNames[currentPlayer.getColor()] << ").\n";

	// Rysuj pionki
	// ...
	this->board.drawPawns();
	this->board.render();

	// Rzut kostką
	if (dicerollLoaded) this->dicerollBuffer.play();
	int diceRoll = rollDice(currentPlayer.getSeed(), currentPlayer.getRollCount());
	std::cout << "Wylosowano " << diceRoll << ".\n";
	this->board.smartSleep(1000);

	// Wybieranie pionka
	bool pickAPlace = false;
	/*bool isMovePossible = checkPossibleMoves(currentPlayer.getColor(), 6) > 0;*/
	bool isMovePossible = checkPossibleMoves(currentPlayer.getColor(), diceRoll) > 0;
	// Wylosowanie szóstki to specjalny przypadek
	if (diceRoll == 6) {

		isMovePossible |= currentPlayer.pawnsAtBase > 0;

		if (isMovePossible) {

			std::string choice;
			bool pawnMoved = false;
			switch (currentPlayer.pawnsActive + currentPlayer.pawnsAtHome) {

				// 0 pionków na planszy - musi wyjść pionkiem
				case 0:
					std::cout << currentPlayer.getName() << " wychodzi pierwszym pionkiem z bazy.\n";
					this->spawnPiece(currentPlayer.getColor(), 0);
					this->pawnmoveBuffer.play();
					this->board.smartSleep(2000);
					break;

				// 1, 2, 3 pionki na planszy - może wyjść z bazy albo ruszyć pionek
				case 1: case 2: case 3:

					std::cout << "Co chcesz zrobic?\n";
					std::cout << "a) Wyjsc pionkiem z bazy\n"
							  << "b) Wykonac ruch wyciagnietym wczesniej pionkiem\n"
							  << "> ";

					while (true) {

						// getline() nie pozostawia nic w buforze wejściowym, dlatego nie musimy
						// aż tak na niego uważać, jak w przypadku cin'a.

						choice = this->board.asyncStrGetline();
						choice[0] |= 32;

						if (choice[0] == 'a') {

							pickAPlace = false;
							std::cout << currentPlayer.getName() << " wychodzi "
									  << currentPlayer.pawnsActive + 1 << ". pionkiem z bazy.\n";
							// Rusz pierwszy możliwy pionek w bazie:
							this->spawnPiece(currentPlayer.getColor(), this->getFirstPawnAtBase(currentPlayer.getColor()));
							this->pawnmoveBuffer.play();
							this->board.smartSleep(2000);
							break;

						} else if (choice[0] == 'b') {

							if (currentPlayer.pawnsActive > 1) {
								pickAPlace = true;
								break;
							} else {

								// Sprawdź, czy gracz może ruszyć jedyny pionek o tyle pól do przodu
								for (int i = 0; i < 4; i++) {
									short* relativePawns = currentPlayer.getRelativePawns();
									if (relativePawns[i] >= 0) {
										pawnMoved = this->movePiece(currentPlayer.getColor(), relPosToField(currentPlayer.getColor(), relativePawns[i]), diceRoll);
										if (pawnMoved) break;
									}
								}
								if (pawnMoved) {
									std::cout << currentPlayer.getName() << " rusza jedyny pionek "
											  << diceRoll << " pol do przodu.\n";
									this->pawnmoveBuffer.play();
									this->board.smartSleep(2000);
									break;
								} else {
									std::cout << "Ups! Nie mozesz tego zrobic!\n";
									std::cout << "Jedyna opcja to zagranie kolejnym pionkiem.\n";
									this->invalidBuffer.play();
									this->board.smartSleep(1000);
									// Zróbmy to, co w a)
									pickAPlace = false;
									std::cout << currentPlayer.getName() << " wychodzi "
											  << currentPlayer.pawnsActive + 1 << ". pionkiem z bazy.\n";
									// Rusz pierwszy możliwy pionek w bazie:
									this->spawnPiece(currentPlayer.getColor(), this->getFirstPawnAtBase(currentPlayer.getColor()));
									this->pawnmoveBuffer.play();
									this->board.smartSleep(2000);
									break;
							  	}

							}

						} else {
							std::cout << "Podaj jedna z wymienionych odpowiedzi!\n> ";
							this->invalidBuffer.play();
							this->board.smartSleep(1000);
						}
					}
					break;

				// 4 pionki na planszy - musi ruszyć pionek, o ile może
				case 4:
					pickAPlace = true;
					break;

			}

		}

	} else {

		// Jeśli nie wypadło 6, gracz musi ruszyć któryś z pionków
		bool pawnMoved = false;
		switch (currentPlayer.pawnsActive) {

			// 0 pionków na planszy
			case 0:
				// Gracz nie może ruszyć pionków, których nie ma.
				pickAPlace = false;
				break;

			// 1 pionek na planszy
			case 1:
				// Nie potrzebujemy, aby gracz wskazał jedyny pionek na planszy,
				// dlatego ruszamy go za niego.
				pickAPlace = false;
				
				for (int i = 0; i < 4; i++) {
					short* relativePawns = currentPlayer.getRelativePawns();
					if (relativePawns[i] >= 0)
						pawnMoved = this->movePiece(currentPlayer.getColor(), relPosToField(currentPlayer.getColor(), relativePawns[i]), diceRoll);
				}
				if (pawnMoved) {
					std::cout << currentPlayer.getName() << " rusza jedyny pionek "
							  << diceRoll << " pol do przodu.\n";
					this->pawnmoveBuffer.play();
					this->board.smartSleep(2000);
				}
				break;

			// 2, 3, 4 pionki na planszy
			case 2: case 3: case 4:
				// Gracz musi wybrać pionek
				pickAPlace = true;
				break;

		}

	}

	if (!isMovePossible) {
		pickAPlace = false;
		std::cout << "Niestety, nie mozna wykonac zadnego ruchu.\n";
		this->board.smartSleep(500);
	}

	if (pickAPlace) {
		std::cout << "Wybierz prosze pionek na planszy, ktorym chcesz wykonac ruch.\n";
	}

	// Użytkownik wybiera pionek na planszy, którym chce się ruszyć
	bool moveResult = false;
	short pickStatus = 0;
	short selectedField = -1;
	if (!pickAPlace) pickStatus = 8; // pomiń pętlę
	while (!moveResult && this->board.running()) {

		while (pickStatus != 8 && this->board.running()) {

			this->board.selectAField(pickStatus, selectedField);

		}

		if (pickAPlace) {
			moveResult = this->movePiece(currentPlayer.getColor(), selectedField, diceRoll);
			if (!moveResult && selectedField != -1) {
				std::cout << "Nie mozesz wykonac tego ruchu.\n";
				this->invalidBuffer.play();
				this->board.smartSleep(1000);
				// Jeżeli błąd nie jest wywołany wyłączeniem gry,
				// to poproś gracza o ponowny wybór ruchu
				if (this->board.running()) pickStatus = 0;
			}
		} else {
			// Nadpisz moveResult jeżeli gracz nie wybiera pionka
			moveResult = true;
		}
		
	}

	// Do debugowania zaznaczonego pola
	#ifdef DEBUG
		std::cout << "(Engine.cpp) selected: " << selectedField << "\n";
	#endif

	// Jeżeli gracz się ruszył, odtwórz dźwięk ruchu
	if (selectedField != -1) {
		this->pawnmoveBuffer.play();
		this->board.smartSleep(1000);
	}

	if (currentPlayer.hasWon()) {

		// Zostanie wykorzystane do zwiększenia liczby wygranych
		this->tadaBuffer.play();
		this->winnerNickname = currentPlayer.getName();
		announceWinner(currentPlayer);

	} else {

		currentPlayer.incrementRollCount();

		if (!currentPlayer.hasLeftBase && currentPlayer.getRollCount() % 3 != 0) {
			// Jeśli gracz nie wyszedł z bazy i nie wyrzucił kostki trzy razy pod rząd,
			// zaktualizuj liczbę losowań, ale pozwól mu spróbować ponownie.
		} else {
			// Jeśli gracz wyszedł z bazy, albo rzucił kostką trzy razy pod rząd,
			// daj szansę kolejnemu graczowi... pod warunkiem, że nie wyrzucono 6.
			if (diceRoll != 6) this->currentPlayerIndex = (this->currentPlayerIndex + 1) % this->players.size();
		}
		
	}

	this->board.smartSleep(1000);
	std::cout << "\n";
	return;
}

/**
 * @brief      Converts field to player's unique color.
 *
 * @param[in]  field  The field
 *
 * @return     Player's color.
 */
short Engine::fieldToColor(short field) {
	for (int i = 0; i < 4; i++) {
		if (this->board.fields[field][i] != 0) {
			return this->board.fields[field][i] - 4;
		}
	}
	return -1;
}

/**
 * @brief      Turn a field into the first pawn inside it.
 *
 * @param[in]  color  Player's color
 * @param[in]  field  Given field
 *
 * @return     Player's pawn ID, between 0 and 3.
 */
short Engine::fieldToFirstPawnId(short color, short field) {
	for (int i = 0; i < 4; i++) {
		if (this->board.fields[field][i] == color + 4) return i;
	}
	return -1;
}

/**
 * @brief      Check if a pawn can move.
 *
 * @param[in]  color      Player's color
 * @param[in]  pawnId     Player's unique pawn ID, ranges from 0-3
 * @param[in]  steps      Amount of steps
 *
 * @return     Returns either true or false, depending if the move is possible or not.
 */
bool Engine::isMoveLegal(short color, short pawnId, short steps) {
	// kopia tego, co jest w Engine::movePawn()
	short field = relPosToField(color, this->players[color].getRelativePawns()[pawnId]);
	// Jeśli ruch, który chcemy wykonać zbiłby pionki w chronionym polu, zwróć false.
	if (this->players[color].getRelativePawns()[pawnId] + steps < 44) {
		short newField = relPosToField(color, this->players[color].getRelativePawns()[pawnId] + steps);
		short occupyingColor = fieldToColor(newField);

		// Nie pozwól na stackowanie pionków w domku
		if (this->players[color].getRelativePawns()[pawnId] + steps > 39) {
			for (int i = 0; i < 4; i++) {
				if (this->players[color].getRelativePawns()[pawnId] + steps == this->players[color].getRelativePawns()[i]) return false;
			}
		}
		if (occupyingColor != -1 && relPosToField(occupyingColor, 0) == newField) return false;
	}
	for (int i = 0; i < 4; i++) {
		if (field == getPawnInitialPosition(color, i))
			return false;
	}
	if (this->board.fields[field][pawnId] == color + 4) {
		short relPos = fieldToRelPos(color, field);
		if (relPos + steps < 44) {
			short newField = relPosToField(color, relPos + steps);
			bool isNewFieldOccupied = false;
			short occupyingColor = -1;
			for (int j = 0; j < 4; j++) {
				if (this->board.fields[newField][j] != 0) {
					isNewFieldOccupied = true;
					occupyingColor = this->board.fields[newField][j] - 4;
					break;
				}
			}
			short takenPawns = -1;
			if (isNewFieldOccupied && color != occupyingColor) {
				// w zamian za takePawns()
				takenPawns = tryTakingPawns(occupyingColor, newField);
				if (takenPawns == -1) return false;
			}
			return true;

		}
		else return false;
	}
	return false;
}

/**
 * @brief      Iterates through player's pawns on the board to check if they can move.
 *
 * @param[in]  color      Player's color
 * @param[in]  steps      Amount of steps
 *
 * @return     Returns the number of pawns which can be moved given the step count.
 */
short Engine::checkPossibleMoves(short color, short steps) {
	short possibleMoves = 0;
	for (int i = 0; i < 4; i++) {
		// Nie sprawdzaj pionków, które nie są na planszy
		if (this->players[this->currentPlayerIndex].getRelativePawns()[i] == -1) continue;
		if (this->isMoveLegal(color, i, steps)) possibleMoves++;
	}
	return possibleMoves;
}

/**
 * @brief      Returns the first pawn at a player's base.
 *
 * @param[in]  color  Player's color
 *
 * @return     The first pawn at base, or -1 if no pawns at base.
 */
short Engine::getFirstPawnAtBase(short color) {
	for (int i = 0; i < 4; i++) {
		if (players[color].getRelativePawns()[i] == -1) {
			// -1 => pionek jest w bazie
			return i;
		}
	}
	return -1;
}

/**
 * @brief      Returns the first pawn at a player's base.
 *
 * @param[in]  color  Player's color
 *
 * @return     The first pawn at base, or -1 if no pawns at base.
 */
short Engine::getFirstPawnNotAtBase(short color) {
	for (int i = 0; i < 4; i++) {
		if (players[color].getRelativePawns()[i] != -1) {
			// -1 => pionek jest w bazie
			return i;
		}
	}
	return -1;
}

/**
 * @brief      Put the piece onto the board
 *
 * @param[in]  color   Player's color
 * @param[in]  pawnId  Unique pawn number
 *
 * @return     Returns true on success, false on failure
 */
short Engine::spawnPiece(short color, short pawnId) {
	short * relativePieces = players[color].getRelativePawns();
	short spawnField = relPosToField(color, 0);
	short existingPieceColor = fieldToColor(spawnField);
	// Zbij pionek, jeśli stoi na nim przeciwnik
	if (existingPieceColor != color && existingPieceColor != -1) {
		/*for (int i = 0; i < 4; i++) {
			if (this->board.fields[spawnField][i] != 0) players[existingPieceColor].sendToBase(i);
		}*/
		this->takePawns(existingPieceColor, spawnField);
	}
	if (relativePieces[pawnId] == -1) {
		players[color].sendToBoard(pawnId);
		this->board.fields[relPosToField(color, relativePieces[pawnId])][pawnId] = color + 4;
		this->board.fields[getPawnInitialPosition(color, pawnId)][pawnId] = 0;
		return true;
	}
	return false;
}

/**
 * @brief      Try taking pawns down
 *
 * @param[in]  field  Field inside field matrix
 *
 * @return     Pawns that can be taken down
 */
short Engine::tryTakingPawns(short color, short field) {
	// Spróbuj zbić pionki

	short counter = 0;

	// Nie można zbić pionka w polu startowym.
	if (field == relPosToField(color, 0)) {
		return -1;
	}
	for (int i = 0; i < 4; i++) {
		if (this->board.fields[field][i] > 0) counter++;
	}
	return counter;
}

/**
 * @brief      Take pawns down
 *
 * @param[in]  field  Field inside field matrix
 *
 * @return     Pawns taken down
 */
short Engine::takePawns(short color, short field) {
	// Zbij wszystkie możliwe pionki
	// usuwając je z pola $field, a następnie
	// przenosząc je do bazy.

	short counter = 0;

	// Nie można zbić pionka w polu startowym.
	if (field == relPosToField(color, 0)) {
		return -1;
	}
	for (int i = 0; i < 4; i++) {
		if (this->board.fields[field][i] > 0) {

			counter++;

			// Zidentyfikuj kolor na podstawie wartości w $fields
			short color = this->board.fields[field][i] - 4;

			// Zaktualizuj liczbę pionków gracza
			/*this->players[color].pawnsActive -= 1;
			this->players[color].pawnsAtBase += 1;*/
			players[color].sendToBase(i);

			// Ustaw pionek na odpowiednie miejsce w bazie
			this->board.fields[getPawnInitialPosition(color, i)][i] = color + 4;
			// i usuń go z planszy
			this->board.fields[field][i] = 0;

		}
	}
	return counter;
}

/**
 * @brief      Try moving a piece
 *
 * @param[in]  color  Player color
 * @param[in]  field  Selected field
 * @param[in]  steps  How many steps to move
 *
 * @return     Returns true on success, false on failure.
 */
bool Engine::movePiece(short color, short field, short steps) { // podajemy color dla sanity-checku, czy użytkownik nie chce ruszyć nie swojego pionka

	// Jeśli ruch, który chcemy wykonać zbiłby pionki w chronionym polu, zwróć false.
	if (fieldToRelPos(color, field) + steps < 44) {
		short newField = relPosToField(color, fieldToRelPos(color, field) + steps);
		short occupyingColor = fieldToColor(newField);
		if (occupyingColor != -1 && relPosToField(occupyingColor, 0) == newField) return false;
	}

	// Nie pozwól użytkownikowi wybrać (i w konsekwencji ruszyć) pionków w bazie
	for (int i = 0; i < 4; i++) {
		if (field == getPawnInitialPosition(color, i))
			return false;
	}

	for (int i = 0; i < 4; i++) {
		if (this->board.fields[field][i] == color + 4) {
			// Znaleźliśmy pionek do przesunięcia, teraz pytanie:
			// - czy możemy się ruszyć do przodu o $steps kroków
			short relPos = fieldToRelPos(color, field);
			if (relPos + steps < 44) {
				short newField = relPosToField(color, relPos + steps);
				bool isNewFieldOccupied = false;
				short occupyingColor = -1;

				// Nie pozwól na stackowanie pionków w domku!
				if (relPos + steps > 39) {
					for (int i = 0; i < 4; i++) {
						if (relPos + steps == this->players[color].getRelativePawns()[i]) return false;
					}
				}

				// - czy tam, gdzie chcemy przenieść pionek gracza
				// inny gracz nie ma swojego
				for (int j = 0; j < 4; j++) {
					if (this->board.fields[newField][j] != 0) {
						isNewFieldOccupied = true;
						occupyingColor = this->board.fields[newField][j] - 4;
						break;
					}
				}
				// - jeśli ma, musimy go (/je) zbić
				short takenPawns = -1;
				if (isNewFieldOccupied && color != occupyingColor) {
					takenPawns = takePawns(occupyingColor, newField);
					//   - uwaga! jeśli nie zbiliśmy żadnego pionka, to dlatego, ponieważ pionki są w chronionym polu!!!
					//     musimy to uszanować, dlatego kończymy metodę zwracając false
					if (takenPawns == -1) return false;
				}
				// - w końcu, dopisujemy pionek bieżącego gracza
				this->board.fields[newField][i] = color + 4;
				this->players[this->currentPlayerIndex].unsafeMovePiece(i, steps);
				// oraz usuwamy z bieżącego pola
				this->board.fields[field][i] = 0;
				// - po przesunięciu jednego z pionków kończymy pętlę, ponieważ
				// nie chcemy przesuwać kolejnych pionków
				return true;
			}
			// jeżeli nie jesteśmy w stanie przesunąć jednego pionka o $steps pozycji,
			// to nie będziemy mogli przesunąć żadnego innego pionka
			else return false;
		}
	}
	return false;
}

// Deterministycznie (lub nie) generuj kolejne rzuty kostką
int Engine::rollDice(unsigned int seed, unsigned int rollNumber) {
	// Podane ziarno gracza oraz numer losowania zostaną użyte
	// do wylosowania liczby od 1 do 6.
	std::mt19937 rng(seed + rollNumber);
	std::uniform_int_distribution<int> dist(1, 6);

	return dist(rng);
}

bool Engine::isGameOver() const {
	for (const auto& player: players) {
		if (player.hasWon()) {
			return true;
		}
	}
	return false;
}

void Engine::announceWinner(const Player& player) const {
	std::cout << "\n\n--------------------\n"
			  << "Koniec gry!\n"
			  << player.getName()
			  << " (" << colorNames[player.getColor()] << ") wygrywa!\n";
}