#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 = false;

    jump_ini = ini.jump_ini;
    state = ini.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});
}

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

// Pinta el jugador en pantalla
void Player::render()
{
    sprite->getTexture()->setColor(color.r, color.g, color.b);
    sprite->render();
}

// Actualiza las variables del objeto
void Player::update()
{
    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
}

// Comprueba las entradas y modifica variables
void Player::checkInput()
{
    // Solo comprueba las entradas de dirección cuando está de pie
    if ((input->checkInput(INPUT_LEFT, REPEAT_TRUE)) && (state == s_standing))
    {
        vx = -0.6f;
        sprite->setFlip(SDL_FLIP_HORIZONTAL);
    }
    else if ((input->checkInput(INPUT_RIGHT, REPEAT_TRUE)) && (state == s_standing))
    {
        vx = 0.6f;
        sprite->setFlip(SDL_FLIP_NONE);
    }
    else if (state == s_standing)
    {
        vx = 0.0f;
    }

    if (input->checkInput(INPUT_UP, REPEAT_TRUE))
    {
        if (state == s_standing)
        {
            state = s_jumping;
            vy = -maxVY;
            jump_ini = y;
        }
    }
}

// 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()
{
    if (state == s_falling)
    {
        vx = 0.0f;
        vy = maxVY;
    }

    else if (state == s_standing)
    {
        vy = 0.0f;
    }
}

// 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 - 1;
        jump_ini += 128;
    }
    else if (border == BORDER_BOTTOM)
    {
        y = PLAY_AREA_TOP + 1;
    }
    else if (border == BORDER_RIGHT)
    {
        x = PLAY_AREA_LEFT + 1;
    }
    if (border == BORDER_LEFT)
    {
        x = PLAY_AREA_RIGHT - w - 1;
    }

    onBorder = false;
}

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

    // La gravedad solo se aplica cuando está saltando
    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()
{
    const int tileSize = room->getTileSize();
    lastPosition = {(int)x, (int)y}; // Guarda la posicion actual antes de modificarla
    applyGravity();                  // Aplica gravedad al jugador
    checkState();                    // Comprueba el estado del jugador

    // Calcula la nueva posición del jugador y compensa en caso de colisión
    x += vx;

    // Comprueba colisiones con muros
    if (checkWalls())
    {
        // Recoloca
        if (vx > 0.0f)
        {
            x = (int)x - ((int)x + w) % tileSize;
        }
        else
        {
            x = (int)x + tileSize - ((int)x % tileSize);
        }
    }

    // Comprueba colisiones con rampas solo si esta quieto o cayendo
    else if (vy >= 0.0f)
    {

        tile_e slope = checkSlopes();
        tile_e slope2 = checkSlopes2();
        debug->add("SLOPE  = " + std::to_string(slope));
        debug->add("SLOPE2 = " + std::to_string(slope2));
        if (slope != t_empty)
        { // Cuesta hacia la derecha
            if (slope == t_slope_r)
            { // Recoloca
                y = -h + room->getSlopeHeight(feet[1], t_slope_r);
                debug->add("feet[1] = " + std::to_string(feet[1].y));
            }

            // Cuesta hacia la izquierda
            if (slope == t_slope_l)
            { // Recoloca
                y = -h + room->getSlopeHeight(feet[0], t_slope_l);
                debug->add("feet[0] = " + std::to_string(feet[0].y));
            }
        }
        else if (slope2 != t_empty)
        { // Cuesta hacia la derecha
            if (slope2 == t_slope_r)
            { // Recoloca
                y = -h + room->getSlopeHeight(underFeet[1], t_slope_r);
                debug->add("ufeet[1] = " + std::to_string(underFeet[1].y));
            }

            // Cuesta hacia la izquierda
            if (slope2 == t_slope_l)
            { // Recoloca
                y = -h + room->getSlopeHeight(underFeet[0], t_slope_l);
                debug->add("ufeet[0] = " + std::to_string(underFeet[0].y));
            }
        }
    }

    y += vy;
    if (checkWalls())
    {
        // Recoloca
        if (vy > 0.0f)
        { // Bajando
            y -= ((int)y + h) % tileSize;
            state = s_standing;
            vy = 0.0f;
        }
        else
        { // Subiendo
            y += tileSize - ((int)y % tileSize);
            state = s_falling;
            vy = maxVY;
        }
    }
    else
    // Si no colisiona con los muros, haz comprobaciones extra
    {
        const int a = (lastPosition.y + h - 1) / tileSize;
        const int b = ((int)y + h - 1) / tileSize;
        const bool tile_change = a != b;

        bool going_down = vy >= 0.0f;
        bool tile_aligned = ((int)y + h) % tileSize == 0;

        // Si está cayendo y hay cambio de tile o está justo sobre uno
        if (going_down && (tile_aligned || tile_change))
        {
            // Comprueba si tiene uno de los pies sobre una superficie
            if (isOnFloor())
            { // Y deja al jugador de pie
                state = s_standing;
                vy = 0.0f;

                // Si ademas ha habido un cambio de tile recoloca al jugador
                if (tile_change && !checkSlopes2())
                {
                    y = ((int)y - ((int)y % tileSize));
                }
            }

            // Si tiene ambos pies sobre el vacío y no está saltando
            else if (state != s_jumping)
            {
                state = s_falling;
                vy = maxVY;
            }
        }

        going_down = vy >= 0.0f;
        tile_aligned = ((int)y + h) % tileSize == 0;

        // Si simplemente está cayendo (sin mirar si hay cambio de tile o si está justo sobre uno)
        if (going_down)
        {
            if (state != s_jumping)
            {
                state = s_falling;
                vy = maxVY;
            }

            // Si está alineado con el tile mira el suelo (para que no lo mire si está
            // dentro de un tile atravesable y lo deje a medias)
            if (tile_aligned)
            {
                if (isOnFloor())
                {
                    state = s_standing;
                    vy = 0.0f;
                }
            }
            // EXPERIMENTAL
            else if (checkSlopes())
            {
                state = s_standing;
                vy = 0.0f;
            }
        }
    }

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

// 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 >= jump_ini)
            {
                state = s_falling;
                vy = maxVY;
            }
}

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

    updateFeet();

    for (auto f : underFeet)
    {
        const tile_e tile = (room->getTile(f));
        onFloor |= (tile == t_wall || tile == t_passable || tile == t_slope_l || tile == t_slope_r);
    }

    return onFloor;
}

// Comprueba que el jugador no atraviese ninguna pared
bool Player::checkWalls()
{
    // Actualiza los puntos de colisión
    updateColliderPoints();

    // Comprueba si ha colisionado con un muro
    bool wall = false;

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

    return wall;
}

// Comprueba si el jugador está en una rampa
tile_e Player::checkSlopes()
{
    // Actualiza los puntos de colisión
    updateFeet();

    bool slope_l = false;
    bool slope_r = false;

    // Comprueba si ha colisionado con una rampa
    for (auto f : feet)
    {
        slope_l |= (room->getTile(f) == t_slope_l);
        slope_r |= (room->getTile(f) == t_slope_r);
    }

    if (slope_l)
    {
        return t_slope_l;
    }

    if (slope_r)
    {
        return t_slope_r;
    }

    return t_empty;
}

// Comprueba si el jugador está en una rampa
tile_e Player::checkSlopes2()
{
    // Actualiza los puntos de colisión
    updateFeet();

    bool slope_l = false;
    bool slope_r = false;
    bool wall = false;
    bool passable = false;

    // Comprueba si ha colisionado con una rampa
    for (auto f : underFeet)
    {
        slope_l |= (room->getTile(f) == t_slope_l);
        slope_r |= (room->getTile(f) == t_slope_r);
        wall |= (room->getTile(f) == t_wall);
        passable |= (room->getTile(f) == t_passable);
    }

    if (wall || passable)
    {
        return t_empty;
    }

    if (slope_l)
    {
        return t_slope_l;
    }

    if (slope_r)
    {
        return t_slope_r;
    }

    return t_empty;
}

// 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.jump_ini = jump_ini;
    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;
}