#include "player.h"
#include <SDL2/SDL_render.h> // for SDL_FLIP_HORIZONTAL, SDL_FLIP_NONE, SDL...
#include <SDL2/SDL_timer.h>	 // for SDL_GetTicks
#include <stdlib.h>			 // for rand
#include <algorithm>		 // for max, min
#include "animated_sprite.h" // for AnimatedSprite
#include "input.h"			 // for inputs_e
#include "param.h"			 // for param
#include "texture.h"		 // for Texture
#include "scoreboard.h"		 // for Texture
#include "options.h"

// Constructor
Player::Player(int id, float x, int y, bool demo, SDL_Rect *playArea, std::vector<std::shared_ptr<Texture>> texture, std::vector<std::vector<std::string> *> animations)
{
	// Reserva memoria para los objetos
	playerSprite = std::make_unique<AnimatedSprite>(texture[0], "", animations[0]);
	powerSprite = std::make_unique<AnimatedSprite>(texture[1], "", animations[1]);
	powerSprite->getTexture()->setAlpha(224);
	enterName = std::make_unique<EnterName>();

	// Rectangulo con la zona de juego
	this->playArea = playArea;

	// Establece la posición inicial del jugador
	defaultPosX = posX = x;
	defaultPosY = posY = y;

	// Establece los offsets para el sprite de PowerUp
	powerUpDespX = (powerSprite->getWidth() - playerSprite->getWidth()) / 2;
	powerSprite->setPosY(y - (powerSprite->getHeight() - playerSprite->getHeight()));

	// Inicializa variables
	this->id = id;
	this->demo = demo;
	statusPlaying = PlayerStatus::WAITING;
	scoreBoardPanel = 0;
	name = "";
	setRecordName(enterName->getName());
	init();
}

// Iniciador
void Player::init()
{
	// Inicializa variables de estado
	posX = defaultPosX;
	posY = defaultPosY;
	statusWalking = PlayerStatus::WALKING_STOP;
	statusFiring = PlayerStatus::FIRING_NO;
	invulnerable = true;
	invulnerableCounter = PLAYER_INVULNERABLE_COUNTER;
	powerUp = false;
	powerUpCounter = PLAYER_POWERUP_COUNTER;
	extraHit = false;
	coffees = 0;
	input = true;
	continueTicks = 0;
	continueCounter = 20;
	width = 30;
	height = 30;
	collider.r = 9;
	shiftColliders();
	velX = 0;
	velY = 0;
	baseSpeed = 1.5;
	score = 0;
	scoreMultiplier = 1.0f;
	cooldown = 10;

	// Establece la posición del sprite
	playerSprite->setPosX(posX);
	playerSprite->setPosY(posY);

	// Selecciona un frame para pintar
	playerSprite->setCurrentAnimation("stand");
}

// Actua en consecuencia de la entrada recibida
void Player::setInput(int input)
{
	switch (statusPlaying)
	{
	case PlayerStatus::PLAYING:
		setInputPlaying(input);
		break;

	case PlayerStatus::ENTERING_NAME:
		setInputEnteringName(input);
		break;

	default:
		break;
	}
}

// Procesa inputs para cuando está jugando
void Player::setInputPlaying(int input)
{
	switch (input)
	{
	case input_left:
		velX = -baseSpeed;
		setWalkingStatus(PlayerStatus::WALKING_LEFT);
		break;

	case input_right:
		velX = baseSpeed;
		setWalkingStatus(PlayerStatus::WALKING_RIGHT);
		break;

	case input_fire_center:
		setFiringStatus(PlayerStatus::FIRING_UP);
		break;

	case input_fire_left:
		setFiringStatus(PlayerStatus::FIRING_LEFT);
		break;

	case input_fire_right:
		setFiringStatus(PlayerStatus::FIRING_RIGHT);
		break;

	default:
		velX = 0;
		setWalkingStatus(PlayerStatus::WALKING_STOP);
		break;
	}
}

// Procesa inputs para cuando está introduciendo el nombre
void Player::setInputEnteringName(int input)
{
	switch (input)
	{
	case input_left:
		enterName->decPos();
		break;

	case input_right:
		enterName->incPos();
		break;

	case input_up:
		enterName->incIndex();
		break;

	case input_down:
		enterName->decIndex();
		break;

	case input_start:
		setRecordName(enterName->getName());
		break;

	default:
		break;
	}
	setRecordName(enterName->getName());
}

// Mueve el jugador a la posición y animación que le corresponde
void Player::move()
{
	if (isPlaying())
	{
		// Mueve el jugador a derecha o izquierda
		posX += velX;

		// Si el jugador abandona el area de juego por los laterales
		if ((posX < param.game.play_area.rect.x - 5) || (posX + width > playArea->w + 5))
		{
			// Restaura su posición
			posX -= velX;
		}

		// Actualiza la posición del sprite
		playerSprite->setPosX(getPosX());
		playerSprite->setPosY(posY);

		powerSprite->setPosX(getPosX() - powerUpDespX);
	}
	else if (isDying())
	{
		playerSprite->update();

		// Si el cadaver abandona el area de juego por los laterales
		if ((playerSprite->getPosX() < param.game.play_area.rect.x) || (playerSprite->getPosX() + width > playArea->w))
		{
			// Restaura su posición
			const float vx = playerSprite->getVelX();
			playerSprite->setPosX(playerSprite->getPosX() - vx);

			// Rebota
			playerSprite->setVelX(-vx);
		}

		// Si el cadaver abandona el area de juego por abajo
		if (playerSprite->getPosY() > param.game.play_area.rect.h)
		{
			setStatusPlaying(PlayerStatus::DIED);
		}
	}
}

// Pinta el jugador en pantalla
void Player::render()
{
	if (powerUp && isPlaying())
	{
		if (powerUpCounter > (PLAYER_POWERUP_COUNTER / 4) || powerUpCounter % 20 > 4)
		{
			powerSprite->render();
		}
	}

	if (isRenderable())
		playerSprite->render();
}

// Establece el estado del jugador cuando camina
void Player::setWalkingStatus(PlayerStatus status)
{
	// Si cambiamos de estado, reiniciamos la animación
	if (statusWalking != status)
	{
		statusWalking = status;
	}
}

// Establece el estado del jugador cuando dispara
void Player::setFiringStatus(PlayerStatus status)
{
	// Si cambiamos de estado, reiniciamos la animación
	if (statusFiring != status)
	{
		statusFiring = status;
	}
}

// Establece la animación correspondiente al estado
void Player::setAnimation()
{
	// Crea cadenas de texto para componer el nombre de la animación
	const std::string aWalking = statusWalking == PlayerStatus::WALKING_STOP ? "stand" : "walk";
	const std::string aFiring = statusFiring == PlayerStatus::FIRING_UP ? "centershoot" : "sideshoot";

	const SDL_RendererFlip flipWalk = statusWalking == PlayerStatus::WALKING_RIGHT ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
	const SDL_RendererFlip flipFire = statusFiring == PlayerStatus::FIRING_RIGHT ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;

	// Establece la animación a partir de las cadenas
	if (isPlaying())
	{
		if (statusFiring == PlayerStatus::FIRING_NO)
		{ // No esta disparando
			playerSprite->setCurrentAnimation(aWalking);
			playerSprite->setFlip(flipWalk);
		}
		else
		{ // Está disparando
			playerSprite->setCurrentAnimation(aWalking + "-" + aFiring);
			// Si dispara de lado, invierte el sprite segun hacia donde dispara
			// Si dispara recto, invierte el sprite segun hacia donde camina
			aFiring == "centershoot" ? playerSprite->setFlip(flipWalk) : playerSprite->setFlip(flipFire);
		}
	}
	else
	{
		playerSprite->setCurrentAnimation("death");
	}

	// Actualiza las animaciones de los sprites
	playerSprite->animate();

	// powerSprite->setFlip(flipWalk);
	powerSprite->animate();
}

// Obtiene el valor de la variable
int Player::getPosX() const
{
	return int(posX);
}

// Obtiene el valor de la variable
int Player::getPosY() const
{
	return posY;
}

// Obtiene el valor de la variable
int Player::getWidth() const
{
	return width;
}

// Obtiene el valor de la variable
int Player::getHeight() const
{
	return height;
}

// Indica si el jugador puede disparar
bool Player::canFire() const
{
	// Si el contador a llegado a cero, podemos disparar. En caso contrario decrementamos el contador
	return cooldown > 0 ? false : true;
}

// Establece el valor de la variable
void Player::setFireCooldown(int time)
{
	cooldown = time;
}

// Actualiza el valor de la variable
void Player::updateCooldown()
{
	if (cooldown > 0)
	{
		cooldown--;
		if (powerUp)
		{
			cooldown--;
		}
	}
	else
	{
		setFiringStatus(PlayerStatus::FIRING_NO);
	}
}

// Actualiza al jugador a su posicion, animación y controla los contadores
void Player::update()
{
	move();
	setAnimation();
	shiftColliders();
	updateCooldown();
	updatePowerUpCounter();
	updateInvulnerable();
	updateContinueCounter();
	updateScoreboard();
}

// Obtiene la puntuación del jugador
int Player::getScore() const
{
	return score;
}

// Asigna un valor a la puntuación del jugador
void Player::setScore(int score)
{
	this->score = score;
}

// Incrementa la puntuación del jugador
void Player::addScore(int score)
{
	if (isPlaying())
	{
		this->score += score;
	}
}

// Indica si el jugador está jugando
bool Player::isPlaying() const
{
	return statusPlaying == PlayerStatus::PLAYING;
}

// Indica si el jugador está continuando
bool Player::isContinue() const
{
	return statusPlaying == PlayerStatus::CONTINUE;
}

// Indica si el jugador está esperando
bool Player::isWaiting() const
{
	return statusPlaying == PlayerStatus::WAITING;
}

// Indica si el jugador está introduciendo su nombre
bool Player::isEnteringName() const
{
	return statusPlaying == PlayerStatus::ENTERING_NAME;
}

// Indica si el jugador está muriendose
bool Player::isDying() const
{
	return statusPlaying == PlayerStatus::DYING;
}

// Indica si el jugador ha terminado de morir
bool Player::hasDied() const
{
	return statusPlaying == PlayerStatus::DIED;
}

// Indica si el jugador ya ha terminado de jugar
bool Player::isGameOver() const
{
	return statusPlaying == PlayerStatus::GAME_OVER;
}

// Actualiza el panel del marcador
void Player::updateScoreboard()
{
	switch (statusPlaying)
	{

	case PlayerStatus::CONTINUE:
	{
		Scoreboard::get()->setContinue(getScoreBoardPanel(), getContinueCounter());
		break;
	}

	case PlayerStatus::ENTERING_NAME:
	{
		Scoreboard::get()->setRecordName(getScoreBoardPanel(), getRecordName());
		Scoreboard::get()->setSelectorPos(getScoreBoardPanel(), getRecordNamePos());
		break;
	}

	default:
		break;
	}
}

// Cambia el modo del marcador
void Player::setScoreboardMode(ScoreboardMode mode)
{
	if (!demo)
	{
		Scoreboard::get()->setMode(getScoreBoardPanel(), mode);
	}
}

// Establece el estado del jugador en el juego
void Player::setStatusPlaying(PlayerStatus value)
{
	statusPlaying = value;

	switch (statusPlaying)
	{
	case PlayerStatus::PLAYING:
	{
		statusPlaying = PlayerStatus::PLAYING;
		init();
		setScoreboardMode(ScoreboardMode::SCORE);
		break;
	}

	case PlayerStatus::CONTINUE:
	{
		// Inicializa el contador de continuar
		continueTicks = SDL_GetTicks();
		continueCounter = 9;
		enterName->init();
		setScoreboardMode(ScoreboardMode::CONTINUE);
		break;
	}

	case PlayerStatus::WAITING:
	{
		setScoreboardMode(ScoreboardMode::WAITING);
		break;
	}

	case PlayerStatus::ENTERING_NAME:
	{
		setScoreboardMode(ScoreboardMode::ENTER_NAME);
		break;
	}

	case PlayerStatus::DYING:
	{
		// Activa la animación de morir
		playerSprite->setAccelY(0.2f);
		playerSprite->setVelY(-6.6f);
		rand() % 2 == 0 ? playerSprite->setVelX(3.3f) : playerSprite->setVelX(-3.3f);
		break;
	}

	case PlayerStatus::DIED:
	{
		const auto nextPlayerStatus = IsEligibleForHighScore() ? PlayerStatus::ENTERING_NAME : PlayerStatus::CONTINUE;
		demo ? setStatusPlaying(PlayerStatus::WAITING) : setStatusPlaying(nextPlayerStatus);
		break;
	}

	case PlayerStatus::GAME_OVER:
	{
		setScoreboardMode(ScoreboardMode::GAME_OVER);
		break;
	}

	default:
		break;
	}
}

// Obtiene el estado del jugador en el juego
PlayerStatus Player::getStatusPlaying() const
{
	return statusPlaying;
}

// Obtiene el valor de la variable
float Player::getScoreMultiplier() const
{
	return scoreMultiplier;
}

// Establece el valor de la variable
void Player::setScoreMultiplier(float value)
{
	scoreMultiplier = value;
}

// Aumenta el valor de la variable hasta un máximo
void Player::incScoreMultiplier()
{
	scoreMultiplier += 0.1f;
	scoreMultiplier = std::min(scoreMultiplier, 5.0f);
}

// Decrementa el valor de la variable hasta un mínimo
void Player::decScoreMultiplier()
{
	scoreMultiplier -= 0.1f;
	scoreMultiplier = std::max(scoreMultiplier, 1.0f);
}

// Obtiene el valor de la variable
bool Player::isInvulnerable() const
{
	return invulnerable;
}

// Establece el valor del estado
void Player::setInvulnerable(bool value)
{
	invulnerable = value;
	invulnerableCounter = invulnerable ? PLAYER_INVULNERABLE_COUNTER : 0;
}

// Obtiene el valor de la variable
int Player::getInvulnerableCounter() const
{
	return invulnerableCounter;
}

// Establece el valor de la variable
void Player::setInvulnerableCounter(int value)
{
	invulnerableCounter = value;
}

// Monitoriza el estado
void Player::updateInvulnerable()
{
	if (invulnerable)
	{
		if (invulnerableCounter > 0)
		{
			invulnerableCounter--;
			invulnerableCounter % 8 > 3 ? playerSprite->getTexture()->setPalette(coffees) : playerSprite->getTexture()->setPalette(3);
		}
		else
		{
			setInvulnerable(false);
			playerSprite->getTexture()->setPalette(coffees);
		}
	}
}

// Obtiene el valor de la variable
bool Player::isPowerUp() const
{
	return powerUp;
}

// Establece el valor de la variable
void Player::setPowerUp()
{
	powerUp = true;
	powerUpCounter = PLAYER_POWERUP_COUNTER;
}

// Obtiene el valor de la variable
int Player::getPowerUpCounter() const
{
	return powerUpCounter;
}

// Establece el valor de la variable
void Player::setPowerUpCounter(int value)
{
	powerUpCounter = value;
}

// Actualiza el valor de la variable
void Player::updatePowerUpCounter()
{
	if ((powerUpCounter > 0) && (powerUp))
	{
		powerUpCounter--;
	}
	else
	{
		powerUp = false;
		// powerUpCounter = PLAYER_POWERUP_COUNTER;
	}
}

// Obtiene el valor de la variable
bool Player::hasExtraHit() const
{
	return extraHit;
}

// Concede un toque extra al jugador
void Player::giveExtraHit()
{
	extraHit = true;
	if (coffees < 2)
	{
		coffees++;
		playerSprite->getTexture()->setPalette(coffees);
	}
}

// Quita el toque extra al jugador
void Player::removeExtraHit()
{
	if (coffees > 0)
	{
		coffees--;
		setInvulnerable(true);
		playerSprite->getTexture()->setPalette(coffees);
	}

	extraHit = coffees == 0 ? false : true;
}

// Habilita la entrada de ordenes
void Player::enableInput()
{
	input = true;
}

// Deshabilita la entrada de ordenes
void Player::disableInput()
{
	input = false;
}

// Devuelve el número de cafes actuales
int Player::getCoffees() const
{
	return coffees;
}

// Obtiene el circulo de colisión
Circle &Player::getCollider()
{
	return collider;
}

// Actualiza el circulo de colisión a la posición del jugador
void Player::shiftColliders()
{
	collider.x = int(posX + (width / 2));
	collider.y = int(posY + (height / 2));
}

// Pone las texturas del jugador
void Player::setPlayerTextures(std::vector<std::shared_ptr<Texture>> texture)
{
	playerSprite->setTexture(texture[0]);
	powerSprite->setTexture(texture[1]);
}

// Obtiene el valor de la variable
int Player::getContinueCounter() const
{
	return continueCounter;
}

// Actualiza el contador de continue
void Player::updateContinueCounter()
{
	if (statusPlaying == PlayerStatus::CONTINUE)
	{
		const Uint32 ticksSpeed = 1000;

		if (SDL_GetTicks() - continueTicks > ticksSpeed)
		{
			decContinueCounter();
		}
	}
}

// Le asigna un panel en el marcador al jugador
void Player::setScoreBoardPanel(int panel)
{
	scoreBoardPanel = panel;
}

// Obtiene el valor de la variable
int Player::getScoreBoardPanel() const
{
	return scoreBoardPanel;
}

// Decrementa el contador de continuar
void Player::decContinueCounter()
{
	continueTicks = SDL_GetTicks();
	continueCounter--;
	if (continueCounter < 0)
	{
		setStatusPlaying(PlayerStatus::GAME_OVER);
	}
}

// Establece el nombre del jugador
void Player::setName(std::string name)
{
	this->name = name;
}

// Establece el nombre del jugador para la tabla de mejores puntuaciones
void Player::setRecordName(std::string recordName)
{
	this->recordName = recordName.substr(0, 8);
}

// Obtiene el nombre del jugador
std::string Player::getName() const
{
	return name;
}

// Obtiene el nombre del jugador para la tabla de mejores puntuaciones
std::string Player::getRecordName() const
{
	return recordName;
}

// Obtiene la posici´´on que se está editando del nombre del jugador para la tabla de mejores puntuaciones
int Player::getRecordNamePos() const
{
	if (enterName)
	{
		return enterName->getPos();
	}

	return 0;
}

// Establece el mando que usará para ser controlado
void Player::setController(int index)
{
	controllerIndex = index;
}

// Obtiene el mando que usa para ser controlado
int Player::getController() const
{
	return controllerIndex;
}

// Obtiene el "id" del jugador
int Player::getId() const
{
	return id;
}

// Indica si el jugador se puede dibujar
bool Player::isRenderable() const
{
	return isPlaying() || isDying();
}

// Comprueba si la puntuación entra en la tabla de mejores puntuaciones
bool Player::IsEligibleForHighScore()
{
	return score > options.game.hi_score_table.back().score;
}