#include "player.h"
#include <fstream>
#include <sstream>

// Constructor
Player::Player(player_t ini, std::string tileset, std::string animation, SDL_Renderer *renderer, Asset *asset, Input *input, Room *room, Debug *debug)
{
    // Obten punteros a objetos
    this->asset = asset;
    this->renderer = renderer;
    this->input = input;
    this->room = room;
    this->debug = debug;

    // Crea objetos
    texture = new LTexture(renderer, asset->get(tileset));
    sprite = new AnimatedSprite(texture, renderer, animation);

    // Inicializa variables
    color = stringToColor("white");
    onBorder = false;
    border = BORDER_TOP;
    invincible = true;
    alive = true;
    maxFallHeight = BLOCK * 4;
    paused = false;

    jumpIni = ini.jumpIni;
    state = ini.state;
    prevState = state;

    x = ini.x;
    y = ini.y;
    vx = ini.vx;
    vy = ini.vy;
    w = 8;
    h = 16;
    maxVY = 1.2f;

    sprite->setPosX(ini.x);
    sprite->setPosY(ini.y);
    sprite->setWidth(8);
    sprite->setHeight(16);

    sprite->setFlip(ini.flip);
    sprite->setCurrentAnimation("walk");
    sprite->animate();

    lastPosition = getRect();
    colliderBox = getRect();
    const SDL_Point p = {0, 0};
    colliderPoints.insert(colliderPoints.end(), {p, p, p, p, p, p, p, p});
    underFeet.insert(underFeet.end(), {p, p});
    feet.insert(feet.end(), {p, p});
    jumpSound.push_back(JA_LoadSound(asset->get("jump1.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump2.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump3.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump4.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump5.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump6.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump7.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump8.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump9.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump10.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump11.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump12.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump13.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump14.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump15.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump16.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump17.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump18.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump19.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump20.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump21.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump22.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump23.wav").c_str()));
    jumpSound.push_back(JA_LoadSound(asset->get("jump24.wav").c_str()));

    fallSound.push_back(JA_LoadSound(asset->get("jump11.wav").c_str()));
    fallSound.push_back(JA_LoadSound(asset->get("jump12.wav").c_str()));
    fallSound.push_back(JA_LoadSound(asset->get("jump13.wav").c_str()));
    fallSound.push_back(JA_LoadSound(asset->get("jump14.wav").c_str()));
    fallSound.push_back(JA_LoadSound(asset->get("jump15.wav").c_str()));
    fallSound.push_back(JA_LoadSound(asset->get("jump16.wav").c_str()));
    fallSound.push_back(JA_LoadSound(asset->get("jump17.wav").c_str()));
    fallSound.push_back(JA_LoadSound(asset->get("jump18.wav").c_str()));
    fallSound.push_back(JA_LoadSound(asset->get("jump19.wav").c_str()));
    fallSound.push_back(JA_LoadSound(asset->get("jump20.wav").c_str()));
    fallSound.push_back(JA_LoadSound(asset->get("jump21.wav").c_str()));
    fallSound.push_back(JA_LoadSound(asset->get("jump22.wav").c_str()));
    fallSound.push_back(JA_LoadSound(asset->get("jump23.wav").c_str()));
    fallSound.push_back(JA_LoadSound(asset->get("jump24.wav").c_str()));

    r = {0, 0, 0, 0};
    jumpCounter = 0;
    fallCounter = 0;
}

// Destructor
Player::~Player()
{
    delete texture;
    delete sprite;

    for (auto s : jumpSound)
    {
        JA_DeleteSound(s);
    }
}

// Pinta el jugador en pantalla
void Player::render()
{
    sprite->getTexture()->setColor(color.r, color.g, color.b);
    sprite->render();
    if (debug->getEnabled())
    {
        // Pinta los underfeet
        SDL_SetRenderDrawColor(renderer, 255, 0, 255, 255);
        SDL_RenderDrawPoint(renderer, underFeet[0].x, underFeet[0].y);
        SDL_RenderDrawPoint(renderer, underFeet[1].x, underFeet[1].y);

        // Pinta rectangulo del jugador
        SDL_SetRenderDrawColor(renderer, 0, 255, 0, 192);
        SDL_Rect rect = getRect();
        SDL_RenderFillRect(renderer, &rect);

        // Pinta el rectangulo de movimiento
        SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
        SDL_RenderFillRect(renderer, &r);
    }
}

// Actualiza las variables del objeto
void Player::update()
{
    if (paused)
    { // Si está en modo pausa no se actualiza nada
        return;
    }

    checkInput();        // Comprueba las entradas y modifica variables
    move();              // Recalcula la posición del jugador
    animate();           // Establece la animación del jugador
    checkBorders();      // Comprueba si está situado en alguno de los cuatro bordes de la habitación
    checkJumpEnd();      // Comprueba si ha finalizado el salto al alcanzar la altura de inicio
    checkKillingTiles(); // Comprueba que el jugador no toque ningun tile de los que matan
}

// Comprueba las entradas y modifica variables
void Player::checkInput()
{
    // Solo comprueba las entradas de dirección cuando está dsobre una superficie
    if (state != s_standing)
    {
        return;
    }

    if (input->checkInput(INPUT_LEFT, REPEAT_TRUE))
    {
        vx = -0.6f;
        sprite->setFlip(SDL_FLIP_HORIZONTAL);
    }

    else if (input->checkInput(INPUT_RIGHT, REPEAT_TRUE))
    {
        vx = 0.6f;
        sprite->setFlip(SDL_FLIP_NONE);
    }

    else
    {
        vx = 0.0f;
    }

    if (input->checkInput(INPUT_UP, REPEAT_TRUE))
    {
        setState(s_jumping);
        vy = -maxVY;
        jumpIni = y;
        jumpCounter = 0;
    }
}

// Indica si el jugador esta en uno de los cuatro bordes de la pantalla
bool Player::getOnBorder()
{
    return onBorder;
}

// Indica en cual de los cuatro bordes se encuentra
int Player::getBorder()
{
    return border;
}

// Comprueba si está situado en alguno de los cuatro bordes de la habitación
void Player::checkBorders()
{
    if (x < PLAY_AREA_LEFT)
    {
        border = BORDER_LEFT;
        onBorder = true;
    }
    else if (x > PLAY_AREA_RIGHT - w)
    {
        border = BORDER_RIGHT;
        onBorder = true;
    }
    else if (y < PLAY_AREA_TOP)
    {
        border = BORDER_TOP;
        onBorder = true;
    }
    else if (y > PLAY_AREA_BOTTOM - h)
    {
        border = BORDER_BOTTOM;
        onBorder = true;
    }
    else
    {
        onBorder = false;
    }
}

// Comprueba el estado del jugador
void Player::checkState()
{
    // Actualiza las variables en función del estado
    if (state == s_falling)
    {
        vx = 0.0f;
        vy = maxVY;
        ++fallCounter;
        playFallSound();
    }

    else if (state == s_standing)
    {
        if (prevState == s_falling && fallCounter > maxFallHeight)
        { // Si cae de muy alto, el jugador muere
            alive = false;
        }
        vy = 0.0f;
        jumpCounter = 0;
        fallCounter = 0;
    }

    else if (state == s_jumping)
    {
        ++jumpCounter;
        playJumpSound();
    }
}

// Cambia al jugador de un borde al opuesto. Util para el cambio de pantalla
void Player::switchBorders()
{
    if (border == BORDER_TOP)
    {
        y = PLAY_AREA_BOTTOM - h - 0 - BLOCK;
        setState(s_standing);
        // jumpIni += 128;
    }
    else if (border == BORDER_BOTTOM)
    {
        y = PLAY_AREA_TOP + 0;
        setState(s_standing);
    }
    else if (border == BORDER_RIGHT)
    {
        x = PLAY_AREA_LEFT + 0;
    }
    if (border == BORDER_LEFT)
    {
        x = PLAY_AREA_RIGHT - w - 0;
    }

    onBorder = false;
}

// Aplica gravedad al jugador
void Player::applyGravity()
{
    const float gf = 0.035f;

    // La gravedad solo se aplica cuando el jugador esta saltando
    // Nunca mientras cae o esta de pie
    if (state == s_jumping)
    {
        vy += gf;
        if (vy > maxVY)
        {
            vy = maxVY;
        }
    }
}

// Obtiene el rectangulo que delimita al jugador
SDL_Rect Player::getRect()
{
    return {(int)x, (int)y, w, h};
}

// Obtiene el rectangulo de colision del jugador
SDL_Rect &Player::getCollider()
{
    colliderBox = getRect();
    return colliderBox;
}

// Recalcula la posición del jugador y su animación
void Player::move()
{
    lastPosition = {(int)x, (int)y}; // Guarda la posicion actual antes de modificarla
    applyGravity();                  // Aplica gravedad al jugador
    checkState();                    // Comprueba el estado del jugador

    // Se mueve hacia la izquierda
    if (vx < 0.0f)
    {
        // Crea el rectangulo de proyección en el eje X para ver si colisiona
        SDL_Rect proj;
        proj.x = (int)(x + vx);
        proj.y = (int)y;
        proj.h = h;
        proj.w = ceil(abs(vx)); // Para evitar que tenga un ancho de 0 pixels

        r = proj;

        // Comprueba la colisión con las superficies
        const int pos = room->checkRightSurfaces(&proj);

        // Calcula la nueva posición
        if (pos == -1)
        { // Si no hay colisión
            x += vx;
        }
        else
        { // Si hay colisión lo mueve hasta donde no colisiona
            x = pos + 1;
        }

        // Si ha tocado alguna rampa mientras camina (sin saltar), asciende
        if (state != s_jumping)
        {
            v_line_t leftSide = {(int)x, (int)y + h - 2, (int)y + h - 1}; // Comprueba solo los dos pixels de abajo
            const int ly = room->checkLeftSlopes(&leftSide);
            if (ly > -1)
            {
                y = ly - h;
            }
        }

        // Si está bajando la rampa, recoloca al jugador
        if (isOnDownSlope())
        {
            y += 1;
        }
    }

    // Se mueve hacia la derecha
    else if (vx > 0.0f)
    {
        // Crea el rectangulo de proyección en el eje X para ver si colisiona
        SDL_Rect proj;
        proj.x = (int)x + w;
        proj.y = (int)y;
        proj.h = h;
        proj.w = ceil(vx); // Para evitar que tenga un ancho de 0 pixels

        r = proj;

        // Comprueba la colisión
        const int pos = room->checkLeftSurfaces(&proj);

        // Calcula la nueva posición
        if (pos == -1)
        { // Si no hay colisión
            x += vx;
        }
        else
        { // Si hay colisión lo mueve hasta donde no colisiona
            x = pos - w;
        }

        // Si ha tocado alguna rampa mientras camina (sin saltar), asciende
        if (state != s_jumping)
        {
            v_line_t rightSide = {(int)x + w - 1, (int)y + h - 2, (int)y + h - 1}; // Comprueba solo los dos pixels de abajo
            const int ry = room->checkRightSlopes(&rightSide);
            if (ry > -1)
            {
                y = ry - h;
            }
        }

        // Si está bajando la rampa, recoloca al jugador
        if (isOnDownSlope())
        {
            y += 1;
        }
    }

    // Si ha salido del suelo, el jugador cae
    if (state == s_standing && !isOnFloor())
    {
        setState(s_falling);
    }

    // Se mueve hacia arriba
    if (vy < 0.0f)
    {
        // Crea el rectangulo de proyección en el eje Y para ver si colisiona
        SDL_Rect proj;
        proj.x = (int)x;
        proj.y = (int)(y + vy);
        proj.h = ceil(abs(vy)); // Para evitar que tenga una altura de 0 pixels
        proj.w = w;

        r = proj;

        // Comprueba la colisión
        const int pos = room->checkBottomSurfaces(&proj);

        // Calcula la nueva posición
        if (pos == -1)
        { // Si no hay colisión
            y += vy;
        }
        else
        { // Si hay colisión lo mueve hasta donde no colisiona y entra en caída
            y = pos + 1;
            setState(s_falling);
        }
    }

    // Se mueve hacia abajo
    else if (vy > 0.0f)
    {
        // Crea el rectangulo de proyección en el eje Y para ver si colisiona
        SDL_Rect proj;
        proj.x = (int)x;
        proj.y = (int)y + h;
        proj.h = ceil(vy); // Para evitar que tenga una altura de 0 pixels
        proj.w = w;

        r = proj;

        // Comprueba la colisión con los muros
        const int pos = room->checkTopSurfaces(&proj);
        if (pos > -1)
        { // Si hay colisión lo mueve hasta donde no colisiona y pasa a estar sobre el suelo
            y = pos - h;
            setState(s_standing);
        }
        else
        { // Si no hay colisión con los muros, comprueba la colisión con las rampas
            if (state != s_jumping)
            { // Las rampas no se miran si se está saltando
                v_line_t leftSide = {proj.x, proj.y, proj.y + proj.h - 1};
                v_line_t rightSide = {proj.x + proj.w - 1, proj.y, proj.y + proj.h - 1};
                const int p = std::max(room->checkRightSlopes(&rightSide), room->checkLeftSlopes(&leftSide));
                if (p > -1)
                { // No está saltando y hay colisión con una rampa
                    // Calcula la nueva posición
                    y = p - h;
                    setState(s_standing); 
                }
                else
                { // No está saltando y no hay colisón con una rampa
                    // Calcula la nueva posición
                    y += vy;
                }
            }
            else
            { // Esta saltando y no hay colisión con los muros
                // Calcula la nueva posición
                y += vy;
            }
        }
    }

    // Actualiza la posición del sprite
    sprite->setPosX(x);
    sprite->setPosY(y);

    debug->add("RECT: " + std::to_string(r.x) + "," + std::to_string(r.y) + "," + std::to_string(r.w) + "," + std::to_string(r.h));
}

// Establece la animación del jugador
void Player::animate()
{
    if (vx != 0)
    {
        sprite->animate();
    }
}

// Comprueba si ha finalizado el salto al alcanzar la altura de inicio
void Player::checkJumpEnd()
{
    if (state == s_jumping)
    {
        if (vy > 0)
        {
            if (y >= jumpIni)
            { // Si alcanza la altura de salto inicial, pasa al estado de caída
                setState(s_falling);
                vy = maxVY;
                jumpCounter = 0;
            }
        }
    }
}

// Calcula y reproduce el sonido de salto
void Player::playJumpSound()
{
    if (jumpCounter % 4 == 0)
    {
        JA_PlaySound(jumpSound[jumpCounter / 4]);
    }
    debug->add("JUMP: " + std::to_string(jumpCounter / 4));
}

// Calcula y reproduce el sonido de caer
void Player::playFallSound()
{
    if (fallCounter % 4 == 0)
    {
        JA_PlaySound(fallSound[std::min((fallCounter / 4), (int)fallSound.size() - 1)]);
    }
    debug->add("FALL: " + std::to_string(fallCounter / 4));
}

// Comprueba si el jugador tiene suelo debajo de los pies
bool Player::isOnFloor()
{
    bool onFloor = false;
    bool onSlopeL = false;
    bool onSlopeR = false;

    updateFeet();

    // Comprueba las superficies
    for (auto f : underFeet)
    {
        onFloor |= room->checkTopSurfaces(&f);
    }

    // Comprueba las rampas
    onSlopeL = room->checkLeftSlopes(&underFeet[0]);
    onSlopeR = room->checkRightSlopes(&underFeet[1]);

    if (onFloor)
    {
        debug->add("ON_FLOOR");
    }

    if (onSlopeL)
    {
        debug->add("ON_SLOPE_L: " + std::to_string(underFeet[0].x) + "," + std::to_string(underFeet[0].y));
    }

    if (onSlopeR)
    {
        debug->add("ON_SLOPE_R: " + std::to_string(underFeet[1].x) + "," + std::to_string(underFeet[1].y));
    }

    return onFloor || onSlopeL || onSlopeR;
}

// Comprueba si el jugador está sobre una rampa hacia abajo
bool Player::isOnDownSlope()
{
    bool onSlope = false;

    updateFeet();

    // Cuando el jugador baja una escalera, se queda volando
    // Hay que mirar otro pixel más por debajo
    underFeet[0].y += 1;
    underFeet[1].y += 1;

    // Comprueba las rampas
    onSlope |= room->checkLeftSlopes(&underFeet[0]);
    onSlope |= room->checkRightSlopes(&underFeet[1]);

    if (onSlope)
    {
        debug->add("ON_DOWN_SLOPE");
    }

    return onSlope;
}

// Comprueba que el jugador no toque ningun tile de los que matan
bool Player::checkKillingTiles()
{
    // Actualiza los puntos de colisión
    updateColliderPoints();

    // Comprueba si hay contacto
    bool check = false;

    for (auto c : colliderPoints)
    {
        check |= (room->getTile(c) == t_kill);
    }

    // Mata al jugador si hay colisión
    if (alive)
    {
        alive = !check;
    }

    return check;
}

// Obtiene algunos parametros del jugador
player_t Player::getSpawnParams()
{
    player_t params;

    params.x = x;
    params.y = y;
    params.vx = vx;
    params.vy = vy;
    params.jumpIni = jumpIni;
    params.state = state;
    params.flip = sprite->getFlip();

    return params;
}

// Recarga la textura
void Player::reLoadTexture()
{
    texture->reLoad();
}

// Establece el valor de la variable
void Player::setRoom(Room *room)
{
    this->room = room;
}

// Actualiza los puntos de colisión
void Player::updateColliderPoints()
{
    const SDL_Rect rect = getRect();
    colliderPoints[0] = {rect.x, rect.y};
    colliderPoints[1] = {rect.x + 7, rect.y};
    colliderPoints[2] = {rect.x + 7, rect.y + 7};
    colliderPoints[3] = {rect.x, rect.y + 7};
    colliderPoints[4] = {rect.x, rect.y + 8};
    colliderPoints[5] = {rect.x + 7, rect.y + 8};
    colliderPoints[6] = {rect.x + 7, rect.y + 15};
    colliderPoints[7] = {rect.x, rect.y + 15};
}

// Actualiza los puntos de los pies
void Player::updateFeet()
{
    const SDL_Point p = {(int)x, (int)y};

    underFeet[0] = {p.x, p.y + h};
    underFeet[1] = {p.x + 7, p.y + h};

    feet[0] = {p.x, p.y + h - 1};
    feet[1] = {p.x + 7, p.y + h - 1};
}

// Obtiene el valor de la variable
bool Player::getInvincible()
{
    return invincible;
}

// Establece el valor de la variable
void Player::setInvincible(bool value)
{
    invincible = value;
}

// Cambia el estado del jugador
void Player::setState(state_e value)
{
    prevState = state;
    state = value;
}

// Comprueba si el jugador esta vivo
bool Player::isAlive()
{
    return alive;
}

// Pone el jugador en modo pausa
void Player::pause()
{
    paused = true;
}

// Quita el modo pausa del jugador
void Player::resume()
{
    paused = false;
}