#include "game/entities/balloon.h"

#include <math.h>  // for abs

#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 = BALLOON_WIDTH_1;
            height = BALLOON_WIDTH_1;
            size = BALLOON_SIZE_1;
            power = 1;

            // Inicializa los valores de velocidad y gravedad
            this->velX = velx;
            velY = 0;
            maxVelY = 3.0f;
            gravity = 0.09f;
            defaultVelY = 2.6f;

            // Puntos que da el globo al ser destruido
            score = BALLOON_SCORE_1;

            // Amenaza que genera el globo
            menace = 1;

            break;

        case BALLOON_2:
            // Alto y ancho del objeto
            width = BALLOON_WIDTH_2;
            height = BALLOON_WIDTH_2;
            size = BALLOON_SIZE_2;
            power = 3;

            // Inicializa los valores de velocidad y gravedad
            this->velX = velx;
            velY = 0;
            maxVelY = 3.0f;
            gravity = 0.10f;
            defaultVelY = 3.5f;

            // Puntos que da el globo al ser destruido
            score = BALLOON_SCORE_2;

            // Amenaza que genera el globo
            menace = 2;

            break;

        case BALLOON_3:
            // Alto y ancho del objeto
            width = BALLOON_WIDTH_3;
            height = BALLOON_WIDTH_3;
            size = BALLOON_SIZE_3;
            power = 7;

            // Inicializa los valores de velocidad y gravedad
            this->velX = velx;
            velY = 0;
            maxVelY = 3.0f;
            gravity = 0.10f;
            defaultVelY = 4.50f;

            // Puntos que da el globo al ser destruido
            score = BALLOON_SCORE_3;

            // Amenaza que genera el globo
            menace = 4;

            break;

        case BALLOON_4:
            // Alto y ancho del objeto
            width = BALLOON_WIDTH_4;
            height = BALLOON_WIDTH_4;
            size = BALLOON_SIZE_4;
            power = 15;

            // Inicializa los valores de velocidad y gravedad
            this->velX = velx;
            velY = 0;
            maxVelY = 3.0f;
            gravity = 0.10f;
            defaultVelY = 4.95f;

            // Puntos que da el globo al ser destruido
            score = BALLOON_SCORE_4;

            // Amenaza que genera el globo
            menace = 8;

            break;

        case HEXAGON_1:
            // Alto y ancho del objeto
            width = BALLOON_WIDTH_1;
            height = BALLOON_WIDTH_1;
            size = BALLOON_SIZE_1;
            power = 1;

            // Inicializa los valores de velocidad y gravedad
            this->velX = velx;
            velY = abs(velx) * 2;
            maxVelY = abs(velx) * 2;
            gravity = 0.00f;
            defaultVelY = abs(velx) * 2;

            // Puntos que da el globo al ser destruido
            score = BALLOON_SCORE_1;

            // Amenaza que genera el globo
            menace = 1;

            break;

        case HEXAGON_2:
            // Alto y ancho del objeto
            width = BALLOON_WIDTH_2;
            height = BALLOON_WIDTH_2;
            size = BALLOON_SIZE_2;
            power = 3;

            // Inicializa los valores de velocidad y gravedad
            this->velX = velx;
            velY = abs(velx) * 2;
            maxVelY = abs(velx) * 2;
            gravity = 0.00f;
            defaultVelY = abs(velx) * 2;

            // Puntos que da el globo al ser destruido
            score = BALLOON_SCORE_2;

            // Amenaza que genera el globo
            menace = 2;

            break;

        case HEXAGON_3:
            // Alto y ancho del objeto
            width = BALLOON_WIDTH_3;
            height = BALLOON_WIDTH_3;
            size = BALLOON_SIZE_3;
            power = 7;

            // Inicializa los valores de velocidad y gravedad
            this->velX = velx;
            velY = abs(velx) * 2;
            maxVelY = abs(velx) * 2;
            gravity = 0.00f;
            defaultVelY = abs(velx) * 2;

            // Puntos que da el globo al ser destruido
            score = BALLOON_SCORE_3;

            // Amenaza que genera el globo
            menace = 4;

            break;

        case HEXAGON_4:
            // Alto y ancho del objeto
            width = BALLOON_WIDTH_4;
            height = BALLOON_WIDTH_4;
            size = BALLOON_SIZE_4;
            power = 15;

            // Inicializa los valores de velocidad y gravedad
            this->velX = velx;
            velY = abs(velx) * 2;
            maxVelY = abs(velx) * 2;
            gravity = 0.00f;
            defaultVelY = abs(velx) * 2;

            // Puntos que da el globo al ser destruido
            score = BALLOON_SCORE_4;

            // Amenaza que genera el globo
            menace = 8;

            break;

        case POWER_BALL:
            // Alto y ancho del objeto
            width = BALLOON_WIDTH_4;
            height = BALLOON_WIDTH_4;
            size = 4;
            power = 0;

            // Inicializa los valores de velocidad y gravedad
            this->velX = velx;
            velY = 0;
            maxVelY = 3.0f;
            gravity = 0.10f;
            defaultVelY = 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 (velX > 0.0f) {
                sprite->setRotateAmount(2.0);
            } else {
                sprite->setRotateAmount(-2.0);
            }

            break;

        default:
            break;
    }

    // Posición inicial
    posX = x;
    posY = y;

    // Valores para el efecto de rebote
    bouncing.enabled = false;
    bouncing.counter = 0;
    bouncing.speed = 2;
    bouncing.zoomW = 1.0f;
    bouncing.zoomH = 1.0f;
    bouncing.despX = 0.0f;
    bouncing.despY = 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)posX);
    sprite->setPosY((int)posY);

    // 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;
    stoppedCounter = 0;
    blinking = false;
    visible = true;
    creationCounter = creationtimer;
    creationCounterIni = creationtimer;
    popping = false;

    // Valores iniciales dependentes del timer
    beingCreated = creationCounter != 0;
    invulnerable = beingCreated;

    counter = 0;
    travelY = 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) {
    posX = float(x - (width / 2));

    if (posX < PLAY_AREA_LEFT)
        posX = PLAY_AREA_LEFT + 1;
    else if ((posX + width) > PLAY_AREA_RIGHT)
        posX = 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.despX);
                sprite->setPosY(getPosY() + bouncing.despY);
                sprite->render();
                sprite->setPosX(getPosX() - bouncing.despX);
                sprite->setPosY(getPosY() - bouncing.despY);
            }
        } else if (isBeingCreated()) {
            // Aplica alpha blending
            sprite->getTexture()->setAlpha(255 - (int)((float)creationCounter * (255.0f / (float)creationCounterIni)));
            sprite->render();
            if (kind == POWER_BALL) {
                Sprite *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) {
                Sprite *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
        posX += (velX * speed);

        // Si queda fuera de pantalla, corregimos su posición y cambiamos su sentido
        if ((posX < PLAY_AREA_LEFT) || (posX + width > PLAY_AREA_RIGHT)) {
            // Corrige posición
            posX -= (velX * speed);

            // Invierte sentido
            velX = -velX;

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

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

        // Mueve el globo hacia arriba o hacia abajo
        posY += (velY * speed);

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

            // Invierte sentido
            velY = -velY;

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

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

            // Invierte colocando una velocidad por defecto
            velY = -defaultVelY;

            // 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
        travelY += speed;

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

            // Aplica la gravedad al objeto sin pasarse de una velocidad máxima
            velY += 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() {
    beingCreated = false;
    blinking = false;
    collider.r = 0;
    collider.x = 0;
    collider.y = 0;
    counter = 0;
    creationCounter = 0;
    creationCounterIni = 0;
    defaultVelY = 0.0f;
    enabled = false;
    gravity = 0.0f;
    height = 0;
    invulnerable = false;
    kind = 0;
    maxVelY = 0.0f;
    menace = 0;
    popping = false;
    posX = 0.0f;
    posY = 0.0f;
    power = 0;
    score = 0;
    size = 0;
    speed = 0;
    stopped = false;
    stoppedCounter = 0;
    travelY = 0;
    velX = 0.0f;
    velY = 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() {
    // Si está explotando
    if (isPopping()) {
        setInvulnerable(true);
        setStop(true);
        if (sprite->animationIsCompleted()) {
            disable();
        }
    }

    // Si se está creando
    if (isBeingCreated()) {
        // Actualiza el valor de las variables
        setStop(true);
        setInvulnerable(true);

        // Todavia tiene tiempo en el contador
        if (creationCounter > 0) {
            // Desplaza lentamente el globo hacia abajo y hacia un lado
            if (creationCounter % 10 == 0) {
                posY++;
                posX += velX;

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

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

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

            creationCounter--;
        }
        // El contador ha llegado a cero
        else {
            setBeingCreated(false);
            setStop(false);
            setVisible(true);
            setInvulnerable(false);
            if (kind == POWER_BALL) {
                sprite->setRotate(true);
            }
        }
    }
    // Solo comprueba el estado detenido cuando no se está creando
    else if (isStopped()) {
        // Si es una powerball deja de rodar
        if (kind == POWER_BALL) {
            sprite->setRotate(false);
        }

        // Reduce el contador
        if (stoppedCounter > 0) {
            stoppedCounter--;
        }
        // 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 creatingAnimation = "blue";
    std::string normalAnimation = "orange";

    if (kind == POWER_BALL) {
        creatingAnimation = "powerball";
        normalAnimation = "powerball";
    } else if (getClass() == HEXAGON_CLASS) {
        creatingAnimation = "red";
        normalAnimation = "green";
    }

    // Establece el frame de animación
    if (isPopping()) {
        sprite->setCurrentAnimation("pop");
    } else if (isBeingCreated()) {
        sprite->setCurrentAnimation(creatingAnimation);
    } else {
        sprite->setCurrentAnimation(normalAnimation);
    }

    sprite->animate();
}

// Comprueba si el globo está habilitado
bool Balloon::isEnabled() {
    return enabled;
}

// Obtiene del valor de la variable
float Balloon::getPosX() {
    return posX;
}

// Obtiene del valor de la variable
float Balloon::getPosY() {
    return posY;
}

// Obtiene del valor de la variable
float Balloon::getVelY() {
    return velY;
}

// Obtiene del valor de la variable
int Balloon::getWidth() {
    return width;
}

// Obtiene del valor de la variable
int Balloon::getHeight() {
    return height;
}

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

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

// Obtiene del valor de la variable
int Balloon::getKind() {
    return kind;
}

// Obtiene del valor de la variable
Uint8 Balloon::getSize() {
    return size;
}

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

    else 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
bool Balloon::isStopped() {
    return stopped;
}

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

// Obtiene del valor de la variable
bool Balloon::isBlinking() {
    return blinking;
}

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

// Obtiene del valor de la variable
bool Balloon::isVisible() {
    return visible;
}

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

// Obtiene del valor de la variable
bool Balloon::isInvulnerable() {
    return invulnerable;
}

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

// Obtiene del valor de la variable
bool Balloon::isBeingCreated() {
    return beingCreated;
}

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

// Obtiene del valor de la variable
bool Balloon::isPopping() {
    return popping;
}

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

// Obtiene del valor de la variable
Uint16 Balloon::getStoppedTimer() {
    return stoppedCounter;
}

// Obtiene del valor de la variable
Uint16 Balloon::getScore() {
    return score;
}

// Obtiene el circulo de colisión
circle_t &Balloon::getCollider() {
    return collider;
}

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

// Obtiene le valor de la variable
Uint8 Balloon::getMenace() {
    if (isEnabled()) {
        return menace;
    } else {
        return 0;
    }
}

// Obtiene le valor de la variable
Uint8 Balloon::getPower() {
    return power;
}

void Balloon::bounceStart() {
    bouncing.enabled = true;
    bouncing.zoomW = 1;
    bouncing.zoomH = 1;
    sprite->setZoomW(bouncing.zoomW);
    sprite->setZoomH(bouncing.zoomH);
    bouncing.despX = 0;
    bouncing.despY = 0;
}

void Balloon::bounceStop() {
    bouncing.enabled = false;
    bouncing.counter = 0;
    bouncing.zoomW = 1.0f;
    bouncing.zoomH = 1.0f;
    sprite->setZoomW(bouncing.zoomW);
    sprite->setZoomH(bouncing.zoomH);
    bouncing.despX = 0.0f;
    bouncing.despY = 0.0f;
}

void Balloon::updateBounce() {
    if (bouncing.enabled) {
        bouncing.zoomW = bouncing.w[bouncing.counter / bouncing.speed];
        bouncing.zoomH = bouncing.h[bouncing.counter / bouncing.speed];
        sprite->setZoomW(bouncing.zoomW);
        sprite->setZoomH(bouncing.zoomH);
        bouncing.despX = (sprite->getSpriteClip().w - (sprite->getSpriteClip().w * bouncing.zoomW));
        bouncing.despY = (sprite->getSpriteClip().h - (sprite->getSpriteClip().h * bouncing.zoomH));
        bouncing.counter++;
        if ((bouncing.counter / bouncing.speed) > (MAX_BOUNCE - 1)) {
            bounceStop();
        }
    }
}