#include "balloon.h"

#include <algorithm>  // Para clamp
#include <array>      // Para array
#include <cmath>      // Para fabs

#include "animated_sprite.h"  // Para AnimatedSprite
#include "audio.h"            // Para Audio
#include "param.h"            // Para Param, ParamBalloon, param
#include "sprite.h"           // Para Sprite
#include "texture.h"          // Para Texture

// Constructor
Balloon::Balloon(float x, float y, Type type, Size size, float vel_x, float speed, Uint16 creation_timer, SDL_FRect play_area, std::shared_ptr<Texture> texture, const std::vector<std::string> &animation)
    : sprite_(std::make_unique<AnimatedSprite>(texture, animation)),
      x_(x),
      y_(y),
      vx_(vel_x),
      being_created_(creation_timer > 0),
      invulnerable_(creation_timer > 0),
      stopped_(creation_timer > 0),
      creation_counter_(creation_timer),
      creation_counter_ini_(creation_timer),
      type_(type),
      size_(size),
      speed_(speed),
      play_area_(play_area) {
    switch (type_) {
        case Type::BALLOON: {
            vy_ = 0;
            max_vy_ = 3.0F;

            const int INDEX = static_cast<int>(size_);
            gravity_ = param.balloon.settings.at(INDEX).grav;
            default_vy_ = param.balloon.settings.at(INDEX).vel;
            h_ = w_ = WIDTH.at(INDEX);
            power_ = POWER.at(INDEX);
            menace_ = MENACE.at(INDEX);
            score_ = SCORE.at(INDEX);
            bouncing_sound_ = BOUNCING_SOUND.at(INDEX);
            popping_sound_ = POPPING_SOUND.at(INDEX);

            break;
        }

        case Type::FLOATER: {
            default_vy_ = max_vy_ = vy_ = fabs(vx_ * 2.0F);
            gravity_ = 0.00F;

            const int INDEX = static_cast<int>(size_);
            h_ = w_ = WIDTH.at(INDEX);
            power_ = POWER.at(INDEX);
            menace_ = MENACE.at(INDEX);
            score_ = SCORE.at(INDEX);
            bouncing_sound_ = BOUNCING_SOUND.at(INDEX);
            popping_sound_ = POPPING_SOUND.at(INDEX);

            break;
        }

        case Type::POWERBALL: {
            constexpr int INDEX = 3;
            h_ = w_ = WIDTH.at(4);
            bouncing_sound_ = BOUNCING_SOUND.at(3);
            popping_sound_ = "power_ball_explosion.wav";
            power_ = score_ = menace_ = 0;

            vy_ = 0;
            max_vy_ = 3.0F;
            gravity_ = param.balloon.settings.at(INDEX).grav;
            default_vy_ = param.balloon.settings.at(INDEX).vel;

            sprite_->setRotate(creation_timer <= 0);
            sprite_->setRotateAmount(vx_ > 0.0F ? 2.0 : -2.0);

            break;
        }

        default:
            break;
    }

    // Configura el sprite
    sprite_->setWidth(w_);
    sprite_->setHeight(h_);
    shiftSprite();

    // Alinea el circulo de colisión con el objeto
    collider_.r = w_ / 2;
    shiftColliders();

    // Establece la animación a usar
    setAnimation();
}

// Centra el globo en la posición X
void Balloon::alignTo(int x) {
    x_ = static_cast<float>(x - (w_ / 2));
    const int MIN_X = play_area_.x;
    const int MAX_X = play_area_.w - w_;
    x_ = std::clamp(x_, static_cast<float>(MIN_X), static_cast<float>(MAX_X));
}

// Pinta el globo en la pantalla
void Balloon::render() {
    if (type_ == Type::POWERBALL) {
        // Renderiza el fondo azul
        {
            auto sp = std::make_unique<Sprite>(sprite_->getTexture(), sprite_->getPosition());
            sp->setSpriteClip(0, 0, WIDTH.at(4), WIDTH.at(4));
            sp->render();
        }

        // Renderiza la estrella
        if (!invulnerable_) {
            SDL_FPoint p = {24.0F, 24.0F};
            sprite_->setRotatingCenter(p);
            sprite_->render();
        }

        // Añade la máscara del borde y los reflejos
        {
            auto sp = std::make_unique<Sprite>(sprite_->getTexture(), sprite_->getPosition());
            sp->setSpriteClip(WIDTH.at(4) * 2, 0, WIDTH.at(4), WIDTH.at(4));
            sp->render();
        }
    } else {
        // Renderizado para el resto de globos
        if (isBeingCreated()) {
            // Renderizado con transparencia
            sprite_->getTexture()->setAlpha(255 - (int)((float)creation_counter_ * (255.0F / (float)creation_counter_ini_)));
            sprite_->render();
            sprite_->getTexture()->setAlpha(255);
        } else {
            // Renderizado normal
            sprite_->render();
        }
    }
}

// Actualiza la posición y estados del globo
void Balloon::move() {
    if (isStopped()) {
        return;
    }

    handleHorizontalMovement();
    handleVerticalMovement();
    applyGravity();
}

void Balloon::handleHorizontalMovement() {
    x_ += vx_ * speed_;

    const int CLIP = 2;
    const float MIN_X = play_area_.x - CLIP;
    const float MAX_X = play_area_.x + play_area_.w - w_ + CLIP;

    if (isOutOfHorizontalBounds(MIN_X, MAX_X)) {
        handleHorizontalBounce(MIN_X, MAX_X);
    }
}

void Balloon::handleVerticalMovement() {
    y_ += vy_ * speed_;

    if (shouldCheckTopCollision()) {
        handleTopCollision();
    }

    handleBottomCollision();
}

auto Balloon::isOutOfHorizontalBounds(float min_x, float max_x) const -> bool {
    return x_ < min_x || x_ > max_x;
}

void Balloon::handleHorizontalBounce(float min_x, float max_x) {
    playBouncingSound();
    x_ = std::clamp(x_, min_x, max_x);
    vx_ = -vx_;

    if (type_ == Type::POWERBALL) {
        sprite_->switchRotate();
    } else {
        enableBounceEffect();
    }
}

auto Balloon::shouldCheckTopCollision() const -> bool {
    // Colisión en la parte superior solo si el globo va de subida
    return vy_ < 0;
}

void Balloon::handleTopCollision() {
    const int MIN_Y = play_area_.y;
    if (y_ < MIN_Y) {
        playBouncingSound();
        y_ = MIN_Y;
        vy_ = -vy_;
        enableBounceEffect();
    }
}

void Balloon::handleBottomCollision() {
    const int MAX_Y = play_area_.y + play_area_.h - h_;
    if (y_ > MAX_Y) {
        playBouncingSound();
        y_ = MAX_Y;
        vy_ = -default_vy_;

        if (type_ != Type::POWERBALL) {
            enableBounceEffect();
        } else {
            setInvulnerable(false);
        }
    }
}

void Balloon::applyGravity() {
    /*
    Para aplicar la gravedad, el diseño original la aplicaba en cada iteración del bucle
    Al añadir el modificador de velocidad se reduce la distancia que recorre el objeto y por
    tanto recibe mas gravedad. Para solucionarlo se va a aplicar la gravedad cuando se haya
    recorrido una distancia igual a la velocidad en Y, que era el cálculo inicial
    */

    travel_y_ += speed_;

    if (travel_y_ >= 1.0F) {
        travel_y_ -= 1.0F;
        vy_ += gravity_;
    }
}

void Balloon::playBouncingSound() {
    if (bouncing_sound_enabled_) {
        playSound(bouncing_sound_);
    }
}

// Actualiza al globo a su posicion, animación y controla los contadores
void Balloon::update() {
    move();
    updateState();
    updateBounceEffect();
    shiftSprite();
    shiftColliders();
    sprite_->update();
    ++counter_;
}

// Actualiza los estados del globo
void Balloon::updateState() {
    // Si se está creando
    if (isBeingCreated()) {
        // Actualiza el valor de las variables
        stop();
        setInvulnerable(true);

        if (creation_counter_ > 0) {
            // Desplaza lentamente el globo hacia abajo y hacia un lado
            if (creation_counter_ % 10 == 0) {
                y_++;
                x_ += vx_;

                // Comprueba no se salga por los laterales
                const int MIN_X = play_area_.x;
                const int MAX_X = play_area_.w - w_;

                if (x_ < MIN_X || x_ > MAX_X) {
                    // Corrige y cambia el sentido de la velocidad
                    x_ -= vx_;
                    vx_ = -vx_;
                }
            }
            --creation_counter_;
        }

        else {
            // El contador ha llegado a cero
            being_created_ = false;
            start();
            setInvulnerable(false);
            setAnimation();
        }
    }
}

// Establece la animación correspondiente al estado
void Balloon::setAnimation() {
    std::string creating_animation;
    std::string normal_animation;

    switch (type_) {
        case Type::POWERBALL:
            creating_animation = "powerball";
            normal_animation = "powerball";
            break;
        case Type::FLOATER:
            creating_animation = param.balloon.color.at(2);
            normal_animation = param.balloon.color.at(3);
            break;
        default:
            creating_animation = param.balloon.color.at(0);
            normal_animation = param.balloon.color.at(1);
            break;
    }

    // Establece el frame de animación
    if (use_reversed_colors_) {
        sprite_->setCurrentAnimation(creating_animation);
    } else {
        sprite_->setCurrentAnimation(isBeingCreated() ? creating_animation : normal_animation);
    }
}

// Detiene el globo
void Balloon::stop() {
    stopped_ = true;
    if (isPowerBall()) {
        sprite_->setRotate(!stopped_);
    }
}

// Pone el globo en movimiento
void Balloon::start() {
    stopped_ = false;
    if (isPowerBall()) {
        sprite_->setRotate(!stopped_);
    }
}

// Alinea el circulo de colisión con la posición del objeto globo
void Balloon::shiftColliders() {
    collider_.x = static_cast<int>(x_) + collider_.r;
    collider_.y = static_cast<int>(y_) + collider_.r;
}

// Alinea el sprite con la posición del objeto globo
void Balloon::shiftSprite() {
    sprite_->setPosX(x_);
    sprite_->setPosY(y_);
}

void Balloon::enableBounceEffect() {
    bounce_effect_.enable(sprite_.get(), size_);
}

void Balloon::disableBounceEffect() {
    bounce_effect_.disable(sprite_.get());
}

void Balloon::updateBounceEffect() {
    bounce_effect_.update(sprite_.get());
}

// Pone el color alternativo en el globo
void Balloon::useReverseColor() {
    if (!isBeingCreated()) {
        use_reversed_colors_ = true;
        setAnimation();
    }
}

// Pone el color normal en el globo
void Balloon::useNormalColor() {
    use_reversed_colors_ = false;
    setAnimation();
}

// Reproduce sonido
void Balloon::playSound(const std::string &name) const {
    if (!sound_enabled_) {
        return;
    }

    static auto *audio_ = Audio::get();
    audio_->playSound(name);
}

// Explota el globo
void Balloon::pop(bool should_sound) {
    if (should_sound) {
        if (poping_sound_enabled_) {
            playSound(popping_sound_);
        }
    }

    enabled_ = false;
}