/*
 *   ORXONOX - the hottest 3D action shooter ever to exist
 *                    > www.orxonox.net <
 *
 *
 *   License notice:
 *
 *   This program is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU General Public License
 *   as published by the Free Software Foundation; either version 2
 *   of the License, or (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 *   Author:
 *      Fabian 'x3n' Landau
 *   Co-authors:
 *      ...
 *
 */

#include "Gametype.h"

#include "util/Math.h"
#include "core/Core.h"
#include "core/CoreIncludes.h"
#include "core/config/ConfigValueIncludes.h"
#include "core/GameMode.h"
#include "core/command/ConsoleCommandIncludes.h"
#include "gamestates/GSLevel.h"

#include "infos/PlayerInfo.h"
#include "infos/Bot.h"
#include "graphics/Camera.h"
#include "worldentities/ControllableEntity.h"
#include "worldentities/SpawnPoint.h"
#include "worldentities/pawns/Spectator.h"
#include "worldentities/pawns/Pawn.h"
#include "overlays/OverlayGroup.h"
#include "Scene.h"

namespace orxonox
{
    static const std::string __CC_addBots_name  = "addBots";
    static const std::string __CC_killBots_name = "killBots";

    SetConsoleCommand("Gametype", __CC_addBots_name,  &Gametype::addBots ).addShortcut().defaultValues(1);
    SetConsoleCommand("Gametype", __CC_killBots_name, &Gametype::killBots).addShortcut().defaultValues(0);

    RegisterUnloadableClass(Gametype);

    Gametype::Gametype(Context* context) : BaseObject(context)
    {
        RegisterObject(Gametype);

        this->setGametype(WeakPtr<Gametype>(this)); // store a weak-pointer to itself (a strong-pointer would create a recursive dependency)

        this->gtinfo_ = new GametypeInfo(context);

        this->defaultControllableEntity_ = Class(Spectator);
        this->scoreboard_ = nullptr;

        this->bAutoStart_ = false;
        this->bForceSpawn_ = false;
        this->bAutoEnd_ = true;
        this->numberOfBots_ = 0;

        this->timeLimit_ = 0;
        this->time_ = 0;
        this->timerIsActive_ = false;

        this->initialStartCountdown_ = 3;

        this->setConfigValues();

        ModifyConsoleCommand(__CC_addBots_name).setObject(this);
        ModifyConsoleCommand(__CC_killBots_name).setObject(this);
    }

    Gametype::~Gametype()
    {
        if (this->isInitialized())
        {
            if (this->gtinfo_)
                this->gtinfo_->destroy();

            ModifyConsoleCommand(__CC_addBots_name).setObject(nullptr);
            ModifyConsoleCommand(__CC_killBots_name).setObject(nullptr);
        }
    }

    /**
     * @brief Initializes sub-objects of the Gametype. This must be called after the constructor.
     * At this point, the context is expected to have the current gametype. This allows to pass the current gametype to the sub-objects via constructor.
     */
    void Gametype::init()
    {
        // load the corresponding score board
        if (GameMode::showsGraphics() && !this->scoreboardTemplate_.empty())
        {
            this->scoreboard_ = new OverlayGroup(this->getContext());
            this->scoreboard_->addTemplate(this->scoreboardTemplate_);
        }
    }

    void Gametype::setConfigValues()
    {
        SetConfigValue(initialStartCountdown_, 3.0f);
        SetConfigValue(bAutoStart_, false);
        SetConfigValue(bForceSpawn_, false);
        SetConfigValue(bAutoEnd_, true);
        SetConfigValue(numberOfBots_, 0);
        SetConfigValue(scoreboardTemplate_, "defaultScoreboard");
    }

    void Gametype::tick(float dt)
    {
        SUPER(Gametype, tick, dt);

        //count timer
        if (timerIsActive_)
        {
            if (this->timeLimit_ == 0)
                this->time_ += dt;
            else
                this->time_ -= dt;
        }

        if (this->gtinfo_->isStartCountdownRunning() && !this->gtinfo_->hasStarted())
            this->gtinfo_->countdownStartCountdown(dt);

        if (!this->gtinfo_->hasStarted())
        {
            for (const auto& mapEntry : this->players_)
            {
                // Inform the GametypeInfo that the player is ready to spawn.
                if(mapEntry.first->isHumanPlayer() && mapEntry.first->isReadyToSpawn())
                    this->gtinfo_->playerReadyToSpawn(mapEntry.first);
            }

            this->checkStart();
        }
        else if (!this->gtinfo_->hasEnded())
            this->spawnDeadPlayersIfRequested();

        this->assignDefaultPawnsIfNeeded();
    }

    void Gametype::start()
    {
        this->addBots(this->numberOfBots_);
        this->gtinfo_->start();
        this->spawnPlayersIfRequested();
    }

    void Gametype::end()
    {
        this->gtinfo_->end();
        if (this->bAutoEnd_)
        {
            this->showMenuTimer_.setTimer(3.0f, true, createExecutor(createFunctor(&Gametype::showMenu, this)));
        }

        for (const auto& mapEntry : this->players_)
        {
            if (mapEntry.first->getControllableEntity())
            {
                ControllableEntity* oldentity = mapEntry.first->getControllableEntity();

                ControllableEntity* entity = this->defaultControllableEntity_.fabricate(oldentity->getContext());
                if (oldentity->getCamera())
                {
                    entity->setPosition(oldentity->getCamera()->getWorldPosition());
                    entity->setOrientation(oldentity->getCamera()->getWorldOrientation());
                }
                else
                {
                    entity->setPosition(oldentity->getWorldPosition());
                    entity->setOrientation(oldentity->getWorldOrientation());
                }
                mapEntry.first->startControl(entity);
            }
            else
                this->spawnPlayerAsDefaultPawn(mapEntry.first);
        }
    }

    void Gametype::playerEntered(PlayerInfo* player)
    {
        this->players_[player].state_ = PlayerState::Joined;
        this->gtinfo_->playerEntered(player);
    }

    bool Gametype::playerLeft(PlayerInfo* player)
    {
        std::map<PlayerInfo*, Player>::iterator it = this->players_.find(player);
        if (it != this->players_.end())
        {
            this->players_.erase(it);
            return true;
        }
        return false;
    }

    void Gametype::playerSwitched(PlayerInfo* player, Gametype* newgametype)
    {
    }

    void Gametype::playerSwitchedBack(PlayerInfo* player, Gametype* oldgametype)
    {
    }

    bool Gametype::playerChangedName(PlayerInfo* player)
    {
        if (this->players_.find(player) != this->players_.end())
        {
            if (player->getName() != player->getOldName())
            {
                return true;
            }
        }
        return false;
    }

    void Gametype::pawnPreSpawn(Pawn* pawn)
    {
    }

    void Gametype::pawnPostSpawn(Pawn* pawn)
    {
    }

    void Gametype::playerPreSpawn(PlayerInfo* player)
    {
    }

    void Gametype::playerPostSpawn(PlayerInfo* player)
    {
    }

    void Gametype::playerStartsControllingPawn(PlayerInfo* player, Pawn* pawn)
    {
    }

    void Gametype::playerStopsControllingPawn(PlayerInfo* player, Pawn* pawn)
    {
    }

    bool Gametype::allowPawnHit(Pawn* victim, Pawn* originator)
    {
        return true;
    }

    bool Gametype::allowPawnDamage(Pawn* victim, Pawn* originator)
    {
        return true;
    }

    bool Gametype::allowPawnDeath(Pawn* victim, Pawn* originator)
    {
        return true;
    }

    void Gametype::pawnKilled(Pawn* victim, Pawn* killer)
    {
        if (victim && victim->getPlayer())
        {
            std::map<PlayerInfo*, Player>::iterator it = this->players_.find(victim->getPlayer());
            if (it != this->players_.end())
            {
                it->second.state_ = PlayerState::Dead;
                it->second.killed_++;

                // Reward killer
                if (killer && killer->getPlayer())
                {
                    std::map<PlayerInfo*, Player>::iterator it = this->players_.find(killer->getPlayer());
                    if (it != this->players_.end())
                    {
                        it->second.frags_++;

                        if (killer->getPlayer()->getClientID() != NETWORK_PEER_ID_UNKNOWN)
                            this->gtinfo_->sendKillMessage("You killed " + victim->getPlayer()->getName(), killer->getPlayer()->getClientID());
                        if (victim->getPlayer()->getClientID() != NETWORK_PEER_ID_UNKNOWN)
                            this->gtinfo_->sendDeathMessage("You were killed by " + killer->getPlayer()->getName(), victim->getPlayer()->getClientID());
                    }
                }

                if(victim->getPlayer()->isHumanPlayer())
                    this->gtinfo_->pawnKilled(victim->getPlayer());

                ControllableEntity* entity = this->defaultControllableEntity_.fabricate(victim->getContext());
                if (victim->getCamera())
                {
                    entity->setPosition(victim->getCamera()->getWorldPosition());
                    entity->setOrientation(victim->getCamera()->getWorldOrientation());
                }
                else
                {
                    entity->setPosition(victim->getWorldPosition());
                    entity->setOrientation(victim->getWorldOrientation());
                }
                it->first->startControl(entity);
            }
            else
                orxout(internal_warning) << "Killed Pawn was not in the playerlist" << endl;
        }
    }

    void Gametype::playerScored(PlayerInfo* player, int score)
    {
        std::map<PlayerInfo*, Player>::iterator it = this->players_.find(player);
        if (it != this->players_.end())
            it->second.frags_ += score;
    }

    int Gametype::getScore(PlayerInfo* player) const
    {
        std::map<PlayerInfo*, Player>::const_iterator it = this->players_.find(player);
        if (it != this->players_.end())
            return it->second.frags_;
        else
            return 0;
    }

    SpawnPoint* Gametype::getBestSpawnPoint(PlayerInfo* player) const
    {
        // If there is at least one SpawnPoint.
        if (this->spawnpoints_.size() > 0)
        {
            // Fallback spawn point if there is no active one, choose a random one.
            SpawnPoint* fallbackSpawnPoint = nullptr;
            unsigned int randomspawn = static_cast<unsigned int>(rnd(static_cast<float>(this->spawnpoints_.size())));
            unsigned int index = 0;
            std::vector<SpawnPoint*> activeSpawnPoints;
            for (SpawnPoint* spawnpoint : this->spawnpoints_)
            {
                if (index == randomspawn)
                    fallbackSpawnPoint = spawnpoint;

                if (spawnpoint != nullptr && spawnpoint->isActive())
                    activeSpawnPoints.push_back(spawnpoint);

                ++index;
            }

            if(activeSpawnPoints.size() > 0)
            {
                randomspawn = static_cast<unsigned int>(rnd(static_cast<float>(activeSpawnPoints.size())));
                return activeSpawnPoints[randomspawn];
            }

            orxout(internal_warning) << "Fallback SpawnPoint was used because there were no active SpawnPoints." << endl;
            return fallbackSpawnPoint;
        }
        return nullptr;
    }

    void Gametype::assignDefaultPawnsIfNeeded()
    {
        for (auto& mapEntry : this->players_)
        {
            if (!mapEntry.first->getControllableEntity())
            {
                mapEntry.second.state_ = PlayerState::Dead;

                if (!mapEntry.first->isReadyToSpawn() || !this->gtinfo_->hasStarted())
                {
                    this->spawnPlayerAsDefaultPawn(mapEntry.first);
                    mapEntry.second.state_ = PlayerState::Dead;
                }
            }
        }
    }

    void Gametype::checkStart()
    {
        if (!this->gtinfo_->hasStarted())
        {
            if (this->gtinfo_->isStartCountdownRunning())
            {
                if (this->gtinfo_->getStartCountdown() <= 0.0f)
                {
                    this->gtinfo_->stopStartCountdown();
                    this->gtinfo_->setStartCountdown(0.0f);
                    this->start();
                }
            }
            else if (this->players_.size() > 0)
            {
                if (this->bAutoStart_)
                {
                    this->start();
                }
                else
                {
                    bool allplayersready = true;
                    bool hashumanplayers = false;
                    for (const auto& mapEntry : this->players_)
                    {
                        if (!mapEntry.first->isReadyToSpawn())
                            allplayersready = false;
                        if (mapEntry.first->isHumanPlayer())
                            hashumanplayers = true;
                    }

                    if (allplayersready && hashumanplayers)
                    {
                        // If in developer's mode, there is no start countdown.
                        if(Core::getInstance().getConfig()->inDevMode())
                            this->start();
                        else
                        {
                            this->gtinfo_->setStartCountdown(this->initialStartCountdown_);
                            this->gtinfo_->startStartCountdown();
                        }
                    }
                }
            }
        }
    }

    void Gametype::spawnPlayersIfRequested()
    {
        for (const auto& mapEntry : this->players_)
        {
            if (mapEntry.first->isReadyToSpawn() || this->bForceSpawn_)
                this->spawnPlayer(mapEntry.first);
        }
    }

    void Gametype::spawnDeadPlayersIfRequested()
    {
        for (const auto& mapEntry : this->players_)
            if (mapEntry.second.state_ == PlayerState::Dead)
                if (mapEntry.first->isReadyToSpawn() || this->bForceSpawn_)
                    this->spawnPlayer(mapEntry.first);
    }

    void Gametype::spawnPlayer(PlayerInfo* player)
    {
        SpawnPoint* spawnpoint = this->getBestSpawnPoint(player);
        if (spawnpoint)
        {
            this->playerPreSpawn(player);
            player->startControl(spawnpoint->spawn());
            this->players_[player].state_ = PlayerState::Alive;

            if(player->isHumanPlayer())
                this->gtinfo_->playerSpawned(player);

            this->playerPostSpawn(player);
        }
        else
        {
            orxout(user_error) << "No SpawnPoints in current Gametype" << endl;
            abort();
        }
    }

    void Gametype::spawnPlayerAsDefaultPawn(PlayerInfo* player)
    {
        SpawnPoint* spawn = this->getBestSpawnPoint(player);
        if (spawn)
        {
            // force spawn at spawnpoint with default pawn
            ControllableEntity* entity = this->defaultControllableEntity_.fabricate(spawn->getContext());
            spawn->spawn(entity);
            player->startControl(entity);
        }
        else
        {
            orxout(user_error) << "No SpawnPoints in current Gametype" << endl;
            abort();
        }
    }

    void Gametype::addBots(unsigned int amount)
    {
        for (unsigned int i = 0; i < amount; ++i)
            this->botclass_.fabricate(this->getContext());
    }

    void Gametype::killBots(unsigned int amount)
    {
        unsigned int i = 0;
        ObjectList<Bot> list;
        for (ObjectList<Bot>::iterator it = list.begin(); (it != list.end()) && ((amount == 0) || (i < amount)); )
        {
            if (it->getGametype() == this)
            {
                (it++)->destroy();
                ++i;
            }
            else
                ++it;
        }
    }

    void Gametype::addTime(float t)
    {
        if (this->timeLimit_ == 0)
          this->time_ -= t;
        else
          this->time_ += t;
    }

    void Gametype::removeTime(float t)
    {
        if (this->timeLimit_ == 0)
          this->time_ += t;
        else
          this->time_ -= t;
    }

    void Gametype::resetTimer()
    {
        this->resetTimer(timeLimit_);
    }

    void Gametype::resetTimer(float t)
    {
        this->timeLimit_ = t;
        this->time_ = t;
    }

    void Gametype::showMenu()
    {
        GSLevel::startMainMenu();
    }

    GSLevelMementoState* Gametype::exportMementoState()
    {
        for (const auto& mapEntry : this->players_)
        {
            if (mapEntry.first->isHumanPlayer() && mapEntry.first->getControllableEntity() && mapEntry.first->getControllableEntity()->getCamera())
            {
                Camera* camera = mapEntry.first->getControllableEntity()->getCamera();

                GametypeMementoState* state = new GametypeMementoState();
                state->cameraPosition_ = camera->getWorldPosition();
                state->cameraOrientation_ = camera->getWorldOrientation();
                state->sceneName_ = camera->getScene()->getName();
                return state;
            }
        }

        return nullptr;
    }

    void Gametype::importMementoState(const std::vector<GSLevelMementoState*>& states)
    {
        // find correct memento state
        GametypeMementoState* state = nullptr;
        for (GSLevelMementoState* temp : states)
        {
            state = dynamic_cast<GametypeMementoState*>(temp);
            if (state)
                break;
        }

        if (!state)
            return;

        // find correct scene
        Scene* scene = nullptr;
        for (Scene* someScene : ObjectList<Scene>())
        {
            if (someScene->getName() == state->sceneName_)
            {
                scene = someScene;
                break;
            }
        }

        if (!scene)
        {
            orxout(internal_warning) << "Could not find scene with name " << state->sceneName_ << endl;
            return;
        }

        // find correct player and assign default entity with original position & orientation
        for (const auto& mapEntry : this->players_)
        {
            if (mapEntry.first->isHumanPlayer())
            {
                ControllableEntity* entity = this->defaultControllableEntity_.fabricate(scene->getContext());
                entity->setPosition(state->cameraPosition_);
                entity->setOrientation(state->cameraOrientation_);
                mapEntry.first->startControl(entity);
                break;
            }
        }
    }
}
