// IWYU pragma: no_include <bits/std_abs.h>
#include "game/entities/player.hpp"

#include <algorithm>  // Para max, min
#include <cmath>      // Para ceil, abs
#include <ranges>     // Para std::ranges::any_of

#include "core/input/input.hpp"                        // Para Input, InputAction
#include "core/rendering/surface_animated_sprite.hpp"  // Para SAnimatedSprite
#include "core/resources/resource.hpp"                 // Para Resource
#include "core/system/debug.hpp"                       // Para Debug
#include "external/jail_audio.h"                       // Para JA_PlaySound
#include "game/gameplay/room.hpp"                      // Para Room, TileType
#include "game/options.hpp"                            // Para Cheat, Options, options
#include "utils/defines.hpp"                           // Para RoomBorder::BOTTOM, RoomBorder::LEFT, RoomBorder::RIGHT

// 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_ = {.x = 0, .y = 0, .w = 0, .h = 0};
    debug_rect_y_ = {.x = 0, .y = 0, .w = 0, .h = 0};
    debug_color_ = static_cast<Uint8>(PaletteColor::GREEN);
    debug_point_ = {.x = 0, .y = 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_ = RoomBorder::LEFT;
        is_on_border_ = true;
    }

    else if (x_ + WIDTH > PLAY_AREA_RIGHT) {
        border_ = RoomBorder::RIGHT;
        is_on_border_ = true;
    }

    else if (y_ < PLAY_AREA_TOP) {
        border_ = RoomBorder::TOP;
        is_on_border_ = true;
    }

    else if (y_ + HEIGHT > PLAY_AREA_BOTTOM) {
        border_ = RoomBorder::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() {
    switch (border_) {
        case RoomBorder::TOP:
            y_ = PLAY_AREA_BOTTOM - HEIGHT - BLOCK;
            setState(PlayerState::STANDING);
            break;

        case RoomBorder::BOTTOM:
            y_ = PLAY_AREA_TOP;
            setState(PlayerState::STANDING);
            break;

        case RoomBorder::RIGHT:
            x_ = PLAY_AREA_LEFT;
            break;

        case RoomBorder::LEFT:
            x_ = PLAY_AREA_RIGHT - WIDTH;
            break;

        default:
            break;
    }

    is_on_border_ = false;
    placeSprite();
    collider_box_ = getRect();
}

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

    // La gravedad solo se aplica cuando el jugador esta saltando
    // Nunca mientras cae o esta de pie
    if (state_ == PlayerState::JUMPING) {
        vy_ += GRAVITY_FORCE;
        vy_ = std::min(vy_, MAX_VY);
    }
}

// Maneja el movimiento horizontal hacia la izquierda
void Player::moveHorizontalLeft() {
    // Crea el rectangulo de proyección en el eje X para ver si colisiona
    SDL_FRect proj;
    proj.x = static_cast<int>(x_ + vx_);
    proj.y = static_cast<int>(y_);
    proj.h = HEIGHT;
    proj.w = static_cast<int>(std::ceil(std::fabs(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) {
        const LineVertical LEFT_SIDE = {.x = static_cast<int>(x_), .y1 = static_cast<int>(y_) + static_cast<int>(HEIGHT) - 2, .y2 = static_cast<int>(y_) + static_cast<int>(HEIGHT) - 1};  // Comprueba solo los dos pixels de abajo
        const int LY = room_->checkLeftSlopes(&LEFT_SIDE);
        if (LY > -1) {
            y_ = LY - HEIGHT;
        }
    }

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

// Maneja el movimiento horizontal hacia la derecha
void Player::moveHorizontalRight() {
    // Crea el rectangulo de proyección en el eje X para ver si colisiona
    SDL_FRect proj;
    proj.x = x_ + WIDTH;
    proj.y = y_;
    proj.h = HEIGHT;
    proj.w = std::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) {
        const LineVertical RIGHT_SIDE = {.x = static_cast<int>(x_) + static_cast<int>(WIDTH) - 1, .y1 = static_cast<int>(y_) + static_cast<int>(HEIGHT) - 2, .y2 = static_cast<int>(y_) + static_cast<int>(HEIGHT) - 1};  // Comprueba solo los dos pixels de abajo
        const int RY = room_->checkRightSlopes(&RIGHT_SIDE);
        if (RY > -1) {
            y_ = RY - HEIGHT;
        }
    }

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

// Maneja el movimiento vertical hacia arriba
void Player::moveVerticalUp() {
    // Crea el rectangulo de proyección en el eje Y para ver si colisiona
    SDL_FRect proj;
    proj.x = static_cast<int>(x_);
    proj.y = static_cast<int>(y_ + vy_);
    proj.h = static_cast<int>(std::ceil(std::fabs(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);
    }
}

// Maneja el movimiento vertical hacia abajo
void Player::moveVerticalDown() {
    // Crea el rectangulo de proyección en el eje Y para ver si colisiona
    SDL_FRect proj;
    proj.x = x_;
    proj.y = y_ + HEIGHT;
    proj.h = std::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 float 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
            auto rect = toSDLRect(proj);
            const LineVertical LEFT_SIDE = {.x = rect.x, .y1 = rect.y, .y2 = rect.y + rect.h - 1};
            const LineVertical RIGHT_SIDE = {.x = rect.x + rect.w - 1, .y1 = rect.y, .y2 = rect.y + rect.h - 1};
            const float POINT = std::max(room_->checkRightSlopes(&RIGHT_SIDE), room_->checkLeftSlopes(&LEFT_SIDE));
            if (POINT > -1) {
                // No está saltando y hay colisión con una rampa
                // Calcula la nueva posición
                y_ = POINT - HEIGHT;
                setState(PlayerState::STANDING);
#ifdef _DEBUG
                debug_color_ = static_cast<Uint8>(PaletteColor::YELLOW);
                debug_point_ = {.x = x_ + (WIDTH / 2), .y = POINT};
#endif
            } else {
                // No está saltando y no hay colisón con una rampa
                // Calcula la nueva posición
                y_ += vy_;
#ifdef _DEBUG
                debug_color_ = static_cast<Uint8>(PaletteColor::RED);
#endif
            }
        } else {
            // Esta saltando y no hay colisión con los muros
            // Calcula la nueva posición
            y_ += vy_;
        }
    }
}

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

#ifdef _DEBUG
    debug_color_ = static_cast<Uint8>(PaletteColor::GREEN);
#endif

    // Movimiento horizontal
    if (vx_ < 0.0F) {
        moveHorizontalLeft();
    } else if (vx_ > 0.0F) {
        moveHorizontalRight();
    }

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

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

    // Movimiento vertical
    if (vy_ < 0.0F) {
        moveVerticalUp();
    } else if (vy_ > 0.0F) {
        moveVerticalDown();
    }

    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
auto Player::isOnFloor() -> bool {
    bool on_floor = false;
    bool on_slope_l = false;
    bool on_slope_r = false;

    updateFeet();

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

    // Comprueba las rampas
    on_slope_l = room_->checkLeftSlopes(under_feet_.data());
    on_slope_r = room_->checkRightSlopes(&under_feet_[1]);

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

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

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

    return on_floor || on_slope_l || on_slope_r;
}

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

    updateFeet();

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

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

    return on_auto_surface;
}

// Comprueba si el jugador está sobre una rampa hacia abajo
auto Player::isOnDownSlope() -> bool {
    bool on_slope = 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
    on_slope |= room_->checkLeftSlopes(under_feet_.data());
    on_slope |= room_->checkRightSlopes(&under_feet_[1]);

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

    return on_slope;
}

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

    // Comprueba si hay contacto con algún tile que mata
    if (std::ranges::any_of(collider_points_, [this](const auto& c) {
            return 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 == Options::Cheat::State::ENABLED) {
        color_ = static_cast<Uint8>(PaletteColor::CYAN);
    } else if (Options::cheats.infinite_lives == Options::Cheat::State::ENABLED) {
        color_ = static_cast<Uint8>(PaletteColor::YELLOW);
    } else {
        color_ = static_cast<Uint8>(PaletteColor::WHITE);
    }
}

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

// Actualiza los puntos de los pies
void Player::updateFeet() {
    const SDL_FPoint P = {x_, y_};

    under_feet_[0] = {.x = P.x, .y = P.y + HEIGHT};
    under_feet_[1] = {.x = P.x + 7, .y = P.y + HEIGHT};

    feet_[0] = {.x = P.x, .y = P.y + HEIGHT - 1};
    feet_[1] = {.x = P.x + 7, .y = 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 sound_file = "jump" + std::to_string(i) + ".wav";
        jumping_sound_.push_back(Resource::get()->getSound(sound_file));

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

// 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<SurfaceAnimatedSprite>(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 surface = Screen::get()->getRendererSurface();

        // Pinta los underfeet
        surface->putPixel(under_feet_[0].x, under_feet_[0].y, static_cast<Uint8>(PaletteColor::BRIGHT_MAGENTA));
        surface->putPixel(under_feet_[1].x, under_feet_[1].y, static_cast<Uint8>(PaletteColor::BRIGHT_MAGENTA));

        // Pinta rectangulo del jugador
        SDL_FRect rect = getRect();
        surface->drawRectBorder(&rect, static_cast<Uint8>(PaletteColor::BRIGHT_CYAN));

        // Pinta el rectangulo de movimiento
        if (vx_ != 0.0F) {
            surface->fillRect(&debug_rect_x_, static_cast<Uint8>(PaletteColor::BRIGHT_RED));
        }
        if (vy_ != 0.0F) {
            surface->fillRect(&debug_rect_y_, static_cast<Uint8>(PaletteColor::BRIGHT_RED));
        }

        // Pinta el punto de debug
        surface->putPixel(debug_point_.x, debug_point_.y, rand() % 16);
    }
}
#endif  // _DEBUG