#include "game.h"
#include <SDL2/SDL_blendmode.h> // for SDL_BLENDMODE_BLEND
#include <SDL2/SDL_error.h>		// for SDL_GetError
#include <SDL2/SDL_pixels.h>	// for SDL_PIXELFORMAT_RGBA8888
#include <SDL2/SDL_scancode.h>	// for SDL_SCANCODE_A, SDL_SCANCODE_D, SDL_...
#include <SDL2/SDL_timer.h>		// for SDL_GetTicks
#include <iostream>				// for basic_ostream, operator<<, cout, endl
#include <vector>				// for vector
#include "asset.h"				// for Asset
#include "cheevos.h"			// for Cheevos
#include "const.h"				// for PLAY_AREA_HEIGHT, GAMECANVAS_WIDTH
#include "debug.h"				// for Debug
#include "input.h"				// for Input, REPEAT_FALSE, inputs_e
#include "item_tracker.h"		// for ItemTracker
#include "jail_audio.h"			// for JA_PauseMusic, JA_PlaySound, JA_Resu...
#include "resource.h"			// for Resource, res_room_t
#include "room.h"				// for Room, room_t
#include "room_tracker.h"		// for RoomTracker
#include "screen.h"				// for Screen
#include "stats.h"				// for Stats
#include "text.h"				// for Text, TXT_CENTER, TXT_COLOR
#include "utils.h"				// for options_t, cheat_t, stringToColor
#include "options.h"
#include "notifier.h"
#include "global_inputs.h"
#include "global_events.h"

// Constructor
Game::Game()
	: screen_(Screen::get()),
	  renderer_(Screen::get()->getRenderer()),
	  asset_(Asset::get()),
	  input_(Input::get()),
	  resource_(Resource::get()),
	  debug_(Debug::get())
{
	// Inicia algunas variables
	board_.iniClock = SDL_GetTicks();
#ifdef DEBUG
	current_room_ = "03.room";
	const int x = 25;
	const int y = 13;
	spawn_point_ = {x * 8, y * 8, 0, 0, 0, s_standing, SDL_FLIP_HORIZONTAL};
	debug_->setEnabled(false);
#else
	current_room_ = "03.room";
	const int x = 25;
	const int y = 13;
	spawn_point_ = {x * 8, y * 8, 0, 0, 0, s_standing, SDL_FLIP_HORIZONTAL};
#endif

	// Crea los objetos
	cheevos_ = Cheevos::get();
	scoreboard_ = new Scoreboard(&board_);
	item_tracker_ = new ItemTracker();
	room_tracker_ = new RoomTracker();
	room_ = new Room(resource_->getRoom(current_room_), item_tracker_, &board_.items, false);
	const std::string playerPNG = options.cheats.alternate_skin == Cheat::CheatState::ENABLED ? "player2.png" : "player.png";
	const std::string playerANI = options.cheats.alternate_skin == Cheat::CheatState::ENABLED ? "player2.ani" : "player.ani";
	const player_t player = {spawn_point_, playerPNG, playerANI, room_};
	player_ = new Player(player);
	text_ = new Text(resource_->getOffset("smb2.txt"), resource_->getTexture("smb2.png"), renderer_);
	music_ = JA_LoadMusic(asset_->get("game.ogg").c_str());
	death_sound_ = JA_LoadSound(asset_->get("death.wav").c_str());
	stats_ = new Stats(asset_->get("stats.csv"), asset_->get("stats_buffer.csv"));

	// Crea la textura para poner el nombre de la habitación
	room_name_texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, GAMECANVAS_WIDTH, text_->getCharacterSize() * 2);
	if (room_name_texture_ == nullptr)
	{
		if (options.console)
		{
			std::cout << "Error: roomNameTexture could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
		}
	}

	// Establece el blend mode de la textura
	SDL_SetTextureBlendMode(room_name_texture_, SDL_BLENDMODE_BLEND);

	// Establece el destino de la textura
	room_name_rect_ = {0, PLAY_AREA_HEIGHT, GAMECANVAS_WIDTH, text_->getCharacterSize() * 2};

	// Pone el nombre de la habitación en la textura
	fillRoomNameTexture();

	// Inicializa el resto de variables
	ticks_ = 0;
	ticks_speed_ = 15;
	board_.lives = 9;
#ifdef DEBUG
	board_.lives = 9;
#endif
	board_.items = 0;
	board_.rooms = 1;
	board_.music = true;
	board_.jail_is_open = options.cheats.jail_is_open == Cheat::CheatState::ENABLED;
	setScoreBoardColor();
	room_tracker_->addRoom(current_room_);
	paused_ = false;
	black_screen_ = false;
	black_screen_counter_ = 0;
	total_items_ = getTotalItems();
	initStats();
	stats_->addVisit(room_->getName());
	cheevos_->enable(!options.cheats.enabled()); // Deshabilita los logros si hay trucos activados

	options.section.section = Section::GAME;
	options.section.subsection = Subsection::NONE;
}

Game::~Game()
{
	// Libera la memoria de los objetos
	delete scoreboard_;
	delete item_tracker_;
	delete room_tracker_;
	delete room_;
	delete player_;
	delete text_;
	delete stats_;

	SDL_DestroyTexture(room_name_texture_);

	JA_DeleteMusic(music_);
	JA_DeleteSound(death_sound_);
}

// Comprueba los eventos de la cola
void Game::checkEvents()
{
	SDL_Event event;
	while (SDL_PollEvent(&event))
	{
		globalEvents::check(event);

#ifdef DEBUG
		if (event.type == SDL_KEYDOWN && event.key.repeat == 0)
		{
			switch (event.key.keysym.scancode)
			{
			case SDL_SCANCODE_G:
				debug_->switchEnabled();
				options.cheats.invincible = static_cast<Cheat::CheatState>(debug_->getEnabled());
				board_.music = !debug_->getEnabled();
				board_.music ? JA_ResumeMusic() : JA_PauseMusic();
				break;

			case SDL_SCANCODE_R:
				resource_->reLoad();
				break;

			case SDL_SCANCODE_W:
				goToRoom(BORDER_TOP);
				break;

			case SDL_SCANCODE_A:
				goToRoom(BORDER_LEFT);
				break;

			case SDL_SCANCODE_S:
				goToRoom(BORDER_BOTTOM);
				break;

			case SDL_SCANCODE_D:
				goToRoom(BORDER_RIGHT);
				break;

			case SDL_SCANCODE_F6:
				Notifier::get()->show("ACHIEVEMENT UNLOCKED!", "I LIKE MY MULTICOLOURED FRIENDS", 2);
				break;

			case SDL_SCANCODE_F7:
				Notifier::get()->show("ACHIEVEMENT UNLOCKED!", "I LIKE MY MULTICOLOURED FRIENDS", 3);
				break;

			case SDL_SCANCODE_F8:
				Notifier::get()->show("JAILDESIGNER IS LOGGED IN", "", 4);
				break;

			case SDL_SCANCODE_F9:
				Notifier::get()->show("JAILDESIGNER IS LOGGED IN", "", 5);
				break;
			default:
				break;
			}
		}
#endif
	}
}

// Comprueba el teclado
void Game::checkInput()
{
	if (input_->checkInput(input_toggle_music, REPEAT_FALSE))
	{
		board_.music = !board_.music;
		board_.music ? JA_ResumeMusic() : JA_PauseMusic();
	}

	else if (input_->checkInput(input_pause, REPEAT_FALSE))
	{
		switchPause();
	}

	globalInputs::check();
}

// Bucle para el juego
void Game::run()
{
	JA_PlayMusic(music_);
	if (!board_.music)
	{
		JA_PauseMusic();
	}

	while (options.section.section == Section::GAME)
	{
		update();
		checkEvents();
		render();
	}

	JA_StopMusic();
}

// Actualiza el juego, las variables, comprueba la entrada, etc.
void Game::update()
{
	// Comprueba que la diferencia de ticks sea mayor a la velocidad del juego
	if (SDL_GetTicks() - ticks_ > ticks_speed_)
	{
		// Actualiza el contador de ticks
		ticks_ = SDL_GetTicks();

		// Comprueba el teclado
		checkInput();

#ifdef DEBUG
		debug_->clear();
#endif

		// Actualiza los objetos
		room_->update();
		player_->update();
		checkPlayerOnBorder();
		checkPlayerAndItems();
		checkPlayerAndEnemies();
		checkIfPlayerIsAlive();
		checkGameOver();
		checkEndGame();
		checkRestoringJail();
		checkSomeCheevos();
		scoreboard_->update();
		input_->update();

		updateBlackScreen();

		screen_->update();

#ifdef DEBUG
		updateDebugInfo();
#endif
	}
}

// Pinta los objetos en pantalla
void Game::render()
{
	// Prepara para dibujar el frame
	screen_->start();

	// Dibuja los elementos del juego en orden
	room_->renderMap();
	room_->renderEnemies();
	room_->renderItems();
	player_->render();
	renderRoomName();
	scoreboard_->render();
	renderBlackScreen();

#ifdef DEBUG
	// Debug info
	renderDebugInfo();
#endif

	// Actualiza la pantalla
	screen_->render();
}

#ifdef DEBUG
// Pasa la información de debug
void Game::updateDebugInfo()
{
	debug_->add("X = " + std::to_string(static_cast<int>(player_->x)) + ", Y = " + std::to_string(static_cast<int>(player_->y)));
	debug_->add("VX = " + std::to_string(player_->vx).substr(0, 4) + ", VY = " + std::to_string(player_->vy).substr(0, 4));
	debug_->add("STATE = " + std::to_string(player_->state));
}

// Pone la información de debug en pantalla
void Game::renderDebugInfo()
{
	if (!debug_->getEnabled())
	{
		return;
	}

	// Borra el marcador
	SDL_Rect rect = {0, 18 * BLOCK, PLAY_AREA_WIDTH, GAMECANVAS_HEIGHT - PLAY_AREA_HEIGHT};
	SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255);
	SDL_RenderFillRect(renderer_, &rect);

	// Pinta la rejilla
	SDL_SetRenderDrawColor(renderer_, 255, 255, 255, 32);
	for (int i = 0; i < PLAY_AREA_BOTTOM; i += 8)
	{ // Lineas horizontales
		SDL_RenderDrawLine(renderer_, 0, i, PLAY_AREA_RIGHT, i);
	}
	for (int i = 0; i < PLAY_AREA_RIGHT; i += 8)
	{ // Lineas verticales
		SDL_RenderDrawLine(renderer_, i, 0, i, PLAY_AREA_BOTTOM - 1);
	}

	// Pinta el texto
	debug_->setPos({1, 18 * 8});
	debug_->render();
}
#endif

// Escribe el nombre de la pantalla
void Game::renderRoomName()
{
	// Dibuja la textura con el nombre de la habitación
	SDL_RenderCopy(renderer_, room_name_texture_, nullptr, &room_name_rect_);
}

// Cambia de habitación
bool Game::changeRoom(std::string file)
{
	// En las habitaciones los limites tienen la cadena del fichero o un 0 en caso de no limitar con nada
	if (file == "0")
	{
		return false;
	}

	// Verifica que exista el fichero que se va a cargar
	if (asset_->get(file) != "")
	{
		// Elimina la habitación actual
		delete room_;
		room_ = nullptr;

		// Crea un objeto habitación nuevo a partir del fichero
		room_ = new Room(resource_->getRoom(file), item_tracker_, &board_.items, board_.jail_is_open);

		// Pone el nombre de la habitación en la textura
		fillRoomNameTexture();

		// Pone el color del marcador en función del color del borde de la habitación
		setScoreBoardColor();

		if (room_tracker_->addRoom(file))
		{
			// Incrementa el contador de habitaciones visitadas
			board_.rooms++;
			options.stats.rooms = board_.rooms;

			// Actualiza las estadisticas
			stats_->addVisit(room_->getName());
		}

		// Pasa la nueva habitación al jugador
		player_->setRoom(room_);

		return true;
	}

	return false;
}

// Comprueba si el jugador esta en el borde de la pantalla
void Game::checkPlayerOnBorder()
{
	if (player_->getOnBorder())
	{
		const std::string roomName = room_->getRoom(player_->getBorder());
		if (changeRoom(roomName))
		{
			player_->switchBorders();
			current_room_ = roomName;
			spawn_point_ = player_->getSpawnParams();
		}
	}
}

// Comprueba las colisiones del jugador con los enemigos
bool Game::checkPlayerAndEnemies()
{
	const bool death = room_->enemyCollision(player_->getCollider());
	if (death)
	{
		killPlayer();
	}
	return death;
}

// Comprueba las colisiones del jugador con los objetos
void Game::checkPlayerAndItems()
{
	room_->itemCollision(player_->getCollider());
}

// Comprueba si el jugador esta vivo
void Game::checkIfPlayerIsAlive()
{
	if (!player_->isAlive())
	{
		killPlayer();
	}
}

// Comprueba si ha terminado la partida
void Game::checkGameOver()
{
	if (board_.lives < 0 && black_screen_counter_ > 17)
	{
		options.section.section = Section::GAME_OVER;
	}
}

// Mata al jugador
void Game::killPlayer()
{
	if (options.cheats.invincible == Cheat::CheatState::ENABLED)
	{
		return;
	}

	// Resta una vida al jugador
	if (options.cheats.infinite_lives == Cheat::CheatState::DISABLED)
	{
		--board_.lives;
	}

	// Actualiza las estadisticas
	stats_->addDeath(room_->getName());

	// Invalida el logro de pasarse el juego sin morir
	cheevos_->invalidate(11);

	// Destruye la habitacion y el jugador
	delete room_;
	delete this->player_;

	// Sonido
	JA_PlaySound(death_sound_);

	// Pone la pantalla en negro un tiempo
	setBlackScreen();

	// Crea la nueva habitación y el nuevo jugador
	room_ = new Room(resource_->getRoom(current_room_), item_tracker_, &board_.items, board_.jail_is_open);
	const std::string playerPNG = options.cheats.alternate_skin == Cheat::CheatState::ENABLED ? "player2.png" : "player.png";
	const std::string playerANI = options.cheats.alternate_skin == Cheat::CheatState::ENABLED ? "player2.ani" : "player.ani";
	const player_t player = {spawn_point_, playerPNG, playerANI, room_};
	player_ = new Player(player);

	// Pone los objetos en pausa mientras esta la habitación en negro
	room_->pause();
	player_->pause();
}

// Recarga todas las texturas
void Game::reLoadTextures()
{
	if (options.console)
	{
		std::cout << "** RELOAD REQUESTED" << std::endl;
	}
	player_->reLoadTexture();
	room_->reLoadTexture();
	scoreboard_->reLoadTexture();
	text_->reLoadTexture();
}

// Establece la pantalla en negro
void Game::setBlackScreen()
{
	black_screen_ = true;
}

// Actualiza las variables relativas a la pantalla en negro
void Game::updateBlackScreen()
{
	if (black_screen_)
	{
		black_screen_counter_++;
		if (black_screen_counter_ > 20)
		{
			black_screen_ = false;
			black_screen_counter_ = 0;

			player_->resume();
			room_->resume();
			screen_->setBorderColor(room_->getBorderColor());
		}
	}
}

// Dibuja la pantalla negra
void Game::renderBlackScreen()
{
	if (black_screen_)
	{
		screen_->clean();
		screen_->setBorderColor(stringToColor(options.video.palette, "black"));
	}
}

// Pone el color del marcador en función del color del borde de la habitación
void Game::setScoreBoardColor()
{
	// Obtiene el color del borde
	const Color colorBorder = room_->getBorderColor();

	const bool isBlack = colorAreEqual(colorBorder, stringToColor(options.video.palette, "black"));
	const bool isBrightBlack = colorAreEqual(colorBorder, stringToColor(options.video.palette, "bright_black"));

	// Si el color del borde es negro o negro brillante cambia el texto del marcador a blanco
	board_.color = isBlack || isBrightBlack ? stringToColor(options.video.palette, "white") : colorBorder;
}

// Comprueba si ha finalizado el juego
bool Game::checkEndGame()
{
	const bool isOnTheRoom = room_->getName() == "THE JAIL";																		  // Estar en la habitación que toca
	const bool haveTheItems = board_.items >= int(total_items_ * 0.9f) || options.cheats.jail_is_open == Cheat::CheatState::ENABLED; // Con mas del 90% de los items recogidos
	const bool isOnTheDoor = player_->getRect().x <= 128;																			  // Y en la ubicación que toca (En la puerta)

	if (haveTheItems)
	{
		board_.jail_is_open = true;
	}

	if (haveTheItems && isOnTheRoom && isOnTheDoor)
	{
		// Comprueba los logros de completar el juego
		checkEndGameCheevos();

		options.section.section = Section::ENDING;
		return true;
	}

	return false;
}

// Obtiene la cantidad total de items que hay en el mapeado del juego
int Game::getTotalItems()
{
	int items = 0;
	std::vector<res_room_t> *rooms = new std::vector<res_room_t>;
	rooms = resource_->getAllRooms();

	for (auto room : *rooms)
	{
		items += room.room->items.size();
	}

	return items;
}

// Va a la habitación designada
void Game::goToRoom(int border)
{
	const std::string roomName = room_->getRoom(border);
	if (changeRoom(roomName))
	{
		current_room_ = roomName;
	}
}

// Pone el juego en pausa
void Game::switchPause()
{
	if (paused_)
	{
		player_->resume();
		room_->resume();
		scoreboard_->resume();
		paused_ = false;
	}
	else
	{
		player_->pause();
		room_->pause();
		scoreboard_->pause();
		paused_ = true;
	}
}

// Da vidas al jugador cuando está en la Jail
void Game::checkRestoringJail()
{
	if (room_->getName() != "THE JAIL" || board_.lives == 9)
	{
		return;
	}

	static int counter = 0;

	if (!paused_)
	{
		counter++;
	}

	// Incrementa el numero de vidas
	if (counter == 100)
	{
		counter = 0;
		board_.lives++;
		JA_PlaySound(death_sound_);

		// Invalida el logro de completar el juego sin entrar a la jail
		const bool haveTheItems = board_.items >= int(total_items_ * 0.9f);
		if (!haveTheItems)
		{
			cheevos_->invalidate(9);
		}
	}
}

// Inicializa el diccionario de las estadísticas
void Game::initStats()
{
	std::vector<res_room_t> *rooms = new std::vector<res_room_t>;
	rooms = resource_->getAllRooms();

	for (auto room : *rooms)
	{
		stats_->addDictionary(room.room->number, room.room->name);
	}

	stats_->init();
}

// Crea la textura con el nombre de la habitación
void Game::fillRoomNameTexture()
{
	// Pone la textura como destino de renderizado
	SDL_SetRenderTarget(renderer_, room_name_texture_);

	// Rellena la textura de color
	const Color color = stringToColor(options.video.palette, "white");
	SDL_SetRenderDrawColor(renderer_, color.r, color.g, color.b, 0xFF);
	SDL_RenderClear(renderer_);

	// Escribe el texto en la textura
	text_->writeDX(TXT_CENTER | TXT_COLOR, GAMECANVAS_CENTER_X, text_->getCharacterSize() / 2, room_->getName(), 1, room_->getBGColor());

	// Deja el renderizador por defecto
	SDL_SetRenderTarget(renderer_, nullptr);
}

// Comprueba algunos logros
void Game::checkSomeCheevos()
{
	// Logros sobre la cantidad de items
	if (board_.items == total_items_)
	{
		cheevos_->unlock(4);
		cheevos_->unlock(3);
		cheevos_->unlock(2);
		cheevos_->unlock(1);
	}
	else if (board_.items >= total_items_ * 0.75f)
	{
		cheevos_->unlock(3);
		cheevos_->unlock(2);
		cheevos_->unlock(1);
	}
	else if (board_.items >= total_items_ * 0.5f)
	{
		cheevos_->unlock(2);
		cheevos_->unlock(1);
	}
	else if (board_.items >= total_items_ * 0.25f)
	{
		cheevos_->unlock(1);
	}

	// Logros sobre las habitaciones visitadas
	if (board_.rooms >= 60)
	{
		cheevos_->unlock(7);
		cheevos_->unlock(6);
		cheevos_->unlock(5);
	}
	else if (board_.rooms >= 40)
	{
		cheevos_->unlock(6);
		cheevos_->unlock(5);
	}
	else if (board_.rooms >= 20)
	{
		cheevos_->unlock(5);
	}
}

// Comprueba los logros de completar el juego
void Game::checkEndGameCheevos()
{
	// "Complete the game"
	cheevos_->unlock(8);

	// "Complete the game without entering the jail"
	cheevos_->unlock(9);

	// "Complete the game with all items"
	if (board_.items == total_items_)
	{
		cheevos_->unlock(10);
	}

	// "Complete the game without dying"
	cheevos_->unlock(11);

	// "Complete the game in under 30 minutes"
	if (scoreboard_->getMinutes() < 30)
	{
		cheevos_->unlock(12);
	}
}