/**
 *  Config class for storing map lists.
 *      Copyright 2022-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 FutilityNicknames_Feature extends Feature
    dependson(FutilityNicknames);

//! This feature's functionality is rather simple, but we will still break up
//! what its various components are.
//!
//!     Fallback nicknames are picked at random from
//! the `fallbackNicknames` array. This is done by copying that array into
//! `unusedNicknames` and then picking and removing its random elements each
//! time we need a fallback. Once `unusedNicknames` is empty - it is copied from
//! `fallbackNicknames` once again, letting already used nicknames to be reused.
//!     `unusedNicknames` contains same references as `fallbackNicknames`,
//! so they need to be separately deallocated and should also be forgotten once
//! `fallbackNicknames` are deallocated`.
//!     This is implemented inside `PickNextFallback()` method.
//!
//!     Nickname changes are applied inside `CensorNickname()` method that uses
//! several auxiliary methods to perform different stages of "censoring".
//!     Censoring is performed:
//!         1. On any player's name change
//!             (using `OnPlayerNameChanging()` signal, connected to
//!             `HandleNicknameChange()`);
//!         2. When new player logins (using `OnNewPlayer()` signal,
//!             conneted to `CensorOriginalNickname()`) to enforce our own
//!             handling of player's original nickname.

var private /*config*/ FutilityNicknames.NicknameSpacesAction spacesAction;
var private /*config*/ FutilityNicknames.NicknameColorPermissions colorPermissions;
var private /*config*/ bool replaceSpacesWithUnderscores;
var private /*config*/ bool removeSingleQuotationMarks;
var private /*config*/ bool removeDoubleQuotationMarks;
var private /*config*/ int maxNicknameLength;
var private /*config*/ bool correctEmptyNicknames;
var private /*config*/ array<Text> fallbackNickname;

/// Guaranteed order of applying changes (only chosen ones) is as following:
///
/// 1. Trim/simplify spaces;
/// 2. Remove single and double quotation marks;
/// 3. Enforce max limit of nickname's length;
/// 4. Replace empty nickname with fallback nickname (no further changes
///     will be applied to fallback nickname in that case);
/// 5. Enforce color limitation;
/// 6. Replace remaining whitespaces with underscores.
///
/// NOTE #1: as follows from the instruction described above, no changes will
/// ever be applied to fallback nicknames (unless player's nickname coincides
/// with one by pure accident).
///
/// NOTE #2: whitespaces inside steam nicknames are converted into underscores
/// before they are passed into the game and this is a change Futility
/// cannot currently abort. Therefore all changes relevant to whitespaces inside
/// nicknames will only be applied to in-game changes.

/// Nicknames from `fallbackNickname` that can still be picked in the current
/// rotation.
var private array<Text> unusedNicknames;

var private const int CODEPOINT_UNDERSCORE;

protected function OnEnabled() {
    if (IsAnyCensoringEnabled()) {
        // Do this before adding event handler to avoid censoring nicknames
        // second time (censoring nickname will trigger `OnPlayerNameChanging()`
        // signal)
        CensorCurrentPlayersNicknames();
        _.players.OnPlayerNameChanging(self).connect = HandleNicknameChange;
        _.players.OnNewPlayer(self).connect = CensorOriginalNickname;
    }
}

protected function OnDisabled() {
    _.memory.FreeMany(fallbackNickname);
    _.memory.FreeMany(unusedNicknames);
    fallbackNickname.length = 0;
    unusedNicknames.length  = 0;
    if (IsAnyCensoringEnabled()) {
        _.players.OnPlayerNameChanging(self).Disconnect();
        _.players.OnNewPlayer(self).Disconnect();
    }
}

protected function SwapConfig(FeatureConfig config) {
    local FutilityNicknames newConfig;

    newConfig = FutilityNicknames(config);
    if (newConfig == none) {
        return;
    }
    replaceSpacesWithUnderscores = newConfig.replaceSpacesWithUnderscores;
    removeSingleQuotationMarks = newConfig.removeSingleQuotationMarks;
    removeDoubleQuotationMarks = newConfig.removeDoubleQuotationMarks;
    correctEmptyNicknames = newConfig.correctEmptyNicknames;
    spacesAction = newConfig.spacesAction;
    colorPermissions = newConfig.colorPermissions;
    maxNicknameLength = newConfig.maxNicknameLength;
    SwapFallbackNicknames(newConfig);
}

private function SwapFallbackNicknames(FutilityNicknames newConfig) {
    local int i;

    _.memory.FreeMany(fallbackNickname);
    fallbackNickname.length = 0;
    for (i = 0; i < newConfig.fallbackNickname.length; i += 1) {
        fallbackNickname[i] = _.text.FromFormattedString(newConfig.fallbackNickname[i]);
    }
    unusedNicknames = fallbackNickname;
}

private function Text PickNextFallback() {
    local int pickedIndex;
    local Text result;

    if (fallbackNickname.length <= 0) {
        // Just in case this feature is really misconfigured
        return P("Fresh Meat").Copy();
    }
    if (unusedNicknames.length <= 0) {
        unusedNicknames = fallbackNickname;
    }
    // Pick one nickname at random.
    // `pickedIndex` will belong to [0; unusedNicknames.length - 1] segment.
    pickedIndex = Rand(unusedNicknames.length);
    result = unusedNicknames[pickedIndex].Copy();
    unusedNicknames.Remove(pickedIndex, 1);
    return result;
}

private function bool IsAnyCensoringEnabled() {
    return (    replaceSpacesWithUnderscores
            ||  removeSingleQuotationMarks
            ||  removeDoubleQuotationMarks
            ||  correctEmptyNicknames
            ||  maxNicknameLength >= 0
            ||  colorPermissions != NCP_AllowAnyColor
            ||  spacesAction != NSA_DoNothing);
}

// For nickname changes mid-game.
private function HandleNicknameChange(
    EPlayer affectedPlayer,
    BaseText oldName,
    MutableText newName
) {
    CensorNickname(newName, affectedPlayer);
}

// For handling of player's original nicknames.
private function CensorOriginalNickname(EPlayer affectedPlayer) {
    local Text originalNickname;
    if (affectedPlayer == none) {
        return;
    }
    originalNickname = affectedPlayer.GetOriginalName();
    // This will automatically trigger `OnPlayerNameChanging()` signal and
    // our `HandleNicknameChange()` handler.
    affectedPlayer.SetName(originalNickname);
    _.memory.Free(originalNickname);
}

// For handling nicknames of players after censoring is re-activated by
// config change.
private function CensorCurrentPlayersNicknames() {
    local int i;
    local Text nextNickname;
    local MutableText alteredNickname;
    local array<EPlayer> currentPlayers;

    currentPlayers = _.players.GetAll();
    for (i = 0; i < currentPlayers.length; i += 1) {
        nextNickname    = currentPlayers[i].GetName();
        alteredNickname = nextNickname.MutableCopy();
        CensorNickname(alteredNickname, currentPlayers[i]);
        if (!alteredNickname.Compare(nextNickname)) {
            currentPlayers[i].SetName(alteredNickname);
        }
        _.memory.Free(alteredNickname);
        _.memory.Free(nextNickname);
    }
}

private function CensorNickname(MutableText nickname, EPlayer affectedPlayer) {
    local Text fallback;
    local BaseText.Formatting newFormatting;

    if (nickname == none)       return;
    if (affectedPlayer == none) return;

    if (spacesAction != NSA_DoNothing) {
        nickname.Simplify(spacesAction == NSA_Simplify);
    }
    if (removeSingleQuotationMarks) {
        nickname.Replace(P("'"), P(""));
    }
    if (removeDoubleQuotationMarks) {
        nickname.Replace(P("\""), P(""));
    }
    if (maxNicknameLength >= 0) {
        nickname.Remove(maxNicknameLength);
    }
    if (correctEmptyNicknames && nickname.IsEmpty()) {
        fallback = PickNextFallback();
        nickname.Append(fallback);
        _.memory.Free(fallback);
        return;
    }
    if (colorPermissions != NCP_AllowAnyColor) {
        if (colorPermissions == NCP_ForceSingleColor) {
            newFormatting = nickname.GetCharacter(0).formatting;
        } else if (colorPermissions == NCP_ForceTeamColor) {
            newFormatting.isColored = true;
            newFormatting.color     = affectedPlayer.GetTeamColor();
        }
        // `colorPermissions == NCP_ForbidColor`
        // `newFormatting` is colorless by default
        nickname.ChangeFormatting(newFormatting);
    }
    if (replaceSpacesWithUnderscores) {
        ReplaceSpaces(nickname);
    }
}

// Asusmes `nickname != none`.
private function ReplaceSpaces(MutableText nickname) {
    local int i;
    local MutableText nicknameCopy;
    local BaseText.Character nextCharacter, underscoreCharacter;

    nicknameCopy = nickname.MutableCopy();
    nickname.Clear();
    underscoreCharacter = _.text.CharacterFromCodePoint(CODEPOINT_UNDERSCORE);
    for (i = 0; i < nicknameCopy.GetLength(); i += 1) {
        nextCharacter = nicknameCopy.GetCharacter(i);
        if (_.text.IsWhitespace(nextCharacter)) {
            //  Replace character with underscore, leaving the formatting
            underscoreCharacter.formatting = nextCharacter.formatting;
            nextCharacter = underscoreCharacter;
        }
        nickname.AppendCharacter(nextCharacter);
    }
    _.memory.Free(nicknameCopy);
}

defaultproperties {
    configClass = class'FutilityNicknames'
    CODEPOINT_UNDERSCORE = 95 // '_'
}