#include "game.h"
#include <iostream>

// Constructor
Game::Game(SDL_Renderer *renderer, Screen *screen, Resource *resource, Asset *asset, options_t *options, Input *input, section_t *section, Debug *debug)
{
	// Copia los punteros
	this->resource = resource;
	this->renderer = renderer;
	this->asset = asset;
	this->screen = screen;
	this->input = input;
	this->debug = debug;
	this->options = options;
	this->section = section;

	// Inicia algunas variables
	board.iniClock = SDL_GetTicks();
#ifdef DEBUG
	currentRoom = "03.room";
	const int x = 25;
	const int y = 13;
	spawnPoint = {x * 8, y * 8, 0, 0, 0, s_standing, SDL_FLIP_HORIZONTAL};
#else
	currentRoom = "03.room";
	const int x = 25;
	const int y = 13;
	spawnPoint = {x * 8, y * 8, 0, 0, 0, s_standing, SDL_FLIP_HORIZONTAL};
#endif

	// Crea los objetos
	cheevos = new Cheevos(screen, options);
	scoreboard = new ScoreBoard(renderer, resource, asset, options, &board);
	itemTracker = new ItemTracker();
	roomTracker = new RoomTracker();
	room = new Room(resource->getRoom(currentRoom), renderer, screen, asset, options, itemTracker, &board.items, false, debug);
	const std::string playerPNG = options->cheat.altSkin ? "player2.png" : "player.png";
	const std::string playerANI = options->cheat.altSkin ? "player2.ani" : "player.ani";
	const player_t player = {spawnPoint, playerPNG, playerANI, renderer, resource, asset, options, input, room, debug};
	this->player = new Player(player);
	eventHandler = new SDL_Event();
	text = new Text(resource->getOffset("smb2.txt"), resource->getTexture("smb2.png"), renderer);
	music = JA_LoadMusic(asset->get("game.ogg").c_str());
	deathSound = JA_LoadSound(asset->get("death.wav").c_str());
	stats = new Stats(asset->get("stats.csv"), asset->get("stats_buffer.csv"), options);

	// Crea la textura para poner el nombre de la habitación
	roomNameTexture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, GAMECANVAS_WIDTH, text->getCharacterSize() * 2);
	if (roomNameTexture == 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(roomNameTexture, SDL_BLENDMODE_BLEND);

	// Establece el destino de la textura
	roomNameRect = {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;
	ticksSpeed = 15;
	board.lives = 9;
#ifdef DEBUG
	board.lives = 9;
#endif
	board.items = 0;
	board.rooms = 1;
	board.music = true;
	board.jailEnabled = options->cheat.jailEnabled;
	setScoreBoardColor();
	roomTracker->addRoom(currentRoom);
	paused = false;
	blackScreen = false;
	blackScreenCounter = 0;
	totalItems = getTotalItems();
	initStats();
	stats->addVisit(room->getName());

	section->name = SECTION_PROG_GAME;
	section->subsection = 0;
}

Game::~Game()
{
	// Libera la memoria de los objetos
	delete cheevos;
	delete scoreboard;
	delete itemTracker;
	delete roomTracker;
	delete room;
	delete player;
	delete eventHandler;
	delete text;
	delete stats;

	SDL_DestroyTexture(roomNameTexture);

	JA_DeleteMusic(music);
	JA_DeleteSound(deathSound);
}

// Comprueba los eventos de la cola
void Game::checkEventHandler()
{
	// Comprueba los eventos que hay en la cola
	while (SDL_PollEvent(eventHandler) != 0)
	{
		// Evento de salida de la aplicación
		if (eventHandler->type == SDL_QUIT)
		{
			section->name = SECTION_PROG_QUIT;
			screen->setBorderColor(stringToColor(options->palette, "black"));
			break;
		}

		if (eventHandler->type == SDL_RENDER_DEVICE_RESET || eventHandler->type == SDL_RENDER_TARGETS_RESET)
		{
			reLoadTextures();
		}

		if ((eventHandler->type == SDL_KEYDOWN) and (eventHandler->key.repeat == 0))
		{
			switch (eventHandler->key.keysym.scancode)
			{
			case SDL_SCANCODE_ESCAPE:
				section->name = SECTION_PROG_TITLE;
				break;
#ifdef DEBUG
			case SDL_SCANCODE_G:
				debug->switchEnabled();
				options->cheat.invincible = 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:
				screen->showNotification("MAMA MIRA! SIN MANOS!");
				break;
#endif

			case SDL_SCANCODE_M:
				board.music = !board.music;
				board.music ? JA_ResumeMusic() : JA_PauseMusic();
				break;

			case SDL_SCANCODE_H:
				switchPause();
				break;

			case SDL_SCANCODE_B:
				screen->switchBorder();
				reLoadTextures();
				break;

			case SDL_SCANCODE_F:
				screen->switchVideoMode();
				reLoadTextures();
				break;

			case SDL_SCANCODE_F1:
				screen->setWindowSize(1);
				reLoadTextures();
				break;

			case SDL_SCANCODE_F2:
				screen->setWindowSize(2);
				reLoadTextures();
				break;

			case SDL_SCANCODE_F3:
				screen->setWindowSize(3);
				reLoadTextures();
				break;

			case SDL_SCANCODE_F4:
				screen->setWindowSize(4);
				reLoadTextures();
				break;

			case SDL_SCANCODE_F5:
				switchPalette();
				break;

			default:
				break;
			}
		}
	}
}

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

	while (section->name == SECTION_PROG_GAME)
	{
		update();
		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 > ticksSpeed)
	{
		// Actualiza el contador de ticks
		ticks = SDL_GetTicks();

		// Comprueba los eventos de la cola
		checkEventHandler();

#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();

		// Actualiza las notificaciones
		screen->updateNotifier();

#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->blit();
}

#ifdef DEBUG
// Pasa la información de debug
void Game::updateDebugInfo()
{
	debug->add("X = " + std::to_string((int)player->x) + ", Y = " + std::to_string((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, roomNameTexture, nullptr, &roomNameRect);
}

// 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), renderer, screen, asset, options, itemTracker, &board.items, board.jailEnabled, debug);

		// 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 (roomTracker->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();
			currentRoom = roomName;
			spawnPoint = 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 && blackScreenCounter > 17)
	{
		section->name = SECTION_PROG_GAME_OVER;
	}
}

// Mata al jugador
void Game::killPlayer()
{
	if (options->cheat.invincible)
	{
		return;
	}

	// Resta una vida al jugador
	if (!options->cheat.infiniteLives)
	{
		board.lives--;
	}

	// Actualiza las estadisticas
	stats->addDeath(room->getName());

	// Destruye la habitacion y el jugador
	delete room;
	delete this->player;

	// Sonido
	JA_PlaySound(deathSound);

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

	// Crea la nueva habitación y el nuevo jugador
	room = new Room(resource->getRoom(currentRoom), renderer, screen, asset, options, itemTracker, &board.items, board.jailEnabled, debug);
	const std::string playerPNG = options->cheat.altSkin ? "player2.png" : "player.png";
	const std::string playerANI = options->cheat.altSkin ? "player2.ani" : "player.ani";
	const player_t player = {spawnPoint, playerPNG, playerANI, renderer, resource, asset, options, input, room, debug};
	this->player = new Player(player);

	// Pone los objetos en pausa mientras esta la habitación en negro
	room->pause();
	this->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();
}

// Cambia la paleta
void Game::switchPalette()
{
	if (options->console)
	{
		std::cout << "** PALETTE SWITCH REQUESTED" << std::endl;
	}

	// Modifica la variable
	options->palette = (options->palette == p_zxspectrum) ? p_zxarne : p_zxspectrum;

	// Recarga las paletas
	room->reLoadPalette();
	player->reLoadPalette();
	scoreboard->reLoadPalette();

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

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

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

			player->resume();
			room->resume();
			screen->setBorderColor(room->getBorderColor());
		}
	}
}

// Dibuja la pantalla negra
void Game::renderBlackScreen()
{
	if (blackScreen)
	{
		screen->clean();
		screen->setBorderColor(stringToColor(options->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_t colorBorder = room->getBorderColor();

	const bool isBlack = colorAreEqual(colorBorder, stringToColor(options->palette, "black"));
	const bool isBrightBlack = colorAreEqual(colorBorder, stringToColor(options->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->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(totalItems * 0.9f) || options->cheat.jailEnabled; // 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.jailEnabled = true;
	}

	if (haveTheItems && isOnTheRoom && isOnTheDoor)
	{
		section->name = SECTION_PROG_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))
	{
		currentRoom = 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++;
	}

	if (counter == 100)
	{
		counter = 0;
		board.lives++;
		JA_PlaySound(deathSound);
	}
}

// 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, roomNameTexture);

	// Rellena la textura de color
	const color_t color = stringToColor(options->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()
{
	if (board.items == 1)
	{
		cheevos->unlockCheevo(2);
	}
}