/*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 "ClientGame.hpp"
#include "Engines/Resources/JoystickManager.hpp"
#include "Configuration/Configuration.hpp"
#include "Network/Serialization/GameMetadata.hpp"
#include "Network/Message/ServerHandshakeMessage.hpp"
#include "Network/Message/GameStateMessage.hpp"
#include "Network/Message/PeerPingMessage.hpp"

#include "Network/Message/ClientHandshakeMessage.hpp"
#include "Network/Message/PlayerActionMessage.hpp"
#include "Network/Message/ServerEventMessage.hpp"
#include "Engines/Sound/PlaylistPlayer.hpp"
#include "Tools/Input/KeyboardKey.hpp"
#include "Tools/ScaledPixel.hpp"
#include "Tools/Log.hpp"
#include "Tools/Format.hpp"
#include "Tools/BuildValues.hpp" ///< Generated at build time

ClientGame::ClientGame(ClientHost &host,
                       JoystickManager &joystickManager,
                       const Configuration &configuration,
                       const Vector &viewSize) :
ClientHost::MessageReceiver(), PlayerActionSender(),
m_host(&host), m_configuration(&configuration),
m_ambientPlayer(configuration.outputAmbientVolume()),

m_fightController((*this), joystickManager, m_textureManager,
    m_ambientPlayer, m_soundEngine, m_fightInterface,
    ClientFightScene::DisplayOptions(configuration.getCameraMode(),
                       configuration.getEnableCameraShake(),
                       false /**< Disable hitbox showing in networked mode */),
    configuration),

m_lastUpdate(0),
m_appliedUpdate(false), m_announcedFight(false),
m_hasInitialGameState(false), m_showPeerInfo(false),

m_pauseDialog(m_fontManager.getDisplay(ScaledPixel{viewSize.x}(54)),
              m_fontManager.getDisplay(ScaledPixel{viewSize.x}(24)),
              m_textureManager, m_soundEngine, viewSize.x),

m_nameTags(viewSize, m_fontManager),
m_eventLogger(m_fontManager, viewSize.x),
m_fpsCounter(!configuration.getShowFps() ? nullptr :
             &m_fontManager.getDefault(ScaledPixel{viewSize.x}(18))),

m_fightInterface(m_fontManager.getDefault(ScaledPixel{viewSize.x}(22)),
                 m_fontManager.getDefault(ScaledPixel{viewSize.x}(14)),
                 m_fontManager.getDefault(ScaledPixel{viewSize.x}(10)),
                 viewSize.x),

m_roundAnnouncer(viewSize),
m_roundResultInfo(viewSize),
m_interfaceCamera(Box(0.f, 0.f, viewSize.x, viewSize.y)),
m_viewSize(viewSize),
m_requestedCharacter("Takino")
{
    const ScaledPixel sp(viewSize.x);

    m_soundEngine.loadSoundEffects("audio/sfx/sfx.xml");
    m_soundEngine.setManager(m_soundBufferManager);
    m_soundEngine.setVolume(m_configuration->outputSoundVolume());

    m_pauseDialog.setPosition((viewSize.x - m_pauseDialog.getWidth()) / 2.f,
                              (viewSize.y - m_pauseDialog.getHeight()) * .3f);

    m_eventLogger.setPosition(sp(20.f), sp(110.f));
    m_fpsCounter.setPosition(viewSize.x - sp(70.f), sp(5.f));

    m_roundAnnouncer.setSoundEngine(m_soundEngine);
    m_roundAnnouncer.setSkin(m_fontManager.getDisplay(sp(106)),
                             Color(255, 255, 255), Color(50, 93, 151));

    m_roundResultInfo.setResources(m_fontManager.getDefault(sp(96)),
                                   m_textureManager);
}

void ClientGame::initialize(const std::string &characterName)
{
    m_requestedCharacter = characterName;
}

void ClientGame::messageReceivedCallback(const ENetPacket &packet,
                                         Channel channel)
{
    Message::Buffer packetBuffer(packet.data, packet.dataLength);

    if (channel == Channel::Handshake)
    {
        ServerHandshakeMessage message;
        if (message.fromBuffer(packetBuffer))
            processHandshake(message);
        else
            processUnintelligible();
    }
    else if (channel == Channel::GameState)
    {
        GameStateMessage gameState;
        if (gameState.fromBuffer(packetBuffer))
            processGameState(gameState);
        else
            processUnintelligible();
    }
    else if (channel == Channel::ServerEvent)
    {
        ServerEventMessage eventMessage;
        if (eventMessage.fromBuffer(packetBuffer))
        {
            const std::string eventLog = eventMessage.getServerEvent().getLog();
            Log::info("Server event: " + eventLog);
            m_eventLogger.push(eventLog);
        }
        else
            processUnintelligible();
    }
    else if (channel == Channel::Ping)
    {
        PeerPingMessage pings;
        if (pings.fromBuffer(packetBuffer))
            processPeerPing(pings);
        else
            processUnintelligible();
    }
}

void ClientGame::reset()
{
    m_fightController.uninitialize();
    m_clock.reset();
    m_lastUpdate = Timer::duration_uint64_us(0);

    m_pauseDialog.setActive(false);
    m_nameTags.setPlayers({});
    m_eventLogger.clear();
    m_peerInfoLayer.reset();
    m_roundAnnouncer.clear();
    m_roundResultInfo.deactivate();
    m_fpsCounter.reset();

    //Change song to signal the reset of the game
    if (m_hasInitialGameState)
        PlaylistPlayer::nextSong();

    m_appliedUpdate = false;
    m_announcedFight = false;
    m_hasInitialGameState = false;
    m_showPeerInfo = false;
}

void ClientGame::processHandshake(const ServerHandshakeMessage &message)
{
    Log::info(Format::format("Server version: {}",
                             message.getServerVersion().text()));

    if (!message.getServerVersion().compatibleWith(BuildValues::VERSION))
    {
        m_host->disconnect(KickReason::VersionMismatch);
        return;
    }

    m_host->send(ClientHandshakeMessage(
                    PeerData(m_configuration->getNetworkUsername(),
                             m_requestedCharacter)),
                 Channel::Handshake);
}

void ClientGame::processGameMetadata(const GameMetadata &game,
                                     bool initialUpdate)
{
    //Change song to signal having successfully joined the game
    if (initialUpdate)
    {
        PlaylistPlayer::nextSong();
        m_fightController.initialize();
    }

    //Initialize arena and fight mode
    m_fightController.setFightMode(game.getFightRules().getMode());
    if (!m_fightController.setArena(game.getArena()))
    {
        m_host->disconnect(KickReason::ClientNoArena);
        return;
    }

    //Set players
    if (!m_fightController.setPlayers(game.getPlayers(),
                                      game.getPeerRole()))
    {
        m_host->disconnect(KickReason::ClientNoCharacter);
        return;
    }

    m_nameTags.setPlayers(TaggedPlayer::make(game.getPlayers(),
                                             m_fightController.getPlayers()));

    m_peerInfoLayer.setPeers(game.getPlayers(), game.getSpectators(),
                             m_viewSize, m_textureManager, m_fontManager);
    m_peerInfoLayer.setPosition(
        (m_viewSize.x - m_peerInfoLayer.getWidth()) / 2.f,
        (m_viewSize.y - m_peerInfoLayer.getHeight()) / 4.f);

    //Update fight state
    if (game.inServerLobby())
    {
        //Reset in-game interface elements
        m_roundResultInfo.deactivate();
        m_fightInterface.resetPlayerResults();
        m_announcedFight = false;
    }
    else //A game is ongoing
    {
        const FightState &fightState = game.getFightState();

        if (fightState.getPhase() == FightState::Phase::Ready)
        {
            Log::info(Format::format("Server: Round {} is about to start...",
                static_cast<unsigned int>(fightState.getRoundId()) + 1));
            m_announcedFight = false;
            m_roundResultInfo.deactivate();
            m_roundAnnouncer.announceRound(fightState.getRoundId(),
                game.getFightRules(), fightState.getWinsPerPlayer());
        }
        else if (fightState.getPhase() == FightState::Phase::Fighting &&
                 !m_announcedFight)
        {
            Log::info("Server: Round started!");
            m_announcedFight = true;
            m_roundAnnouncer.announceFight();
        }
        else if (fightState.getPhase() == FightState::Phase::Break ||
                 fightState.getPhase() == FightState::Phase::Finished)
        {
            if (fightState.getPhase() == FightState::Phase::Break)
                Log::info("Server: Round over!");
            else Log::info("Server: Game over!");

            m_announcedFight = false;
            RoundResult resultInfo = fightState.getLastRoundResult();
            if (resultInfo.drawGame)
            {
                Log::info("  Result: Draw game");
                m_roundResultInfo.activateDraw();
            }
            else
            {
                PeerData winnerInfo;
                if (m_fightController.getPeerData(resultInfo.winnerIx,
                                                  winnerInfo))
                {
                    Log::info(Format::format(
                        "  Winner: {} ({}) ({}/{} rounds to win)",
                        winnerInfo.username.get(), winnerInfo.character,
                        +fightState.getWinsPerPlayer()[resultInfo.winnerIx],
                        +game.getFightRules().getRoundsToWin()));

                    m_roundResultInfo.activate(
                        winnerInfo.username,
                        winnerInfo.character,
                            //resultInfo.winnerIx < getWinsPerPlayer().size()
                            //is guaranteed by FightState
                        fightState.getWinsPerPlayer()[resultInfo.winnerIx],
                        game.getFightRules().getRoundsToWin());
                }
            }

            m_roundResultInfo.positionToCenter();
        }

        m_fightInterface.updatePlayerResults(
            game.getFightRules().getRoundsToWin(),
            fightState.getWinsPerPlayer());
    }
}

void ClientGame::processGameState(const GameStateMessage &message)
{
    const Timer::TimePoint clientTime = Timer::getTime();
    const Timer::duration_uint64_us clientTicks = m_clock.getTicks(clientTime);
    const Timer::duration_uint64_us &serverTicks = message.getClockTicks();

    const std::chrono::microseconds clockDelta =
        std::chrono::duration_cast<std::chrono::microseconds>(clientTicks) -
        std::chrono::duration_cast<std::chrono::microseconds>(serverTicks);

    Timer::duration_uint64_us leapForward(0);
    if (!m_hasInitialGameState || serverTicks > clientTicks ||
        clockDelta > std::chrono::seconds(1))
        m_clock.resetToElapsed(serverTicks); //Reset client to server clock
    else leapForward = clientTicks - serverTicks;

    if (const GameMetadata *metadata = message.getGameMetadata())
        processGameMetadata((*metadata), !m_hasInitialGameState);

    m_fightController.applyStatus(clientTime,
                                  leapForward,
                                  message.getPlayerStatus(),
                                  message.getPlatformStatus(),
                                  message.getDamageStatus(),
                                  message.getEtherStatus());
    m_lastUpdate = clientTicks;
    m_appliedUpdate = true;

    if (!m_hasInitialGameState)
    {
        m_fpsCounter.reset();
        m_hasInitialGameState = true;
    }
}

void ClientGame::processPeerPing(const PeerPingMessage &message)
{
    m_peerInfoLayer.updatePings(message.getPings());
}

void ClientGame::processUnintelligible()
{
    Log::err("Could not interpret server command");
    m_host->disconnect(KickReason::UnintelligibleByClient);
}

void ClientGame::sendPlayerAction(PlayerAction::Index actionIx)
{
    m_host->send(PlayerActionMessage(actionIx), Channel::PlayerAction);
}

void ClientGame::mouseMove(const Vector &viewCoords)
{
    m_pauseDialog.mouseMove(viewCoords);
}

void ClientGame::mouseRelease(const Vector &viewCoords)
{
    if (m_pauseDialog.mouseRelease(viewCoords) && m_pauseDialog.chosen())
    {
        const ClientPauseTab::Result result = m_pauseDialog.result();

        if (result == ClientPauseTab::Result::Disconnect)
            m_host->disconnect(KickReason::ClientDecision);
        else
            m_pauseDialog.setActive(false);
    }
}

void ClientGame::keyEvent(const SDL_Event &event)
{
    //Pause dialog toggle
    if (KeyboardKey::layoutDependent(SDLK_ESCAPE).keyPressed(event))
        m_pauseDialog.setActive(!m_pauseDialog.active());

    //Fight input
    m_fightController.keyEvent(event);

    //Peer info layer toggle
    const KeyboardKey peerInfoKey = KeyboardKey::layoutDependent(SDLK_TAB);

    if (peerInfoKey.keyPressed(event))
        m_showPeerInfo = true;
    else if (peerInfoKey.keyReleased(event))
        m_showPeerInfo = false;
}

void ClientGame::update()
{
    //If an update was just applied from server, do not predict on that frame
    if (!m_appliedUpdate)
    {
        const Timer::duration_uint64_us clientTicks =
            m_clock.getTicks(Timer::getTime());

        if (m_lastUpdate.count() == 0)
            m_lastUpdate = clientTicks;

        m_fightController.update(
            std::chrono::duration_cast<Timer::duration_float_ms>(
                clientTicks - m_lastUpdate).count());

        m_lastUpdate = clientTicks;
    }
    else m_appliedUpdate = false;

    m_nameTags.update();
    m_eventLogger.update();
    m_fightInterface.update();
    m_soundEngine.update();

    if (m_hasInitialGameState)
        m_fpsCounter.tick();
}

void ClientGame::draw(float frameTime)
{
    m_fightController.draw(frameTime, m_viewSize);
    m_nameTags.setCameraView(m_fightController.getCameraView());

    //Draw the interface
    m_interfaceCamera.look(m_viewSize.x, m_viewSize.y);
    m_nameTags.draw();
    m_eventLogger.draw();
    m_fpsCounter.draw();
    m_fightInterface.draw();
    m_roundResultInfo.draw();
    m_roundAnnouncer.draw();

    if (m_showPeerInfo && !m_pauseDialog.active())
        m_peerInfoLayer.draw();

    m_pauseDialog.draw();
}
