/**
 * 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 ACommandVote extends Command
    dependson(CommandAPI)
    dependson(VotingModel);

var private CommandDataBuilder dataBuilder;

protected function Constructor() {
    ResetVotingInfo();
    _.commands.OnVotingAdded(self).connect = AddVotingInfo;
    _.commands.OnVotingRemoved(self).connect = HandleRemovedVoting;
    _.chat.OnVoiceMessage(self).connect = VoteWithVoice;
}

protected function Finalizer() {
    super.Finalizer();
    _.memory.Free(dataBuilder);
    dataBuilder = none;
    _.commands.OnVotingAdded(self).Disconnect();
    _.commands.OnVotingRemoved(self).Disconnect();
    _.chat.OnVoiceMessage(self).Disconnect();
}

protected function BuildData(CommandDataBuilder builder) {
    builder.Group(P("core"));
    builder.Summary(P("Allows players to initiate any available voting."
        @ "Voting options themselves are specified as sub-commands."));
    builder.Describe(P("Default command simply displaces information about current vote."));

    dataBuilder.SubCommand(P("yes"));
    builder.Describe(P("Vote `yes` on the current vote."));
    dataBuilder.SubCommand(P("no"));
    builder.Describe(P("Vote `no` on the current vote."));

    builder.Option(P("force"));
    builder.Describe(P("Tries to force voting to end immediately with the desired result."));
}

protected function Executed(
    CallData arguments,
    EPlayer instigator,
    CommandPermissions permissions
) {
    local bool forcingVoting;
    local VotingModel.ForceEndingType forceType;
    local Voting currentVoting;

    forcingVoting = arguments.options.HasKey(P("force"));
    currentVoting = _.commands.GetCurrentVoting();
    if (arguments.subCommandName.IsEmpty()) {
        DisplayInfoAboutVoting(instigator, currentVoting);
    } else if (arguments.subCommandName.Compare(P("yes"), SCASE_INSENSITIVE)) {
        CastVote(currentVoting, instigator, true);
        forceType = FET_Success;
    } else if (arguments.subCommandName.Compare(P("no"), SCASE_INSENSITIVE)) {
        CastVote(currentVoting, instigator, false);
        forceType = FET_Failure;
    } else if (StartVoting(arguments, currentVoting, instigator)) {
        _.memory.Free(currentVoting);
        currentVoting = _.commands.GetCurrentVoting();
        forceType = FET_Success;
    } else {
        forcingVoting = false;
    }
    if (currentVoting != none && !currentVoting.HasEnded() && forcingVoting) {
        if (currentVoting.ForceEnding(instigator, forceType) == FEO_Forbidden) {
            callerConsole
                .WriteLine(F("You {$TextNegative aren't allowed} to forcibly end current voting"));
        }
    }
    _.memory.Free(currentVoting);
}

private final function VoteWithVoice(EPlayer sender, ChatApi.BuiltInVoiceMessage message) {
    local Voting currentVoting;

    currentVoting = _.commands.GetCurrentVoting();
    if (message == BIVM_AckYes) {
        CastVote(currentVoting, sender, true);
    }
    if (message == BIVM_AckNo) {
        CastVote(currentVoting, sender, false);
    }
    _.memory.Free(currentVoting);
}

/// Adds sub-command information about given voting with a given name.
public final function AddVotingInfo(class<Voting> processClass, Text processName) {
    if (processName == none)    return;
    if (processClass == none)   return;
    if (dataBuilder == none)    return;

    dataBuilder.SubCommand(processName);
    processClass.static.AddInfo(dataBuilder);
    commandData = dataBuilder.BorrowData();
}

public final function HandleRemovedVoting(class<Voting> votingClass) {
    local int i;
    local array<Text> votingsNames;

    ResetVotingInfo();
    // Rebuild the whole voting data
    votingsNames = _.commands.GetAllVotingsNames();
    for (i = 0; i < votingsNames.length; i += 1) {
        AddVotingInfo(_.commands.GetVotingClass(votingsNames[i]), votingsNames[i]);
    }
    _.memory.FreeMany(votingsNames);
}

/// Clears all sub-command information added from [`Voting`]s.
public final function ResetVotingInfo() {
    _.memory.Free(dataBuilder);
    dataBuilder = CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder'));
    BuildData(dataBuilder);
    commandData = dataBuilder.BorrowData();
}

private final function DisplayInfoAboutVoting(EPlayer instigator, Voting currentVoting) {
    if (currentVoting == none) {
        callerConsole.WriteLine(P("No voting is active right now."));
    } else {
        currentVoting.PrintVotingInfoFor(instigator);
    }
}

private final function CastVote(Voting currentVoting, EPlayer voter, bool voteForSuccess) {
    if (currentVoting != none) {
        currentVoting.CastVote(voter, voteForSuccess);
    } else {
        callerConsole.UseColor(_.color.TextWarning).WriteLine(P("No voting is active right now."));
    }
}

// Assumes all arguments aren't `none`.
private final function bool StartVoting(
    CallData arguments,
    Voting currentVoting,
    EPlayer instigator
) {
    local Voting newVoting;
    local User callerUser;
    local CommandAPI.VotingConfigInfo pair;
    local CommandAPI.StartVotingResult result;

    callerUser = instigator.GetIdentity();
    pair = _.commands.ResolveVotingForUser(arguments.subCommandName, callerUser);
    _.memory.Free(callerUser);
    if (pair.votingClass == none) {
        callerConsole
            .UseColor(_.color.TextFailure)
            .Write(P("Unknown voting option \""))
            .Write(arguments.subCommandName)
            .WriteLine(P("\""));
        return false;
    }
    if (pair.usageForbidden) {
        callerConsole
            .UseColor(_.color.TextFailure)
            .Write(P("You aren't allowed to start \""))
            .Write(arguments.subCommandName)
            .WriteLine(P("\" voting"));
        return false;
    }
    result = _.commands.StartVoting(pair, arguments.parameters);
    Log("Result:" @ result);
    // Handle errors.
    // `SVR_UnknownVoting` is impossible, since we've already checked that
    // `pair.votingClass != none`)
    if (result == SVR_AlreadyInProgress) {
        callerConsole
            .UseColor(_.color.TextWarning)
            .WriteLine(P("Another voting is already in progress!"));
        return false;
    }
    if (result == SVR_NoVoters) {
        callerConsole
            .UseColor(_.color.TextWarning)
            .WriteLine(P("There are no players eligible for that voting."));
        return false;
    }
    // Cast a vote from instigator
    newVoting = _.commands.GetCurrentVoting();
    if (newVoting != none) {
        newVoting.CastVote(instigator, true);
    } else {
        callerConsole
            .UseColor(_.color.TextFailure)
            .WriteLine(P("Voting should be available, but it isn't."
                @ "This is unexpected, something broke terribly."));
        _.memory.Free(newVoting);
        return false;
    }
    _.memory.Free(newVoting);
    return true;
}

defaultproperties {
    preferredName = "vote"
}