#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 "defines.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"
// #include "surface.h"

// Constructor
Game::Game()
	: board_(std::make_shared<ScoreboardData>(0, 9, 0, true, Color(0, 0, 0), SDL_GetTicks(), options.cheats.jail_is_open == Cheat::CheatState::ENABLED)),
	  scoreboard_(std::make_shared<Scoreboard>(board_)),
	  room_tracker_(std::make_shared<RoomTracker>()),
	  stats_(std::make_shared<Stats>(Asset::get()->get("stats.csv"), Asset::get()->get("stats_buffer.csv"))),
#ifdef DEBUG
	  current_room_("03.room"),
	  spawn_point_(PlayerSpawn(25 * BLOCK, 13 * BLOCK, 0, 0, 0, PlayerState::STANDING, SDL_FLIP_HORIZONTAL))
#else
	  current_room_("03.room"),
	  spawn_point_(PlayerSpawn(25 * BLOCK, 13 * BLOCK, 0, 0, 0, PlayerState::STANDING, SDL_FLIP_HORIZONTAL))
#endif
{
#ifdef DEBUG
	Debug::get()->setEnabled(false);
#endif

	// Crea objetos e inicializa variables
	ItemTracker::init();
	room_ = std::make_shared<Room>(current_room_, board_);
	initPlayer(spawn_point_, room_);
	initStats();
	total_items_ = getTotalItems();

	createRoomNameTexture();
	changeRoom(current_room_);

	Cheevos::get()->enable(!options.cheats.enabled()); // Deshabilita los logros si hay trucos activados
	Cheevos::get()->clearUnobtainableState();

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

Game::~Game()
{
	ItemTracker::destroy();
	SDL_DestroyTexture(room_name_texture_);
}

// Comprueba los eventos de la cola
void Game::checkEvents()
{
	SDL_Event event;
	while (SDL_PollEvent(&event))
	{
		globalEvents::check(event);
#ifdef DEBUG
		checkDebugEvents(event);
#endif
	}
}

// Comprueba el teclado
void Game::checkInput()
{
	if (Input::get()->checkInput(InputAction::TOGGLE_MUSIC, REPEAT_FALSE))
	{
		board_->music = !board_->music;
		board_->music ? JA_ResumeMusic() : JA_PauseMusic();
		Notifier::get()->show({"MUSIC " + std::string(board_->music ? "ENABLED" : "DISABLED")}, NotificationText::CENTER);
	}

	else if (Input::get()->checkInput(InputAction::PAUSE, REPEAT_FALSE))
	{
		togglePause();
		Notifier::get()->show({std::string(paused_ ? "GAME PAUSED" : "GAME RUNNING")}, NotificationText::CENTER);
	}

	globalInputs::check();
}

// Bucle para el juego
void Game::run()
{
	JA_PlayMusic(Resource::get()->getMusic("game.ogg"));
	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_ > GAME_SPEED)
	{
		// Actualiza el contador de ticks
		ticks_ = SDL_GetTicks();

		// Comprueba el teclado
		checkInput();

#ifdef DEBUG
		Debug::get()->clear();
#endif

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

		updateBlackScreen();

		Screen::get()->update();

#ifdef DEBUG
		updateDebugInfo();
#endif
	}
}

// Pinta los objetos en pantalla
void Game::render()
{
	// Prepara para dibujar el frame
	Screen::get()->start();
	// test_surface_->render(0, 0, 10, 10, 64, 64);

	// 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::get()->render();
}

#ifdef DEBUG
// Pasa la información de debug
void Game::updateDebugInfo()
{
	Debug::get()->add("X = " + std::to_string(static_cast<int>(player_->x_)) + ", Y = " + std::to_string(static_cast<int>(player_->y_)));
	Debug::get()->add("VX = " + std::to_string(player_->vx_).substr(0, 4) + ", VY = " + std::to_string(player_->vy_).substr(0, 4));
	Debug::get()->add("STATE = " + std::to_string(static_cast<int>(player_->state_)));
}

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

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

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

	// Pinta el texto
	Debug::get()->setPos({1, 18 * 8});
	Debug::get()->render();
}

// Comprueba los eventos
void Game::checkDebugEvents(const SDL_Event &event)
{
	if (event.type == SDL_KEYDOWN && event.key.repeat == 0)
	{
		switch (event.key.keysym.scancode)
		{
		case SDL_SCANCODE_G:
			Debug::get()->toggleEnabled();
			options.cheats.invincible = static_cast<Cheat::CheatState>(Debug::get()->getEnabled());
			board_->music = !Debug::get()->getEnabled();
			board_->music ? JA_ResumeMusic() : JA_PauseMusic();
			break;

		case SDL_SCANCODE_R:
			Resource::get()->reload();
			break;

		case SDL_SCANCODE_W:
			changeRoom(room_->getRoom(BORDER_TOP));
			break;

		case SDL_SCANCODE_A:
			changeRoom(room_->getRoom(BORDER_LEFT));
			break;

		case SDL_SCANCODE_S:
			changeRoom(room_->getRoom(BORDER_BOTTOM));
			break;

		case SDL_SCANCODE_D:
			changeRoom(room_->getRoom(BORDER_RIGHT));
			break;

		case SDL_SCANCODE_F6:
			Notifier::get()->show({"ACHIEVEMENT UNLOCKED!", "I LIKE MY MULTICOLOURED FRIENDS"}, NotificationText::LEFT, 2, false, "F6");
			break;

		case SDL_SCANCODE_F7:
			Notifier::get()->show({"ACHIEVEMENT UNLOCKED!", "I LIKE MY MULTICOLOURED FRIENDS"}, NotificationText::LEFT, 3, false, "F7");
			break;

		case SDL_SCANCODE_F8:
			Notifier::get()->show({"JAILDESIGNER", "IS LOGGED IN"}, NotificationText::LEFT, 4, false);
			break;

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

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

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

	// Verifica que exista el fichero que se va a cargar
	if (Asset::get()->get(room_path) != "")
	{
		// Crea un objeto habitación nuevo a partir del fichero
		room_ = std::make_shared<Room>(room_path, board_);

		// 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(room_path))
		{
			// 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_);

		// Cambia la habitación actual
		current_room_ = room_path;

		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();
			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::get()->setUnobtainable(11);

	// Sonido
	JA_PlaySound(Resource::get()->getSound("death.wav"));

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

	// Crea la nueva habitación y el nuevo jugador
	room_ = std::make_shared<Room>(current_room_, board_);
	initPlayer(spawn_point_, room_);

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

// 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_->setPaused(false);
			room_->setPaused(false);
			Screen::get()->setBorderColor(room_->getBorderColor());
		}
	}
}

// Dibuja la pantalla negra
void Game::renderBlackScreen()
{
	if (black_screen_)
	{
		Screen::get()->clean();
		Screen::get()->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;
	auto rooms = Resource::get()->getRooms();

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

	return items;
}

// Pone el juego en pausa
void Game::togglePause()
{
	paused_ = !paused_;

	player_->setPaused(paused_);
	room_->setPaused(paused_);
	scoreboard_->setPaused(paused_);
}

// 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(Resource::get()->getSound("death.wav"));

		// 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::get()->setUnobtainable(9);
		}
	}
}

// Inicializa el diccionario de las estadísticas
void Game::initStats()
{
	auto rooms = Resource::get()->getRooms();

	for (const 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(Screen::get()->getRenderer(), room_name_texture_);

	// Rellena la textura de color
	const Color color = stringToColor(options.video.palette, "white");
	SDL_SetRenderDrawColor(Screen::get()->getRenderer(), color.r, color.g, color.b, 0xFF);
	SDL_RenderClear(Screen::get()->getRenderer());

	// Escribe el texto en la textura
	auto text = Resource::get()->getText("smb2");
	text->writeDX(TEXT_CENTER | TEXT_COLOR, GAMECANVAS_CENTER_X, text->getCharacterSize() / 2, room_->getName(), 1, room_->getBGColor());

	// Deja el renderizador por defecto
	SDL_SetRenderTarget(Screen::get()->getRenderer(), nullptr);
}

// Comprueba algunos logros
void Game::checkSomeCheevos()
{
	auto cheevos = Cheevos::get();

	// 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()
{
	auto cheevos = Cheevos::get();

	// "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);
	}
}

// Inicializa al jugador
void Game::initPlayer(const PlayerSpawn &spawn_point, std::shared_ptr<Room> room)
{
	std::string player_texture = options.cheats.alternate_skin == Cheat::CheatState::ENABLED ? "player2.png" : "player.png";
	std::string player_animations = options.cheats.alternate_skin == Cheat::CheatState::ENABLED ? "player2.ani" : "player.ani";
	const PlayerData player(spawn_point, player_texture, player_animations, room);
	player_ = std::make_shared<Player>(player);
}

// Crea la textura para poner el nombre de la habitación
void Game::createRoomNameTexture()
{
	auto text = Resource::get()->getText("smb2");
	room_name_texture_ = SDL_CreateTexture(Screen::get()->getRenderer(), SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, GAMECANVAS_WIDTH, text->getCharacterSize() * 2);
	if (room_name_texture_ == nullptr)
	{
		if (options.console)
		{
			std::cout << "Error: room_name_texture_ 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};
}