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

#include "Tools/Log.hpp"
#include <algorithm>
#include <numeric>
#include <cassert>

namespace
{
    /// Checks if \p data is unique (no duplicate elements)
    /// and bounded (no elements over \p bound)
    bool isUniqueBounded(const std::vector<std::size_t> &data,
                         std::size_t bound)
    {
        for (std::size_t i = 0; i < data.size(); i++)
        {
            if (data[i] >= bound)
                return false;

            for (std::size_t j = 0; j < i; j++)
            {
                if (data[i] == data[j])
                    return false;
            }
        }

        return true;
    }

    /// Makes a matchmaking assignment based on \p ixs, which:
    ///
    /// - Contains exactly playerCount + spectatorCount unique elements
    /// - Elements e = playerIx < playerCount refer to player indexes
    /// - Elements e = (spectatorIx + playerCount) >= playerCount refer
    ///     to spectator indexes
    /// - Elements of position < playerLimit will become or remain players
    /// - Elements of position >= playerLimit will become or remain spectators
    ///
    /// Example: playerLimit = 3, playerCount = 2, spectatorCount = 2,
    /// ixs = [3,1,2, || 0]:
    /// - The three first elements will become/remain players, i.e.,
    ///     - 3 >= playerCount=2 -> spectator of ix 1,
    ///     - 1 < playerCount=2 -> player of ix 1
    ///     - 2: spectator of ix 0
    /// - The last element, player of ix 0, will become a spectator
    MatchmakingAssignment assignFromVector(const std::vector<std::size_t> &ixs,
                                           std::size_t playerLimit,
                                           std::size_t playerCount)
    {
        MatchmakingAssignment assignment;
        for (std::size_t i = 0; i < ixs.size(); i++)
        {
            //A spectator will become a player
            if (i < playerLimit && ixs[i] >= playerCount)
                assignment.becomesPlayer.push_back(ixs[i] - playerCount);
            //A player will become a spectator
            else if (i >= playerLimit && ixs[i] < playerCount)
                assignment.becomesSpectator.push_back(ixs[i]);
        }

        return assignment;
    }
}

///////////////////////////
///MatchmakingAssignment///
///////////////////////////
bool MatchmakingAssignment::validate(std::size_t playerLimit,
                                     std::size_t playerCount,
                                     std::size_t spectatorCount) const
{
    //Validate the range of player and spectator indices
    if (!isUniqueBounded(becomesPlayer, spectatorCount) ||
        !isUniqueBounded(becomesSpectator, playerCount))
    {
        Log::err("Matchmaking has duplicates or invalid indices");
        return false;
    }

    //Not enough peers to start a game: all spectators must be moved to players
    if (playerCount + spectatorCount < playerLimit &&
        (!becomesSpectator.empty() || becomesPlayer.size() < spectatorCount))
    {
        Log::err("Matchmaking does not move all peers to playing slots");
        return false;
    }
    else if (playerCount + spectatorCount >= playerLimit &&
                becomesPlayer.size() - becomesSpectator.size() !=
                playerLimit - playerCount)
    {
        //Enough peers to start a game:
        //missing playing slots must be taken by spectators
        Log::err("Matchmaking does not move enough peers to playing slots");
        return false;
    }

    return true;
}

/////////////////
///Matchmaking///
/////////////////
Matchmaking *Matchmaking::allocate(MatchmakingStrategy::Type strategy)
{
    if (strategy == MatchmakingStrategy::Type::Stack)
        return new StackMatchmaking();
    else if (strategy == MatchmakingStrategy::Type::Random)
        return new RandomMatchmaking();
    else if (strategy == MatchmakingStrategy::Type::Challenger)
        return new ChallengerMatchmaking();

    Log::err("Requested matchmaking strategy unknown, using stack strategy");
    return new StackMatchmaking();
}

//////////////////////
///StackMatchmaking///
//////////////////////
MatchmakingAssignment StackMatchmaking::reassign(
    std::size_t playerLimit,
    std::size_t playerCount,
    std::size_t spectatorCount) const
{
    //Move as many spectators as available to fill in the missing playing slots
    //Do not change the current players
    std::vector<std::size_t> becomesPlayer(
        (spectatorCount > playerLimit - playerCount) ? playerLimit - playerCount
                                                     : spectatorCount);

    std::iota(becomesPlayer.begin(), becomesPlayer.end(), 0);
    return MatchmakingAssignment({}, std::move(becomesPlayer));
}

///////////////////////
///RandomMatchmaking///
///////////////////////
RandomMatchmaking::RandomMatchmaking() : m_generator(std::random_device{}()) {}

MatchmakingAssignment RandomMatchmaking::reassign(
    std::size_t playerLimit,
    std::size_t playerCount,
    std::size_t spectatorCount) const
{
    //Not enough or exactly enough players to start a game: no spectators
    if (playerCount + spectatorCount <= playerLimit)
        return StackMatchmaking().reassign(playerLimit,
                                           playerCount, spectatorCount);

    //Generate a randomly ordered list of indexes from 0 to the peer count
    std::vector<std::size_t> ixs(playerCount + spectatorCount);
    std::iota(ixs.begin(), ixs.end(), 0);
    std::shuffle(ixs.begin(), ixs.end(), m_generator);

    return assignFromVector(ixs, playerLimit, playerCount);
}

///////////////////////////
///ChallengerMatchmaking///
///////////////////////////
ChallengerMatchmaking::ChallengerMatchmaking() : m_lastWinnerIx(0),
m_generator(std::random_device{}()) {}

void ChallengerMatchmaking::setLastWinner(std::size_t lastWinnerIx)
{
    m_lastWinnerIx = lastWinnerIx;
}

MatchmakingAssignment ChallengerMatchmaking::reassign(
    std::size_t playerLimit,
    std::size_t playerCount,
    std::size_t spectatorCount) const
{
    if (m_lastWinnerIx >= playerCount) //Invalid last winner index
    {
        Log::err("Challenger matchmaking: The last winner index is invalid");
        return RandomMatchmaking().reassign(playerLimit,
                                            playerCount, spectatorCount);
    }
    else if (playerCount + spectatorCount <= playerLimit) //All peers will play
        return StackMatchmaking().reassign(playerLimit,
                                           playerCount, spectatorCount);

    //Shuffle the role of all peers except the winner player
    std::vector<std::size_t> ixs;
    assert(playerCount + spectatorCount > 1);
    ixs.reserve(playerCount + spectatorCount - 1);
    for (std::size_t i = 0; i < playerCount + spectatorCount; i++)
    {
        if (i != m_lastWinnerIx)
            ixs.push_back(i);
    }
    std::shuffle(ixs.begin(), ixs.end(), m_generator);
    ixs.insert(ixs.begin(), m_lastWinnerIx);

    return assignFromVector(ixs, playerLimit, playerCount);
}
