/**
 * Author: dkanus
 * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
 * License: GPL
 * Copyright 2023 Anton Tarasenko
 *------------------------------------------------------------------------------
 * This file is part of Acedia.
 *
 * Acedia 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, version 3 of the License, or
 * (at your option) any later version.
 *
 * Acedia 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 Acedia.  If not, see <https://www.gnu.org/licenses/>.
 */
class VotingModel extends AcediaObject
    dependsOn(MathApi);

//! This class counts votes according to the configured voting policies.
//!
//! Its main purpose is to separate the voting logic from the voting interface,
//! making the implementation simpler and the logic easier to test.
//!
//! # Usage
//!
//! 1. Allocate an instance of the [`VotingModel`] class.
//! 2. Call [`Start()`] to start voting with required policies.
//! 3. Use [`UpdatePotentialVoters()`] to specify which users are allowed to vote.
//!     You can change this set at any time before the voting has concluded.
//!     The method used to recount the votes will depend on the policies set
//!     during the previous [`Initialize()`] call.
//! 4. Use [`CastVote()`] to add a vote from a user.
//! 5. After calling either [`UpdatePotentialVoters()`] or [`CastVote()`],
//!     check [`GetStatus()`] to see if the voting has concluded.
//!     Once voting has concluded, the result cannot be changed, so you can
//!     release the reference to the [`VotingModel`] object.
//! 6. Alternatively, before voting has concluded naturally, you can use
//!     [`ForceEnding()`] method to immediately end voting with result being
//!     determined by provided [`ForceEndingType`] argument.

/// Current state of voting for this model.
enum VotingModelStatus {
    /// Voting hasn't even started, waiting for [`Initialize()`] call
    VPM_Uninitialized,
    /// Voting is currently in progress
    VPM_InProgress,
    /// Voting has ended with majority for its success
    VPM_Success,
    /// Voting has ended with majority for its failure
    VPM_Failure
};

/// A result of user trying to make a vote
enum VotingResult {
    /// Vote accepted
    VFR_Success,
    /// Voting is not allowed for this particular user
    VFR_NotAllowed,
    /// User already made a vote and changing votes isn't allowed
    VFR_CannotChangeVote,
    /// User has already voted the same way
    VFR_AlreadyVoted,
    /// Voting has already ended and doesn't accept new votes
    VFR_VotingEnded
};

/// Checks how given user has voted
enum PlayerVoteStatus {
    /// User hasn't voted yet
    PVS_NoVote,
    /// User voted for the change
    PVS_VoteFor,
    /// User voted against the change
    PVS_VoteAgainst
};

/// Types of possible outcomes when forcing a voting to end
enum ForceEndingType {
    /// Result will be decided by the votes that already have been cast
    FET_CurrentLeader,
    /// Voting will end in success
    FET_Success,
    /// Voting will end in failure
    FET_Failure
};

var private VotingModelStatus status;

/// Specifies whether draw would count as a victory for corresponding voting.
var private bool policyDrawWinsVoting;

var private array<UserID> votesFor, votesAgainst;
/// Votes of people that voted before, but then were forbidden to vote
/// (either because they have left or simply lost the right to vote)
var private array<UserID> storedVotesFor, storedVotesAgainst;
/// List of users currently allowed to vote
var private array<UserID> allowedVoters;

protected function Constructor() {
    status = VPM_Uninitialized;
}

protected function Finalizer() {
    _.memory.FreeMany(allowedVoters);
    _.memory.FreeMany(votesFor);
    _.memory.FreeMany(votesAgainst);
    _.memory.FreeMany(storedVotesFor);
    _.memory.FreeMany(storedVotesAgainst);
    allowedVoters.length = 0;
    votesFor.length = 0;
    votesAgainst.length = 0;
    storedVotesFor.length = 0;
    storedVotesAgainst.length = 0;
}

/// Initializes voting by providing it with a set of policies to follow.
///
/// The only available policy is configuring whether draw means victory or loss
/// in voting.
///
/// Can only be called once, after that will do nothing.
public final function Start(bool drawWinsVoting) {
    if (status != VPM_Uninitialized) {
        return;
    }
    policyDrawWinsVoting = drawWinsVoting;
    status = VPM_InProgress;
}

/// Returns whether voting has already concluded.
///
/// This method should be checked after both [`CastVote()`] and
/// [`UpdatePotentialVoters()`] to check whether either of them was enough to
/// conclude the voting result.
public final function bool HasEnded() {
    return (status != VPM_Uninitialized && status != VPM_InProgress);
}

/// Returns current status of voting.
///
/// This method should be checked after both [`CastVote()`] and
/// [`UpdatePotentialVoters()`] to check whether either of them was enough to
/// conclude the voting result.
public final function VotingModelStatus GetStatus() {
    return status;
}

/// Changes set of [`User`]s that are allowed to vote.
///
/// Generally you want to provide this method with a list of current players,
/// optionally filtered from spectators, users not in priviledged group or any
/// other relevant criteria.
public final function UpdatePotentialVoters(array<UserID> potentialVoters) {
    local int i;

    _.memory.FreeMany(allowedVoters);
    allowedVoters.length = 0;
    for (i = 0; i < potentialVoters.length; i += 1) {
        potentialVoters[i].NewRef();
        allowedVoters[i] = potentialVoters[i];
    }
    RestoreStoredVoters(potentialVoters);
    FilterCurrentVoters(potentialVoters);
    RecountVotes();
}

/// Attempts to add a vote  from specified user.
///
/// Adding a vote can fail if [`voter`] isn't allowed to vote.
public final function VotingResult CastVote(UserID voter, bool voteForSuccess) {
    local bool votesSameWay;
    local PlayerVoteStatus currentVote;

    if (status != VPM_InProgress) {
        return VFR_VotingEnded;
    }
    if (!IsVotingAllowedFor(voter)) {
        return VFR_NotAllowed;
    }
    currentVote = GetVote(voter);
    votesSameWay = (voteForSuccess && currentVote == PVS_VoteFor)
        || (!voteForSuccess && currentVote == PVS_VoteAgainst);
    if (votesSameWay) {
        return VFR_AlreadyVoted;
    }
    EraseVote(voter);
    voter.NewRef();
    if (voteForSuccess) {
        votesFor[votesFor.length] = voter;
    } else {
        votesAgainst[votesAgainst.length] = voter;
    }
    RecountVotes();
    return VFR_Success;
}

/// Checks if the provided user is allowed to vote based on the current list of
/// potential voters.
///
/// The right to vote is decided solely by the list of potential voters set
/// using [`UpdatePotentialVoters()`].
///
/// Returns true if the user is allowed to vote, false otherwise.
public final function bool IsVotingAllowedFor(UserID voter) {
    local int i;

    if (voter == none) {
        return false;
    }
    for (i = 0; i < allowedVoters.length; i += 1) {
        if (voter.IsEqual(allowedVoters[i])) {
            return true;
        }
    }
    return false;
}

/// Returns the current vote status for the given voter.
///
/// If the voter was previously allowed to vote, voted, and had their right to
/// vote revoked, their vote won't count.
public final function PlayerVoteStatus GetVote(UserID voter) {
    local int i;

    if (voter == none) {
        return PVS_NoVote;
    }
    for (i = 0; i < votesFor.length; i += 1) {
        if (voter.IsEqual(votesFor[i])) {
            return PVS_VoteFor;
        }
    }
    for (i = 0; i < votesAgainst.length; i += 1) {
        if (voter.IsEqual(votesAgainst[i])) {
            return PVS_VoteAgainst;
        }
    }
    return PVS_NoVote;
}

/// Returns amount of current valid votes for the success of this voting.
public final function int GetVotesFor() {
    return votesFor.length;
}

/// Returns amount of current valid votes against the success of this voting.
public final function int GetVotesAgainst() {
    return votesAgainst.length;
}

/// Returns amount of users that are currently allowed to vote in this voting.
public final function int GetTotalPossibleVotes() {
    return allowedVoters.length;
}

/// Checks whether, if stopped now, voting will win.
public final function bool IsVotingWinning() {
    if (status == VPM_Success)              return true;
    if (status == VPM_Failure)              return false;
    if (GetVotesFor() > GetVotesAgainst())  return true;
    if (GetVotesFor() < GetVotesAgainst())  return false;

    return policyDrawWinsVoting;
}

/// Forcibly ends the voting, deciding winner depending on the argument.
///
/// Only does anything if voting is currently in progress
/// (in `VPM_InProgress` state).
///
/// By default decides result by the votes that already have been cast.
///
/// Returns `true` only if voting was actually ended with this call.
public final function bool ForceEnding(optional ForceEndingType type) {
    if (status != VPM_InProgress) {
        return false;
    }
    switch (type) {
        case FET_CurrentLeader:
            if (IsVotingWinning()) {
                status = VPM_Success;
            } else {
                status = VPM_Failure;
            }
            break;
        case FET_Success:
            status = VPM_Success;
            break;
        case FET_Failure:
        default:
            status = VPM_Failure;
            break;
    }
    return true;
}

private final function RecountVotes() {
    local MathApi.IntegerDivisionResult divisionResult;
    local int winningScore, losingScore;
    local int totalPossibleVotes;

    if (status != VPM_InProgress) {
        return;
    }
    totalPossibleVotes = GetTotalPossibleVotes();
    divisionResult = _.math.IntegerDivision(totalPossibleVotes, 2);
    if (divisionResult.remainder == 1) {
        // For odd amount of voters winning is simply majority
        winningScore = divisionResult.quotient + 1;
    } else {
        if (policyDrawWinsVoting) {
            // For even amount of voters, exactly half is enough if draw means victory
            winningScore = divisionResult.quotient;
        } else {
            // Otherwise - majority
            winningScore = divisionResult.quotient + 1;
        }
    }
    // The `winningScore` represents the number of votes required for a mean victory.
    // If the number of votes against the mean is less than or equal to
    // `totalPossibleVotes - winningScore`, then victory is still possible.
    // However, if there is even one additional vote against, then victory is no longer achievable 
    // and a loss is inevitable.
    losingScore = (totalPossibleVotes - winningScore) + 1;
    // `totalPossibleVotes < losingScore + winningScore`, so only one of these inequalities
    // can be satisfied at a time
    if (GetVotesFor() >= winningScore) {
        status = VPM_Success;
    } else if (GetVotesAgainst() >= losingScore) {
        status = VPM_Failure;
    }
}

private final function EraseVote(UserID voter) {
    local int i;

    if (voter == none) {
        return;
    }
    while (i < votesFor.length) {
        if (voter.IsEqual(votesFor[i])) {
            _.memory.Free(votesFor[i]);
            votesFor.Remove(i, 1);
        } else {
            i += 1;
        }
    }
    i = 0;
    while (i < votesAgainst.length) {
        if (voter.IsEqual(votesAgainst[i])) {
            _.memory.Free(votesAgainst[i]);
            votesAgainst.Remove(i, 1);
        } else {
            i += 1;
        }
    }
}

private final function RestoreStoredVoters(array<UserID> potentialVoters) {
    local int i, j;
    local bool isPotentialVoter;

    while (i < storedVotesFor.length) {
        isPotentialVoter = false;
        for (j = 0; j < potentialVoters.length; j += 1) {
            if (storedVotesFor[i].IsEqual(potentialVoters[j])) {
                isPotentialVoter = true;
                break;
            }
        }
        if (isPotentialVoter) {
            votesFor[votesFor.length] = storedVotesFor[i];
            storedVotesFor.Remove(i, 1);
        } else {
            i += 1;
        }
    }
    i = 0;
    while (i < storedVotesAgainst.length) {
        isPotentialVoter = false;
        for (j = 0; j < potentialVoters.length; j += 1) {
            if (storedVotesAgainst[i].IsEqual(potentialVoters[j])) {
                isPotentialVoter = true;
                break;
            }
        }
        if (isPotentialVoter) {
            votesAgainst[votesAgainst.length] = storedVotesAgainst[i];
            storedVotesAgainst.Remove(i, 1);
        } else {
            i += 1;
        }
    }
}

private final function FilterCurrentVoters(array<UserID> potentialVoters) {
    local int i, j;
    local bool isPotentialVoter;

    while (i < votesFor.length) {
        isPotentialVoter = false;
        for (j = 0; j < potentialVoters.length; j += 1) {
            if (votesFor[i].IsEqual(potentialVoters[j])) {
                isPotentialVoter = true;
                break;
            }
        }
        if (isPotentialVoter) {
            i += 1;
        } else {
            storedVotesFor[storedVotesFor.length] = votesFor[i];
            votesFor.Remove(i, 1);
        }
    }
    i = 0;
    while (i < votesAgainst.length) {
        isPotentialVoter = false;
        for (j = 0; j < potentialVoters.length; j += 1) {
            if (votesAgainst[i].IsEqual(potentialVoters[j])) {
                isPotentialVoter = true;
                break;
            }
        }
        if (isPotentialVoter) {
            i += 1;
        } else {
            storedVotesAgainst[storedVotesAgainst.length] = votesAgainst[i];
            votesAgainst.Remove(i, 1);
        }
    }
}

defaultproperties {
}