#include <iostream>
#include <cmath>
#include "utility.hpp"
#include "manager.hpp"
#include "game.hpp"
#include "player.hpp"

/**
 * Définition des variables et fonctions globales internes
 * (accessibles uniquement dans ce fichier)
 */
namespace {
    // nombre maximum de frames que l'on peut sauter
    // en cas de charge CPU importante
    const unsigned int MAX_FRAME_SKIP = 5;
}

Game::Game(Manager& manager) : Level(manager),
    mode(Game::Mode::NORMAL),
    next_frame_time(manager.getCurrentTime()),
    skipped_frames(0),
    death_cause(Game::DeathCause::NONE),
    widget_timer(manager, false) {}

Game::~Game() {}

void Game::enable() {
    Level::enable();

    // attributs de la fenêtre
    getManager().setTitle(getName());
    getManager().setFramerate(0);

    // si musique il y a, on la joue
    if (getMusic() != "") {
        getResourceManager().playMusic(getMusic());
    } else {
        getResourceManager().stopMusic();
    }
}

void Game::processEvent(const sf::Event& event) {
    Level::processEvent(event);

    if (event.type == sf::Event::KeyPressed) {
        // appui sur espace : retour
        if (event.key.code == sf::Keyboard::Space) {
            getManager().popState();
        }

        // appui sur retour échap : échange entre le mode pause et normal
        if (event.key.code == sf::Keyboard::Escape ||
            event.key.code == sf::Keyboard::BackSpace) {
            if (getMode() == Game::Mode::NORMAL) {
                setMode(Game::Mode::PAUSED);
            } else if (getMode() == Game::Mode::PAUSED) {
                setMode(Game::Mode::NORMAL);
            }
        }
    }
}

void Game::frame() {
    sf::Time current_time = getManager().getCurrentTime();

    if (current_time >= next_frame_time) {
        // si nous sommes en retard ou dans les temps
        // on replanifie la prochaine frame
        next_frame_time += Manager::FRAME_TIME;

        // on met à jour la physique d'un cran temporel,
        // si on est en mode normal
        if (getMode() == Game::Mode::NORMAL) {
            update();
        } else {
            // TODO: pour le débogage affichage du mode actuel
            switch (getMode()) {
            case Game::Mode::NORMAL:
                std::cout << "<< Reprise >>" << std::endl;
                break;
            case Game::Mode::PAUSED:
                std::cout << "<< En pause >>" << std::endl;
                break;
            case Game::Mode::WON:
                std::cout << "<< Gagné ! >>" << std::endl;
                break;
            case Game::Mode::LOST:
                std::cout << "<< Perdu : ";

                switch (getDeathCause()) {
                case Game::DeathCause::OUT_OF_BOUNDS:
                    std::cout << "sortie du cadre";
                    break;
                case Game::DeathCause::KILLED:
                    std::cout << "tué par bloc";
                    break;
                case Game::DeathCause::TIME_OUT:
                    std::cout << "temps écoulé";
                    break;
                case Game::DeathCause::NONE:
                    std::cout << "sans aucune raison";
                    break;
                }

                std::cout << " ! >>" << std::endl;
                break;
            }
        }

        // on s'assure que la caméra soit centrée sur nos joueurs
        ensureCentered();

        // si on a encore suffisamment de temps, ou si on a sauté
        // trop de frames, on dessine
        if (current_time < next_frame_time || skipped_frames >= MAX_FRAME_SKIP) {
            draw();
        }

        // sinon, on saute une frame de dessin. On stocke
        // le nombre de frames sautées pour ne pas sauter plus que
        // le maximum
        else {
            skipped_frames++;
        }
    } else {
        // si nous sommes en avance, on endort le processus
        // le temps nécessaire pour revenir dans les temps
        sf::sleep(next_frame_time - current_time);
    }
}

void Game::draw() {
    sf::Vector2i window_size = (sf::Vector2i) getWindow().getSize();

    // dessin des objets du niveau
    Level::draw();

    // on passe au dessin d'éléments d'interface.
    // Changement de vue sur la vue par défaut
    getManager().useGUIView();

    // dessin du timer
    widget_timer.setTimeLeft(
        std::max(std::ceil(time_left), 0.f)
    );

    widget_timer.draw(sf::Vector2f(window_size.x / 2 - 50, 0));
}

void Game::ensureCentered() {
    std::vector<Player::Ptr>& players = getPlayers();
    unsigned int player_count = players.size();

    sf::View camera = getCamera();
    sf::Vector2f previous_center = camera.getCenter();
    sf::Vector2f position_sum, goal_center;

    for (unsigned int i = 0; i < player_count; i++) {
        position_sum += players[i]->getPosition();
    }

    if (player_count == 0) {
        // on évite la division par zéro
        goal_center = sf::Vector2f(0, 0);
    } else {
        // on place la caméra à la position médiane de tous les joueurs
        goal_center = position_sum / (float) player_count;
    }

    // on anime le centre vers la nouvelle position
    previous_center.x = Utility::animateValue(previous_center.x, 5, goal_center.x);
    previous_center.y = Utility::animateValue(previous_center.y, 5, goal_center.y);

    camera.setCenter(previous_center);
    setCamera(camera);
}

void Game::update() {
    std::vector<CollisionData> colliding;
    std::vector<Object::Ptr>& objects = getObjects();

    // on décrémente le temps, si le temps est nul,
    // on a perdu
    if (time_left <= 0) {
        setMode(Game::Mode::LOST);
        setDeathCause(Game::DeathCause::TIME_OUT);
        return;
    } else {
        time_left -= Manager::FRAME_TIME.asSeconds();
    }

    // on tue les objets qui étaient déjà planifiés pour mourir
    for (auto it = pending_kill.begin(); it != pending_kill.end(); it++) {
        removeObject(*it);
    }

    pending_kill.empty();

    // détection des objets en collision
    for (auto it = objects.begin(); it != objects.end(); it++) {
        Object::Ptr obj_a = *it;

        // l'objet est sorti de la zone, on le signale et on
        // planifie sa mort à la prochaine frame
        if (obj_a->getMass() != 0 && !isInZone(obj_a)) {
            kill(obj_a);

            // si c'était un joueur, on a perdu
            if (obj_a->getTypeId() == Player::TYPE_ID) {
                setMode(Game::Mode::LOST);
                setDeathCause(Game::DeathCause::OUT_OF_BOUNDS);
            }
        }

        // on regarde s'il est en collision avec
        // d'autres objets
        for (auto jt = it + 1; jt != objects.end(); jt++) {
            Object::Ptr obj_b = *jt;

            // si les objets ne sont pas sur la même couche,
            // ils ne peuvent pas entrer en collision
            if (obj_a->getLayer() != obj_b->getLayer()) {
                continue;
            }

            // si les deux boîtes englobantes des deux objets ne
            // s'intersectent pas, il ne risque pas d'y avoir de collision
            if (!obj_a->getAABB().intersects(obj_b->getAABB())) {
                continue;
            }

            CollisionData data;
            data.obj_a = obj_a;
            data.obj_b = obj_b;

            if (getCollisionData(data)) {
                colliding.push_back(data);
            }
        }
    }

    // intégration des forces dans la vitesse
    for (auto it = objects.begin(); it != objects.end(); it++) {
        (*it)->updateVelocity(*this);
    }

    // résolution des collisions détectées
    for (auto it = colliding.begin(); it != colliding.end(); it++) {
        it->obj_a->solveCollision(*this, it->obj_b, it->normal);
    }

    // intégration de la vitesse dans la position
    for (auto it = objects.begin(); it != objects.end(); it++) {
        (*it)->updatePosition();
    }

    // application de la correction positionnelle
    for (auto it = colliding.begin(); it != colliding.end(); it++) {
        it->obj_a->positionalCorrection(it->obj_b, it->normal, it->depth);
    }
}

void Game::kill(Object::Ptr object) {
    pending_kill.push_back(object);
}

/**
 * Implémentation tirée de
 * http://stackoverflow.com/a/2922778/3452708
 */
bool Game::isInZone(Object::Ptr object) {
    std::vector<sf::Vector2f>& zone = getZone();
    sf::Vector2f pos = object->getPosition();
    unsigned int vertices = getZone().size();
    bool result = false;

    for (unsigned int i = 0, j = vertices - 1; i < vertices; j = i++) {
        if (((zone[i].y > pos.y) != (zone[j].y > pos.y)) &&
           (pos.x < (zone[j].x - zone[i].x) * (pos.y - zone[i].y) /
           (zone[j].y - zone[i].y) + zone[i].x)) {
            result = !result;
        }
    }

    return result;
}

Game::Mode Game::getMode() {
    return mode;
}

void Game::setDeathCause(Game::DeathCause set_death_cause) {
    death_cause = set_death_cause;
}

Game::DeathCause Game::getDeathCause() {
    return death_cause;
}

void Game::setMode(Game::Mode set_mode) {
    mode = set_mode;
}

void Game::setTotalTime(int set_total_time) {
    Level::setTotalTime(set_total_time);
    time_left = getTotalTime();
}
