/*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 "ClientFightController.hpp"
#include "Engines/Resources/JoystickManager.hpp"
#include "Engines/Resources/TextureManager.hpp"
#include "Engines/Sound/AmbientPlayer.hpp"
#include "Engines/Sound/SoundEngine.hpp"
#include "Network/Serialization/PlayerStatus.hpp"
#include "Network/Serialization/PhysicsObjectStatus.hpp"
#include "Network/Serialization/DamageStatus.hpp"
#include "Network/Serialization/EtherStatus.hpp"
#include "Network/Serialization/PlayerAction.hpp"
#include "GUI/Fight/FightInterface.hpp"
#include "Configuration/Configuration.hpp"

#include "Arena/Arena.hpp" ///< ArenaTools
#include "Structs/JoystickData.hpp"
#include "Structs/Character.hpp"
#include "Tools/Log.hpp"
#include "Tools/Format.hpp"

ClientFightController::ClientFightController(PlayerActionSender &actionSender,
                            JoystickManager &joystickManager,
                            TextureManager &textureManager,
                            AmbientPlayer &ambientPlayer,
                            SoundEngine &soundEngine,
                            FightInterface &fightInterface,
                            const ClientFightScene::DisplayOptions &options,
                            const Configuration &configuration) :
m_fightScene(textureManager, &ambientPlayer, &soundEngine, options),
m_joystickManager(&joystickManager), m_textureManager(&textureManager),
m_playerActionSender(&actionSender), m_fightInterface(&fightInterface),
m_configuration(&configuration),
m_validArenas(ArenaTools::fetchNames()),
m_validCharacters(Character::fetchNames())
{

}

void ClientFightController::initialize()
{
    m_fightScene.start();
}

void ClientFightController::uninitialize()
{
    m_fightScene.uninitialize();
    m_playersData.clear();
    m_currentArena.clear();
    m_currentRole = PeerRole();
    m_fightInterface->setPlayers(m_fightScene.getPlayers(),
                                     (*m_textureManager));
}

void ClientFightController::setFightMode(FightRules::Mode mode)
{
    m_fightScene.addFightModeController(mode);
}

bool ClientFightController::setArena(const std::string &arena)
{
    if (m_currentArena != arena)
    {
        if (!ArenaTools::validateName(m_validArenas, arena))
            return false;

        Log::info(Format::format("Updating arena: {}", arena));

        m_fightScene.setArena(arena);
        m_currentArena = arena;
    }

    return true;
}

bool ClientFightController::setPlayers(const std::vector<PeerData> &players,
                                       const PeerRole &peerRole)
{
    if (!playersHaveChanged(players) && peerRole == m_currentRole)
        return true;

    if (!PeerData::validate(players, m_validCharacters))
        return false;

    Log::info("Updating players:");

    ClientInputState inputState;
    if (!m_currentRole.spectator &&
        m_playersData.size() > m_currentRole.playerIx &&
        m_playersData[m_currentRole.playerIx].control != nullptr)
    {
        //The client is playing as of this update:
        //Export the current input state so it can be maintained if the
        //player ix of the client simply changes
        inputState =
            m_playersData[m_currentRole.playerIx].control->getInputState();
    }

    m_fightScene.clearPlayers();
    m_playersData.clear();

    int deviceID = DEVICE_KEYBOARD;
    PlayerKeys keys;
    getActiveKeys(deviceID, keys);

    std::vector<Utf8::utf8_string> usernames;
    usernames.reserve(players.size());
    m_playersData.reserve(players.size());
    for (std::size_t i = 0; i < players.size(); i++)
    {
        ClientPlayerInterface *playerControl =
            m_fightScene.addNetClientPlayer(players[i].character,
                                            peerRole.hasControlOver(i) ?
                                                m_playerActionSender : nullptr,
                                            deviceID, keys, inputState);
        if (playerControl == nullptr)
        {
            //Failure in player allocation, return immediately
            Log::err(Format::format("Could not allocate player {} for peer {}",
                                    players[i].character,
                                    players[i].username.get()));
            m_fightScene.clearPlayers();
            m_playersData.clear();
            return false;
        }

        Log::info(Format::format("  - {} ({}){}",
                                 players[i].username.get(),
                                 players[i].character,
                                 peerRole.hasControlOver(i) ? " *" : ""));

        m_playersData.push_back(ClientPlayerData((*playerControl), players[i]));
        usernames.push_back(players[i].username);
    }

    m_currentRole = peerRole;

    m_fightInterface->setPlayers(m_fightScene.getPlayers(),
                                 (*m_textureManager));
    m_fightInterface->replaceNames(usernames);
    return true;
}

void ClientFightController::applyStatus(const Timer::TimePoint &clientTime,
                            const Timer::duration_uint64_us &leapDuration,
                            const std::vector<PlayerStatus> &players,
                            const std::vector<PhysicsObjectStatus> &platforms,
                            const std::vector<DamageStatus> &damages,
                            const EtherStatus &weapons)
{
    if (players.size() != m_playersData.size())
        return; //Skipping until an updated game state is received

    //Notify the matching player controller of the updated status
    for (std::size_t i = 0; i < m_playersData.size(); i++)
        m_playersData[i].control->playerStatusReceived(players[i]);

    //Apply the updated arena status
    m_fightScene.applyPlatformStatus(platforms);

    //Apply the updated weapons status
    m_fightScene.applyWeaponEtherStatus(weapons);

    //Apply the updated damage status
    m_fightScene.applyDamageStatus(damages, clientTime);

    //Leap forward in fight scene time to match the client clock
    //and avoid network jitter hiccups
    m_fightScene.update(std::chrono::duration_cast<Timer::duration_float_ms>
                        (leapDuration).count());
}

void ClientFightController::keyEvent(const SDL_Event &event)
{
    m_fightScene.processKeyEvent(event);
    m_fightScene.updateKeyState(event);
}

void ClientFightController::update(float frameTime)
{
    m_fightScene.update(frameTime);
}

void ClientFightController::draw(float frameTime, const Vector &viewSize)
{
    m_fightScene.lookCamera(viewSize, frameTime);
    m_fightScene.draw();
}

const Box &ClientFightController::getCameraView() const
{
    return m_fightScene.getCameraView();
}

bool ClientFightController::getPeerData(std::size_t playerIx,
                                        PeerData &dst) const
{
    if (playerIx < m_playersData.size())
    {
        dst = m_playersData[playerIx].peer;
        return true;
    }

    return false;
}

std::vector<const Player*> ClientFightController::getPlayers() const
{
    return m_fightScene.getPlayers();
}

bool ClientFightController::playersHaveChanged(
    const std::vector<PeerData> &players) const
{
    if (m_playersData.size() != players.size())
        return true;

    for (std::size_t i = 0; i < players.size(); i++)
    {
        if (players[i] != m_playersData[i].peer)
            return true;
    }

    return false;
}

void ClientFightController::getActiveKeys(int &deviceID, PlayerKeys &keys) const
{
    constexpr std::size_t playerInputId = 1;
    deviceID = m_configuration->getPlayerDevice(playerInputId);

    if (deviceID == DEVICE_KEYBOARD) //Keyboard
        m_configuration->getPlayerKeys(playerInputId, keys);
    else
    {
        m_configuration->getPlayerJoystickKeys(playerInputId,
            m_joystickManager->getJoystickData(deviceID).name, keys);
    }
}
