/*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 "PeerList.hpp"
#include "Matchmaking.hpp"

#include "Structs/Character.hpp"
#include <algorithm>
#include <cassert>

namespace
{
    /// Functor to find a given peer in m_players or m_spectators
    struct FindPeer
    {
        explicit FindPeer(std::uint16_t peer) : peerID(peer) {}
        bool operator() (const ConnectedPeer &peer) const
        {
            return peer.peerID == peerID;
        }

        std::uint16_t peerID;
    };

    std::vector<PeerData> extractPeerData(
        const std::vector<ConnectedPeer> &peers)
    {
        std::vector<PeerData> peerData;
        peerData.reserve(peers.size());

        for (const ConnectedPeer &peer : peers)
            peerData.push_back(peer.peerData);

        return peerData;
    }
}

PeerList::PeerList(std::uint8_t playerLimit, std::size_t spectatorLimit,
                   const std::vector<std::string> &validCharacters,
                   PeerListCallbackReceiver &receiver) :
m_validCharacters(validCharacters),
m_playerLimit(playerLimit > 1 ? playerLimit : 2),
m_spectatorLimit(spectatorLimit), m_receiver(&receiver)
{

}

bool PeerList::addPeer(std::uint16_t peerID)
{
    if (m_unknownPeers.size() + m_players.size() + m_spectators.size() >=
        m_playerLimit + m_spectatorLimit)
        return false;

    m_unknownPeers.emplace(peerID, Timer::getTime());
    return true;
}

void PeerList::removePeer(std::uint16_t peerID)
{
    if (m_unknownPeers.erase(peerID) == 1) //A peer was found and removed
        return;

    //Attempt to erase from players
    for (std::size_t i = 0; i < m_players.size(); i++)
    {
        if (FindPeer(peerID)(m_players[i]))
        {
            const PeerData peerData = std::move(m_players[i].peerData);
            m_players.erase(std::next(m_players.begin(),
                static_cast<std::vector<ConnectedPeer>::difference_type>(i)));

            m_receiver->playerRemoved(i);
            m_receiver->knownPeerLeft(peerID, peerData, false);
            m_receiver->knownPeersUpdated();
            return;
        }
    }

    //Attempt to erase from spectators
    std::vector<ConnectedPeer>::iterator it_spectator = std::find_if(
        m_spectators.begin(), m_spectators.end(), FindPeer(peerID));

    if (it_spectator != m_spectators.end())
    {
        const PeerData peerData = std::move(it_spectator->peerData);
        m_spectators.erase(it_spectator);

        m_receiver->knownPeerLeft(peerID, peerData, true);
        m_receiver->knownPeersUpdated();
    }
}

bool PeerList::reassignPeers(const Matchmaking &matchmaking)
{
    /// Flag written in place of a peer id to indicate that the associated peer
    /// should be removed. Safe to use as peerID < 4096 is guaranteed by enet
    constexpr std::uint16_t REMOVE_FLAG = 65535;

    const MatchmakingAssignment assignment =
        matchmaking.reassign(m_playerLimit,
                             m_players.size(), m_spectators.size());

    if (!assignment.validate(m_playerLimit,
                             m_players.size(), m_spectators.size()))
        return false; //Guarantees that past this point the indices are valid

    //Make temporary copies of the added players and spectators pending the move
    std::vector<ConnectedPeer> addedPlayers, addedSpectators;

    for (std::size_t playerIx : assignment.becomesSpectator)
    {
        addedSpectators.push_back(std::move(m_players[playerIx]));
        m_players[playerIx].peerID = REMOVE_FLAG; //Flag for later removal
    }

    for (std::size_t spectatorIx : assignment.becomesPlayer)
    {
        addedPlayers.push_back(std::move(m_spectators[spectatorIx]));
        m_spectators[spectatorIx].peerID = REMOVE_FLAG; //Flag for later removal
    }

    //Remove moved players/spectators
    std::vector<ConnectedPeer>::iterator it = m_players.begin();
    while (it != m_players.end())
    {
        if (it->peerID == REMOVE_FLAG)
        {
            const std::size_t playerIx = static_cast<std::size_t>(
                std::distance(m_players.begin(), it));

            it = m_players.erase(it);
            m_receiver->playerRemoved(playerIx);
        }
        else
            ++it;
    }

    std::vector<ConnectedPeer>::iterator it_spectators = m_spectators.begin();
    while (it_spectators != m_spectators.end())
    {
        if (it_spectators->peerID == REMOVE_FLAG)
            it_spectators = m_spectators.erase(it_spectators);
        else
            ++it_spectators;
    }

    //Adding moved players/spectators back in their updated slot
    for (ConnectedPeer &addedPlayer : addedPlayers)
    {
        m_players.push_back(std::move(addedPlayer));
        m_receiver->playerAdded(m_players.back().peerData);
    }

    for (ConnectedPeer &addedSpectator : addedSpectators)
        m_spectators.push_back(std::move(addedSpectator));

    m_receiver->knownPeersUpdated();
    return true;
}

PeerList::Introduction PeerList::introducePeer(std::uint16_t peerID,
                                               const PeerData &peerData,
                                               bool inLobby)
{
    if (m_unknownPeers.count(peerID) == 0)
        return Introduction::Unexpected;
    else if (!Character::validateName(m_validCharacters, peerData.character))
        return Introduction::CharacterUnknown;

    //Add default username if none was provided
    const PeerData addedPeer(peerData.username.empty() ? "Unnamed player"
                                                       : peerData.username,
                             peerData.character);

    //Only add the peer as a player if not in game and slots are available
    if (inLobby && m_players.size() < m_playerLimit)
    {
        m_players.emplace_back(peerID, addedPeer);
        m_unknownPeers.erase(peerID);

        m_receiver->knownPeerJoined(peerID, addedPeer, false);
        m_receiver->playerAdded(addedPeer);
        m_receiver->knownPeersUpdated();

        return Introduction::Success;
    }
    else if (m_spectators.size() < m_spectatorLimit)
    {
        m_spectators.emplace_back(peerID, addedPeer);
        m_unknownPeers.erase(peerID);

        m_receiver->knownPeerJoined(peerID, addedPeer, true);
        m_receiver->knownPeersUpdated();

        return Introduction::Success;
    }

    return Introduction::ServerIsFull;
}

std::vector<std::uint16_t> PeerList::notIntroducedFor(
    const std::chrono::seconds &duration) const
{
    const Timer::TimePoint now = Timer::getTime();
    std::vector<std::uint16_t> notIntroduced;

    std::map<std::uint16_t, Timer::TimePoint>::const_iterator it;
    for (it = m_unknownPeers.begin(); it != m_unknownPeers.end(); ++it)
    {
        if (now - it->second > duration)
            notIntroduced.push_back(it->first);
    }

    return notIntroduced;
}

bool PeerList::getRole(std::uint16_t peerID, PeerRole &dstRole) const
{
    for (std::size_t i = 0; i < m_players.size(); i++)
    {
        if (FindPeer(peerID)(m_players[i]))
        {
            dstRole = PeerRole(i);
            return true;
        }
    }

    if (std::find_if(m_spectators.begin(), m_spectators.end(),
                      FindPeer(peerID)) != m_spectators.end())
    {
        dstRole = PeerRole(); //spectator
        return true;
    }

    return false;
}

bool PeerList::getPlayerData(std::uint16_t peerID, PeerData &dstData) const
{
    std::vector<ConnectedPeer>::const_iterator it =
        std::find_if(m_players.begin(), m_players.end(), FindPeer(peerID));

    if (it != m_players.end())
    {
        dstData = it->peerData;
        return true;
    }

    return false;
}

const std::vector<ConnectedPeer> &PeerList::getPlayers() const
{
    return m_players;
}

const std::vector<ConnectedPeer> &PeerList::getSpectators() const
{
    return m_spectators;
}

std::vector<PeerData> PeerList::getPlayersData() const
{
    return extractPeerData(m_players);
}

std::vector<PeerData> PeerList::getSpectatorsData() const
{
    return extractPeerData(m_spectators);
}

bool PeerList::playersReady() const
{
    return m_players.size() == m_playerLimit;
}

bool PeerList::spectatorsAvailable() const
{
    return !playersReady() && !m_spectators.empty();
}
