/*Copyright (C) 2010-2023 Duncan Deveaux

This file is part of Hikou no mizu.

Hikou no mizu 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 3 of the License, or
(at your option) any later version.

Hikou no mizu 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 Hikou no mizu.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "FightScene.hpp"
#include "Player/Player.hpp"
#include "Engines/Sound/SoundInterface.hpp"

#include "Tools/Log.hpp"
#include <cassert>
#include <cstddef>

namespace
{
    /// Find a free location to spawn object \p object in \p world
    Box findFreeSpawn(const PhysicsWorld &world, const PhysicsObject &object)
    {
        constexpr float spawnGap = 20.f;
        const Box &worldBox = world.getWorldBox();

        Box spawn(worldBox.left + spawnGap, worldBox.top + spawnGap,
                  object.getBox().width, object.getBox().height);

        //Browse the world in a grid until a free spawn location is found
        while (world.worldContains(spawn))
        {
            if (world.isFree(spawn))
                return spawn;

            spawn.left += spawnGap;
            if (!world.worldContains(spawn))
            {
                spawn.left = worldBox.left;
                spawn.top += spawnGap;
            }
        }

        //No free box, return the top left corner as
        //a fallback but the position change will fail
        return Box(worldBox.left + spawnGap, worldBox.top + spawnGap,
                   object.getBox().width, object.getBox().height);
    }
}

FightScene::FightScene(Ambient *ambient, SoundInterface *soundInterface) :
m_started(false), m_ambient(ambient), m_soundInterface(soundInterface),
m_modeController(nullptr)
{
    m_weapons.setSoundInterface(m_soundInterface);
}

FightScene::~FightScene()
{
    clearPlayersCore();

    if (m_modeController != nullptr)
        delete m_modeController;
}

void FightScene::addFightModeController(FightRules::Mode mode)
{
    if (m_modeController != nullptr)
        delete m_modeController;

    m_modeController = FightModeControl::allocate(mode);
}

void FightScene::setArena(const std::string &arenaName)
{
    //Clear physics world
    m_physicsWorld.resetVelocities();
    m_arena.unsubscribePlatforms(m_physicsWorld);
    for (const Player *player : m_players)
        m_physicsWorld.removeObject((*player));

    m_weapons.clear();
    m_damageDealer.clear();

    //Reposition players and physics objects
    m_arena = ArenaTools::load(arenaName);
    m_arena.applySoundInterface(m_soundInterface);
    m_arena.applyToPhysics(m_physicsWorld);

    for (std::size_t i = 0; i < m_players.size(); i++)
    {
        m_players[i]->reset();
        respawnPlayer((*m_players[i]), i);
    }

    //Resubscribe objects to the physics world
    m_arena.subscribePlatforms(m_physicsWorld);
    for (Player *player : m_players)
        m_physicsWorld.addObject((*player));

    //Update arena ambient sound
    if (m_ambient != nullptr)
        m_ambient->play(m_arena.getAmbient());
}

void FightScene::clearPlayersCore()
{
    for (Player *player : m_players)
        deletePlayer(player);

    m_players.clear();
}

void FightScene::uninitializeCore()
{
    m_started = false;

    clearPlayersCore();
    m_physicsWorld = PhysicsWorld();
    m_arena = Arena();

    if (m_ambient != nullptr)
        m_ambient->stop();

    m_weapons.clear();
    m_damageDealer.clear();
}

Player *FightScene::addPlayer(const std::string &characterName)
{
    Player *player = Player::allocFromXML(characterName, "cfg/characters.xml");
    if (player != nullptr)
    {
        player->setPhysicsWorld(m_physicsWorld);
        player->setDamageTracker(m_damageDealer);
        player->setSoundInterface(m_soundInterface);

        player->loadMoves();
        player->initWeapons(m_weapons);

        //Position & subscribe to physics
        respawnPlayer((*player), m_players.size());
        m_physicsWorld.addObject((*player));

        m_players.push_back(player);
    }

    return player;
}

void FightScene::deletePlayer(Player *player)
{
    if (player != nullptr)
    {
        //Clear any reference to the player in the damage dealer
        m_damageDealer.clear_player(player);

        m_physicsWorld.removeObject((*player));
        delete player;
    }
}

void FightScene::respawnPlayer(Player &player, std::size_t spawnIx)
{
    Vector playerSpawn;
    if (!m_arena.getPlayerSpawn(spawnIx, playerSpawn) ||
        !m_physicsWorld.isFree(Box(playerSpawn.x,
                                   playerSpawn.y - player.getBox().height,
                                   player.getBox().width,
                                   player.getBox().height)))
    {
        const Box spawnBox = findFreeSpawn(m_physicsWorld, player);
        playerSpawn = Vector(spawnBox.left, spawnBox.top + spawnBox.height);
    }

    player.setPosition(playerSpawn.x, playerSpawn.y - player.getBox().height);
    if (playerSpawn.x > m_physicsWorld.getWorldBox().getCenter().x)
        player.setLooksLeft(true);
}

void FightScene::reset()
{
    //Clear physics world
    m_physicsWorld.resetVelocities();
    m_arena.unsubscribePlatforms(m_physicsWorld);
    for (const Player *player : m_players)
        m_physicsWorld.removeObject((*player));

    m_weapons.clear();
    m_damageDealer.clear();

    //Reposition players and physics objects
    for (std::size_t i = 0; i < m_players.size(); i++)
    {
        m_players[i]->reset();
        respawnPlayer((*m_players[i]), i);
    }

    //Resubscribe objects to the physics world
    m_arena.subscribePlatforms(m_physicsWorld);
    for (Player *player : m_players)
        m_physicsWorld.addObject((*player));

    m_started = false;
}

void FightScene::start()
{
    m_started = true;
}

void FightScene::update(float frameTime)
{
    if (m_started)
        updatePlayerControllers();

    m_physicsWorld.update(frameTime);
    m_weapons.update(frameTime);

    //Ensure the match mode rules are respected prior to update
    //after world update, player control input
    if (m_modeController != nullptr)
        m_modeController->preUpdate(m_players);

    for (Player *player : m_players)
        player->update(frameTime);

    //Ensure the match mode rules are still respected after update
    if (m_modeController != nullptr)
        m_modeController->postUpdate(m_players);

    m_damageDealer.deal();
}

std::vector<const Player*> FightScene::getPlayers() const
{
    return std::vector<const Player*>(m_players.begin(), m_players.end());
}

bool FightScene::isFinished(int *winnerIxDst, const Player **winnerDst) const
{
    //To be set to the index of a not KO player
    int notKO = FightScene::DrawGame;

    for (std::size_t i = 0; i < m_players.size(); i++)
    {
        const Player *player = m_players[i];
        if (!player->isKo())
        {
            //At least two players are still not KO
            if (notKO != FightScene::DrawGame) return false;
            else notKO = static_cast<int>(i); //Last standing player
        }
    }

    if (notKO != FightScene::DrawGame)
    {
        //One player standing, deal the remaining registered damage
        //to make sure it is not actually a draw
        m_damageDealer.deal_queue();
        if (m_players[static_cast<std::size_t>(notKO)]->isKo())
            notKO = FightScene::DrawGame; //Actually a draw
    }

    if (winnerIxDst != nullptr)
        (*winnerIxDst) = notKO;

    if (winnerDst != nullptr && notKO != FightScene::DrawGame) //Not a draw game
        (*winnerDst) = m_players[static_cast<std::size_t>(notKO)];

    return true;
}
