// IWYU pragma: no_include <bits/std_abs.h>
#include "player.h"
#include <stdlib.h>            // for rand
#include <algorithm>           // for max, min
#include <cmath>               // for ceil, abs
#include "s_animated_sprite.h" // for SAnimatedSprite
#include "debug.h"             // for Debug
#include "defines.h"           // for BORDER_BOTTOM, BORDER_LEFT, BORDER_RIGHT
#include "input.h"             // for Input, InputAction
#include "jail_audio.h"        // for JA_PlaySound
#include "options.h"           // for Options, options, Cheat, OptionsVideo
#include "resource.h"          // for Resource
#include "room.h"              // for Room, JA_Sound_t, TileType
#include "screen.h"            // for Screen
#include "surface.h"           // for Texture

// Constructor
Player::Player(const PlayerData &player)
    : room_(player.room)
{
    // Inicializa algunas variables
    initSprite(player.texture_path, player.animations_path);
    setColor();
    applySpawnValues(player.spawn);
    placeSprite();
    initSounds();

    previous_state_ = state_;
    last_position_ = getRect();
    collider_box_ = getRect();
    collider_points_.resize(collider_points_.size() + 8, {0, 0});
    under_feet_.resize(under_feet_.size() + 2, {0, 0});
    feet_.resize(feet_.size() + 2, {0, 0});

#ifdef DEBUG
    debug_rect_x_ = {0, 0, 0, 0};
    debug_rect_y_ = {0, 0, 0, 0};
    debug_color_ = {0, 255, 0};
    debug_point_ = {0, 0};
#endif
}

// Pinta el jugador en pantalla
void Player::render()
{
    sprite_->render(1, color_);

#ifdef DEBUG
    renderDebugInfo();
#endif
}

// Actualiza las variables del objeto
void Player::update()
{
    if (!is_paused_)
    {
        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á sobre una superficie
    if (state_ != PlayerState::STANDING)
    {
        return;
    }

    if (!auto_movement_)
    {
        // Comprueba las entradas de desplazamiento lateral solo en el caso de no estar enganchado a una superficie automatica
        if (Input::get()->checkInput(InputAction::LEFT))
        {
            vx_ = -0.6f;
            sprite_->setFlip(SDL_FLIP_HORIZONTAL);
        }

        else if (Input::get()->checkInput(InputAction::RIGHT))
        {
            vx_ = 0.6f;
            sprite_->setFlip(SDL_FLIP_NONE);
        }

        else
        {
            // No se pulsa ninguna dirección
            vx_ = 0.0f;
            if (isOnAutoSurface())
            {
                // Si deja de moverse sobre una superficie se engancha
                auto_movement_ = true;
            }
        }
    }
    else
    { // El movimiento lo proporciona la superficie
        vx_ = 0.6f * room_->getAutoSurfaceDirection();

        if (vx_ > 0.0f)
        {
            sprite_->setFlip(SDL_FLIP_NONE);
        }
        else
        {
            sprite_->setFlip(SDL_FLIP_HORIZONTAL);
        }
    }

    if (Input::get()->checkInput(InputAction::JUMP))
    {
        // Solo puede saltar si ademas de estar (state == s_standing)
        // Esta sobre el suelo, rampa o suelo que se mueve
        // Esto es para evitar el salto desde el vacio al cambiar de pantalla verticalmente
        // Ya que se coloca el estado s_standing al cambiar de pantalla

        if (isOnFloor() || isOnAutoSurface())
        {
            setState(PlayerState::JUMPING);
            vy_ = -MAX_VY_;
            jump_init_pos_ = y_;
            jumping_counter_ = 0;
        }
    }
}

// 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;
        is_on_border_ = true;
    }

    else if (x_ + WIDTH_ > PLAY_AREA_RIGHT)
    {
        border_ = BORDER_RIGHT;
        is_on_border_ = true;
    }

    else if (y_ < PLAY_AREA_TOP)
    {
        border_ = BORDER_TOP;
        is_on_border_ = true;
    }

    else if (y_ + HEIGHT_ > PLAY_AREA_BOTTOM)
    {
        border_ = BORDER_BOTTOM;
        is_on_border_ = true;
    }

    else
    {
        is_on_border_ = false;
    }
}

// Comprueba el estado del jugador
void Player::checkState()
{
    // Actualiza las variables en función del estado
    if (state_ == PlayerState::FALLING)
    {
        vx_ = 0.0f;
        vy_ = MAX_VY_;
        falling_counter_++;
        playFallSound();
    }

    else if (state_ == PlayerState::STANDING)
    {
        if (previous_state_ == PlayerState::FALLING && falling_counter_ > MAX_FALLING_HEIGHT_)
        { // Si cae de muy alto, el jugador muere
            is_alive_ = false;
        }
        vy_ = 0.0f;
        jumping_counter_ = 0;
        falling_counter_ = 0;
        if (!isOnFloor() && !isOnAutoSurface() && !isOnDownSlope())
        {
            setState(PlayerState::FALLING);
            vx_ = 0.0f;
            vy_ = MAX_VY_;
            falling_counter_++;
            playFallSound();
        }
    }

    else if (state_ == PlayerState::JUMPING)
    {
        falling_counter_ = 0;
        jumping_counter_++;
        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 - HEIGHT_ - 0 - BLOCK;
        setState(PlayerState::STANDING);
    }
    else if (border_ == BORDER_BOTTOM)
    {
        y_ = PLAY_AREA_TOP + 0;
        setState(PlayerState::STANDING);
    }
    else if (border_ == BORDER_RIGHT)
    {
        x_ = PLAY_AREA_LEFT + 0;
    }
    if (border_ == BORDER_LEFT)
    {
        x_ = PLAY_AREA_RIGHT - WIDTH_ - 0;
    }

    is_on_border_ = false;
}

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

    // La gravedad solo se aplica cuando el jugador esta saltando
    // Nunca mientras cae o esta de pie
    if (state_ == PlayerState::JUMPING)
    {
        vy_ += GF;
        if (vy_ > MAX_VY_)
        {
            vy_ = MAX_VY_;
        }
    }
}

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

#ifdef DEBUG
    debug_color_ = {0, 255, 0};
#endif

    // 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 = static_cast<int>(x_ + vx_);
        proj.y = static_cast<int>(y_);
        proj.h = HEIGHT_;
        proj.w = ceil(abs(vx_)); // Para evitar que tenga un ancho de 0 pixels

#ifdef DEBUG
        debug_rect_x_ = proj;
#endif

        // 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_ != PlayerState::JUMPING)
        {
            LineVertical leftSide = {static_cast<int>(x_), static_cast<int>(y_) + HEIGHT_ - 2, static_cast<int>(y_) + HEIGHT_ - 1}; // Comprueba solo los dos pixels de abajo
            const int ly = room_->checkLeftSlopes(&leftSide);
            if (ly > -1)
            {
                y_ = ly - HEIGHT_;
            }
        }

        // Si está bajando la rampa, recoloca al jugador
        if (isOnDownSlope() && state_ != PlayerState::JUMPING)
        {
            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 = static_cast<int>(x_) + WIDTH_;
        proj.y = static_cast<int>(y_);
        proj.h = HEIGHT_;
        proj.w = ceil(vx_); // Para evitar que tenga un ancho de 0 pixels

#ifdef DEBUG
        debug_rect_x_ = proj;
#endif

        // 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 - WIDTH_;
        }

        // Si ha tocado alguna rampa mientras camina (sin saltar), asciende
        if (state_ != PlayerState::JUMPING)
        {
            LineVertical rightSide = {static_cast<int>(x_) + WIDTH_ - 1, static_cast<int>(y_) + HEIGHT_ - 2, static_cast<int>(y_) + HEIGHT_ - 1}; // Comprueba solo los dos pixels de abajo
            const int ry = room_->checkRightSlopes(&rightSide);
            if (ry > -1)
            {
                y_ = ry - HEIGHT_;
            }
        }

        // Si está bajando la rampa, recoloca al jugador
        if (isOnDownSlope() && state_ != PlayerState::JUMPING)
        {
            y_ += 1;
        }
    }

    // Si ha salido del suelo, el jugador cae
    if (state_ == PlayerState::STANDING && !isOnFloor())
    {
        setState(PlayerState::FALLING);

        // Deja de estar enganchado a la superficie automatica
        auto_movement_ = false;
    }

    // Si ha salido de una superficie automatica, detiene el movimiento automatico
    if (state_ == PlayerState::STANDING && isOnFloor() && !isOnAutoSurface())
    {
        // Deja de estar enganchado a la superficie automatica
        auto_movement_ = false;
    }

    // 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 = static_cast<int>(x_);
        proj.y = static_cast<int>(y_ + vy_);
        proj.h = ceil(abs(vy_)); // Para evitar que tenga una altura de 0 pixels
        proj.w = WIDTH_;

#ifdef DEBUG
        debug_rect_y_ = proj;
#endif

        // 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(PlayerState::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 = static_cast<int>(x_);
        proj.y = static_cast<int>(y_) + HEIGHT_;
        proj.h = ceil(vy_); // Para evitar que tenga una altura de 0 pixels
        proj.w = WIDTH_;

#ifdef DEBUG
        debug_rect_y_ = proj;
#endif

        // Comprueba la colisión con las superficies normales y las automáticas
        const int pos = std::max(room_->checkTopSurfaces(&proj), room_->checkAutoSurfaces(&proj));
        if (pos > -1)
        {
            // Si hay colisión lo mueve hasta donde no colisiona y pasa a estar sobre la superficie
            y_ = pos - HEIGHT_;
            setState(PlayerState::STANDING);

            // Deja de estar enganchado a la superficie automatica
            auto_movement_ = false;
        }
        else
        {
            // Si no hay colisión con los muros, comprueba la colisión con las rampas
            if (state_ != PlayerState::JUMPING)
            { // Las rampas no se miran si se está saltando
                LineVertical leftSide = {proj.x, proj.y, proj.y + proj.h - 1};
                LineVertical 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 - HEIGHT_;
                    setState(PlayerState::STANDING);
#ifdef DEBUG
                    debug_color_ = {255, 255, 0};
                    debug_point_ = {(int)x_ + (WIDTH_ / 2), p};
#endif
                }
                else
                {
                    // No está saltando y no hay colisón con una rampa
                    // Calcula la nueva posición
                    y_ += vy_;
#ifdef DEBUG
                    debug_color_ = {255, 0, 0};
#endif
                }
            }
            else
            {
                // Esta saltando y no hay colisión con los muros
                // Calcula la nueva posición
                y_ += vy_;
            }
        }
    }

    placeSprite();             // Coloca el sprite en la nueva posición
    collider_box_ = getRect(); // Actualiza el rectangulo de colisión

#ifdef DEBUG
    Debug::get()->add("RECT_X: " + std::to_string(debug_rect_x_.x) + "," + std::to_string(debug_rect_x_.y) + "," + std::to_string(debug_rect_x_.w) + "," + std::to_string(debug_rect_x_.h));
    Debug::get()->add("RECT_Y: " + std::to_string(debug_rect_y_.x) + "," + std::to_string(debug_rect_y_.y) + "," + std::to_string(debug_rect_y_.w) + "," + std::to_string(debug_rect_y_.h));
#endif
}

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

// Comprueba si ha finalizado el salto al alcanzar la altura de inicio
void Player::checkJumpEnd()
{
    if (state_ == PlayerState::JUMPING)
    {
        if (vy_ > 0)
        {
            if (y_ >= jump_init_pos_)
            {
                // Si alcanza la altura de salto inicial, pasa al estado de caída
                setState(PlayerState::FALLING);
                vy_ = MAX_VY_;
                jumping_counter_ = 0;
            }
        }
    }
}

// Calcula y reproduce el sonido de salto
void Player::playJumpSound()
{
    if (jumping_counter_ % 4 == 0)
    {
        JA_PlaySound(jumping_sound_[jumping_counter_ / 4]);
    }

#ifdef DEBUG
    Debug::get()->add("JUMP: " + std::to_string(jumping_counter_ / 4));
#endif
}

// Calcula y reproduce el sonido de caer
void Player::playFallSound()
{
    if (falling_counter_ % 4 == 0)
    {
        JA_PlaySound(falling_sound_[std::min((falling_counter_ / 4), (int)falling_sound_.size() - 1)]);
    }

#ifdef DEBUG
    Debug::get()->add("FALL: " + std::to_string(falling_counter_ / 4));
#endif
}

// 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 : under_feet_)
    {
        onFloor |= room_->checkTopSurfaces(&f);
        onFloor |= room_->checkAutoSurfaces(&f);
    }

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

#ifdef DEBUG
    if (onFloor)
    {
        Debug::get()->add("ON_FLOOR");
    }

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

    if (onSlopeR)
    {
        Debug::get()->add("ON_SLOPE_R: " + std::to_string(under_feet_[1].x) + "," + std::to_string(under_feet_[1].y));
    }
#endif

    return onFloor || onSlopeL || onSlopeR;
}

// Comprueba si el jugador esta sobre una superficie automática
bool Player::isOnAutoSurface()
{
    bool onAutoSurface = false;

    updateFeet();

    // Comprueba las superficies
    for (auto f : under_feet_)
    {
        onAutoSurface |= room_->checkAutoSurfaces(&f);
    }

#ifdef DEBUG
    if (onAutoSurface)
    {
        Debug::get()->add("ON_AUTO_SURFACE");
    }
#endif

    return onAutoSurface;
}

// 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
    under_feet_[0].y += 1;
    under_feet_[1].y += 1;

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

#ifdef DEBUG
    if (onSlope)
    {
        Debug::get()->add("ON_DOWN_SLOPE");
    }
#endif

    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 y retorna en cuanto se encuentra colisión
    for (const auto &c : collider_points_)
    {
        if (room_->getTile(c) == TileType::KILL)
        {
            is_alive_ = false; // Mata al jugador inmediatamente
            return true;       // Retorna en cuanto se detecta una colisión
        }
    }

    return false; // No se encontró ninguna colisión
}

// Establece el color del jugador
void Player::setColor()
{
    if (options.cheats.invincible == Cheat::CheatState::ENABLED)
    {
        color_ = stringToColor("cyan");
    }
    else if (options.cheats.infinite_lives == Cheat::CheatState::ENABLED)
    {
        color_ = stringToColor("yellow");
    }
    else
    {
        color_ = stringToColor("white");
    }
}

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

// Actualiza los puntos de los pies
void Player::updateFeet()
{
    const SDL_Point p = {static_cast<int>(x_), static_cast<int>(y_)};

    under_feet_[0] = {p.x, p.y + HEIGHT_};
    under_feet_[1] = {p.x + 7, p.y + HEIGHT_};

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

// Cambia el estado del jugador
void Player::setState(PlayerState value)
{
    previous_state_ = state_;
    state_ = value;

    checkState();
}

// Inicializa los sonidos de salto y caida
void Player::initSounds()
{
    jumping_sound_.clear();
    falling_sound_.clear();

    for (int i = 1; i <= 24; ++i)
    {
        std::string soundFile = "jump" + std::to_string(i) + ".wav";
        jumping_sound_.push_back(Resource::get()->getSound(soundFile));

        if (i >= 11)
        {
            falling_sound_.push_back(Resource::get()->getSound(soundFile));
        }
    }
}

// Aplica los valores de spawn al jugador
void Player::applySpawnValues(const PlayerSpawn &spawn)
{
    x_ = spawn.x;
    y_ = spawn.y;
    vx_ = spawn.vx;
    vy_ = spawn.vy;
    jump_init_pos_ = spawn.jump_init_pos;
    state_ = spawn.state;
    sprite_->setFlip(spawn.flip);
}

// Inicializa el sprite del jugador
void Player::initSprite(const std::string &surface_path, const std::string &animations_path)
{
    auto surface = Resource::get()->getSurface(surface_path);
    auto animations = Resource::get()->getAnimations(animations_path);

    sprite_ = std::make_shared<SAnimatedSprite>(surface, animations);
    sprite_->setWidth(WIDTH_);
    sprite_->setHeight(HEIGHT_);
    sprite_->setCurrentAnimation("walk");
}

#ifdef DEBUG
// Pinta la información de debug del jugador
void Player::renderDebugInfo()
{
    /*
    if (Debug::get()->getEnabled())
    {
        auto renderer = Screen::get()->getRenderer();

        // Pinta los underfeet
        SDL_SetRenderDrawColor(renderer, 255, 0, 255, 255);
        SDL_RenderDrawPoint(renderer, under_feet_[0].x, under_feet_[0].y);
        SDL_RenderDrawPoint(renderer, under_feet_[1].x, under_feet_[1].y);

        // Pinta rectangulo del jugador
        SDL_SetRenderDrawColor(renderer, debug_color_.r, debug_color_.g, debug_color_.b, 192);
        SDL_Rect rect = getRect();
        SDL_RenderFillRect(renderer, &rect);
        SDL_SetRenderDrawColor(renderer, 0, 255, 255, 255);
        SDL_RenderDrawRect(renderer, &rect);

        // Pinta el rectangulo de movimiento
        SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
        if (vx_ != 0.0f)
        {
            SDL_RenderFillRect(renderer, &debug_rect_x_);
        }
        if (vy_ != 0.0f)
        {
            SDL_RenderFillRect(renderer, &debug_rect_y_);
        }

        // Pinta el punto de debug
        SDL_SetRenderDrawColor(renderer, rand() % 256, rand() % 256, rand() % 256, 255);
        SDL_RenderDrawPoint(renderer, debug_point_.x, debug_point_.y);
    }
        */
}
#endif