#include "game/entities/balloon.h"

#include <cmath>  // for std::fabs

#include "core/rendering/animatedsprite.h"  // for AnimatedSprite
#include "core/rendering/movingsprite.h"    // for MovingSprite
#include "core/rendering/sprite.h"          // for Sprite
#include "core/rendering/texture.h"         // for Texture
#include "game/defaults.hpp"                // for PLAY_AREA_LEFT, PLAY_AREA_RIGHT, PLAY_AR...

// Constructor
Balloon::Balloon(float x, float y, Uint8 kind, float velx, float speed, Uint16 creationtimer, Texture *texture, std::vector<std::string> *animation, SDL_Renderer *renderer) {
    sprite_ = new AnimatedSprite(texture, renderer, "", animation);
    disable();

    enabled_ = true;

    switch (kind) {
        case BALLOON_1:
            // Alto y ancho del objeto
            width_ = WIDTH_1;
            height_ = WIDTH_1;
            size_ = SIZE_1;
            power_ = 1;

            // Inicializa los valores de velocidad y gravedad
            this->vel_x_ = velx;
            vel_y_ = 0;
            max_vel_y_ = 3.0F;
            gravity_ = 0.09F;
            default_vel_y_ = 2.6F;

            // Puntos que da el globo al ser destruido
            score_ = SCORE_1;

            // Amenaza que genera el globo
            menace_ = 1;

            break;

        case BALLOON_2:
            // Alto y ancho del objeto
            width_ = WIDTH_2;
            height_ = WIDTH_2;
            size_ = SIZE_2;
            power_ = 3;

            // Inicializa los valores de velocidad y gravedad
            this->vel_x_ = velx;
            vel_y_ = 0;
            max_vel_y_ = 3.0F;
            gravity_ = 0.10F;
            default_vel_y_ = 3.5F;

            // Puntos que da el globo al ser destruido
            score_ = SCORE_2;

            // Amenaza que genera el globo
            menace_ = 2;

            break;

        case BALLOON_3:
            // Alto y ancho del objeto
            width_ = WIDTH_3;
            height_ = WIDTH_3;
            size_ = SIZE_3;
            power_ = 7;

            // Inicializa los valores de velocidad y gravedad
            this->vel_x_ = velx;
            vel_y_ = 0;
            max_vel_y_ = 3.0F;
            gravity_ = 0.10F;
            default_vel_y_ = 4.50F;

            // Puntos que da el globo al ser destruido
            score_ = SCORE_3;

            // Amenaza que genera el globo
            menace_ = 4;

            break;

        case BALLOON_4:
            // Alto y ancho del objeto
            width_ = WIDTH_4;
            height_ = WIDTH_4;
            size_ = SIZE_4;
            power_ = 15;

            // Inicializa los valores de velocidad y gravedad
            this->vel_x_ = velx;
            vel_y_ = 0;
            max_vel_y_ = 3.0F;
            gravity_ = 0.10F;
            default_vel_y_ = 4.95F;

            // Puntos que da el globo al ser destruido
            score_ = SCORE_4;

            // Amenaza que genera el globo
            menace_ = 8;

            break;

        case HEXAGON_1:
            // Alto y ancho del objeto
            width_ = WIDTH_1;
            height_ = WIDTH_1;
            size_ = SIZE_1;
            power_ = 1;

            // Inicializa los valores de velocidad y gravedad
            this->vel_x_ = velx;
            vel_y_ = std::fabs(velx) * 2;
            max_vel_y_ = std::fabs(velx) * 2;
            gravity_ = 0.00F;
            default_vel_y_ = std::fabs(velx) * 2;

            // Puntos que da el globo al ser destruido
            score_ = SCORE_1;

            // Amenaza que genera el globo
            menace_ = 1;

            break;

        case HEXAGON_2:
            // Alto y ancho del objeto
            width_ = WIDTH_2;
            height_ = WIDTH_2;
            size_ = SIZE_2;
            power_ = 3;

            // Inicializa los valores de velocidad y gravedad
            this->vel_x_ = velx;
            vel_y_ = std::fabs(velx) * 2;
            max_vel_y_ = std::fabs(velx) * 2;
            gravity_ = 0.00F;
            default_vel_y_ = std::fabs(velx) * 2;

            // Puntos que da el globo al ser destruido
            score_ = SCORE_2;

            // Amenaza que genera el globo
            menace_ = 2;

            break;

        case HEXAGON_3:
            // Alto y ancho del objeto
            width_ = WIDTH_3;
            height_ = WIDTH_3;
            size_ = SIZE_3;
            power_ = 7;

            // Inicializa los valores de velocidad y gravedad
            this->vel_x_ = velx;
            vel_y_ = std::fabs(velx) * 2;
            max_vel_y_ = std::fabs(velx) * 2;
            gravity_ = 0.00F;
            default_vel_y_ = std::fabs(velx) * 2;

            // Puntos que da el globo al ser destruido
            score_ = SCORE_3;

            // Amenaza que genera el globo
            menace_ = 4;

            break;

        case HEXAGON_4:
            // Alto y ancho del objeto
            width_ = WIDTH_4;
            height_ = WIDTH_4;
            size_ = SIZE_4;
            power_ = 15;

            // Inicializa los valores de velocidad y gravedad
            this->vel_x_ = velx;
            vel_y_ = std::fabs(velx) * 2;
            max_vel_y_ = std::fabs(velx) * 2;
            gravity_ = 0.00F;
            default_vel_y_ = std::fabs(velx) * 2;

            // Puntos que da el globo al ser destruido
            score_ = SCORE_4;

            // Amenaza que genera el globo
            menace_ = 8;

            break;

        case POWER_BALL:
            // Alto y ancho del objeto
            width_ = WIDTH_4;
            height_ = WIDTH_4;
            size_ = 4;
            power_ = 0;

            // Inicializa los valores de velocidad y gravedad
            this->vel_x_ = velx;
            vel_y_ = 0;
            max_vel_y_ = 3.0F;
            gravity_ = 0.10F;
            default_vel_y_ = 4.95F;

            // Puntos que da el globo al ser destruido
            score_ = 0;

            // Amenaza que genera el globo
            menace_ = 0;

            // Añade rotación al sprite
            sprite_->setRotate(false);
            sprite_->setRotateSpeed(0);
            if (vel_x_ > 0.0F) {
                sprite_->setRotateAmount(2.0);
            } else {
                sprite_->setRotateAmount(-2.0);
            }

            break;

        default:
            break;
    }

    // Posición inicial
    pos_x_ = x;
    pos_y_ = y;

    // Valores para el efecto de rebote
    bouncing_.enabled = false;
    bouncing_.counter = 0;
    bouncing_.speed = 2;
    bouncing_.zoom_width = 1.0F;
    bouncing_.zoom_height = 1.0F;
    bouncing_.desp_x = 0.0F;
    bouncing_.desp_y = 0.0F;
    bouncing_.w = {1.10F, 1.05F, 1.00F, 0.95F, 0.90F, 0.95F, 1.00F, 1.02F, 1.05F, 1.02F};
    bouncing_.h = {0.90F, 0.95F, 1.00F, 1.05F, 1.10F, 1.05F, 1.00F, 0.98F, 0.95F, 0.98F};

    // Alto y ancho del sprite
    sprite_->setWidth(width_);
    sprite_->setHeight(height_);

    // Posición X,Y del sprite
    sprite_->setPosX((int)pos_x_);
    sprite_->setPosY((int)pos_y_);

    // Tamaño del circulo de colisión
    collider_.r = width_ / 2;

    // Alinea el circulo de colisión con el objeto
    updateColliders();

    // Inicializa variables
    stopped_ = true;
    stopped_counter_ = 0;
    blinking_ = false;
    visible_ = true;
    creation_counter_ = creationtimer;
    creation_counter_ini_ = creationtimer;
    popping_ = false;

    // Valores iniciales dependentes del timer
    being_created_ = creation_counter_ != 0;
    invulnerable_ = being_created_;

    counter_ = 0;
    travel_y_ = 1.0F;
    this->speed_ = speed;

    // Tipo
    this->kind_ = kind;
}

// Destructor
Balloon::~Balloon() {
    delete sprite_;
}

// Centra el globo en la posición X
void Balloon::allignTo(int x) {
    pos_x_ = float(x - (width_ / 2));

    if (pos_x_ < PLAY_AREA_LEFT) {
        pos_x_ = PLAY_AREA_LEFT + 1;
    } else if ((pos_x_ + width_) > PLAY_AREA_RIGHT) {
        pos_x_ = float(PLAY_AREA_RIGHT - width_ - 1);
    }

    // Posición X,Y del sprite
    sprite_->setPosX(getPosX());
    sprite_->setPosY(getPosY());

    // Alinea el circulo de colisión con el objeto
    updateColliders();
}

// Pinta el globo en la pantalla
void Balloon::render() {
    if ((visible_) && (enabled_)) {
        if (bouncing_.enabled) {
            if (kind_ != POWER_BALL) {
                // Aplica desplazamiento para el zoom
                sprite_->setPosX(getPosX() + bouncing_.desp_x);
                sprite_->setPosY(getPosY() + bouncing_.desp_y);
                sprite_->render();
                sprite_->setPosX(getPosX() - bouncing_.desp_x);
                sprite_->setPosY(getPosY() - bouncing_.desp_y);
            }
        } else if (isBeingCreated()) {
            // Aplica alpha blending
            sprite_->getTexture()->setAlpha(255 - (int)((float)creation_counter_ * (255.0F / (float)creation_counter_ini_)));
            sprite_->render();
            if (kind_ == POWER_BALL) {
                auto *sp = new Sprite(sprite_->getRect(), sprite_->getTexture(), sprite_->getRenderer());
                sp->setSpriteClip(407, 0, 37, 37);
                sp->render();
                delete sp;
            }
            sprite_->getTexture()->setAlpha(255);
        } else {
            sprite_->render();

            if (kind_ == POWER_BALL and !popping_) {
                auto *sp = new Sprite(sprite_->getRect(), sprite_->getTexture(), sprite_->getRenderer());
                sp->setSpriteClip(407, 0, 37, 37);
                sp->render();
                delete sp;
            }
        }
    }
}

// Actualiza la posición y estados del globo
void Balloon::move() {
    // Comprueba si se puede mover
    if (!isStopped()) {
        // Lo mueve a izquierda o derecha
        pos_x_ += (vel_x_ * speed_);

        // Si queda fuera de pantalla, corregimos su posición y cambiamos su sentido
        if ((pos_x_ < PLAY_AREA_LEFT) || (pos_x_ + width_ > PLAY_AREA_RIGHT)) {
            // Corrige posición
            pos_x_ -= (vel_x_ * speed_);

            // Invierte sentido
            vel_x_ = -vel_x_;

            // Invierte la rotación
            sprite_->switchRotate();

            // Activa el efecto de rebote
            if (kind_ != POWER_BALL) {
                bounceStart();
            }
        }

        // Mueve el globo hacia arriba o hacia abajo
        pos_y_ += (vel_y_ * speed_);

        // Si se sale por arriba
        if (pos_y_ < PLAY_AREA_TOP) {
            // Corrige
            pos_y_ = PLAY_AREA_TOP;

            // Invierte sentido
            vel_y_ = -vel_y_;

            // Activa el efecto de rebote
            if (kind_ != POWER_BALL) {
                bounceStart();
            }
        }

        // Si el globo se sale por la parte inferior
        if (pos_y_ + height_ > PLAY_AREA_BOTTOM) {
            // Corrige
            pos_y_ = PLAY_AREA_BOTTOM - height_;

            // Invierte colocando una velocidad por defecto
            vel_y_ = -default_vel_y_;

            // Activa el efecto de rebote
            if (kind_ != POWER_BALL) {
                bounceStart();
            }
        }

        /*

        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

        */

        // Incrementa la variable que calcula la distancia acumulada en Y
        travel_y_ += speed_;

        // Si la distancia acumulada en Y es igual a la velocidad, se aplica la gravedad
        if (travel_y_ >= 1.0F) {
            // Quita el excedente
            travel_y_ -= 1.0F;

            // Aplica la gravedad al objeto sin pasarse de una velocidad máxima
            vel_y_ += gravity_;

            // Al parecer esta asignación se quedó sin hacer y ahora el juego no funciona
            // correctamente si se aplica, así que se deja sin efecto
            // velY = std::min(velY, maxVelY);
        }

        // Actualiza la posición del sprite
        sprite_->setPosX(getPosX());
        sprite_->setPosY(getPosY());
    }
}

// Deshabilita el globo y pone a cero todos los valores
void Balloon::disable() {
    being_created_ = false;
    blinking_ = false;
    collider_.r = 0;
    collider_.x = 0;
    collider_.y = 0;
    counter_ = 0;
    creation_counter_ = 0;
    creation_counter_ini_ = 0;
    default_vel_y_ = 0.0F;
    enabled_ = false;
    gravity_ = 0.0F;
    height_ = 0;
    invulnerable_ = false;
    kind_ = 0;
    max_vel_y_ = 0.0F;
    menace_ = 0;
    popping_ = false;
    pos_x_ = 0.0F;
    pos_y_ = 0.0F;
    power_ = 0;
    score_ = 0;
    size_ = 0;
    speed_ = 0;
    stopped_ = false;
    stopped_counter_ = 0;
    travel_y_ = 0;
    vel_x_ = 0.0F;
    vel_y_ = 0.0F;
    visible_ = false;
    width_ = 0;
    sprite_->clear();
}

// Explosiona el globo
void Balloon::pop() {
    setPopping(true);
    sprite_->disableRotate();
    setStop(true);
    setStoppedTimer(2000);
    setInvulnerable(true);
    menace_ = 0;
}

// Actualiza al globo a su posicion, animación y controla los contadores
void Balloon::update() {
    if (enabled_) {
        sprite_->MovingSprite::update();
        move();
        updateAnimation();
        updateColliders();
        updateState();
        updateBounce();
        counter_++;
    }
}

// Actualiza los estados del globo
void Balloon::updateState() {
    if (isPopping()) {
        updateStatePopping();
    }

    if (isBeingCreated()) {
        updateStateBeingCreated();
    }
    // Solo comprueba el estado detenido cuando no se está creando
    else if (isStopped()) {
        updateStateStopped();
    }
}

// Rama de updateState: globo explotando
void Balloon::updateStatePopping() {
    setInvulnerable(true);
    setStop(true);
    if (sprite_->animationIsCompleted()) {
        disable();
    }
}

// Rama de updateState: globo creándose
void Balloon::updateStateBeingCreated() {
    setStop(true);
    setInvulnerable(true);

    // Todavia tiene tiempo en el contador
    if (creation_counter_ > 0) {
        // Desplaza lentamente el globo hacia abajo y hacia un lado
        if (creation_counter_ % 10 == 0) {
            pos_y_++;
            pos_x_ += vel_x_;

            // Comprueba no se salga por los laterales
            if ((pos_x_ < PLAY_AREA_LEFT) || (pos_x_ > (PLAY_AREA_RIGHT - width_))) {
                // Corrige y cambia el sentido de la velocidad
                pos_x_ -= vel_x_;
                vel_x_ = -vel_x_;
            }

            // Actualiza la posición del sprite
            sprite_->setPosX(getPosX());
            sprite_->setPosY(getPosY());

            // Actualiza la posición del circulo de colisión
            updateColliders();
        }

        creation_counter_--;
    }
    // El contador ha llegado a cero
    else {
        setBeingCreated(false);
        setStop(false);
        setVisible(true);
        setInvulnerable(false);
        if (kind_ == POWER_BALL) {
            sprite_->setRotate(true);
        }
    }
}

// Rama de updateState: globo detenido (no creándose)
void Balloon::updateStateStopped() {
    // Si es una powerball deja de rodar
    if (kind_ == POWER_BALL) {
        sprite_->setRotate(false);
    }

    // Reduce el contador
    if (stopped_counter_ > 0) {
        stopped_counter_--;
    }
    // Quitarles el estado "detenido" si no estan explosionando
    else if (!isPopping()) {
        // Si es una powerball vuelve a rodar
        if (kind_ == POWER_BALL) {
            sprite_->setRotate(true);
        }

        setStop(false);
    }
}

// Establece la animación correspondiente al estado
void Balloon::updateAnimation() {
    std::string creating_animation = "blue";
    std::string normal_animation = "orange";

    if (kind_ == POWER_BALL) {
        creating_animation = "powerball";
        normal_animation = "powerball";
    } else if (getClass() == HEXAGON_CLASS) {
        creating_animation = "red";
        normal_animation = "green";
    }

    // Establece el frame de animación
    if (isPopping()) {
        sprite_->setCurrentAnimation("pop");
    } else if (isBeingCreated()) {
        sprite_->setCurrentAnimation(creating_animation);
    } else {
        sprite_->setCurrentAnimation(normal_animation);
    }

    sprite_->animate();
}

// Comprueba si el globo está habilitado
auto Balloon::isEnabled() const -> bool {
    return enabled_;
}

// Obtiene del valor de la variable
auto Balloon::getPosX() const -> float {
    return pos_x_;
}

// Obtiene del valor de la variable
auto Balloon::getPosY() const -> float {
    return pos_y_;
}

// Obtiene del valor de la variable
auto Balloon::getVelY() const -> float {
    return vel_y_;
}

// Obtiene del valor de la variable
auto Balloon::getWidth() const -> int {
    return width_;
}

// Obtiene del valor de la variable
auto Balloon::getHeight() const -> int {
    return height_;
}

// Establece el valor de la variable
void Balloon::setVelY(float vel_y) {
    this->vel_y_ = vel_y;
}

// Establece el valor de la variable
void Balloon::setSpeed(float speed) {
    this->speed_ = speed;
}

// Obtiene del valor de la variable
auto Balloon::getKind() const -> int {
    return kind_;
}

// Obtiene del valor de la variable
auto Balloon::getSize() const -> Uint8 {
    return size_;
}

// Obtiene la clase a la que pertenece el globo
auto Balloon::getClass() const -> Uint8 {
    if ((kind_ >= BALLOON_1) && (kind_ <= BALLOON_4)) {
        return BALLOON_CLASS;
    }

    if ((kind_ >= HEXAGON_1) && (kind_ <= HEXAGON_4)) {
        return HEXAGON_CLASS;
    }

    return BALLOON_CLASS;
}

// Establece el valor de la variable
void Balloon::setStop(bool state) {
    stopped_ = state;
}

// Obtiene del valor de la variable
auto Balloon::isStopped() const -> bool {
    return stopped_;
}

// Establece el valor de la variable
void Balloon::setBlink(bool value) {
    blinking_ = value;
}

// Obtiene del valor de la variable
auto Balloon::isBlinking() const -> bool {
    return blinking_;
}

// Establece el valor de la variable
void Balloon::setVisible(bool value) {
    visible_ = value;
}

// Obtiene del valor de la variable
auto Balloon::isVisible() const -> bool {
    return visible_;
}

// Establece el valor de la variable
void Balloon::setInvulnerable(bool value) {
    invulnerable_ = value;
}

// Obtiene del valor de la variable
auto Balloon::isInvulnerable() const -> bool {
    return invulnerable_;
}

// Establece el valor de la variable
void Balloon::setBeingCreated(bool value) {
    being_created_ = value;
}

// Obtiene del valor de la variable
auto Balloon::isBeingCreated() const -> bool {
    return being_created_;
}

// Establece el valor de la variable
void Balloon::setPopping(bool value) {
    popping_ = value;
}

// Obtiene del valor de la variable
auto Balloon::isPopping() const -> bool {
    return popping_;
}

// Establece el valor de la variable
void Balloon::setStoppedTimer(Uint16 time) {
    stopped_counter_ = time;
}

// Obtiene del valor de la variable
auto Balloon::getStoppedTimer() const -> Uint16 {
    return stopped_counter_;
}

// Obtiene del valor de la variable
auto Balloon::getScore() const -> Uint16 {
    return score_;
}

// Obtiene el circulo de colisión
auto Balloon::getCollider() -> Circle & {
    return collider_;
}

// Alinea el circulo de colisión con la posición del objeto globo
void Balloon::updateColliders() {
    collider_.x = Uint16(pos_x_ + collider_.r);
    collider_.y = pos_y_ + collider_.r;
}

// Obtiene le valor de la variable
auto Balloon::getMenace() const -> Uint8 {
    if (isEnabled()) {
        return menace_;
    }
    return 0;
}

// Obtiene le valor de la variable
auto Balloon::getPower() const -> Uint8 {
    return power_;
}

void Balloon::bounceStart() {
    bouncing_.enabled = true;
    bouncing_.zoom_width = 1;
    bouncing_.zoom_height = 1;
    sprite_->setZoomW(bouncing_.zoom_width);
    sprite_->setZoomH(bouncing_.zoom_height);
    bouncing_.desp_x = 0;
    bouncing_.desp_y = 0;
}

void Balloon::bounceStop() {
    bouncing_.enabled = false;
    bouncing_.counter = 0;
    bouncing_.zoom_width = 1.0F;
    bouncing_.zoom_height = 1.0F;
    sprite_->setZoomW(bouncing_.zoom_width);
    sprite_->setZoomH(bouncing_.zoom_height);
    bouncing_.desp_x = 0.0F;
    bouncing_.desp_y = 0.0F;
}

void Balloon::updateBounce() {
    if (bouncing_.enabled) {
        bouncing_.zoom_width = bouncing_.w[bouncing_.counter / bouncing_.speed];
        bouncing_.zoom_height = bouncing_.h[bouncing_.counter / bouncing_.speed];
        sprite_->setZoomW(bouncing_.zoom_width);
        sprite_->setZoomH(bouncing_.zoom_height);
        bouncing_.desp_x = (sprite_->getSpriteClip().w - (sprite_->getSpriteClip().w * bouncing_.zoom_width));
        bouncing_.desp_y = (sprite_->getSpriteClip().h - (sprite_->getSpriteClip().h * bouncing_.zoom_height));
        bouncing_.counter++;
        if ((bouncing_.counter / bouncing_.speed) > (MAX_BOUNCE - 1)) {
            bounceStop();
        }
    }
}