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

#include <algorithm>  // Para max, min
#include <cmath>      // Para ceil, abs

#include "core/audio/audio.hpp"                       // Para Audio
#include "core/input/input.hpp"                       // Para Input, InputAction
#include "core/rendering/sprite/animated_sprite.hpp"  // Para AnimatedSprite
#include "core/resources/resource_cache.hpp"          // Para Resource
#include "game/gameplay/room.hpp"                     // Para Room
#include "game/gameplay/tile_collider.hpp"            // Para TileCollider
#include "game/options.hpp"                           // Para Cheat, Options
#include "utils/defines.hpp"                          // Para PlayArea, Collision

#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_;
}

// ============================================================================
// Render
// ============================================================================

void Player::render() {
    sprite_->render(1, color_);
}

// ============================================================================
// Update — pipeline principal (6 fases)
// ============================================================================

void Player::update(float delta_time) {
    if (is_paused_) { return; }

    // 1. Leer input
    handleInput();

    // 2. Calcular velocidades
    updateVelocity(delta_time);
    if (state_ == State::ON_AIR) {
        applyGravity(delta_time);
    }

    // 3. Saltar o caer a través de plataformas/slopes
    handleJumpAndDrop();

    // 4. Aplicar movimiento con colisión
    moveHorizontal(delta_time);
    moveVertical(delta_time);

    // 5. Detectar caída
    checkFalling();

    // 6. Kill tiles
    if (room_->getTileCollider().touchesKillTile(x_, y_, WIDTH, HEIGHT)) {
        markAsDead();
    }

    // 7. Finalizar
    syncSpriteAndCollider();
    animate(delta_time);
    border_ = handleBorders();

#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::ON_AIR:
            Debug::get()->set("P.STATE", "ON_AIR");
            break;
    }
#endif
}

// ============================================================================
// Fase 1: Input
// ============================================================================

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

    const bool JUMP_PRESSED = Input::get()->checkAction(InputAction::JUMP);
    wanna_jump_ = JUMP_PRESSED && !jump_held_;
    jump_held_ = JUMP_PRESSED;
    wanna_down_ = Input::get()->checkAction(InputAction::DOWN);
}

// ============================================================================
// Fase 2: Velocidades
// ============================================================================

void Player::updateVelocity(float delta_time) {
    float target = 0.0F;
    switch (wanna_go_) {
        case Direction::LEFT:
            target = -HORIZONTAL_VELOCITY;
            break;
        case Direction::RIGHT:
            target = HORIZONTAL_VELOCITY;
            break;
        default:
            target = 0.0F;
            break;
    }

    // Orientación del sprite
    if (target > 0.0F) {
        sprite_->setFlip(Flip::RIGHT);
    } else if (target < 0.0F) {
        sprite_->setFlip(Flip::LEFT);
    }

    // Inercia: aire = gradual ambas direcciones, suelo = instantáneo arranque, gradual frenada
    const float STEP = HORIZONTAL_ACCEL * delta_time;
    if (state_ == State::ON_AIR) {
        if (vx_ < target) {
            vx_ = std::min(vx_ + STEP, target);
        } else if (vx_ > target) {
            vx_ = std::max(vx_ - STEP, target);
        }
    } else {
        if (target != 0.0F) {
            vx_ = target;
        } else if (vx_ > 0.0F) {
            vx_ = std::max(vx_ - STEP, 0.0F);
        } else if (vx_ < 0.0F) {
            vx_ = std::min(vx_ + STEP, 0.0F);
        }
    }
}

void Player::applyGravity(float delta_time) {
    const float GRAVITY = (vy_ < 0.0F && !jump_held_)
        ? GRAVITY_FORCE * LOW_JUMP_GRAVITY_MULT
        : GRAVITY_FORCE;
    vy_ += GRAVITY * delta_time;
    vy_ = std::min(vy_, MAX_VY);
}

// ============================================================================
// Fase 3: Saltar y drop-through
// ============================================================================

void Player::handleJumpAndDrop() {
    if (state_ == State::ON_AIR) { return; }

    if (wanna_jump_) {
        startJump();
        return;
    }

    // Drop-through: slope
    if (wanna_down_ && state_ == State::ON_SLOPE) {
        y_ += 1.0F;
        vy_ = 0.0F;
        transitionToState(State::ON_AIR);
        return;
    }

    // Drop-through: plataforma passable
    if (wanna_down_ && state_ == State::ON_GROUND) {
        const auto& tc = room_->getTileCollider();
        float foot_y = y_ + HEIGHT;
        int foot_row = static_cast<int>(foot_y) / Tile::SIZE;
        int left_col = static_cast<int>(x_) / Tile::SIZE;
        int right_col = static_cast<int>(x_ + WIDTH - 1) / Tile::SIZE;

        for (int col = left_col; col <= right_col; ++col) {
            if (tc.getTileAt(col, foot_row) == TileCollider::Tile::PASSABLE) {
                y_ += 1.0F;
                vy_ = 0.0F;
                transitionToState(State::ON_AIR);
                return;
            }
        }
    }
}

void Player::startJump() {
    vy_ = JUMP_VELOCITY;
    last_grounded_position_ = y_;
    Audio::get()->playSound(jump_sound_, Audio::Group::GAME);
    transitionToState(State::ON_AIR);
}

// ============================================================================
// Fase 4a: Movimiento horizontal
// ============================================================================

void Player::moveHorizontal(float delta_time) {
    if (vx_ == 0.0F) {
        // Aunque no haya movimiento horizontal, si estamos en slope hay que seguirla
        // (por si la gravedad nos ha movido o algo)
        return;
    }

    const auto& tc = room_->getTileCollider();
    float new_x = x_ + vx_ * delta_time;

    // Colisión con paredes
    if (vx_ < 0.0F) {
        float wall = tc.checkWallLeft(new_x, y_, WIDTH, HEIGHT);
        if (wall != Collision::NONE) {
            new_x = wall;
        }
    } else {
        float wall = tc.checkWallRight(new_x, y_, WIDTH, HEIGHT);
        if (wall != Collision::NONE) {
            new_x = wall - WIDTH;
        }
    }

    x_ = new_x;

    // Si estamos en una slope, ajustar Y para seguirla
    if (state_ == State::ON_SLOPE) {
        followSlope();
    }

    // Si estamos en suelo plano, detectar entrada a slope
    if (state_ == State::ON_GROUND) {
        detectSlopeEntry();
    }
}

// Ajusta Y del jugador para seguir la superficie de la slope mientras camina.
// Si el pie sale del tile actual, busca el siguiente tile de slope en la fila
// actual y la inferior (las slopes en escalera bajan una fila por tile).
// Si no encuentra slope, llama a exitSlope().
void Player::followSlope() {
    const auto& tc = room_->getTileCollider();

    // SLOPE_L (\): pie izquierdo. SLOPE_R (/): pie derecho.
    float foot_x = (slope_type_ == TileCollider::Tile::SLOPE_L) ? x_ : x_ + WIDTH - 1;

    // Calcular Y en la slope actual
    float surface_y = tc.getSlopeY(slope_tile_x_, slope_tile_y_, foot_x);
    y_ = surface_y - HEIGHT;

    // Comprobar si hemos salido del tile actual
    int foot_tile_x = static_cast<int>(foot_x) / Tile::SIZE;
    int foot_tile_y = static_cast<int>(y_ + HEIGHT) / Tile::SIZE;

    if (foot_tile_x != slope_tile_x_ || foot_tile_y != slope_tile_y_) {
        // Buscar slope en el tile calculado y en el de abajo (la escalera de slopes
        // siempre tiene el siguiente tile una fila arriba o abajo)
        for (int row = foot_tile_y; row <= foot_tile_y + 1; ++row) {
            auto new_tile = tc.getTileAt(foot_tile_x, row);
            if (new_tile == TileCollider::Tile::SLOPE_L || new_tile == TileCollider::Tile::SLOPE_R) {
                slope_tile_x_ = foot_tile_x;
                slope_tile_y_ = row;
                slope_type_ = new_tile;
                surface_y = tc.getSlopeY(slope_tile_x_, slope_tile_y_, foot_x);
                y_ = surface_y - HEIGHT;
                return;
            }
        }
        exitSlope();
    }
}

// El jugador ha salido del tile de slope sin encontrar otra slope adyacente.
// Comprueba si hay suelo debajo (foot_y y foot_y+1 para cubrir el boundary exacto
// entre filas cuando se sale por el extremo inferior de la slope).
// Si hay suelo, snapea al borde del tile. Si no, empieza a caer.
void Player::exitSlope() {
    const auto& tc = room_->getTileCollider();
    float foot_y = y_ + HEIGHT;

    // Comprobar suelo en la fila actual y la siguiente (al salir por abajo de una slope,
    // los pies pueden estar en el último pixel de la fila, justo antes del suelo)
    for (int check = 0; check <= 1; ++check) {
        float check_y = foot_y + check;
        if (tc.hasGroundBelow(x_, check_y, WIDTH)) {
            int row = static_cast<int>(check_y) / Tile::SIZE;
            y_ = static_cast<float>(row * Tile::SIZE) - HEIGHT;
            transitionToState(State::ON_GROUND);
            return;
        }
    }

    vy_ = 0.0F;
    transitionToState(State::ON_AIR);
}

// Detecta si el jugador ha pisado una slope caminando desde suelo plano.
// Las slopes en escalera están una fila arriba del suelo, así que checkSlopeBelow
// también mira la fila superior.
void Player::detectSlopeEntry() {
    const auto& tc = room_->getTileCollider();
    float foot_y = y_ + HEIGHT;

    auto slope = tc.checkSlopeBelow(x_, foot_y, WIDTH);
    if (slope.on_slope) {
        y_ = slope.surface_y - HEIGHT;
        slope_tile_x_ = slope.tile_x;
        slope_tile_y_ = slope.tile_y;
        slope_type_ = slope.type;
        transitionToState(State::ON_SLOPE);
    }
}

// ============================================================================
// Fase 4b: Movimiento vertical
// ============================================================================

void Player::moveVertical(float delta_time) {
    if (state_ != State::ON_AIR) { return; }

    const auto& tc = room_->getTileCollider();
    float displacement = vy_ * delta_time;

    if (vy_ < 0.0F) {
        // Subiendo: comprobar techo
        float new_y = y_ + displacement;
        float ceiling = tc.checkCeiling(x_, new_y, WIDTH);
        if (ceiling != Collision::NONE) {
            y_ = ceiling;
            vy_ = 0.0F;
        } else {
            y_ = new_y;
        }
    } else if (vy_ > 0.0F) {
        // Bajando: comprobar suelo
        float foot_y = y_ + HEIGHT;
        float new_foot_y = foot_y + displacement;
        auto hit = tc.checkFloor(x_, foot_y, WIDTH, new_foot_y);

        if (hit.y != Collision::NONE) {
            y_ = hit.y - HEIGHT;
            if (hit.type == TileCollider::Tile::SLOPE_L || hit.type == TileCollider::Tile::SLOPE_R) {
                slope_tile_x_ = hit.tile_x;
                slope_tile_y_ = hit.tile_y;
                slope_type_ = hit.type;
                transitionToState(State::ON_SLOPE);
            } else {
                transitionToState(State::ON_GROUND);
            }
        } else {
            y_ += displacement;
#ifdef _DEBUG
            if (y_ > PlayArea::BOTTOM + HEIGHT) { y_ = PlayArea::TOP + 2; }
#endif
        }
    }
}

// ============================================================================
// Fase 5: Detección de caída
// ============================================================================

void Player::checkFalling() {
    if (state_ == State::ON_AIR) { return; }

    const auto& tc = room_->getTileCollider();

    if (state_ == State::ON_SLOPE) {
        // Verificar que el tile de slope sigue existiendo
        auto tile = tc.getTileAt(slope_tile_x_, slope_tile_y_);
        if (tile != TileCollider::Tile::SLOPE_L && tile != TileCollider::Tile::SLOPE_R) {
            vy_ = 0.0F;
            transitionToState(State::ON_AIR);
        }
        return;
    }

    // ON_GROUND: comprobar si sigue habiendo suelo
    float foot_y = y_ + HEIGHT;
    if (!tc.hasGroundBelow(x_, foot_y, WIDTH)) {
        vy_ = 0.0F;
        transitionToState(State::ON_AIR);
    }
}

// ============================================================================
// Gestión de estado
// ============================================================================

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

    switch (state) {
        case State::ON_GROUND:
            vy_ = 0;
            if (previous_state_ == State::ON_AIR) {
                Audio::get()->playSound(land_sound_, Audio::Group::GAME);
            }
            break;
        case State::ON_SLOPE:
            vy_ = 0;
            if (previous_state_ == State::ON_AIR) {
                Audio::get()->playSound(land_sound_, Audio::Group::GAME);
            }
            break;
        case State::ON_AIR:
            last_grounded_position_ = static_cast<int>(y_);
            break;
    }
}

// ============================================================================
// Bordes de pantalla
// ============================================================================

auto Player::handleBorders() -> Room::Border {
    const float CENTER_X = x_ + WIDTH / 2.0F;
    const float CENTER_Y = y_ + HEIGHT / 2.0F;

    if (CENTER_X < PlayArea::LEFT) { return Room::Border::LEFT; }
    if (CENTER_X > PlayArea::RIGHT) { return Room::Border::RIGHT; }
    if (CENTER_Y < PlayArea::TOP) { return Room::Border::TOP; }
    if (CENTER_Y > PlayArea::BOTTOM) { return Room::Border::BOTTOM; }
    return Room::Border::NONE;
}

void Player::switchBorders() {
    switch (border_) {
        case Room::Border::TOP:
            y_ += PlayArea::HEIGHT;
            last_grounded_position_ = static_cast<int>(y_);
            break;
        case Room::Border::BOTTOM:
            y_ -= PlayArea::HEIGHT;
            last_grounded_position_ = static_cast<int>(y_);
            break;
        case Room::Border::RIGHT:
            x_ -= PlayArea::WIDTH;
            break;
        case Room::Border::LEFT:
            x_ += PlayArea::WIDTH;
            break;
        default:
            break;
    }
    border_ = Room::Border::NONE;
    syncSpriteAndCollider();
}

// ============================================================================
// Geometría y renderizado
// ============================================================================

void Player::syncSpriteAndCollider() {
    placeSprite();
    collider_box_ = getRect();
}

void Player::placeSprite() {
    sprite_->setPos(x_, y_);
}

void Player::animate(float delta_time) {  // NOLINT(readability-make-member-function-const)
    if (state_ == State::ON_AIR) {
        sprite_->setCurrentAnimation("jump");
    } else if (vx_ != 0) {
        sprite_->setCurrentAnimation("default");
        sprite_->update(delta_time);
    } else {
        sprite_->setCurrentAnimation("stand");
    }
}

// ============================================================================
// Color y skin
// ============================================================================

void Player::setColor(Uint8 color) {
    if (color != 0) {
        color_ = color;
        return;
    }
    if (Options::game.player_color >= 0) {
        color_ = static_cast<Uint8>(Options::game.player_color);
    } else {
        color_ = 14;
    }
    if (room_ != nullptr && color_ == room_->getBGColor()) {
        color_ = (room_->getBGColor() != 14) ? 14 : 1;
    }
}

auto Player::skinToAnimationPath(const std::string& skin_name) -> std::string {
    if (skin_name == "default") { return "player.yaml"; }
    return skin_name + ".yaml";
}

void Player::setSkin(const std::string& skin_name) {
    const auto FLIP = sprite_->getFlip();
    initSprite(skinToAnimationPath(skin_name));
    sprite_->setFlip(FLIP);
}

// ============================================================================
// Inicialización
// ============================================================================

void Player::initSprite(const std::string& animations_path) {  // NOLINT(readability-convert-member-functions-to-static)
    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");
}

void Player::initSounds() {  // NOLINT(readability-convert-member-functions-to-static)
    jump_sound_ = Resource::Cache::get()->getSound("jump.wav");
    land_sound_ = Resource::Cache::get()->getSound("land.wav");
}

void Player::applySpawnValues(const SpawnData& spawn) {
    x_ = spawn.x;
    y_ = spawn.y;
    vx_ = spawn.vx;
    vy_ = spawn.vy;
    last_grounded_position_ = spawn.last_grounded_position;
    state_ = spawn.state;
    sprite_->setFlip(spawn.flip);
}

// ============================================================================
// Utilidades
// ============================================================================

void Player::markAsDead() {
    is_alive_ = (Options::cheats.invincible == Options::Cheat::State::ENABLED);
}

#ifdef _DEBUG
void Player::setDebugPosition(float x, float y) {
    x_ = x;
    y_ = y;
    syncSpriteAndCollider();
}

void Player::finalizeDebugTeleport() {
    vx_ = 0.0F;
    vy_ = 0.0F;
    last_grounded_position_ = static_cast<int>(y_);
    slope_tile_x_ = 0;
    slope_tile_y_ = 0;
    slope_type_ = TileCollider::Tile::EMPTY;
    transitionToState(State::ON_GROUND);
    syncSpriteAndCollider();
}
#endif
