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

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

#include "core/audio/audio.hpp"                       // Para Audio
#include "core/input/input.hpp"                       // Para Input, InputAction
#include "core/rendering/sprite/animated_sprite.hpp"  // Para SAnimatedSprite
#include "core/resources/resource_cache.hpp"          // Para Resource
#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

#ifdef _DEBUG
#include "core/system/debug.hpp"  // Para Debug
#endif

// Constructor
Player::Player(const Data& player)
    : room_(player.room) {
    initSprite(player.animations_path);
    setColor();
    applySpawnValues(player.spawn_data);
    placeSprite();
    initSounds();

    previous_state_ = state_;
}

// Pinta el jugador en pantalla
void Player::render() {
    sprite_->render(1, color_);
#ifdef _DEBUG
    if (Debug::get()->isEnabled()) {
        Screen::get()->getRendererSurface()->putPixel(under_right_foot_.x, under_right_foot_.y, static_cast<Uint8>(PaletteColor::GREEN));
        Screen::get()->getRendererSurface()->putPixel(under_left_foot_.x, under_left_foot_.y, static_cast<Uint8>(PaletteColor::GREEN));
    }
#endif
}

// Actualiza las variables del objeto
void Player::update(float delta_time) {
    if (!is_paused_) {
        handleInput();
        updateState(delta_time);
        move(delta_time);
        handleKillingTiles();  // Los collider_points_ están actualizados por syncSpriteAndCollider() dentro de move()
        animate(delta_time);
        border_ = handleBorders();
    }
}

// Comprueba las entradas y modifica variables
void Player::handleInput() {
    if (ignore_input_) { return; }
    if (Input::get()->checkAction(InputAction::LEFT)) {
        wanna_go_ = Direction::LEFT;
    } else if (Input::get()->checkAction(InputAction::RIGHT)) {
        wanna_go_ = Direction::RIGHT;
    } else {
        wanna_go_ = Direction::NONE;
    }

    wanna_jump_ = Input::get()->checkAction(InputAction::JUMP);
}

// La lógica de movimiento está distribuida en move
void Player::move(float delta_time) {
    switch (state_) {
        case State::ON_GROUND:
            moveOnGround(delta_time);
            break;
        case State::ON_SLOPE:
            moveOnSlope(delta_time);
            break;
        case State::JUMPING:
            moveJumping(delta_time);
            break;
        case State::FALLING:
            moveFalling(delta_time);
            break;
    }
    syncSpriteAndCollider();  // Actualiza la posición del sprite y las colisiones
#ifdef _DEBUG
    Debug::get()->set("P.X", std::to_string(static_cast<int>(x_)));
    Debug::get()->set("P.Y", std::to_string(static_cast<int>(y_)));
    Debug::get()->set("P.LGP", std::to_string(last_grounded_position_));
    switch (state_) {
        case State::ON_GROUND:
            Debug::get()->set("P.STATE", "ON_GROUND");
            break;
        case State::ON_SLOPE:
            Debug::get()->set("P.STATE", "ON_SLOPE");
            break;
        case State::JUMPING:
            Debug::get()->set("P.STATE", "JUMPING");
            break;
        case State::FALLING:
            Debug::get()->set("P.STATE", "FALLING");
            break;
    }
#endif
}

void Player::handleConveyorBelts() {
    if (!auto_movement_ and isOnConveyorBelt() and wanna_go_ == Direction::NONE) {
        auto_movement_ = true;
    }

    if (auto_movement_ and !isOnConveyorBelt()) {
        auto_movement_ = false;
    }
}

void Player::handleShouldFall() {
    if (!isOnFloor() and (state_ == State::ON_GROUND || state_ == State::ON_SLOPE)) {
        transitionToState(State::FALLING);
    }
}

void Player::transitionToState(State state) {
    previous_state_ = state_;
    state_ = state;

    switch (state) {
        case State::ON_GROUND:
            vy_ = 0;
            handleDeathByFalling();
            resetSoundControllersOnLanding();
            current_slope_ = nullptr;
            break;
        case State::ON_SLOPE:
            vy_ = 0;
            handleDeathByFalling();
            resetSoundControllersOnLanding();
            updateCurrentSlope();
            if (current_slope_ == nullptr) {
                // Los pies no coinciden con ninguna rampa: tratar como suelo plano
                state_ = State::ON_GROUND;
            }
            break;
        case State::JUMPING:
            //  Puede saltar desde ON_GROUND o ON_SLOPE
            if (previous_state_ == State::ON_GROUND || previous_state_ == State::ON_SLOPE) {
                vy_ = -MAX_VY;
                last_grounded_position_ = y_;
                updateVelocity();
                jump_sound_ctrl_.start();
                current_slope_ = nullptr;
            }
            break;
        case State::FALLING:
            fall_start_position_ = static_cast<int>(y_);
            last_grounded_position_ = static_cast<int>(y_);
            vy_ = MAX_VY;
            vx_ = 0.0F;
            jump_sound_ctrl_.reset();
            fall_sound_ctrl_.start(y_);
            current_slope_ = nullptr;
            break;
    }
}

void Player::updateState(float delta_time) {
    switch (state_) {
        case State::ON_GROUND:
            updateOnGround(delta_time);
            break;
        case State::ON_SLOPE:
            updateOnSlope(delta_time);
            break;
        case State::JUMPING:
            updateJumping(delta_time);
            break;
        case State::FALLING:
            updateFalling(delta_time);
            break;
    }
}

// Actualización lógica del estado ON_GROUND
void Player::updateOnGround(float delta_time) {
    (void)delta_time;       // No usado en este método, pero se mantiene por consistencia
    handleConveyorBelts();  // Gestiona las cintas transportadoras
    handleShouldFall();     // Verifica si debe caer (no tiene suelo)

    // Verifica si el jugador quiere saltar
    if (wanna_jump_) { transitionToState(State::JUMPING); }
}

// Actualización lógica del estado ON_SLOPE
void Player::updateOnSlope(float delta_time) {
    (void)delta_time;  // No usado en este método, pero se mantiene por consistencia

    handleShouldFall();
    // NOTA: No llamamos handleShouldFall() aquí porque moveOnSlope() ya maneja
    // todas las condiciones de salida de la rampa (out of bounds, transición a superficie plana)

    // Verifica si el jugador quiere saltar
    if (wanna_jump_) { transitionToState(State::JUMPING); }
}

// Actualización lógica del estado JUMPING
void Player::updateJumping(float delta_time) {
    auto_movement_ = false;     // Desactiva el movimiento automático durante el salto
    playJumpSound(delta_time);  // Reproduce los sonidos de salto
    handleJumpEnd();            // Verifica si el salto ha terminado (alcanzó la altura inicial)
}

// Actualización lógica del estado FALLING
void Player::updateFalling(float delta_time) {
    auto_movement_ = false;     // Desactiva el movimiento automático durante la caída
    playFallSound(delta_time);  // Reproduce los sonidos de caída
}

// Movimiento físico del estado ON_GROUND
void Player::moveOnGround(float delta_time) {
    // Determinama cuál debe ser la velocidad a partir de automovement o de wanna_go_
    updateVelocity();

    if (vx_ == 0.0F) { return; }

    // Movimiento horizontal y colision con muros
    applyHorizontalMovement(delta_time);

    // Comprueba colision con rampas, corrige y cambia estado
    const int SIDE_X = vx_ < 0.0F ? static_cast<int>(x_) : static_cast<int>(x_) + WIDTH - 1;
    const LineVertical SIDE = {
        .x = SIDE_X,
        .y1 = static_cast<int>(y_) + HEIGHT - 2,
        .y2 = static_cast<int>(y_) + HEIGHT - 1};

    // Comprueba la rampa correspondiente según la dirección
    const int SLOPE_Y = vx_ < 0.0F ? room_->checkLeftSlopes(SIDE) : room_->checkRightSlopes(SIDE);
    if (SLOPE_Y != Collision::NONE) {
        // Hay rampa: sube al jugador para pegarlo a la rampa
        y_ = SLOPE_Y - HEIGHT;
        transitionToState(State::ON_SLOPE);
    }
#ifdef _DEBUG
    Debug::get()->set("sl.detect_y", SLOPE_Y != Collision::NONE ? std::to_string(SLOPE_Y) : "-");
#endif

    // Comprueba si está sobre una rampa
    if (isOnSlope()) { transitionToState(State::ON_SLOPE); }
}

// Movimiento físico del estado ON_SLOPE
void Player::moveOnSlope(float delta_time) {
    // Determinama cuál debe ser la velocidad a partir de automovement o de wanna_go_
    updateVelocity();

    // Verificar rampa válida antes de comprobar velocidad: si no hay rampa siempre caer,
    // independientemente de si hay o no input (evita bloqueo con vx_=0 y slope null)
    if (current_slope_ == nullptr) {
        transitionToState(State::FALLING);
        return;
    }

    if (vx_ == 0.0F) { return; }

    // Determinar el tipo de rampa
    const bool IS_LEFT_SLOPE = isLeftSlope();

    // Movimiento horizontal con colisión lateral
    applyHorizontalMovement(delta_time);

    // Seleccionar el pie apropiado según el tipo de rampa
    // Left slopes (forma \) colisionan con el pie izquierdo
    // Right slopes (forma /) colisionan con el pie derecho
    const int X = IS_LEFT_SLOPE ? x_ : x_ + WIDTH - 1;

    // Calcular la Y basada en la ecuación de la rampa (45 grados)
    // Left slope (\): y aumenta con x -> y = y1 + (x - x1)
    // Right slope (/): y disminuye con x -> y = y1 - (x - x1)
    if (IS_LEFT_SLOPE) {
        y_ = current_slope_->y1 + (X - current_slope_->x1) - HEIGHT;
    } else {
        y_ = current_slope_->y1 - (X - current_slope_->x1) - HEIGHT;
    }

    // Verificar si el pie ha salido de los límites horizontales de la rampa
    // Usar min/max porque LEFT slopes tienen x1<x2 pero RIGHT slopes tienen x1>x2
    const int MIN_X = std::min(current_slope_->x1, current_slope_->x2);
    const int MAX_X = std::max(current_slope_->x1, current_slope_->x2);
    const bool OUT_OF_BOUNDS = (X < MIN_X) || (X > MAX_X);

#ifdef _DEBUG
    Debug::get()->set("sl.foot", std::to_string(X));
    Debug::get()->set("sl.y_c", std::to_string(static_cast<int>(y_)));
    Debug::get()->set("sl.oob", OUT_OF_BOUNDS ? "YES" : "ok");
#endif

    if (OUT_OF_BOUNDS) {
        // Determinar si estamos saliendo por arriba o por abajo de la rampa
        const bool EXITING_DOWNWARD = (X > current_slope_->x2 && IS_LEFT_SLOPE) ||
            (X < current_slope_->x1 && !IS_LEFT_SLOPE);
        const bool EXITING_UPWARD = (X < current_slope_->x1 && IS_LEFT_SLOPE) ||
            (X > current_slope_->x2 && !IS_LEFT_SLOPE);
#ifdef _DEBUG
        Debug::get()->set("sl.oob", EXITING_DOWNWARD ? "DOWN" : "UP");
#endif

        if (EXITING_DOWNWARD) {
            // Salida por abajo: no hacer nada
            // y_ += 1.0F;
        }

        if (EXITING_UPWARD) {
            // Salida por arriba: bajar un pixel ya que ha subido 1 de mas al salirse de la recta
            y_ += 1.0F;
        }

        // Verificar si hay soporte debajo (suelo plano o conveyor belt)
        if (isOnTopSurface() || isOnConveyorBelt()) {
            // Hay soporte: transición a ON_GROUND (podría ser superficie o conveyor belt)
            transitionToState(State::ON_GROUND);
        } else {
            // Sin soporte: empezar a caer
            transitionToState(State::FALLING);
        }
        return;
    }

    // Verificar transición a superficie plana
    /*if (isOnTopSurface()) {
        transitionToState(State::ON_GROUND);
        return;
    }*/
}

// Movimiento físico del estado JUMPING
void Player::moveJumping(float delta_time) {
    // Movimiento horizontal
    applyHorizontalMovement(delta_time);

    // Movimiento vertical
    applyGravity(delta_time);

    const float DISPLACEMENT_Y = vy_ * delta_time;
    // Movimiento vertical hacia arriba
    if (vy_ < 0.0F) {
        const SDL_FRect PROJECTION = getProjection(Direction::UP, DISPLACEMENT_Y);

        // Comprueba la colisión
        const int POS = room_->checkBottomSurfaces(PROJECTION);

        // Calcula la nueva posición
        if (POS == Collision::NONE) {
            // Si no hay colisión
            y_ += DISPLACEMENT_Y;
        } else {
            // Si hay colisión lo mueve hasta donde no colisiona -> FALLING
            y_ = POS + 1;
            transitionToState(State::FALLING);
        }
    }
    // Movimiento vertical hacia abajo
    else if (vy_ > 0.0F) {
        // Crea el rectangulo de proyección en el eje Y para ver si colisiona
        const SDL_FRect PROJECTION = getProjection(Direction::DOWN, DISPLACEMENT_Y);

        // JUMPING colisiona con rampas solo si vx_ == 0
        if (vx_ == 0.0F) {
            handleLandingFromAir(DISPLACEMENT_Y, PROJECTION);
        } else {
            // Comprueba la colisión con las superficies y las cintas transportadoras (sin rampas)
            // Extendemos 1px hacia arriba para detectar suelos traversados ligeramente al
            // entrar horizontalmente (consecuencia del margen h=HEIGHT-1 en la proyección horizontal)
            const SDL_FRect ADJ = {.x = PROJECTION.x, .y = PROJECTION.y - 1.0F, .w = PROJECTION.w, .h = PROJECTION.h + 1.0F};
            const float POS = std::max(room_->checkTopSurfaces(ADJ), room_->checkAutoSurfaces(ADJ));
            if (POS != Collision::NONE) {
                // Si hay colisión lo mueve hasta donde no colisiona y pasa a estar sobre la superficie
                y_ = POS - HEIGHT;
                transitionToState(State::ON_GROUND);
            } else {
                // Esta saltando con movimiento horizontal y no hay colisión con los muros
                // Calcula la nueva posición (atraviesa rampas)
                y_ += DISPLACEMENT_Y;
            }
        }
    }
}

// Movimiento físico del estado FALLING
void Player::moveFalling(float delta_time) {
    // Crea el rectangulo de proyección en el eje Y para ver si colisiona
    const float DISPLACEMENT = vy_ * delta_time;
    const SDL_FRect PROJECTION = getProjection(Direction::DOWN, DISPLACEMENT);

    // Comprueba aterrizaje en superficies y rampas
    handleLandingFromAir(DISPLACEMENT, PROJECTION);
}

// Comprueba si está situado en alguno de los cuatro bordes de la habitación
auto Player::handleBorders() -> Room::Border {
    if (x_ < PlayArea::LEFT) {
        return Room::Border::LEFT;
    }

    if (x_ + WIDTH > PlayArea::RIGHT) {
        return Room::Border::RIGHT;
    }

    if (y_ < PlayArea::TOP) {
        return Room::Border::TOP;
    }

    if (y_ + HEIGHT > PlayArea::BOTTOM) {
        // Si llega en estado terminal, muere y no cruza
        const bool SHOULD_DIE = static_cast<int>(y_) - last_grounded_position_ > MAX_FALLING_HEIGHT;
        if (SHOULD_DIE) { markAsDead(); }
        return is_alive_ ? Room::Border::BOTTOM : Room::Border::NONE;
    }

    return Room::Border::NONE;
}

// Cambia al jugador de un borde al opuesto. Util para el cambio de pantalla
void Player::switchBorders() {
    switch (border_) {
        case Room::Border::TOP:
            y_ = PlayArea::BOTTOM - HEIGHT - Tile::SIZE;
            // CRÍTICO: Resetear last_grounded_position_ para evitar muerte falsa por diferencia de Y entre pantallas
            last_grounded_position_ = static_cast<int>(y_);
            transitionToState(State::ON_GROUND);  // TODO: Detectar si debe ser ON_SLOPE
            break;

        case Room::Border::BOTTOM:
            y_ = PlayArea::TOP;
            // CRÍTICO: Resetear last_grounded_position_ para evitar muerte falsa por diferencia de Y entre pantallas
            last_grounded_position_ = static_cast<int>(y_);
            transitionToState(State::ON_GROUND);  // TODO: Detectar si debe ser ON_SLOPE
            break;

        case Room::Border::RIGHT:
            x_ = PlayArea::LEFT;
            break;

        case Room::Border::LEFT:
            x_ = PlayArea::RIGHT - WIDTH;
            break;

        default:
            break;
    }

    border_ = Room::Border::NONE;
    syncSpriteAndCollider();
}

// Aplica gravedad al jugador
void Player::applyGravity(float delta_time) {
    // La gravedad solo se aplica cuando el jugador esta saltando
    // Nunca mientras cae o esta de pie
    if (state_ == State::JUMPING) {
        vy_ += GRAVITY_FORCE * delta_time;
        vy_ = std::min(vy_, MAX_VY);
    }
}

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

// Comprueba si ha finalizado el salto al alcanzar la altura de inicio
void Player::handleJumpEnd() {
    // Si el jugador vuelve EXACTAMENTE a la altura inicial, debe CONTINUAR en JUMPING
    // Solo cuando la SUPERA (desciende más allá) cambia a FALLING
    if (state_ == State::JUMPING && vy_ > 0.0F && static_cast<int>(y_) > last_grounded_position_) {
        transitionToState(State::FALLING);
    }
}

// Calcula y reproduce el sonido de salto basado en tiempo transcurrido
void Player::playJumpSound(float delta_time) {
    size_t sound_index;
    if (jump_sound_ctrl_.shouldPlay(delta_time, sound_index)) {
        if (sound_index < jumping_sound_.size()) {
            Audio::get()->playSound(jumping_sound_[sound_index], Audio::Group::GAME);
        }
    }
}

// Calcula y reproduce el sonido de caída basado en distancia vertical recorrida
void Player::playFallSound(float delta_time) {
    size_t sound_index;
    if (fall_sound_ctrl_.shouldPlay(delta_time, y_, sound_index)) {
        if (sound_index < falling_sound_.size()) {
            Audio::get()->playSound(falling_sound_[sound_index], Audio::Group::GAME);
        }
    }
}

// Comprueba si el jugador tiene suelo debajo de los pies
auto Player::isOnFloor() -> bool {
    bool on_top_surface = false;
    bool on_conveyor_belt = false;
    updateFeet();

    // Comprueba las superficies
    on_top_surface |= room_->checkTopSurfaces(under_left_foot_);
    on_top_surface |= room_->checkTopSurfaces(under_right_foot_);

    // Comprueba las cintas transportadoras
    on_conveyor_belt |= room_->checkConveyorBelts(under_left_foot_);
    on_conveyor_belt |= room_->checkConveyorBelts(under_right_foot_);

    // Comprueba las rampas
    auto on_slope_l = room_->checkLeftSlopes(under_left_foot_);
    auto on_slope_r = room_->checkRightSlopes(under_right_foot_);

    return on_top_surface || on_conveyor_belt || on_slope_l || on_slope_r;
}

// Comprueba si el jugador está sobre una superficie
auto Player::isOnTopSurface() -> bool {
    bool on_top_surface = false;
    updateFeet();

    // Comprueba las superficies
    on_top_surface |= room_->checkTopSurfaces(under_left_foot_);
    on_top_surface |= room_->checkTopSurfaces(under_right_foot_);

    return on_top_surface;
}

// Comprueba si el jugador esta sobre una cinta transportadora
auto Player::isOnConveyorBelt() -> bool {
    bool on_conveyor_belt = false;
    updateFeet();

    // Comprueba las superficies
    on_conveyor_belt |= room_->checkConveyorBelts(under_left_foot_);
    on_conveyor_belt |= room_->checkConveyorBelts(under_right_foot_);

    return on_conveyor_belt;
}

// Comprueba si el jugador está sobre una rampa
// Retorna true SOLO si un pie está en rampa Y el otro pie está volando (sin soporte)
auto Player::isOnSlope() -> bool {
    updateFeet();

    // Verificar qué pie está en qué tipo de rampa
    const bool LEFT_FOOT_ON_LEFT_SLOPE = room_->checkLeftSlopes(under_left_foot_);
    const bool RIGHT_FOOT_ON_RIGHT_SLOPE = room_->checkRightSlopes(under_right_foot_);

    // Verificar si cada pie está "volando" (sin soporte: ni top surface ni conveyor belt)
    const bool LEFT_FOOT_FLYING = !(room_->checkTopSurfaces(under_left_foot_) ||
        room_->checkConveyorBelts(under_left_foot_));
    const bool RIGHT_FOOT_FLYING = !(room_->checkTopSurfaces(under_right_foot_) ||
        room_->checkConveyorBelts(under_right_foot_));

    // Retornar true si UN pie en rampa Y el OTRO volando
    return (LEFT_FOOT_ON_LEFT_SLOPE && RIGHT_FOOT_FLYING) ||
        (RIGHT_FOOT_ON_RIGHT_SLOPE && LEFT_FOOT_FLYING);
}

// Comprueba si current_slope_ es una rampa izquierda (ascendente a la izquierda)
// Las rampas izquierdas tienen forma \ con x1 < x2 (x aumenta de izq a der)
auto Player::isLeftSlope() -> bool {
    if (current_slope_ == nullptr) {
        return false;
    }
    // Left slopes (\): x1 < x2 (x aumenta de izquierda a derecha)
    // Right slopes (/): x1 > x2 (x decrece de izquierda a derecha)
    return current_slope_->x1 < current_slope_->x2;
}

// Actualiza current_slope_ con la rampa correcta según el pie que toca
void Player::updateCurrentSlope() {
    updateFeet();

    // Left slopes (\) ascendentes a izquierda tocan el pie izquierdo
    if (room_->checkLeftSlopes(under_left_foot_)) {
        current_slope_ = room_->getSlopeAtPoint(under_left_foot_);
    }
    // Right slopes (/) ascendentes a derecha tocan el pie derecho
    else if (room_->checkRightSlopes(under_right_foot_)) {
        current_slope_ = room_->getSlopeAtPoint(under_right_foot_);
    }
    // Fallback para casos edge
    else {
        current_slope_ = room_->getSlopeAtPoint(under_left_foot_);
        if (current_slope_ == nullptr) {
            current_slope_ = room_->getSlopeAtPoint(under_right_foot_);
        }
    }

#ifdef _DEBUG
    if (current_slope_ != nullptr) {
        Debug::get()->set("sl.type", isLeftSlope() ? "L\\" : "R/");
        Debug::get()->set("sl.p1", std::to_string(current_slope_->x1) + "," + std::to_string(current_slope_->y1));
        Debug::get()->set("sl.p2", std::to_string(current_slope_->x2) + "," + std::to_string(current_slope_->y2));
    } else {
        Debug::get()->set("sl.type", "null");
        Debug::get()->unset("sl.p1");
        Debug::get()->unset("sl.p2");
    }
#endif
}

// Comprueba que el jugador no toque ningun tile de los que matan
auto Player::handleKillingTiles() -> bool {
    // Comprueba si hay contacto con algún tile que mata
    if (std::ranges::any_of(collider_points_, [this](const auto& c) -> bool {
            return room_->getTile(c) == Room::Tile::KILL;
        })) {
        markAsDead();  // 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 (0 = automático según options)
void Player::setColor(Uint8 color) {
    if (color != 0) {
        color_ = color;
        return;
    }

    // Color personalizado desde opciones
    if (Options::game.player_color >= 0) {
        color_ = static_cast<Uint8>(Options::game.player_color);
    } else {
        color_ = static_cast<Uint8>(PaletteColor::WHITE);
    }

    // Si el color coincide con el fondo de la habitación, usar fallback
    if (room_ != nullptr && color_ == room_->getBGColor()) {
        color_ = (room_->getBGColor() != static_cast<Uint8>(PaletteColor::WHITE))
            ? static_cast<Uint8>(PaletteColor::WHITE)
            : static_cast<Uint8>(PaletteColor::BRIGHT_BLACK);
    }
}

// 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() {
    under_left_foot_ = {
        .x = x_,
        .y = y_ + HEIGHT};
    under_right_foot_ = {
        .x = x_ + WIDTH - 1,
        .y = y_ + HEIGHT};
}

// Inicializa los sonidos de salto y caida
void Player::initSounds() {
    for (int i = 0; i < 24; ++i) {
        std::string sound_file = "jump" + std::to_string(i + 1) + ".wav";
        jumping_sound_[i] = Resource::Cache::get()->getSound(sound_file);

        if (i >= 10) {  // i+1 >= 11
            falling_sound_[i - 10] = Resource::Cache::get()->getSound(sound_file);
        }
    }
}

// Implementación de JumpSoundController::start
void Player::JumpSoundController::start() {
    current_index = 0;
    elapsed_time = 0.0F;
    active = true;
}

// Implementación de JumpSoundController::reset
void Player::JumpSoundController::reset() {
    active = false;
    current_index = 0;
    elapsed_time = 0.0F;
}

// Implementación de JumpSoundController::shouldPlay
auto Player::JumpSoundController::shouldPlay(float delta_time, size_t& out_index) -> bool {
    if (!active) {
        return false;
    }

    // Acumula el tiempo transcurrido durante el salto
    elapsed_time += delta_time;

    // Calcula qué sonido debería estar sonando según el tiempo
    size_t target_index = FIRST_SOUND + static_cast<size_t>((elapsed_time / SECONDS_PER_SOUND));
    target_index = std::min(target_index, LAST_SOUND);

    // Reproduce si hemos avanzado a un nuevo sonido
    if (target_index > current_index) {
        current_index = target_index;
        out_index = current_index;
        return true;
    }

    return false;
}

// Implementación de FallSoundController::start
void Player::FallSoundController::start(float start_y) {
    current_index = 0;
    distance_traveled = 0.0F;
    last_y = start_y;
    active = true;
}

// Implementación de FallSoundController::reset
void Player::FallSoundController::reset() {
    active = false;
    current_index = 0;
    distance_traveled = 0.0F;
}

// Implementación de FallSoundController::shouldPlay
auto Player::FallSoundController::shouldPlay(float delta_time, float current_y, size_t& out_index) -> bool {
    (void)delta_time;  // No usado actualmente, pero recibido por consistencia

    if (!active) {
        return false;
    }

    // Acumula la distancia recorrida (solo hacia abajo)
    if (current_y > last_y) {
        distance_traveled += (current_y - last_y);
    }
    last_y = current_y;

    // Calcula qué sonido debería estar sonando según el intervalo
    size_t target_index = FIRST_SOUND + static_cast<size_t>((distance_traveled / PIXELS_PER_SOUND));

    // El sonido a reproducir se limita a LAST_SOUND (13), pero el índice interno sigue creciendo
    size_t sound_to_play = std::min(target_index, LAST_SOUND);

    // Reproduce si hemos avanzado a un nuevo índice (permite repetición de sonido 13)
    if (target_index > current_index) {
        current_index = target_index;  // Guardamos el índice real (puede ser > LAST_SOUND)
        out_index = sound_to_play;     // Pero reproducimos LAST_SOUND cuando corresponde
        return true;
    }

    return false;
}

// Aplica los valores de spawn al jugador
void Player::applySpawnValues(const SpawnData& spawn) {
    x_ = spawn.x;
    y_ = spawn.y;
    y_prev_ = spawn.y;  // Inicializar y_prev_ igual a y_ para evitar saltos en primer frame
    vx_ = spawn.vx;
    vy_ = spawn.vy;
    last_grounded_position_ = spawn.last_grounded_position;
    state_ = spawn.state;
    sprite_->setFlip(spawn.flip);
}

// Resuelve nombre de skin a fichero de animación
auto Player::skinToAnimationPath(const std::string& skin_name) -> std::string {
    if (skin_name == "default") {
        return "player.yaml";
    }
    return skin_name + ".yaml";
}

// Cambia la skin del jugador en caliente preservando la orientación actual
void Player::setSkin(const std::string& skin_name) {
    const auto FLIP = sprite_->getFlip();
    initSprite(skinToAnimationPath(skin_name));
    sprite_->setFlip(FLIP);
}

// Inicializa el sprite del jugador
void Player::initSprite(const std::string& animations_path) {
    const auto& animation_data = Resource::Cache::get()->getAnimationData(animations_path);
    sprite_ = std::make_unique<AnimatedSprite>(animation_data);
    sprite_->setWidth(WIDTH);
    sprite_->setHeight(HEIGHT);
    sprite_->setCurrentAnimation("default");
}

// Actualiza la posición del sprite y las colisiones
void Player::syncSpriteAndCollider() {
    placeSprite();              // Coloca el sprite en la posición del jugador
    collider_box_ = getRect();  // Actualiza el rectangulo de colisión
    updateColliderPoints();     // Actualiza los puntos de colisión
#ifdef _DEBUG
    updateFeet();
#endif
}

// Coloca el sprite en la posición del jugador
void Player::placeSprite() {
    sprite_->setPos(x_, y_);
}

// Gestiona la muerta al ccaer desde muy alto
void Player::handleDeathByFalling() {
    const int FALL_DISTANCE = static_cast<int>(y_) - last_grounded_position_;
    if (previous_state_ == State::FALLING && FALL_DISTANCE > MAX_FALLING_HEIGHT) {
        markAsDead();  // Muere si cae más de 32 píxeles
    }
}

// Calcula la velocidad en x
void Player::updateVelocity() {
    if (auto_movement_) {
        // La cinta transportadora tiene el control
        vx_ = HORIZONTAL_VELOCITY * room_->getConveyorBeltDirection();
        sprite_->setFlip(vx_ < 0.0F ? Flip::LEFT : Flip::RIGHT);
    } else {
        // El jugador tiene el control
        switch (wanna_go_) {
            case Direction::LEFT:
                vx_ = -HORIZONTAL_VELOCITY;
                sprite_->setFlip(Flip::LEFT);
                break;
            case Direction::RIGHT:
                vx_ = HORIZONTAL_VELOCITY;
                sprite_->setFlip(Flip::RIGHT);
                break;
            case Direction::NONE:
            default:
                vx_ = 0.0F;
                break;
        }
    }
}

// Aplica movimiento horizontal con colisión de muros
void Player::applyHorizontalMovement(float delta_time) {
    if (vx_ == 0.0F) { return; }

    const float DISPLACEMENT = vx_ * delta_time;
    if (vx_ < 0.0F) {
        const SDL_FRect PROJECTION = getProjection(Direction::LEFT, DISPLACEMENT);
        const int POS = room_->checkRightSurfaces(PROJECTION);
        if (POS == Collision::NONE) {
            x_ += DISPLACEMENT;
        } else {
            x_ = POS + 1;
        }
    } else {
        const SDL_FRect PROJECTION = getProjection(Direction::RIGHT, DISPLACEMENT);
        const int POS = room_->checkLeftSurfaces(PROJECTION);
        if (POS == Collision::NONE) {
            x_ += DISPLACEMENT;
        } else {
            x_ = POS - WIDTH;
        }
    }
}

// Detecta aterrizaje en superficies y rampas
auto Player::handleLandingFromAir(float displacement, const SDL_FRect& projection) -> bool {
    // Comprueba la colisión con las superficies y las cintas transportadoras
    const float POS = std::max(room_->checkTopSurfaces(projection), room_->checkAutoSurfaces(projection));
    if (POS != Collision::NONE) {
        // Si hay colisión lo mueve hasta donde no colisiona y pasa a estar sobre la superficie
        y_ = POS - HEIGHT;
        transitionToState(State::ON_GROUND);
        return true;
    }

    // Comprueba la colisión con las rampas
    auto rect = toSDLRect(projection);
    const LineVertical LEFT_SIDE = {.x = rect.x, .y1 = rect.y, .y2 = rect.y + rect.h};
    const LineVertical RIGHT_SIDE = {.x = rect.x + rect.w - 1, .y1 = rect.y, .y2 = rect.y + rect.h};
    const float POINT = std::max(room_->checkRightSlopes(RIGHT_SIDE), room_->checkLeftSlopes(LEFT_SIDE));
    if (POINT != Collision::NONE) {
        y_ = POINT - HEIGHT;
        transitionToState(State::ON_SLOPE);
        return true;
    }

    // No hay colisión
    y_ += displacement;
#ifdef _DEBUG
    // Guarda por si en debug el jugador se sale de la pantalla, para que no esté cayendo infinitamente
    if (y_ > PlayArea::BOTTOM + HEIGHT) { y_ = PlayArea::TOP + 2; }
#endif
    return false;
}

// Resetea los controladores de sonido al aterrizar
void Player::resetSoundControllersOnLanding() {
    jump_sound_ctrl_.reset();
    fall_sound_ctrl_.reset();
}

// Devuelve el rectangulo de proyeccion
auto Player::getProjection(Direction direction, float displacement) -> SDL_FRect {
    switch (direction) {
        case Direction::LEFT:
            return {
                .x = x_ + displacement,
                .y = y_,
                .w = std::ceil(std::fabs(displacement)),  // Para evitar que tenga una anchura de 0 pixels
                .h = HEIGHT - 1};                         // -1 para dar ventana de 2px en aperturas de altura exacta

        case Direction::RIGHT:
            return {
                .x = x_ + WIDTH,
                .y = y_,
                .w = std::ceil(displacement),  // Para evitar que tenga una anchura de 0 pixels
                .h = HEIGHT - 1};              // -1 para dar ventana de 2px en aperturas de altura exacta

        case Direction::UP:
            return {
                .x = x_,
                .y = y_ + displacement,
                .w = WIDTH,
                .h = std::ceil(std::fabs(displacement))  // Para evitar que tenga una altura de 0 pixels
            };

        case Direction::DOWN:
            return {
                .x = x_,
                .y = y_ + HEIGHT,
                .w = WIDTH,
                .h = std::ceil(displacement)  // Para evitar que tenga una altura de 0 pixels
            };

        default:
            return {
                .x = 0.0F,
                .y = 0.0F,
                .w = 0.0F,
                .h = 0.0F};
    }
}

// Marca al jugador como muerto
void Player::markAsDead() {
    is_alive_ = (Options::cheats.invincible == Options::Cheat::State::ENABLED);
}

#ifdef _DEBUG
// Establece la posición del jugador directamente (debug)
void Player::setDebugPosition(float x, float y) {
    x_ = x;
    y_ = y;
    syncSpriteAndCollider();
}

// Fija estado ON_GROUND, velocidades a 0, actualiza last_grounded_position_ (debug)
void Player::finalizeDebugTeleport() {
    vx_ = 0.0F;
    vy_ = 0.0F;
    last_grounded_position_ = static_cast<int>(y_);
    transitionToState(State::ON_GROUND);
    syncSpriteAndCollider();
}
#endif