/*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 "Server.hpp"
#include "Configuration/ServerConfiguration.hpp"
#include "Network/Serialization/GameMetadata.hpp"
#include "Network/Message/ClientHandshakeMessage.hpp"
#include "Network/Message/PlayerActionMessage.hpp"
#include "Player/Player.hpp"
#include "Matchmaking.hpp"

#include "Network/Host/Address.hpp"
#include "Network/Message/ServerHandshakeMessage.hpp"
#include "Network/Message/ServerEventMessage.hpp"
#include "Network/Message/GameStateMessage.hpp"
#include "Network/Message/PeerPingMessage.hpp"
#include "Structs/Character.hpp"
#include "Tools/Log.hpp"
#include "Tools/Format.hpp"
#include "Tools/I18n.hpp"
#include "Tools/BuildValues.hpp" ///< Generated at build time
#include <cstddef>

#ifdef HNMSERVER_DRAW
    #include "Graphics/Window.hpp"
    #include "Graphics/Display.hpp"
    #include <SDL2/SDL_image.h>
#endif

namespace
{
    constexpr std::chrono::seconds PING_BROADCAST_PERIOD(1);
    constexpr std::chrono::seconds ACTIVITY_CHECK_PERIOD(5);

    /// Broadcast \p message to all known \p peers, that is,
    /// only peers that have successfully completed handshake
    void broadcast(ServerHost &host, const Message &message,
                   const PeerList &peers, Channel channel)
    {
        for (const ConnectedPeer &player : peers.getPlayers())
            host.send(message, player.peerID, channel);

        for (const ConnectedPeer &spectator : peers.getSpectators())
            host.send(message, spectator.peerID, channel);
    }

    void logPlayers(const std::vector<ConnectedPeer> &players)
    {
        for (const ConnectedPeer &peer : players)
            Log::info(Format::format("Matchmaking> - {}: {} ({})", peer.peerID,
                                     peer.peerData.username.get(),
                                     peer.peerData.character));
    }
}

//////////////////
///ServerStatus///
//////////////////
ServerStatus::ServerStatus() : m_phase(ServerStatus::Phase::InLobby) {}

void ServerStatus::goToLobby()
{
    Log::info("Gamestate> Back to lobby");
    m_phase = Phase::InLobby;
}

void ServerStatus::queueGame()
{
    Log::info("Gamestate> Queued game, starting in 10 seconds...");
    m_phase = Phase::AwaitingGame;
    m_gameStart = Timer::getTime() + std::chrono::seconds(10);
}

void ServerStatus::startGame()
{
    Log::info("Gamestate> Started game");
    m_phase = Phase::InGame;
}

void ServerStatus::finishGame()
{
    Log::info("Gamestate> Finished game, transition in 5 seconds...");
    m_phase = Phase::GameFinished;
    m_gameTransition = Timer::getTime() + std::chrono::seconds(5);
}

bool ServerStatus::shouldStartGame() const
{
    return (m_phase == Phase::AwaitingGame &&
            m_gameStart <= Timer::getTime());
}

bool ServerStatus::shouldTransition() const
{
    return (m_phase == Phase::GameFinished &&
            m_gameTransition <= Timer::getTime());
}

bool ServerStatus::isInLobby() const
{
    return m_phase == Phase::InLobby;
}

bool ServerStatus::isAwaitingGame() const
{
    return (m_phase == Phase::AwaitingGame);
}

bool ServerStatus::isInGame() const
{
    return (m_phase == Phase::InGame);
}

bool ServerStatus::hasGameFinished() const
{
    return (m_phase == Phase::GameFinished);
}

////////////
///Server///
////////////
Server::Server(ServerHost &host, const ServerConfiguration &serverConf) :
ServerHostCallbackReceiver(), RefereeListener(),
m_arena(serverConf.getArenas()),
m_fightController(serverConf.getFightRules().getMode(), m_arena.getArena()),
m_peers(serverConf.getFightRules().getPlayerCount(),
        serverConf.getSpectatorLimit(),
        Character::fetchNames(), (*this)),
m_metadataUpdated(false),
m_referee(serverConf.getFightRules()),
m_matchmaking(Matchmaking::allocate(serverConf.getMatchmakingStrategy())),
m_host(&host), m_serverConf(&serverConf)
{
    m_referee.instruct(this);
}

Server::~Server()
{
    delete m_matchmaking;
}

void Server::update(float frameTime)
{
    //Update the lobby/game status
    if (m_status.shouldStartGame()) //Start a game after the waiting period
    {
        m_status.startGame();
        m_referee.ready();
    }
    else if (m_status.shouldTransition()) //Transition after a game ended
    {
        //Reset referee rules (useful if a player disconnected
        //during the last 3+ players game)
        m_referee.changeRules(m_serverConf->getFightRules());

        //Perform matchmaking according to the server strategy
        const RoundResult &result =
            m_referee.getFightState().getLastRoundResult();
        if (!result.drawGame) //Draw game should be impossible after a game
            m_matchmaking->setLastWinner(result.winnerIx);

        m_peers.reassignPeers((*m_matchmaking));
        if (m_peers.getPlayers().empty())
            Log::info("Matchmaking> No players left");
        else
        {
            Log::info("Matchmaking> Performed matchmaking. Playing:");
            logPlayers(m_peers.getPlayers());
        }

        const MatchmakingEvent matchmakingEvent(
            m_serverConf->getMatchmakingStrategy());
        broadcast((*m_host), ServerEventMessage(matchmakingEvent),
                  m_peers, Channel::ServerEvent);

        //Rotate the arena
        m_fightController.setArena(m_arena.choose());

        //Queue a game if enough players are connected or go to lobby
        if (m_peers.playersReady()) queueGame();
        else m_status.goToLobby();

        m_metadataUpdated = true;
    }

    m_fightController.update(frameTime);
    if (m_status.isInGame())
        m_fightController.showReferee(m_referee);
    else if (m_status.isInLobby())
        m_fightController.revivePlayers();

    //Broadcast the game state, optionally the updated game metadata
    broadcastGameState();

    //Periodically broadcast the ping
    broadcastPings();

    //Periodically check for peer activity to disconnect inactive peers
    checkActivity();
}

void Server::peerConnectedCallback(std::uint16_t peerID)
{
    const Address peerAddress = m_host->getAddress(peerID);
    Log::info(Format::format("Peer> Peer {} connected [{}:{}]", peerID,
                             peerAddress.getHostIP(), peerAddress.getPort()));

    if (m_peers.addPeer(peerID))
        m_host->send(ServerHandshakeMessage(), peerID, Channel::Handshake);
    else
        m_host->kick(peerID, KickReason::ServerIsFull);
}

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

    if (channel == Channel::Handshake)
    {
        ClientHandshakeMessage message;
        if (message.fromBuffer(packetBuffer))
            processHandshake(peerID, message);
        else
            processUnintelligible(peerID);
    }
    else if (channel == Channel::PlayerAction)
    {
        PlayerActionMessage message;
        if (message.fromBuffer(packetBuffer))
            processPlayerAction(peerID, message);
        else
            processUnintelligible(peerID);
    }
    else
        processIllegalAction(peerID);
}

void Server::peerDisconnectedCallback(std::uint16_t peerID)
{
    m_peers.removePeer(peerID);

    const Address peerAddress = m_host->getAddress(peerID);
    Log::info(Format::format("Peer> Peer {} disconnected [{}:{}]", peerID,
                             peerAddress.getHostIP(), peerAddress.getPort()));

    if (m_status.isInLobby() && m_peers.spectatorsAvailable())
    {
        m_peers.reassignPeers(StackMatchmaking());
        Log::info("Matchmaking> Spawned a spectator into play. Playing:");
        logPlayers(m_peers.getPlayers());

        const MatchmakingEvent event(MatchmakingStrategy::Type::Stack);
        broadcast((*m_host), ServerEventMessage(event), m_peers,
                  Channel::ServerEvent);
    }
}

void Server::playerAdded(const PeerData &peerData)
{
    // ** WARNING: This method may have been called from PeerList::reassignPeers
    // ** as players and spectators are being swapped in-place
    // ** Do not call any methods on m_peers that could invalidate
    // ** its player or spectator indices

    m_fightController.addPlayer(peerData);

    //Check if a game can be queued (if enough players)
    if (m_peers.playersReady())
        queueGame();
}

void Server::playerRemoved(std::size_t playerIx)
{
    // ** WARNING: This method may have been called from PeerList::reassignPeers
    // ** as players and spectators are being swapped in-place
    // ** Do not call any methods on m_peers that could invalidate
    // ** its player or spectator indices

    m_fightController.removePlayer(playerIx);

    if (m_status.isAwaitingGame())
        m_status.goToLobby(); //Go back to lobby immediately
    else if (m_status.isInGame())
        //Keep going to finish the game with one less player
        m_referee.playerWasErased(playerIx);
}

void Server::knownPeerJoined(std::uint16_t peerID, const PeerData &peerData,
                             bool spectator)
{
    Log::info(Format::format("Peer> Peer {} has joined as {} ({}){}",
              peerID, peerData.username.get(), peerData.character,
              spectator ? " [spectator]" : ""));

    //Notify all the peers about the connection
    const PeerConnectedEvent connectedEvent(peerData.username);
    broadcast((*m_host), ServerEventMessage(connectedEvent),
              m_peers, Channel::ServerEvent);

    //Welcome the new peer and send them the rules
    m_host->send(ServerEventMessage(RulesEvent(m_serverConf->getFightRules())),
                 peerID, Channel::ServerEvent);

    m_host->send(ServerEventMessage(
                    WelcomeEvent(m_serverConf->getWelcomeMessage())),
                 peerID, Channel::ServerEvent);
}

void Server::knownPeerLeft(std::uint16_t peerID, const PeerData &peerData,
                           bool spectator)
{
    Log::info(Format::format("Peer> Peer {} [{} ({})] has left{}",
              peerID, peerData.username.get(), peerData.character,
              spectator ? " [spectator]" : ""));

    //Notify all the peers about the disconnection
    PeerDisconnectedEvent event(peerData.username);

    broadcast((*m_host), ServerEventMessage(event),
              m_peers, Channel::ServerEvent);
}

void Server::knownPeersUpdated()
{
    m_activity.updatePeerList(m_peers, -ACTIVITY_CHECK_PERIOD);
    m_metadataUpdated = true;
}

/// Referee instructions
void Server::readyRound(unsigned int roundId)
{
    Log::info(Format::format("Referee> Round {} is about to start...",
                             roundId + 1));
    m_fightController.readyRound();
    m_metadataUpdated = true;
}

void Server::startRound()
{
    Log::info("Referee> Round started!");
    m_fightController.startRound();
    m_metadataUpdated = true;
}

void Server::roundOver(std::size_t winnerIx, const Player &winner,
                       std::uint8_t wonRounds)
{
    Log::info("Referee> Round over!");
    const PeerData &winnerData = m_peers.getPlayers().at(winnerIx).peerData;

    Log::info(Format::format("Referee> Winner: {} ({}) ({}/{} rounds to win)",
                             winnerData.username.get(), winner.getName(),
                             +wonRounds,
                             +m_referee.getRules().getRoundsToWin()));
    m_metadataUpdated = true;
}

void Server::roundOverDraw()
{
    Log::info("Referee> Round over!\n  Referee> Result: Draw game");
    m_metadataUpdated = true;
}

void Server::gameOver()
{
    Log::info("Referee> Game over!");
    m_status.finishGame();

    //Make sure the players can move
    //(the fight scene is not stuck in ready state)
    m_fightController.startRound();
    m_metadataUpdated = true;
}

void Server::processHandshake(std::uint16_t peerID,
                              const ClientHandshakeMessage &message)
{
    //Check version compatibility
    Log::info(Format::format("Peer> Peer {} runs version {}", peerID,
                             message.getClientVersion().text()));
    if (!message.getClientVersion().compatibleWith(BuildValues::VERSION))
    {
        m_host->kick(peerID, KickReason::VersionMismatch);
        return;
    }

    //Introduce the peer
    PeerList::Introduction result = m_peers.introducePeer(peerID,
                                                          message.getPeerData(),
                                                          m_status.isInLobby());
    if (result == PeerList::Introduction::Unexpected)
    {
        Log::err(Format::format("Peer {} sent an unexpected handshake",
                                peerID));
        m_host->kick(peerID, KickReason::IllegalCommand);
    }
    else if (result == PeerList::Introduction::CharacterUnknown)
    {
        Log::err(Format::format("Peer {} requested the unknown character {}",
                                peerID, message.getPeerData().character));
        m_host->kick(peerID, KickReason::ServerNoCharacter);
    }
    else if (result == PeerList::Introduction::ServerIsFull)
        m_host->kick(peerID, KickReason::ServerIsFull);
    else if (result != PeerList::Introduction::Success)
        m_host->kick(peerID, KickReason::Unknown);
}

void Server::processPlayerAction(std::uint16_t peerID,
                                 const PlayerActionMessage &message)
{
    PeerRole peerRole;
    if (!m_peers.getRole(peerID, peerRole))
    {
        Log::err(Format::format("Peer {} requested an action but it has "
                                "not completed handshake", peerID));
        processIllegalAction(peerID);
    }
    else if (!peerRole.spectator && (!m_status.isInGame() ||
             m_referee.getFightState().getPhase() != FightState::Phase::Ready))
    {
        m_activity.reportActivity(peerID, -ACTIVITY_CHECK_PERIOD);
        m_fightController.applyAction(peerRole.playerIx,
                                      message.getPlayerAction());
    }
}

void Server::processIllegalAction(std::uint16_t peerID)
{
    m_host->kick(peerID, KickReason::IllegalCommand);
}

void Server::processUnintelligible(std::uint16_t peerID)
{
    m_host->kick(peerID, KickReason::UnintelligibleByServer);
}

GameMetadata Server::getGameMetadata() const
{
    GameMetadata metadata(m_arena.getArena(),
                          m_referee.getRules(),
                          m_peers.getPlayersData(),
                          m_peers.getSpectatorsData());

    //Not in lobby: add the game fight state
    if (m_status.isInGame() || m_status.hasGameFinished())
        metadata.setFightState(m_referee.getFightState());

    return metadata;
}

void Server::broadcastGameState()
{
    const Timer::TimePoint serverTime = Timer::getTime();
    const Timer::duration_uint64_us ticks = m_clock.getTicks(serverTime);

    GameStateMessage message(ticks,
                             m_fightController.getPlayerStatus(),
                             m_fightController.getPlatformStatus(),
                             m_fightController.getDamageStatus(serverTime),
                             m_fightController.getWeaponStatus());

    if (!m_metadataUpdated)
    {
        broadcast((*m_host), message, m_peers, Channel::GameState);
        return;
    }

    //Append game metadata
    message.setGameMetadata(getGameMetadata());

    //Send to spectators
    for (const ConnectedPeer &spectator : m_peers.getSpectators())
        m_host->send(message, spectator.peerID, Channel::GameState, true);

    //Send to players
    const std::vector<ConnectedPeer> &players = m_peers.getPlayers();
    for (std::size_t playerIx = 0; playerIx < players.size(); playerIx++)
    {
        message.setMetadataPeerRole(PeerRole(playerIx));
        m_host->send(message, players[playerIx].peerID,
                     Channel::GameState, true);
    }

    m_metadataUpdated = false;
}

void Server::broadcastPings()
{
    if (Timer::getTime() - m_lastSentPing > PING_BROADCAST_PERIOD)
    {
        //Retrieve pings
        std::vector<std::uint32_t> pings;
        pings.reserve(m_peers.getPlayers().size());

        for (const ConnectedPeer &peer : m_peers.getPlayers())
            pings.push_back(m_host->getPing(peer.peerID));

        //Transmit the playing peers ping to the clients
        broadcast((*m_host), PeerPingMessage(pings), m_peers, Channel::Ping);

        //Update the last sent ping time
        m_lastSentPing = Timer::getTime();
    }
}

std::set<std::uint16_t> Server::getKoPeers() const
{
    std::set<std::uint16_t> koPeers;

    const std::vector<ConnectedPeer> &peers = m_peers.getPlayers();
    const std::vector<PlayerStatus> players =
        m_fightController.getPlayerStatus();

    std::size_t playerCount = (peers.size() < players.size()) ?
                               peers.size() : players.size();
    for (std::size_t playerIx = 0; playerIx < playerCount; playerIx++)
    {
        if (players[playerIx].getState() == StateIndex::KO)
            koPeers.insert(peers[playerIx].peerID);
    }

    return koPeers;
}

void Server::checkActivity()
{
    if (Timer::getTime() - m_lastActivityCheck > ACTIVITY_CHECK_PERIOD)
    {
        //Disconnect inactive peers that have not sent a handshake
        std::vector<std::uint16_t> unintroducedPeers =
            m_peers.notIntroducedFor(std::chrono::seconds(5));

        for (std::uint16_t peerID : unintroducedPeers)
        {
            Log::info(Format::format(
                "Peer> Kicking {}: Not introduced for > 5s", peerID));
            m_host->kick(peerID, KickReason::Inactivity);
        }

        //Disconnect inactive peers that are currently playing
        m_activity.update(ACTIVITY_CHECK_PERIOD, getKoPeers());

        const std::chrono::seconds timeout = m_status.isInGame() ?
                    m_serverConf->getInactivityTimeoutInGame() :
                    m_serverConf->getInactivityTimeoutLobby();

        std::vector<std::uint16_t> inactivePeers =
            m_activity.getInactivePeers(timeout);

        for (std::uint16_t peerID : inactivePeers)
        {
            //Kick the peer for inactivity
            m_host->kick(peerID, KickReason::Inactivity);

            //Broadcast a server event about the inactivity kick
            PeerData playerData;
            if (m_peers.getPlayerData(peerID, playerData))
            {
                const PeerInactiveEvent inactiveEvent(playerData.username);
                broadcast((*m_host), ServerEventMessage(inactiveEvent),
                          m_peers, Channel::ServerEvent);
            }
        }

        //Update the last activity check time
        m_lastActivityCheck = Timer::getTime();
    }
}

void Server::queueGame()
{
    if (m_status.isAwaitingGame())
        return; //Game already queued

    m_status.queueGame();
    broadcast((*m_host), ServerEventMessage(GameScheduledEvent()),
              m_peers, Channel::ServerEvent);
}

bool Server::run(const ServerConfiguration &serverConf)
{
    #ifdef HNMSERVER_DRAW
        // Initialization: Video
        if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK) < 0)
            return false;

        IMG_Init(IMG_INIT_PNG);

        Window window;
        if (!window.create("Hikou no mizu (server)",
                           Display::DisplayLayout(),
                           Display::VSyncMode::Off))
        {
            IMG_Quit();
            SDL_Quit();
            return false;
        }

        window.setFrameRate(0);
        window.initView(1024.f, 576.f);

        SDL_Event event;
    #endif

    //Initialize the enet host
    if (enet_initialize() != 0)
    {
        Log::err("Could not initialize ENet");
        return false;
    }

    ENetVersion version = enet_linked_version();
    Log::info(Format::format("ENet {}.{}.{}",
                             ENET_VERSION_GET_MAJOR(version),
                             ENET_VERSION_GET_MINOR(version),
                             ENET_VERSION_GET_PATCH(version)));

    ServerHost host;

    //The peer limit is the number of players and spectators, plus one so
    //that one extra peer can briefly join and receive a notification
    //that the server is full
    if (!host.init(Address(ENET_HOST_ANY, serverConf.getPort()),
         serverConf.getFightRules().getPlayerCount() +
         serverConf.getSpectatorLimit() + 1))
    {
        Log::err("Could not initialize the server");
        enet_deinitialize();
        return false;
    }

    Log::info(Format::format("Listening on port {}...",
                             serverConf.getPort()));

    //Load i18n entries (kick reason and server event messages)
    I18n::loadEntries("en");

    Server server(host, serverConf);
    host.setHostCallbackReceiver(&server);

    //Initialize clock
    constexpr float tickDelay = 20.f;
    Timer time;
    while (1)
    {
        //Manage time
        float frameTime = time.getElapsed();
        time.reset();

        #ifdef HNMSERVER_DRAW
            //Manage exit
            while (window.pollEvent(event))
            {
                if (event.type == SDL_QUIT)
                {
                    host.destroy();
                    enet_deinitialize();
                    window.destroy();
                    IMG_Quit();
                    SDL_Quit();
                    return true;
                }
            }
        #endif

        //Listen to/handle incoming messages
        host.listen();

        #ifdef HNMSERVER_DRAW
            window.clear();
        #endif

        //Update the current state
        server.update(frameTime);

        #ifdef HNMSERVER_DRAW
            window.flush();
        #endif

        //Send any updates to connected peers
        host.flush();

        time.waitUntilElapsed(Timer::duration_float_ms(tickDelay));
    }

    return true;
}
