/*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 "ServerConfiguration.hpp"

#include "Configuration/ConfWriter.hpp"
#include "Configuration/ConfReader.hpp"

/// WelcomeEvent::MaxWelcomeLength
#include "Network/Serialization/ServerEvent.hpp"

#include "Arena/Arena.hpp"
#include "Tools/Filesystem.hpp"
#include "Tools/Log.hpp"
#include "Tools/Format.hpp"
#include <set>

namespace
{
    namespace ArenaCfg
    {
        /// Parse a list of \p arenas of the form "arena1[,arena2...]"
        std::vector<std::string> parse(const std::string &arenas)
        {
            std::set<std::string> uniqueArenas;

            std::size_t startPosition = 0;
            std::size_t endPosition = arenas.find(',', 0);

            while (startPosition != std::string::npos)
            {
                const std::string &arena = (endPosition != std::string::npos) ?
                    arenas.substr(startPosition, endPosition - startPosition) :
                    arenas.substr(startPosition);

                if (ArenaTools::validateName(ArenaTools::fetchNames(), arena))
                    uniqueArenas.insert(arena);
                else
                    Log::err(Format::format("Unknown arena: {}", arena));

                startPosition = (endPosition == std::string::npos) ?
                    std::string::npos : endPosition + 1;
                endPosition = arenas.find(',', startPosition);
            }

            return uniqueArenas.empty() ? ArenaTools::fetchNames() :
                std::vector<std::string>(uniqueArenas.begin(),
                                         uniqueArenas.end());
        }

        /// Serialize a vector of arenas to the form "arena1[,arena2...]"
        std::string serialize(const std::vector<std::string> &arenas)
        {
            std::string value;
            for (const std::string &arena : arenas)
                value += ',' + arena;

            return value.empty() ? value : value.substr(1); //Remove initial ','
        }
    }

    namespace ModeCfg
    {
        const std::string defaultMode("default");
        const std::string versusMode("versus");

        bool isValid(const std::string &modeValue)
        {
            return (modeValue == defaultMode || modeValue == versusMode);
        }

        std::string getValue(FightRules::Mode mode)
        {
            if (mode == FightRules::Mode::Versus)
                return versusMode;

            return defaultMode;
        }

        FightRules::Mode getMode(const std::string &modeValue)
        {
            if (modeValue == versusMode)
                return FightRules::Mode::Versus;

             return FightRules::Mode::Default;
        }
    }

    namespace Labels
    {
        const std::string port("port");
        const std::string arenas("arenas");
        const std::string matchmaking("matchmaking");
        const std::string mode("mode");
        const std::string roundsToWin("rounds_to_win");
        const std::string playerCount("player_count");
        const std::string spectatorLimit("spectator_limit");
        const std::string inactivityLobby("inactivity_timeout_lobby");
        const std::string inactivityInGame("inactivity_timeout_ingame");
        const std::string welcome("welcome_message");
    }
}

ServerConfiguration::ServerConfiguration() : m_port(16026),
m_matchmaking(MatchmakingStrategy::Type::Random),
m_arenas(ArenaTools::fetchNames()), m_spectatorLimit(16),
m_inactivityLobby(600), m_inactivityInGame(60),
m_welcome("Welcome and have fun!")
{

}

bool ServerConfiguration::load(const std::string &path)
{
    ConfReader cfgReader;
    if (cfgReader.load(path))
    {
        cfgReader.getItem(Labels::port, m_port);

        //Read arena
        std::string arenas;
        if (cfgReader.getItem(Labels::arenas, arenas))
            m_arenas = ArenaCfg::parse(arenas);

        //Read matchmaking strategy
        std::string strategy;
        if (cfgReader.getItem(Labels::matchmaking, strategy))
        {
            if (!MatchmakingStrategy::isValid(strategy))
                Log::err(Format::format("Unknown matchmaking strategy: {}",
                                        strategy));
            else
                m_matchmaking = MatchmakingStrategy::getStrategy(strategy);
        }

        //Read fight rules
        std::string mode = ModeCfg::defaultMode;
        std::uint8_t roundsToWin = 2;
        std::uint8_t playerCount = 2;

        if (cfgReader.getItem(Labels::mode, mode) && !ModeCfg::isValid(mode))
            Log::err(Format::format("Unknown fight mode: {}", mode));

        if (cfgReader.getItem(Labels::roundsToWin, roundsToWin) &&
            roundsToWin == 0)
        {
            Log::err(Labels::roundsToWin + " cannot be 0");
            roundsToWin = 2;
        }

        if (cfgReader.getItem(Labels::playerCount, playerCount))
        {
            if (playerCount < 2)
            {
                Log::err(Labels::playerCount + " cannot be less than 2");
                playerCount = 2;
            }
            else if (mode == ModeCfg::versusMode && playerCount != 2)
            {
                Log::err(Labels::playerCount +
                         " reverted to 2 for versus mode");
                playerCount = 2;
            }
        }

        m_fightRules.setMode(ModeCfg::getMode(mode));
        m_fightRules.setRoundsToWin(roundsToWin);
        m_fightRules.setPlayerCount(playerCount);

        //Read spectator limit
        std::size_t spectatorLimit;
        if (cfgReader.getItem(Labels::spectatorLimit, spectatorLimit))
        {
            constexpr std::size_t enetPeerLimit = 4095;
            //Ensure that the peer limit is always below enetPeerLimit
            //(playerCount + spectatorLimit + 1 (to allow full server kicking))
            if (spectatorLimit <= enetPeerLimit - playerCount - 1)
                m_spectatorLimit = spectatorLimit;
            else
                Log::err(Format::format(
                    "{} cannot exceed {} (player_count + "
                    "{} < 4095)", Labels::spectatorLimit,
                    enetPeerLimit - playerCount,
                    Labels::spectatorLimit));
        }

        //Read inactivity timeouts
        std::uint32_t timeoutLobby;
        if (cfgReader.getItem(Labels::inactivityLobby, timeoutLobby))
            m_inactivityLobby = std::chrono::seconds(timeoutLobby);

        std::uint32_t timeoutInGame;
        if (cfgReader.getItem(Labels::inactivityInGame, timeoutInGame))
            m_inactivityInGame = std::chrono::seconds(timeoutInGame);

        std::string welcome;
        if (cfgReader.getItem(Labels::welcome, welcome))
        {
            if (welcome.size() > 255)
                Log::err("Welcome message is too long (limit: 255 bytes)");
            else
            {
                m_welcome = Utf8::utf8_string(welcome);
                if (m_welcome.length() > WelcomeEvent::MaxWelcomeLength)
                {
                    m_welcome.clear_after(WelcomeEvent::MaxWelcomeLength);
                    Log::err(Format::format("Truncated welcome message "
                                            "(limit: {} characters)",
                                            WelcomeEvent::MaxWelcomeLength));
                }
            }
        }

        return true;
    }

    return false;
}

bool ServerConfiguration::save(const std::string &path)
{
    ConfWriter cfgWriter;
    cfgWriter.addItem(Labels::port, m_port);

    cfgWriter.addItem(Labels::matchmaking,
        MatchmakingStrategy::getValue(m_matchmaking));

    cfgWriter.addItem(Labels::mode, ModeCfg::getValue(m_fightRules.getMode()));
    cfgWriter.addItem(Labels::roundsToWin, m_fightRules.getRoundsToWin());
    cfgWriter.addItem(Labels::playerCount, m_fightRules.getPlayerCount());
    cfgWriter.addItem(Labels::arenas, ArenaCfg::serialize(m_arenas));
    cfgWriter.addItem(Labels::spectatorLimit, m_spectatorLimit);
    cfgWriter.addItem(Labels::inactivityLobby, m_inactivityLobby.count());
    cfgWriter.addItem(Labels::inactivityInGame, m_inactivityInGame.count());
    cfgWriter.addItem(Labels::welcome, m_welcome.get());

    return cfgWriter.writeFile(path);
}

void ServerConfiguration::setPort(std::uint16_t port)
{
    m_port = port;
}

std::uint16_t ServerConfiguration::getPort() const
{
    return m_port;
}

MatchmakingStrategy::Type ServerConfiguration::getMatchmakingStrategy() const
{
    return m_matchmaking;
}

const FightRules &ServerConfiguration::getFightRules() const
{
    return m_fightRules;
}

const std::vector<std::string> &ServerConfiguration::getArenas() const
{
    return m_arenas;
}

std::size_t ServerConfiguration::getSpectatorLimit() const
{
    return m_spectatorLimit;
}

std::chrono::seconds ServerConfiguration::getInactivityTimeoutLobby() const
{
    return m_inactivityLobby;
}

std::chrono::seconds ServerConfiguration::getInactivityTimeoutInGame() const
{
    return m_inactivityInGame;
}

const Utf8::utf8_string &ServerConfiguration::getWelcomeMessage() const
{
    return m_welcome;
}
