/**
 * 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 Voting extends AcediaObject
    dependsOn(VotingModel)
    dependson(CommandAPI);

//! Class that describes a single voting option.
//!
//! [`Voting`] is created to be heavily integrated with [`Commands_Feature`] and
//! shouldn't be used separately from it.
//! You shouldn't allocate its instances directly unless you're working on
//! the [`Commands_Feature`]'s or related code.
//!
//! Generally, [`Voting`] will only update whenever its methods are called.
//! The only exception is when a time limit was specified, then
//! `TryAnnounceTimer()` will be called every tick, voting emulating countdown.
//!
//! ## Usage
//!
//! This class takes care of the voting process by itself, one only needs to
//! call [`Start()`] method.
//! If you wish to prematurely end voting (e.g. forcing it to end), then call
//! [`ForceEnding()`] method.
//!
//! When implementing your own voting:
//!
//!     1. You normally would override [`Execute()`] method to perform any
//!         actions you want upon the voting success.
//!     2. If you want your voting to take any arguments, you should also
//!         overload [`AddInfo()`].
//!     3. You can also override [`HandleVotingStart()`] method to setup custom
//!         voting's messages inside `currentAnnouncements` or to reject
//!         starting this voting altogether.

/// Describes possible results of [`ForceEnding()`] method.
enum ForceEndingOutcome {
    /// Voting forcing was successful.
    FEO_Success,
    /// User is not allowed to force voting.
    FEO_Forbidden,
    /// No voting to end at this moment.
    FEO_NotApplicable
};

/*******************************************************************************
 * Voting settings that should be specified for child classes.
 ******************************************************************************/

/// During its lifecycle voting outputs several messages to players about its
/// current state.
/// This struct contains all such messages.
struct VotingAnnouncementSet {
    /// Message that is displayed when voting starts, inviting others to vote
    /// (e.g. "Voting to end trader has started")
    var public Text started;
    /// Message that is displayed once voting succeeds
    /// (e.g. "{$TextPositive Voting to end trader was successful}")
    var public Text succeeded;
    /// Message that is displayed once voting succeeds
    /// (e.g. "{$TextNegative Voting to end trader has failed}")
    var public Text failed;
    /// Message that is displayed when voting info is displayed mid-voting.
    /// (e.g. "Voting to end trader currently active.")
    var public Text info;
};
/// Variable that contains current messages that voting will use to communicate
/// its status to the players.
///
/// It is auto-filled with `string` values [`votingStartedLine`],
/// [`votingSucceededLine`], [`votingFailedLine`], [`votingInfoLine`].
/// If you want to change/customize these values based on voting arguments,
/// then override [`HandleVotingStart()`] method and set values inside of this
/// variable directly (they will all be `none` at this point).
var protected VotingAnnouncementSet currentAnnouncements;

/// Preferred name of this voting. Actual name is decided by
/// server owner/mod author.
///
/// Has to satisfy limitations described in the `BaseText::IsValidName()`
var protected const string preferredName;
/// Text that should be displayed when voting starts.
///
/// There isn't any hard limitations, but for the sake of uniformity try to
/// mimic "Voting to end trader has started" line, avoid adding formatting and
/// don't add comma/exclamation mark at the end.
var protected const string votingStartedLine;
/// Text that should be displayed when voting has ended with a success.
///
/// There isn't any hard limitations, but for the sake of uniformity try to
/// mimic "{$TextPositive Voting to end trader was successful}" line, coloring
/// it in a positive color and adding comma/exclamation mark at the end.
var protected const string votingSucceededLine;
/// Text that should be displayed when voting has ended in a failure.
///
/// There isn't any hard limitations, but for the sake of uniformity try to
/// mimic "{$TextNegative Voting to end trader has failed}" line, coloring it in
/// a negative color and adding comma/exclamation mark at the end.
var protected const string votingFailedLine;
/// Text that should be displayed when voting info is displayed mid-voting.
///
/// There isn't any hard limitations, but for the sake of uniformity try to
/// mimic "Voting to end trader is currently active." line, avoid adding
/// formatting and don't add comma/exclamation mark at the end.
var protected const string votingInfoLine;
/// Settings variable that defines a class to be used for this [`Voting`]'s
/// permissions config.
var protected const class<VotingPermissions> permissionsConfigClass;

/*******************************************************************************
 * Variables that describe current state of the voting.
 ******************************************************************************/

/// Underlying voting model that does actual vote calculations.
var private VotingModel model;
/// How much time remains in the voting.
/// Both negative and zero values mean that countdown either ended or wasn't
/// started to begin with.
/// This value can only be decreased inside [`TryAnnounceTimer()`] event method;
/// voting end due to countdown is also expected to be handled there.
var private float remainingVotingTime;
/// Tracks index of the next timing inside [`announcementTimings`] to announce.
var private int nextTimingToAnnounce;
/// Records whether end of the voting announcement was already made.
var private bool endingHandled;
/// Arguments that this voting was called with.
var private HashTable usedArguments;

var private array<Text> policyAllowedToVoteGroups;
var private array<Text> policyAllowedToSeeVotingGroups;
var private array<Text> policyAllowedToForceVoting;
var private bool policySpectatorsCanVote;

/// Fake voters that are only used in debug mode to allow for simpler vote
/// testing.
var private array<UserID> debugVoters;

// Timings at which to announce how much time is left for this voting
var private const array<int> announcementTimings;

/*******************************************************************************
 * Auxiliary variables (`string`s + templates from them) used for producing
 * output to the user.
 ******************************************************************************/

/// Text that serves as a template for announcing current vote counts.
var private const string voteSummaryTemplateString;
/// Text that serves as a template for announcing player making a new vote.
var private const string playerVotedTemplateString, playerVotedAnonymousTemplateString;
var private const string timeRemaningAnnounceTemplateString;
// [`TextTemplate`]s made from the above `string` templates.
var private TextTemplate voteSummaryTemplate, playerVotedTemplate, playerVotedAnonymousTemplate;
var private TextTemplate timeRemaningAnnounceTemplate;
/// Text that is used instead of how to vote hint for players not allowed
/// to vote
var private const string cannotVoteHint;

/*******************************************************************************
 * Signals.
 ******************************************************************************/
var private CommandsAPI_OnVotingEnded_Signal onVotingEndedSignal;

protected function Constructor() {
    nextTimingToAnnounce = 0;
    voteSummaryTemplate = _.text.MakeTemplate_S(voteSummaryTemplateString);
    playerVotedTemplate = _.text.MakeTemplate_S(playerVotedTemplateString);
    playerVotedAnonymousTemplate = _.text.MakeTemplate_S(playerVotedAnonymousTemplateString);
    timeRemaningAnnounceTemplate = _.text.MakeTemplate_S(timeRemaningAnnounceTemplateString);
    onVotingEndedSignal = CommandsAPI_OnVotingEnded_Signal(
        _.memory.Allocate(class'CommandsAPI_OnVotingEnded_Signal'));
}

protected function Finalizer() {
    _.memory.Free(model);
    model = none;
    endingHandled = false;
    policySpectatorsCanVote = false;

    _.memory.Free2(usedArguments, onVotingEndedSignal);
    usedArguments = none;
    onVotingEndedSignal = none;
    _server.unreal.OnTick(self).Disconnect();

    _.memory.Free4(currentAnnouncements.started,
        currentAnnouncements.succeeded,
        currentAnnouncements.failed,
        currentAnnouncements.info);
    currentAnnouncements.started = none;
    currentAnnouncements.succeeded = none;
    currentAnnouncements.failed = none;
    currentAnnouncements.info = none;

    _.memory.Free4(voteSummaryTemplate,
        playerVotedTemplate,
        playerVotedAnonymousTemplate,
        timeRemaningAnnounceTemplate);
    voteSummaryTemplate = none;
    playerVotedTemplate = none;
    playerVotedAnonymousTemplate = none;
    timeRemaningAnnounceTemplate = none;

    _.memory.FreeMany(policyAllowedToVoteGroups);
    _.memory.FreeMany(policyAllowedToSeeVotingGroups);
    _.memory.FreeMany(policyAllowedToForceVoting);
    policyAllowedToVoteGroups.length = 0;
    policyAllowedToSeeVotingGroups.length = 0;
    policyAllowedToForceVoting.length = 0;
}

/// Signal that will be emitted when voting ends.
///
/// # Slot description
///
/// bool <slot>(bool success, HashTable arguments)
///
/// ## Parameters
///
/// * [`success`]: `true` if voting ended successfully and `false` otherwise.
/// * [`arguments`]: Arguments with which voting was called.
public /*signal*/ function CommandsAPI_OnVotingEnded_Slot OnVotingEnded(AcediaObject receiver) {
    return CommandsAPI_OnVotingEnded_Slot(onVotingEndedSignal.NewSlot(receiver));
}

/// Override this to specify arguments for your voting command.
///
/// This method is for adding arguments only.
/// DO NOT call [`CommandDataBuilder::SubCommand()`] or
/// [`CommandDataBuilder::Option()`] methods, otherwise you'll cause unexpected
/// behavior for your mod's users.
public static function AddInfo(CommandDataBuilder builder) {
}

/// Loads permissions config with a given name for the caller [`Voting`] class.
///
/// Permission configs describe allowed usage of the [`Voting`].
/// Basic settings are contained inside [`VotingPermissions`], but votings
/// should derive their own child classes for storing their settings.
///
/// Returns `none` if caller [`Voting`] class didn't specify custom permission
/// settings class or provided name is invalid (according to
/// [`BaseText::IsValidName()`]).
/// Otherwise guaranteed to return a config reference.
public final static function VotingPermissions LoadConfig(BaseText configName) {
    if (configName == none)                     return none;
    if (default.permissionsConfigClass == none) return none;

    default.permissionsConfigClass.static.Initialize();
    // This creates default config if it is missing
    default.permissionsConfigClass.static.NewConfig(configName);
    return VotingPermissions(default.permissionsConfigClass.static
        .GetConfigInstance(configName));
}

/// Returns name of this voting in the lower case.
///
/// If voting class was configured incorrectly (with a `preferredName`
/// that doesn't satisfy limitations, described in `BaseText::IsValidName()`),
/// then this method will return `none`.
public final static function Text GetPreferredName() {
    local Text result;

    result = __().text.FromString(Locs(default.preferredName));
    if (result.IsValidName()) {
        return result;
    }
    __().memory.Free(result);
    return none;
}

/// Forcibly ends the voting, deciding winner depending on the argument.
/// By default decides result by the votes that already have been cast.
///
/// Only does anything if voting is currently in progress
/// (in `VPM_InProgress` state).
public final function ForceEndingOutcome ForceEnding(
    EPlayer instigator,
    VotingModel.ForceEndingType type
) {
    local int i;
    local UserID id;
    local bool canForce;

    if (model == none)      return FEO_NotApplicable;
    if (instigator == none) return FEO_Forbidden;
    id = instigator.GetUserID();
    if (id == none)         return FEO_Forbidden;

    for (i = 0; i < policyAllowedToForceVoting.length; i += 1) {
        if (_.users.IsUserIDInGroup(id, policyAllowedToForceVoting[i])) {
            canForce = true;
            break;
        }
    }
    if (canForce) {
        if (model.ForceEnding(type)) {
            TryEnding(instigator);
            return FEO_Success;
        }
        return FEO_NotApplicable;
    }
    _.memory.Free(id);
    return FEO_Forbidden;
}

/// Starts caller [`Voting`] using policies, loaded from the given config.
///
/// Provided config instance must not be `none`, otherwise method is guaranteed
/// to fail with `SVR_InvalidState`.
/// Method will also fail if voting was already started (even if it already
/// ended), there is no one eligible to vote or [`Voting`] itself has decided to
/// reject being started at this moment, with given arguments.
public final function CommandAPI.StartVotingResult Start(
    VotingPermissions config,
    HashTable arguments
) {
    local bool hasDebugVoters;
    local array<EPlayer> voters;

    if (model != none)  return SVR_InvalidState;
    if (config == none) return SVR_InvalidState;

    // Check whether we even have enough voters
    ReadConfigIntoPolicies(config); // we need to know permission policies
    voters = FindAllVotingPlayers();
    hasDebugVoters = _.environment.IsDebugging()
        && class'ACommandFakers'.static.BorrowDebugVoters().length > 0;
    if (voters.length == 0 && !hasDebugVoters) {
        return SVR_NoVoters;
    }

    // Check if voting even wants to start with these arguments
    if (HandleVotingStart(config, arguments)) {
        // ^ this was supposed to pre-fill `currentAnnouncements` struct if
        // it wanted to change any messages, so now is the good time to fill
        // the rest with defaults/fallback
        FillAnnouncementGaps();
        if (arguments != none) {
            arguments.NewRef();
            usedArguments = arguments;
        }
    } else {
        _.memory.FreeMany(voters);
        return SVR_Rejected;
    }

    // Actually start voting
    model = VotingModel(_.memory.Allocate(class'VotingModel'));
    model.Start(config.drawEqualsSuccess);
    // Inform new voting about fake voters, in case we're debugging
    if (hasDebugVoters) {
        // This method will call also `UpdateVoters()`
        SetDebugVoters(class'ACommandFakers'.static.BorrowDebugVoters());
    } else {
        UpdateVoters(voters);
    }
    SetupCountdownTimer(config);
    AnnounceStart();
    _.memory.FreeMany(voters);
    return SVR_Success;
}

/// Checks if the [`Voting`] process has reached its conclusion.
///
/// Please note that this differs from determining whether voting is currently
//  active. Even voting that hasn't started is not considered concluded.
public final function bool HasEnded() {
    if (model == none) {
        return false;
    }
    return model.HasEnded();
}

/// Retrieves the current voting status for the specified voter.
///
/// If the voter was previously eligible to vote, cast a vote, but later had
/// their voting rights revoked, their vote will not be counted, and this method
/// will return [`PVS_NoVote`].
///
/// In case the voter regains their voting rights while the voting process is
/// still ongoing, their previous vote will be automatically reinstated by
/// the caller [`Voting`].
public final function VotingModel.PlayerVoteStatus GetVote(UserID voter) {
    if (model != none) {
        return model.GetVote(voter);
    }
    return PVS_NoVote;
}

/// Adds specified [`UserID`]s as additional voters in debug mode.
///
/// This method is intended for debugging purposes and only functions when
/// the game is running in debug mode.
public final function SetDebugVoters(array<UserID> newDebugVoters) {
    local int i;
    local array<EPlayer> realVoters;

    if(!_.environment.IsDebugging()) {
        return;
    }
    _.memory.FreeMany(debugVoters);
    debugVoters.length = 0;
    for (i = 0; i < newDebugVoters.length; i += 1) {
        if (newDebugVoters[i] != none) {
            debugVoters[debugVoters.length] = newDebugVoters[i];
            newDebugVoters[i].NewRef();
        }
    }
    realVoters = FindAllVotingPlayers();
    UpdateVoters(realVoters);
    _.memory.FreeMany(realVoters);
    TryEnding();
}

/// Adds a new vote by a given [`UserID`].
///
/// NOTE: this method is intended for use only in debug mode, and is will not do
/// anything otherwise. This method silently adds a vote using the provided
/// [`UserID`], without any prompt or notification of updated voting status.
/// It was added to facilitate testing with fake [`UserID`]s, and is limited
/// to debug mode to prevent misuse and unintended behavior in production code.
public final function VotingModel.VotingResult CastVoteByID(UserID voter, bool voteForSuccess) {
    local array<EPlayer> realVoters;
    local VotingModel.VotingResult result;

    if (model == none)                  return VFR_NotAllowed;
    if (voter == none)                  return VFR_NotAllowed;
    if (!_.environment.IsDebugging())   return VFR_NotAllowed;

    realVoters = FindAllVotingPlayers();
    UpdateVoters(realVoters);
    result = model.CastVote(voter, voteForSuccess);
    if (result == VFR_Success) {
        AnnounceNewVote(none, voteForSuccess);
    }
    TryEnding();
    _.memory.FreeMany(realVoters);
    return result;
}

/// Registers a vote on behalf of the specified player.
///
/// This method updates the voting status for the specified player and may
/// initiate the conclusion of the voting process.
/// After a vote is registered, the updated voting status is broadcast to all
/// players.
public final function VotingModel.VotingResult CastVote(EPlayer voter, bool voteForSuccess) {
    local UserID voterID;
    local array<EPlayer> realVoters;
    local VotingModel.VotingResult result;

    if (model == none)  return VFR_NotAllowed;
    if (voter == none)  return VFR_NotAllowed;

    voterID = voter.GetUserID();
    realVoters = FindAllVotingPlayers();
    UpdateVoters(realVoters);
    result = model.CastVote(voterID, voteForSuccess);
    switch (result) {
        case VFR_Success:
            AnnounceNewVote(voter, voteForSuccess);
            break;
        case VFR_NotAllowed:
            voter
                .BorrowConsole()
                .WriteLine(F("You are {$TextNegative not allowed} to vote right now."));
            break;
        case VFR_CannotChangeVote:
            voter.BorrowConsole().WriteLine(F("Changing vote is {$TextNegative forbidden}."));
            break;
        case VFR_VotingEnded:
            voter.BorrowConsole().WriteLine(F("Voting has already {$TextNegative ended}!"));
            break;
        default:
    }
    TryEnding();
    _.memory.Free(voterID);
    _.memory.FreeMany(realVoters);
    return result;
}

/// Prints information about caller [`Voting`] to the given player.
public final function PrintVotingInfoFor(EPlayer requester) {
    local ConsoleWriter writer;
    local MutableText summaryPart, timeRemaining;

    if (requester == none) {
        return;
    }
    voteSummaryTemplate.Reset();
    voteSummaryTemplate.ArgInt(model.GetVotesFor());
    voteSummaryTemplate.ArgInt(model.GetVotesAgainst());
    summaryPart = voteSummaryTemplate.CollectFormattedM();
    writer = requester.BorrowConsole();
    writer.Write(currentAnnouncements.info);
    writer.Write(P(". "));
    writer.Write(summaryPart);
    writer.WriteLine(P("."));
    if (remainingVotingTime > 0) {
        timeRemaining = _.text.FromIntM(int(Ceil(remainingVotingTime)));
        writer.Write(P("Time remaining: "));
        writer.Write(timeRemaining);
        writer.WriteLine(P(" seconds."));
        _.memory.Free(timeRemaining);
    }
    _.memory.Free(summaryPart);
}

/// Override this to perform necessary logic after voting has succeeded.
protected function Execute(HashTable arguments) {}

/// Override this method to:
///
/// 1. Specify any of the messages inside `currentAnnouncements` to fit passed
///     [`arguments`].
/// 2. Optionally reject starting this voting altogether by returning `false`
///     (returning `true` will allow voting to proceed).
protected function bool HandleVotingStart(VotingPermissions config, HashTable arguments) {
    return true;
}

// Assembles "Say {$TextPositive !yes} or {$TextNegative !no} to vote" hint.
// Replaces "!yes"/"!no" with "!vote yes"/"!vote no" if corresponding aliases
// aren't properly setup.
private final function MutableText MakeHowToVoteHint() {
    local Text resolvedAlias;
    local MutableText result;

    result = P("Say ").MutableCopy();
    resolvedAlias = _.alias.ResolveCommand(P("yes"));
    if (resolvedAlias != none && resolvedAlias.Compare(P("vote.yes"), SCASE_SENSITIVE)) {
        result.Append(P("!yes"), _.text.FormattingFromColor(_.color.TextPositive));
    } else {
        result.Append(P("!vote yes"), _.text.FormattingFromColor(_.color.TextPositive));
    }
    _.memory.Free(resolvedAlias);
    result.Append(P(" or "));
    resolvedAlias = _.alias.ResolveCommand(P("no"));
    if (resolvedAlias != none && resolvedAlias.Compare(P("vote.no"), SCASE_SENSITIVE)) {
        result.Append(P("!no"), _.text.FormattingFromColor(_.color.TextNegative));
    } else {
        result.Append(P("!vote no"), _.text.FormattingFromColor(_.color.TextNegative));
    }
    _.memory.Free(resolvedAlias);
    result.Append(P(" to vote"));
    return result;
}

private final function ReadConfigIntoPolicies(VotingPermissions config) {
    local int i;

    if (config != none) {
        policySpectatorsCanVote = config.allowSpectatorVoting;
        for (i = 0; i < config.allowedToVoteGroup.length; i += 1) {
            policyAllowedToVoteGroups[i] = _.text.FromString(config.allowedToVoteGroup[i]);
        }
        for (i = 0; i < config.allowedToSeeVotesGroup.length; i += 1) {
            policyAllowedToSeeVotingGroups[i] = _.text.FromString(config.allowedToSeeVotesGroup[i]);
        }
        for (i = 0; i < config.allowedToForceGroup.length; i += 1) {
            policyAllowedToForceVoting[i] = _.text.FromString(config.allowedToForceGroup[i]);
        }
    }
}

private final function SetupCountdownTimer(VotingPermissions config) {
    if (config != none && config.votingTime > 0) {
        remainingVotingTime = config.votingTime;
        _server.unreal.OnTick(self).connect = TryAnnounceTimer;
        nextTimingToAnnounce = 0;
        while (nextTimingToAnnounce < announcementTimings.length) {
            if (announcementTimings[nextTimingToAnnounce] <= remainingVotingTime) {
                break;
            }
            nextTimingToAnnounce += 1;
        }
    }
}

private function TryAnnounceTimer(float delta, float dilationCoefficient) {
    local MutableText message;
    local ConsoleWriter writer;

    if (remainingVotingTime <= 0) {
        return;
    }
    remainingVotingTime -= delta / dilationCoefficient;
    if (remainingVotingTime <= 0) {
        model.ForceEnding();
        TryEnding();
        return;
    }
    if (nextTimingToAnnounce >= announcementTimings.length) {
        return;
    }
    if (announcementTimings[nextTimingToAnnounce] > int(remainingVotingTime)) {
        writer = _.console.ForAll();
        timeRemaningAnnounceTemplate.Reset();
        timeRemaningAnnounceTemplate.ArgInt(announcementTimings[nextTimingToAnnounce]);
        message = timeRemaningAnnounceTemplate.CollectFormattedM();
        writer.WriteLine(message);
        _.memory.Free(writer);
        nextTimingToAnnounce += 1;
    }
}

/// Outputs message about new vote being submitted to all relevant voters.
private final function AnnounceNewVote(EPlayer voter, bool voteForSuccess) {
    local int i, j;
    local bool playerAllowedToSee;
    local Text voterName;
    local array<EPlayer> allPlayers;
    local UserID nextID;
    local MutableText playerVotedPart, playerVotedAnonymousPart, summaryPart;

    voteSummaryTemplate.Reset();
    voteSummaryTemplate.ArgInt(model.GetVotesFor());
    voteSummaryTemplate.ArgInt(model.GetVotesAgainst());
    summaryPart = voteSummaryTemplate.CollectFormattedM();

    playerVotedTemplate.Reset();
    playerVotedAnonymousTemplate.Reset();
    if (voter != none) {
        voterName = voter.GetName();
    } else {
        voterName = P("DEBUG:FAKER").Copy();
    }
    playerVotedTemplate.TextArg(P("player_name"), voterName, true);
    _.memory.Free(voterName);
    if (voteForSuccess) {
        playerVotedTemplate.TextArg(P("vote_type"), F("{$TextPositive for}"));
        playerVotedAnonymousTemplate.TextArg(P("vote_type"), F("{$TextPositive for}"));
    } else {
        playerVotedTemplate.TextArg(P("vote_type"), F("{$TextNegative against}"));
        playerVotedAnonymousTemplate.TextArg(P("vote_type"), F("{$TextNegative against}"));
    }
    playerVotedPart = playerVotedTemplate.CollectFormattedM();
    playerVotedAnonymousPart = playerVotedAnonymousTemplate.CollectFormattedM();
    allPlayers = _.players.GetAll();
    for (i = 0; i < allPlayers.length; i += 1) {
        nextID = allPlayers[i].GetUserID();
        playerAllowedToSee = false;
        for (j = 0; j < policyAllowedToSeeVotingGroups.length; j += 1) {
            if (_.users.IsUserIDInGroup(nextID, policyAllowedToSeeVotingGroups[j])) {
                playerAllowedToSee = true;
                break;
            }
        }
        if (playerAllowedToSee) {
            allPlayers[i].BorrowConsole().Write(playerVotedPart);
        } else {
            allPlayers[i].BorrowConsole().Write(playerVotedAnonymousPart);
        }
        allPlayers[i].BorrowConsole().Write(P(". ")).Write(summaryPart).WriteLine(P("."));
        _.memory.Free(nextID);
    }
    _.memory.Free3(playerVotedPart, playerVotedAnonymousPart, summaryPart);
    _.memory.FreeMany(allPlayers);
}

/// Tries to end voting.
///
/// Returns `true` iff this method was called for the first time after
/// the voting concluded.
private final function bool TryEnding(optional EPlayer forcedBy) {
    local Text outcomeMessage;

    if (model == none)  return false;
    if (endingHandled)  return false;
    if (!HasEnded())    return false;

    endingHandled = true;
    if (model.GetStatus() == VPM_Success) {
        outcomeMessage = currentAnnouncements.succeeded;
    } else {
        outcomeMessage = currentAnnouncements.failed;
    }
    onVotingEndedSignal.Emit(model.GetStatus() == VPM_Success, usedArguments);
    AnnounceOutcome(outcomeMessage, forcedBy);
    if (model.GetStatus() == VPM_Success) {
        Execute(usedArguments);
    }
    _server.unreal.OnTick(self).Disconnect();
    return true;
}

private final function FillAnnouncementGaps() {
    if (currentAnnouncements.started == none) {
        currentAnnouncements.started = _.text.FromFormattedString(votingStartedLine);
    }
    if (currentAnnouncements.succeeded == none) {
        currentAnnouncements.succeeded = _.text.FromFormattedString(votingSucceededLine);
    }
    if (currentAnnouncements.failed == none) {
        currentAnnouncements.failed = _.text.FromFormattedString(votingFailedLine);
    }
    if (currentAnnouncements.info == none) {
        currentAnnouncements.info = _.text.FromFormattedString(votingInfoLine);
    }
}

private final function array<EPlayer> FindAllVotingPlayers() {
    local int i, j;
    local bool userAllowedToVote;
    local UserID nextID;
    local array<EPlayer> currentPlayers, voterPlayers;

    currentPlayers = _.players.GetAll();
    for (i = 0; i < currentPlayers.length; i += 1) {
        if (!policySpectatorsCanVote && currentPlayers[i].IsSpectator()) {
            continue;
        }
        nextID = currentPlayers[i].GetUserID();
        userAllowedToVote = false;
        for (j = 0; j < policyAllowedToVoteGroups.length; j += 1) {
            if (_.users.IsUserIDInGroup(nextID, policyAllowedToVoteGroups[j])) {
                userAllowedToVote = true;
                break;
            }
        }
        if (userAllowedToVote) {
            currentPlayers[i].NewRef();
            voterPlayers[voterPlayers.length] = currentPlayers[i];
        }
        _.memory.Free(nextID);
    }
    _.memory.FreeMany(currentPlayers);
    return voterPlayers;
}

/// Updates the inner voting model with current list of players allowed to vote.
/// Also returns said list.
private final function UpdateVoters(array<EPlayer> votingPlayers) {
    local int i;
    local array<UserID> votersIDs;

    if (model == none) {
        return;
    }
    for (i = 0; i < votingPlayers.length; i += 1) {
        votersIDs[votersIDs.length] = votingPlayers[i].GetUserID();
    }
    for (i = 0; i < debugVoters.length; i += 1) {
        debugVoters[i].NewRef();
        votersIDs[votersIDs.length] = debugVoters[i];
    }
    model.UpdatePotentialVoters(votersIDs);
    _.memory.FreeMany(votersIDs);
}

/// Prints given voting outcome message in console and publishes it as
/// a notification.
private final function AnnounceStart() {
    local int i, j;
    local bool playerAllowedToSee;
    local UserID nextID;
    local MutableText howToVoteHint;
    local array<EPlayer> currentPlayers;

    howToVoteHint = MakeHowToVoteHint();
    currentPlayers = _.players.GetAll();
    for (i = 0; i < currentPlayers.length; i += 1) {
        nextID = currentPlayers[i].GetUserID();
        playerAllowedToSee = false;
        for (j = 0; j < policyAllowedToVoteGroups.length; j += 1) {
            if (_.users.IsUserIDInGroup(nextID, policyAllowedToVoteGroups[j])) {
                playerAllowedToSee = true;
                break;
            }
        }
        _.memory.Free(nextID);
        if (playerAllowedToSee) {
            currentPlayers[i].Notify(currentAnnouncements.started, howToVoteHint,, P("voting"));
            currentPlayers[i].BorrowConsole().WriteLine(currentAnnouncements.started);
            currentPlayers[i].BorrowConsole().WriteLine(howToVoteHint);
        } else {
            currentPlayers[i].Notify(currentAnnouncements.started, F(cannotVoteHint),, P("voting"));
            currentPlayers[i].BorrowConsole().WriteLine(currentAnnouncements.started);
        }
    }
    _.memory.Free(howToVoteHint);
    _.memory.FreeMany(currentPlayers);
}

/// Prints given voting outcome message in console and publishes it as
/// a notification.
private final function AnnounceOutcome(BaseText outcomeMessage, optional EPlayer forcedBy) {
    local int i;
    local Text playerName;
    local MutableText editedOutcomeMessage, summaryLine;
    local ConsoleWriter writer;
    local array<EPlayer> currentPlayers;

    if (model == none) {
        return;
    }
    if (outcomeMessage != none) {
        editedOutcomeMessage = outcomeMessage.MutableCopy();
    }
    if (editedOutcomeMessage != none && forcedBy != none) {
        editedOutcomeMessage.Append(F(" {$TextEmphasis (forced by }"));
        playerName = forcedBy.GetName();
        editedOutcomeMessage.Append(playerName, _.text.FormattingFromColor(_.color.Gray));
        _.memory.Free(playerName);
        editedOutcomeMessage.Append(F("{$TextEmphasis )}"));
    }
    voteSummaryTemplate.Reset();
    voteSummaryTemplate.ArgInt(model.GetVotesFor());
    voteSummaryTemplate.ArgInt(model.GetVotesAgainst());
    summaryLine = voteSummaryTemplate.CollectFormattedM();
    currentPlayers = _.players.GetAll();
    for (i = 0; i < currentPlayers.length; i += 1) {
        writer = currentPlayers[i].BorrowConsole();
        writer.Write(editedOutcomeMessage);
        writer.Write(P(" / "));
        writer.WriteLine(summaryLine);
        currentPlayers[i].Notify(editedOutcomeMessage, summaryLine,, P("voting"));
    }
    _.memory.FreeMany(currentPlayers);
    _.memory.Free(summaryLine);
}

defaultproperties {
    // You can override these
    preferredName = "test"
    votingInfoLine = "Debug voting is running"
    votingStartedLine = "Test voting has started"
    votingSucceededLine = "{$TextPositive Test voting passed}"
    votingFailedLine = "{$TextNegative Test voting has failed}"
    permissionsConfigClass = class'VotingPermissions'
    // You cannot override these
    voteSummaryTemplateString = "Vote tally: {$TextPositive %1} vs {$TextNegative %2}"
    playerVotedTemplateString = "Player {$TextSubtle %%player_name%%} has voted %%vote_type%% passing test voting"
    playerVotedAnonymousTemplateString = "Someone has voted %%vote_type%% passing test voting"
    timeRemaningAnnounceTemplateString = "Time remaining for voting: %1 seconds"
    cannotVoteHint = "{$TextNegative You aren't allowed to vote :(}"
    announcementTimings(0) = 60
    announcementTimings(1) = 30
    announcementTimings(2) = 15
    announcementTimings(3) = 10
    announcementTimings(4) = 5
    announcementTimings(5) = 4
    announcementTimings(6) = 3
    announcementTimings(7) = 2
    announcementTimings(8) = 1
}