#include "player.h"

// Constructor
Player::Player(SDL_Renderer *renderer, Asset *asset, Input *input, Map *map)
{
    this->asset = asset;
    this->renderer = renderer;
    this->input = input;
    this->map = map;

    sound_jump = JA_LoadSound(asset->get("sound_player_jump.wav").c_str());
    sound_death = JA_LoadSound(asset->get("sound_player_death.wav").c_str());
    sound_coin = JA_LoadSound(asset->get("sound_player_coin.wav").c_str());

    texture = new LTexture(renderer, asset->get("player.png"));
    sprite = new AnimatedSprite(texture, renderer, asset->get("player.ani"));

    w = 16;
    h = 24;
    x = 8 * map->getTileSize();
    y = 20 * map->getTileSize();
    vx = 0;
    vy = 0;
    lastPosition = {(int)x, (int)y};
    sprite->setRect({(int)x, (int)y, w, h});
    sprite->setCurrentAnimation("stand");
    sprite->setFlip(SDL_FLIP_NONE);

    jumpStrenght = 2.0f;
    gravity = 0.3f;
    accelX = 0.12f;
    maxVX = 1.5f;
    maxVY = 4.0f;

    state = s_standing;
    jumpPressed = false;
    key.insert(key.end(), {0, 0, 0, 0, 0, 0});
    const SDL_Point p = {0, 0};
    collider.insert(collider.end(), {p, p, p, p, p, p, p, p, p, p, p, p});
    underFeet.insert(underFeet.end(), {p, p, p});
    hookedOnMovingPlatform = -1;
    diamonds = 0;
}

// Destructor
Player::~Player()
{
    JA_DeleteSound(sound_jump);
    JA_DeleteSound(sound_death);
    JA_DeleteSound(sound_coin);

    texture->unload();
    delete texture;

    delete sprite;
}

// Actualiza todas las variables
void Player::update()
{
    checkInput();
    move();
    animate();
    checkActors();
}

// Dibuja el objeto
void Player::render()
{
    sprite->render();
}

// Comprueba las entradas y modifica variables
void Player::checkInput()
{
    if (input->checkInput(INPUT_LEFT, REPEAT_TRUE))
    {
        vx = std::max(vx -= accelX, -maxVX);
        sprite->setFlip(SDL_FLIP_HORIZONTAL);
    }
    else if (input->checkInput(INPUT_RIGHT, REPEAT_TRUE))
    {
        vx = std::min(vx += accelX, maxVX);
        sprite->setFlip(SDL_FLIP_NONE);
    }
    else
    {
        if (vx > 0.0f)
        {
            vx = std::max(vx -= accelX, 0.0f);
        }
        else
        {
            vx = std::min(vx += accelX, 0.0f);
        }
    }

    if (input->checkInput(INPUT_UP, REPEAT_TRUE))
    {
        if (state == s_standing)
        {
            if (!jumpPressed)
            {
                jumpStrenght = 2.0f;
                vy -= jumpStrenght;
                state = s_jumping;
                isOn = f_none;
                jumpPressed = true;
                JA_PlaySound(sound_jump);
            }
        }
        else if (state == s_jumping)
        {
            if (jumpPressed)
            {
                jumpStrenght = std::max(jumpStrenght -= 0.4f, 0.0f);
                vy -= jumpStrenght;
            }
        }

        jumpPressed = true;

        // Si salta sale de la plataforma movil
        hookedOnMovingPlatform = -1;
    }
    else
    {
        jumpPressed = false;
        if (state == s_jumping)
        {
            state = s_falling;
        }
    }
}

// Aplica la gravedad
void Player::addGravity()
{
    // *** Falta ver pq la gravedad empuja al muñeco hacia abajo en los tiles atravesables
    if (state != s_standing)
    {
        vy = std::min(vy += gravity, maxVY);
    }
}

// Actualiza los puntos de colisión
void Player::updateColliders()
{
    const SDL_Point p = {(int)x, (int)y};

    // Lado izquierdo
    collider[0] = p;
    collider[1] = {p.x, p.y + 7};
    collider[2] = {p.x, p.y + 12};
    collider[3] = {p.x, p.y + 18};
    collider[4] = {p.x, p.y + 23};

    // Lado derecho
    collider[5] = {p.x + 15, p.y};
    collider[6] = {p.x + 15, p.y + 7};
    collider[7] = {p.x + 15, p.y + 12};
    collider[8] = {p.x + 15, p.y + 18};
    collider[9] = {p.x + 15, p.y + 23};

    // Centro
    collider[10] = {p.x + 7, p.y};
    collider[11] = {p.x + 7, p.y + 23};
}

// Actualiza los puntos de los pies
void Player::updateFeet()
{
    const SDL_Point p = {(int)x, (int)y};

    underFeet[0] = {p.x, p.y + h};
    underFeet[1] = {p.x + 7, p.y + h};
    underFeet[2] = {p.x + 15, p.y + h};
}

// Compruena las colisiones con el mapa
bool Player::checkMapCollisions()
{
    bool collision = false;

    updateColliders();

    for (auto c : collider)
    {
        collision |= (map->getTile(c) == wall);
    }

    return collision;
}

// Mueve al jugador en función de la velocidad/desplazamiento
void Player::move()
{
    const int tileSize = map->getTileSize();
    lastPosition = {(int)x, (int)y};
    addGravity();

    // Mueve en el eje X
    x += vx;

    // Comprueba colisiones con muros
    if (checkMapCollisions())
    {
        // Recoloca
        if (vx > 0)
        {
            x -= ((int)x + w) % tileSize;
        }
        else
        {
            x += tileSize - ((int)x % tileSize);
        }

        vx = 0.0f;
    }

    // Mueve en el eje Y y comprueba colisiones con muros
    y += vy;
    if (checkMapCollisions())
    {
        // Recoloca
        if (vy > 0.0f)
        {
            y -= ((int)y + h) % tileSize;
            state = s_standing;
        }
        else
        {
            y += tileSize - ((int)y % tileSize);
            state = s_falling;
        }
        vy = 0.0f;
    }

    // Si no hay colisiones con los muros en el eje Y, comprueba
    // no haya atravesado el suelo de un tile atravesble
    else
    {
        const int a = (lastPosition.y + h) / tileSize;
        const int b = ((int)y + h) / tileSize;
        const bool tile_change = a != b;

        const bool going_down = vy >= 0.0f;
        const bool tile_aligned = ((int)y + h) % tileSize == 0;

        if (((going_down) && (tile_aligned)) || ((going_down) && (tile_change)))
        {
            // Tiene uno de los pies sobre una superficie
            if (isOnFloor())
            {
                state = s_standing;
                vy = 0.0f;
                y -= ((int)y + h) % tileSize;
            }
            // Tiene ambos pies sobre el vacío
            else
            {
                state = s_falling;
            }
        }

        const bool going_down2 = vy >= 0.0f;
        const bool tile_aligned2 = ((int)y + h) % tileSize == 0;
        // Si está cayendo
        if (going_down2)
        {
            state = s_falling;
            // Si está alineado con el tile mira el suelo (para que no lo mire si está
            // dentro de un tile atravesable y lo deje a medias)
            if (tile_aligned2)
            {
                if (isOnFloor())
                {
                    state = s_standing;
                }
            }

            // Si no esta enganchado a una plataforma
            if (hookedOnMovingPlatform == -1)
            {
                // Si esta sobre una plataforma
                if (isOnMovingPlatform())
                {
                    // Detener la caída y alinearlo con la plataforma
                    state = s_standing;
                    vy = 0.0f;
                    y = map->getActorCollider(hookedOnMovingPlatform).y - h;
                }
            }
        }

        // Si está enganchado a una plataforma movil
        if (hookedOnMovingPlatform != -1)
        {
            // Dejarlo alineado con la plataforma
            state = s_standing;
            vy = 0.0f;
            y = map->getActorCollider(hookedOnMovingPlatform).y - h;
            x += map->getActorIncX(hookedOnMovingPlatform);
            isOnMovingPlatform();
        }
    }

    // Actualiza la posición del sprite
    sprite->setPosX(x);
    sprite->setPosY(y);
}

// Anima al jugador
void Player::animate()
{
    if (state != s_standing)
    {
        sprite->setCurrentAnimation("jump");
    }
    else
    {
        if (abs(vx) < 0.50f)
        {
            sprite->setCurrentAnimation("stand");
        }
        else
        {
            sprite->setCurrentAnimation("walk");
        }
    }

    sprite->animate();
}

// Comprueba si el jugador tiene suelo debajo de los pies
bool Player::isOnFloor()
{
    if (isOn == f_platform)
    {
        return false;
    }

    bool onFloor = false;

    updateFeet();

    for (auto f : underFeet)
    {
        onFloor |= ((map->getTile(f) == wall) || (map->getTile(f) == passable));
    }

    if (onFloor)
    {
        isOn = f_wall;
    }
    else
    {
        isOn = f_none;
    }

    return onFloor;
}

// Comprueba si el jugador tiene una plataforma movil bajo sus pies
bool Player::isOnMovingPlatform()
{
    bool onMovingPlatform = false;
    hookedOnMovingPlatform = -1;

    // Si esta sobre el suelo, no puede estar tambien sobre una plataforma movil
    if (isOn == f_wall)
    {
        return false;
    }

    updateFeet();

    for (auto f : underFeet)
    {
        onMovingPlatform |= (map->getActorName(map->actorCollision(f)) == a_moving_platform);
        hookedOnMovingPlatform = std::max(hookedOnMovingPlatform, (map->actorCollision(f)));
    }

    if (!onMovingPlatform)
    {
        hookedOnMovingPlatform = -1;
        isOn = f_none;
    }
    else
    {
        isOn = f_platform;
    }

    return onMovingPlatform;
}

// Comprueba si está situado en alguno de los cuatro bordes de la habitación
bool Player::isOnScreenBorder()
{
    bool success = false;
    border = b_none;

    if (x < map->getPlayArea(b_left))
    {
        border = b_left;
        success = true;
    }
    else if (x > map->getPlayArea(b_right) - w)
    {
        border = b_right;
        success = true;
    }
    else if (y < map->getPlayArea(b_top))
    {
        border = b_top;
        success = true;
    }
    else if (y > map->getPlayArea(b_bottom) - h)
    {
        border = b_bottom;
        success = true;
    }

    return success;
}

// Devuelve el valor de la variable
e_border Player::getBorder()
{
    return border;
}

// Cambia al jugador de un borde al opuesto. Util para el cambio de pantalla
void Player::switchBorders()
{
    switch (border)
    {
    case b_top:
        y = map->getPlayArea(b_bottom) - h;
        break;

    case b_bottom:
        y = map->getPlayArea(b_top);
        break;

    case b_right:
        x = map->getPlayArea(b_left);
        break;

    case b_left:
        x = map->getPlayArea(b_right) - w;
        break;

    default:
        break;
    }

    border = b_none;
}

// Pasa la referencia del mapa
void Player::setMap(Map *map)
{
    this->map = map;
}

// Comprueba las interacciones con los actores
int Player::checkActors()
{
    SDL_Rect rect = sprite->getRect();
    const int index = map->actorCollision(rect);
    const int name = map->getActorName(index);

    if (name == a_diamond)
    {
        diamonds++;
        JA_PlaySound(sound_coin);
        map->getItem(index);
        map->deleteActor(index);
    }

    return index;
}

// Recarga las texturas
void Player::reLoadTextures()
{
    texture->reLoad();
}