#include "state.hpp"

#include <cmath>

#include <glm/vec2.hpp>
#include <glm/vec3.hpp>
#include <glm/gtx/norm.hpp>

#include "missile_hit_type.hpp"
#include "object.hpp"
#include "missile.hpp"
#include "player.hpp"
#include "ship.hpp"
#include "commands.hpp"
#include "trace.hpp"
#include "explosion.hpp"

#include "events/explosion_event.hpp"
#include "events/missile_event.hpp"
#include "events/ship_event.hpp"

#include "util.hpp"

namespace game {
    void State::init(int numPlanets)
    {
        // clear
        for (Planet *planet : planets) {
            delete(planet);
        }
        planets.clear();

        // TODO: clear shots etc. too

        m_time               = 0.0;
        m_shipRadius         = 0.02;
        m_maxMissileDistance = 2.0;
        m_playerRespawnTime  = 2.0;
        m_defaultEnergy      = 10.0;
        m_maxNumTraces       = 10;

        setPlayingFieldCenter(0, 0);

        // TODO: need aspect ratio or data!
        //setPlayingFieldSize(1000, 300);

        setupPlanets(numPlanets);
    }

    bool State::findPlanetSpawnPosition(bool planetIsSun, float radius, glm::vec2 *pos)
    {
        (void) planetIsSun;

        bool tooNearToCenter         = true;
        bool collidesWithOtherPlanet = true;

        const glm::vec2 spawnArea = 0.9f * (m_playingFieldSize/std::max(m_playingFieldSize.x, m_playingFieldSize.y));

        // distribute but not in the center and not next to other planets
        int tries = 0;
        do {
            *pos = spawnArea * util::randv2_m1_1();

            collidesWithOtherPlanet = false;
            tooNearToCenter = glm::length(*pos) < 0.1;

            if (!tooNearToCenter) {
                for (const Planet *other : planets) {
                    float d = glm::distance(other->position, *pos);

                    float extraDist = (other->material == Planet::Material::Sun)
                        ? 4.0
                        : 1.0;

                    if (d < extraDist*other->radius + radius) {
                        collidesWithOtherPlanet = true;
                        break;
                    }
                }
            }
            if (tries++ > 1000) {
                return false;
            }

        } while(collidesWithOtherPlanet || tooNearToCenter);

        return true;
    }

    void State::setupPlanets(int numPlanets)
    {
        for (int i=0; i<numPlanets; i++) {
            Planet::Material mat = materialForStandardPlanetDistribution(i);

            float radius = 0.03 + 0.07*util::randf_0_1();
            if (i == 0) {
                // sun is bigger but not too big
                radius += 0.05;
                if (radius > 0.9) {
                    radius = 0.9;
                }

            }

            glm::vec2 pos;
            if (findPlanetSpawnPosition(mat == Planet::Material::Sun, radius, &pos)) {
                planets.push_back(new Planet(i, pos, radius, mat));
            }
        }
    }

    Planet::Material State::materialForStandardPlanetDistribution(int index)
    {
        // a few sun/water/sand/metall planents and the rest rocks
        switch(index) {
            case 0:
                return Planet::Material::Sun;

            case 1:
            case 2:
                return Planet::Material::Water;

            case 3:
            case 4:
                return Planet::Material::Sand;

            case 5:
                return Planet::Material::Metal;

            default:
                return Planet::Material::Rock;
        }
    }

    bool State::spawnShipForPlayer(Player *player)
    {
        glm::vec2 spawnPos;
        if (!findFreePositionWithRadius(5.0 * m_shipRadius, spawnPos)) {
            return false;
        }

        Ship *ship = new Ship(generateId(), spawnPos, player->color, m_shipRadius);
        player->ship = ship;
        ships.push_back(ship);

        player->energy = m_defaultEnergy;

        addShip(ship);

        return true;
    }

    size_t State::addPlayer()
    {
        Player *player = new Player(generateId());
        players.push_back(player);
        return player->id;
    }

    void State::quitPlayer(size_t playerId)
    {
        std::cout << playerId << " quit" << std::endl;

        Player *player = playerForId(playerId);
        if (player != nullptr) {
            for (Missile *missile : player->missiles) {
        player->missiles.remove(missile);
                missile->player = nullptr;
                m_nextEvents.push_back(new MissileEvent(LifeCycle::Destroy, missile));
			}

            if (player->ship != nullptr) {
                ships.remove(player->ship);
                m_nextEvents.push_back(new ShipEvent(LifeCycle::Destroy, player->ship));
            }

            players.remove(player);
            delete(player);
        }
    }

    void State::clear(size_t playerId)
    {
        std::cout << playerId << " clear" << std::endl;
    }

    void State::setName(size_t playerId, std::string name)
    {
        // discard if not unique
        for (const Player *other : players) {
            if (name == other->name) {
                std::cout << "name '" << name << "' already given to player #" << other->id << std::endl;
                return;
            }
        }

        playerForId(playerId)->name = name;
    }

    void State::addShip(Ship *ship)
    {
        m_nextEvents.push_back(new ShipEvent(LifeCycle::Create, ship));
    }

    void State::setSpeed(size_t playerId, double speed)
    {
        playerForId(playerId)->speed = speed;
    }

    void State::advancePlayerShipSpawns(float dt)
    {
        (void) dt;

        for (Player *player : players) {
            if (!player->alive) {
                player->deadTimeCounter += dt;
                if (player->deadTimeCounter >= m_playerRespawnTime) {
                    player->deadTimeCounter = 0;
                    player->alive = true;
                    std::cout<<"respawning player " << player->id << " now!" << std::endl;
                }
            }

            if (player->alive && player->ship == NULL) {
                if (!spawnShipForPlayer(player)) {
                    std::cout<<"could not spawn ship for player!" << std::endl;
                }
            }
        }
    }

    void State::advancePlayerCommands(float dt)
    {
        (void) dt;
        for (Player *player : players) {
            if (player->alive) {
                player->energy += dt * ENERGY_PER_DT;
            }
            // try to execute as much queued commands as possible.
            while (player->hasCommandInQueue()) {
                Command *command = player->peekCommand();
                if (!command->ready(player, this)) {
                    break;
                }

                command->apply(player, this);
                player->popCommand();
            }
        }
    }

    void State::playerKillsPlayer(Player *killer, Player *victim)
    {
        if (killer == nullptr || victim == nullptr) {
            std::cerr <<"error: killer / victim is NULL!" << std::endl;
            exit(-1);
            return;
        }

        std::cout<<"player " << killer->id << " killed " << victim->id << std::endl;

        // destroy ship
        ships.remove(victim->ship);

        delete(victim->ship);
        victim->ship = nullptr;

        victim->alive = false;
        victim->deadTimeCounter = 0.0;

        // TODO
        // add points

        // TODO
        // message
    }

    void State::advancePlayerMissiles(float dt)
    {
        // advance missiles
        for (Player *player : players) {

            std::vector<Missile*> rm;
            for (Missile *missile : player->missiles) {
                const Missile::Event evt = missile->advance(this, dt);
                const bool isHit = (evt.hit != Hit::Nothing);

                if (missile->trace != nullptr) {
                    missile->trace->addPointFromMissile(isHit); // force point if missile gets destroyed a
                }

                if (!isHit) {
                    continue;
                }

                bool spawnExplosion = true;

                switch(evt.hit) {
                    case Hit::Planet:
                        // TODO: add black spot on the planet.
                        // TODO: if water planet, add waves
                        // TODO: if gas planet, add nice gas explosion effect
                        // and start burning on this spot for some time.

                        //std::cout<<"hit Planet" << std::endl;
                        break;

                    case Hit::Ship:
                        //std::cout<<"hit Player" << std::endl;
                        playerKillsPlayer(playerForId(evt.playerIdKiller), playerForId(evt.playerIdVictim));
                        break;

                    case Hit::BorderOfUniverse:
                        //std::cout<<"missile left the universe." << std::endl;
                        spawnExplosion = false;
                        break;

                    default:
                        break;
                }

                if (spawnExplosion) {
                    addExplosionFromHit(&evt);
                }

                if (missile->trace != nullptr) {
                    missile->trace->finish();
                }

                rm.push_back(missile);
            }

            for (Missile *missile : rm) {
                player->missiles.remove(missile);

                m_nextEvents.push_back(new MissileEvent(LifeCycle::Destroy, missile));
            }
        }
    }

    void State::advanceExplosions(float dt)
    {
        std::vector<Explosion*> rm;

        for (Explosion *explosion : explosions) {
            explosion->age += dt;
            if (explosion->age >= explosion->maxAge) {
                rm.push_back(explosion);
            }
        }

        for (Explosion *explosion : rm) {
            explosions.remove(explosion);

            m_nextEvents.push_back(new ExplosionEvent(LifeCycle::Destroy, explosion));

            //delete(explosion);
        }
    }

    void State::advance(float dt)
    {
        //std::cout<<"[state] (init) cycle: update events length is " << m_nextEvents.size() << std::endl;

        m_time += dt;

        advancePlayerShipSpawns(dt);

        advanceExplosions(dt);

        advanceTraceAges(dt);

        advancePlayerCommands(dt);

        advancePlayerMissiles(dt);

        //std::cout<<"[state] (before move) cycle: update events length is " << m_nextEvents.size() << std::endl;

        // put collected events into that list.
        //m_allEvents.push_back(std::move(m_nextEvents));
        for (auto *evt : m_nextEvents) {
            m_allEvents.push_back(evt);
        }
        m_nextEvents.clear();

        //std::cout<<"[state] (after move) cycle: update events length is " << m_nextEvents.size() << std::endl;

        // finally remove things.
        //m_nextEvents.clear();
    }

    Player *State::playerForId(size_t playerId)
    {
        for (Player *p : players) {
            if (p->id == playerId) {
                return p;
            }
        }

        return nullptr;
    }

    bool State::findFreePositionWithRadius(float radius, glm::vec2 &pos)
    {
        bool first = ships.size() == 0;

        int tries = 0;
        while(true) {
            bool noCollision = true;

            //pos = glm::linearRand2(-1.0, 1.0);
            if (first) {
                first = false;
                pos = glm::vec2(0.0, 0.0);
            } else {
                pos = glm::vec2(util::randf_0_1(), util::randf_m1_1());
            }

            for (Planet *p : planets) {
                if (glm::distance(p->position, pos) <= radius) {
                    noCollision = false;
                    break;
                }
            }

            if (noCollision) {
                return true;
            }

            if (tries++ >= 1000) {
                pos = glm::vec2(0.0, 0.0);
                return false;
            }
        }
    }

    void State::commandForPlayer(size_t playerId, game::Command *cmd)
    {
        Player *player = playerForId(playerId);
        if (player != nullptr) {
            player->addCommand(cmd);
        }
    }

    void State::addTrace(Trace *trace)
    {
        //int count = 0;
        //for (Trace *old : traces) {
        //    if (old->playerId == trace->playerId) {
        //        count++;
        //    }
        //}

        //if (count > m_maxNumTraces) {
        //}

        traces.push_back(trace);
    }

    void State::addMissile(Missile *missile)
    {
        Trace *trace = new Trace(missile);
        missile->trace = trace;

        addTrace(trace);

        m_nextEvents.push_back(new MissileEvent(LifeCycle::Create, missile));
    }

    void State::deleteTrace(Trace *trace)
    {
        if (trace->missile != nullptr) {
            // delete backlink.
            // XXX: there's a missile without a trace now.
            trace->missile->trace = nullptr;
        }

        delete(trace);
    }

    void State::addExplosionFromHit(const Missile::Event *evt)
    {
        if (evt->hit == Hit::Nothing || evt->hit == Hit::BorderOfUniverse) {
            return;
        }

        Explosion *explosion = new Explosion(
                generateId(), evt->position,
                evt->missileVelocity, evt->hit);

        explosions.push_back(explosion);

        m_nextEvents.push_back(new ExplosionEvent(LifeCycle::Create, explosion));
    }

    void State::advanceTraceAges(float dt)
    {
        std::vector<Trace*> rm;
        for (Trace *trace : traces) {
            if (trace->missile == nullptr) {
                trace->age += dt;
                if (trace->age >= trace->maxAge) {
                    rm.push_back(trace);
                }
            }
        }

        for (Trace *trace : rm) {
            traces.remove(trace);
            deleteTrace(trace);
        }
    }

    void State::setPlayingFieldCenter(int center_x, int center_y)
    {
        m_playingFieldCenter = glm::vec2((float) center_x, (float) center_y);
    }

    void State::setPlayingFieldSize(int width, int height)
    {
        m_playingFieldSize = glm::vec2(width, height);
    }

    size_t State::generateId()
    {
        return m_ids.makeNextId();
    }

    void State::applyAndClearAllOldStateUpdates()
    {
        for (auto *evt : m_allEvents) {
            if (evt->lifeCycle() == LifeCycle::Destroy) {
                switch(evt->eventType()) {
                    case EventType::Explosion:
                        {
                            ExplosionEvent *ee = static_cast<ExplosionEvent*>(evt);
                            //std::cout<<"got explosion delete event, finally deleting explosion #"
                            //    << ee->object()->id << std::endl;

                            delete(ee->object());
                        }
                    break;

                    case EventType::Ship:
                    {
                            auto *se = static_cast<ShipEvent*>(evt);
                            //std::cout<<"got missile delete event, finally deleting missile #"
                            //    << me->object()->id << std::endl;

                            delete(se->object());
                        }
                    break;

                    case EventType::Missile:
                    {
                            auto *me = static_cast<MissileEvent*>(evt);
                            //std::cout<<"got missile delete event, finally deleting missile #"
                            //    << me->object()->id << std::endl;

                            delete(me->object());
                    }
                    break;

                default:
                    std::cout<<"warning: unhandled deletion event for: event type "
                        << (int) evt->eventType() << std::endl;
                }
            }
            delete(evt);
        }

        m_allEvents.clear();
    }

    const std::list<StateUpdateEvent*> &State::currentStateUpdateEvents() const
    {
        return m_allEvents;
    }
}
