/**
 * Author: dkanus
 * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
 * License: GPL
 * 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 ChatApi extends AcediaObject;

///! API for accessing chat-related events.
///!
///! # Implementation
///!
///! Signal functions that track text chat messages `OnMessage()` and `OnMessageFor()` simply
///! hook into [`BroadcastApi`] the first time such signal is requested.
///!
///! Signal function [`OnVoiceMessage()`] for tracking voice replaces a function in
///! [`KFPlayerController`] to track when they are replicated.
///! Then replaced function informs [`ChatApi`] about new voice message transmissions via
///! internal [`_EmitOnVoiceMessage()`] method.

/// Lists voice messages built-in in the game.
enum BuiltInVoiceMessage {
    // Support
    BIVM_SupportMedic,
    BIVM_SupportHelp,
    BIVM_SupportAskForMoney,
    BIVM_SupportAskForWeapon,
    // Acknowledgements
    BIVM_AckYes,
    BIVM_AckNo,
    BIVM_AckThanks,
    BIVM_AckSorry,
    // Alert
    BIVM_AlretLookOut,
    BIVM_AlretRun,
    BIVM_AlretWaitForMe,
    BIVM_AlretWeldTheDoors,
    BIVM_AlretLetsHoleUpHere,
    BIVM_AlretFollowMe,
    // Direction
    BIVM_DirectionGetToTheTrader,
    BIVM_DirectionGoUpstairs,
    BIVM_DirectionGoDownstairs,
    BIVM_DirectionGetOutside,
    BIVM_DirectionGetInside,
    // Insult
    BIVM_InsultSpecimens,
    BIVM_InsultPlayers,
    // Trader
    BIVM_TraderCheckWhereTheShopIs,
    BIVM_TraderGetClose,
    BIVM_TraderShopOpen,
    BIVM_TraderShopOpenFinal,
    BIVM_Trader30SecondsUntilShopCloses,
    BIVM_TraderShopClosed,
    BIVM_TraderCompliment,
    BIVM_TraderNotEnoughMoney,
    BIVM_TraderCannotCarry,
    BIVM_TraderHurryUp1,
    BIVM_TraderHurryUp2,
    // Auto
    BIVM_AutoWelding,
    BIVM_AutoUnwelding,
    BIVM_AutoReload,
    BIVM_AutoOutOfAmmo,
    BIVM_AutoDosh,
    BIVM_AutoStandStillTryingToHealYou,
    BIVM_AutoLowOnHealth,
    BIVM_AutoBloatAcid,
    BIVM_AutoPatriarchCloack,
    BIVM_AutoPatriarchMinigun,
    BIVM_AutoPatriarchRocketLauncher,
    BIVM_AutoGrabbedByClot,
    BIVM_AutoFleshpoundSpotted,
    BIVM_AutoGorefastSpotted,
    BIVM_AutoScrakeSpotted,
    BIVM_AutoSirenSpotten,
    BIVM_AutoSirenScream,
    BIVM_AutoStalkerSpotted,
    BIVM_AutoCrawlertSpotted,
    BIVM_AutoMeleeKilledAStalker,
    BIVM_AutoUsingFlamethrower,
    BIVM_AutoEquipHuntingShotgun,
    BIVM_AutoEquipHandcannons,
    BIVM_AutoEquipLAW,
    BIVM_AutoEquipFireaxe,
    // Fallback
    BIVM_Unknown
};

/// Killing Floor's native voice message is defined by `name` and `byte` pair.
/// This struct is added to allow returning them as a pair.
struct NativeVoiceMessage {
    var name type;
    var byte index;
};

/// Tracks whether we've already connected to broadcast signals.
var protected bool connectedToBroadcastAPI;
/// Tracks whether we've already replaced a function that allows us to catch voice messages.
var private bool replacedSendVoiceMessage;

/// Auxiliary constants that store amount of values in [`BuiltInVoiceMessage`] before
/// a certain group.
/// Used for conversion between native voice messages and [`BuiltInVoiceMessage`]
var private const int VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS;
var private const int VOICE_MESSAGES_BEFORE_ALERTS;
var private const int VOICE_MESSAGES_BEFORE_DIRECTIONS;
var private const int VOICE_MESSAGES_BEFORE_INSULTS;
var private const int VOICE_MESSAGES_BEFORE_TRADER;
var private const int VOICE_MESSAGES_BEFORE_AUTO;
var private const int VOICE_MESSAGES_TOTAL;

var protected ChatAPI_OnMessage_Signal onMessageSignal;
var protected ChatAPI_OnMessageFor_Signal onMessageForSignal;
var protected ChatAPI_OnVoiceMessage_Signal onVoiceMessageSignal;

protected function Constructor() {
    onMessageSignal = ChatAPI_OnMessage_Signal(_.memory.Allocate(class'ChatAPI_OnMessage_Signal'));
    onMessageForSignal =
        ChatAPI_OnMessageFor_Signal(_.memory.Allocate(class'ChatAPI_OnMessageFor_Signal'));
    onVoiceMessageSignal =
        ChatAPI_OnVoiceMessage_Signal(_.memory.Allocate(class'ChatAPI_OnVoiceMessage_Signal'));
}

protected function Finalizer() {
    _.memory.Free(onMessageSignal);
    _.memory.Free(onMessageForSignal);
    _.memory.Free(onVoiceMessageSignal);
    onMessageSignal = none;
    onMessageForSignal = none;
    onVoiceMessageSignal = none;
    _server.unreal.broadcasts.OnHandleText(self).Disconnect();
    _server.unreal.broadcasts.OnHandleTextFor(self).Disconnect();
    connectedToBroadcastAPI = false;
}

/// Signal that will be emitted when a player sends a message into the chat.
///
/// Allows to modify message before sending it, as well as prevent it from being sent at all.
///
/// Return `false` to prevent message from being sent.
/// If `false` is returned, signal propagation to the remaining handlers will also be interrupted.
///
/// # Slot description
///
/// bool <slot>(EPlayer sender, MutableText message, bool teamMessage)
///
/// ## Parameters
///
/// * [`sender`]: `EPlayer` that has sent the message.
/// * [`message`]: Message that `sender` has sent.
///     This is a mutable variable and can be modified from message will be sent.
/// * [`teamMessage`]: Is this a team message (to be sent only to players on the same team)?
///
/// ## Returns
///
/// Return `false` to prevent this message from being sent at all and `true` otherwise.
/// Message will be sent only if all handlers will return `true`.
public /*signal*/ function ChatAPI_OnMessage_Slot OnMessage(AcediaObject receiver) {
    TryConnectingBroadcastSignals();
    return ChatAPI_OnMessage_Slot(onMessageSignal.NewSlot(receiver));
}

/// Signal that will be emitted when a player sends a message into the chat.
///
/// Allows to modify message before sending it, as well as prevent it from being sent at all.
///
/// Return `false` to prevent message from being sent to a specific player.
/// If `false` is returned, signal propagation to the remaining handlers will also be interrupted.
///
/// # Slot description
///
/// bool <slot>(EPlayer receiver, EPlayer sender, BaseText message)
///
/// ## Parameters
///
/// * [`receiver`]: `EPlayer` that will receive the message.
/// * [`sender`]: `EPlayer` that has sent the message.
/// * [`message`]: Message that `sender` has sent. This is an immutable variable and cannot
///     be changed at this point. Use `OnMessage()` signal function for that.
///
/// ## Returns
///
/// Return `false` to prevent this message from being sent to a particular player and
/// `true` otherwise.
/// Message will be sent only if all handlers will return `true`.
/// However decision whether to send message or not is made for every player separately.
public /*signal*/ function ChatAPI_OnMessageFor_Slot OnMessageFor(AcediaObject receiver) {
    TryConnectingBroadcastSignals();
    return ChatAPI_OnMessageFor_Slot(onMessageForSignal.NewSlot(receiver));
}

/// Signal that will be emitted when a player sends a voice message.
///
/// # Slot description
///
/// void <slot>(EPlayer sender, ChatApi.BuiltInVoiceMessage message)
///
/// ## Parameters
///
/// * [`sender`]: `EPlayer` that has sent the voice message.
/// * [`message`]: Message that `sender` has sent.
public /*signal*/ function ChatAPI_OnVoiceMessage_Slot OnVoiceMessage(AcediaObject receiver) {
    if (!replacedSendVoiceMessage) {
        _.unflect.ReplaceFunction_S(
            "KFMod.KFPlayerController.SendVoiceMessage",
            "AcediaCore.Unflect_ChatApi_Controller.SendVoiceMessage",
            "`ChatApi` was required to catch voice messages");
        replacedSendVoiceMessage = true;
    }
    return ChatAPI_OnVoiceMessage_Slot(onVoiceMessageSignal.NewSlot(receiver));
}

public /*internal*/ function NativeVoiceMessage _enumIntoNativeVoiceMessage(
    BuiltInVoiceMessage voiceMessage
) {
    local int enumValue;
    local NativeVoiceMessage result;

    enumValue = int(voiceMessage);
    if (enumValue < VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS) {
        result.type = 'SUPPORT';
        result.index = enumValue;
    } else if (enumValue < VOICE_MESSAGES_BEFORE_ALERTS) {
        result.type = 'ACK';
        result.index = enumValue - VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS;
    } else if (enumValue < VOICE_MESSAGES_BEFORE_DIRECTIONS) {
        result.type = 'ALERT';
        result.index = enumValue - VOICE_MESSAGES_BEFORE_ALERTS;
    } else if (enumValue < VOICE_MESSAGES_BEFORE_INSULTS) {
        result.type = 'DIRECTION';
        result.index = enumValue - VOICE_MESSAGES_BEFORE_DIRECTIONS;
    } else if (enumValue < VOICE_MESSAGES_BEFORE_TRADER) {
        result.type = 'INSULT';
        result.index = enumValue - VOICE_MESSAGES_BEFORE_INSULTS;
    } else if (enumValue < VOICE_MESSAGES_BEFORE_AUTO) {
        result.type = 'TRADER';
        result.index = enumValue - VOICE_MESSAGES_BEFORE_TRADER;
        if (result.index >= 5) {
            result.index += 1;
        }
    } else if (enumValue < VOICE_MESSAGES_TOTAL) {
        result.type = 'AUTO';
        result.index = enumValue - VOICE_MESSAGES_BEFORE_AUTO;
    }
    return result;
}

public /*internal*/ function BuiltInVoiceMessage _nativeVoiceMessageIntoEnum(
    NativeVoiceMessage voiceMessage
) {
    switch (voiceMessage.type) {
        case 'SUPPORT':
            if (voiceMessage.index < 4) {
                return BuiltInVoiceMessage(voiceMessage.index);
            }
            break;
        case 'ACK':
            if (voiceMessage.index < 4) {
                return BuiltInVoiceMessage(
                    VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS + voiceMessage.index);
            }
            break;
        case 'ALERT':
            if (voiceMessage.index < 6) {
                return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_ALERTS + voiceMessage.index);
            }
            break;
        case 'DIRECTION':
            if (voiceMessage.index < 5) {
                return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_DIRECTIONS + voiceMessage.index);
            }
            break;
        case 'INSULT':
            if (voiceMessage.index < 2) {
                return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_INSULTS + voiceMessage.index);
            }
            break;
        case 'TRADER':
            if (voiceMessage.index < 12) {
                if (voiceMessage.index < 5) {
                    return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_TRADER + voiceMessage.index);
                } else if (voiceMessage.index > 5) {
                    return BuiltInVoiceMessage(
                        VOICE_MESSAGES_BEFORE_TRADER + voiceMessage.index - 1);
                }
            }
            break;
        case 'AUTO':
            if (voiceMessage.index < 25) {
                return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_AUTO + voiceMessage.index);
            }
            break;
        default:
            return BIVM_Unknown;
    }
    return BIVM_Unknown;
}

public final /*internal*/ /*native*/ function _EmitOnVoiceMessage(
    PlayerController sender,
    name messageType,
    byte messageID
) {
    local EPlayer wrapper;
    local NativeVoiceMessage nativeVoiceMessage;
    local BuiltInVoiceMessage builtInVoiceMessage;

    if (sender == none)     return;
    wrapper = _.players.FromController(sender);
    if (wrapper == none)    return;

    nativeVoiceMessage.type = messageType;
    nativeVoiceMessage.index = messageID;
    builtInVoiceMessage = _nativeVoiceMessageIntoEnum(nativeVoiceMessage);
    if (builtInVoiceMessage != BIVM_Unknown) {
        onVoiceMessageSignal.Emit(wrapper, builtInVoiceMessage);
    }
    _.memory.Free(wrapper);
}

private final function TryConnectingBroadcastSignals() {
    if (connectedToBroadcastAPI) {
        return;
    }
    connectedToBroadcastAPI = true;
    _server.unreal.broadcasts.OnHandleText(self).connect = HandleText;
    _server.unreal.broadcasts.OnHandleTextFor(self).connect = HandleTextFor;
}

private function bool HandleText(
    Actor sender,
    out string message,
    name messageType,
    bool teamMessage
) {
    local bool result;
    local MutableText messageAsText;
    local EPlayer senderPlayer;

    // We only want to catch chat messages from a player
    if (messageType != 'Say' && messageType != 'TeamSay')   return true;
    senderPlayer = _.players.FromController(PlayerController(sender));
    if (senderPlayer == none)                               return true;

    messageAsText = __().text.FromColoredStringM(message);
    result = onMessageSignal.Emit(senderPlayer, messageAsText, teamMessage);
    message = messageAsText.ToColoredString();
    //      To correctly display chat messages we want to drop default color tag
    //  at the beginning (the one `ToColoredString()` adds if first character
    //  has no defined color).
    //      This is a compatibility consideration with vanilla UI that expects
    //  uncolored text. Not removing initial color tag will make chat text
    //  appear black.
    if (!messageAsText.GetFormatting(0).isColored) {
        message = Mid(message, 4);
    }
    _.memory.Free(messageAsText);
    _.memory.Free(senderPlayer);
    return result;
}

private function bool HandleTextFor(
    PlayerController receiver,
    Actor sender,
    out string message,
    name messageType
) {
    local bool result;
    local Text messageAsText;
    local EPlayer senderPlayer, receiverPlayer;

    // We only want to catch chat messages from another player
    if (messageType != 'Say' && messageType != 'TeamSay')   return true;
    senderPlayer = _.players.FromController(PlayerController(sender));
    if (senderPlayer == none)                               return true;

    receiverPlayer = _.players.FromController(receiver);
    if (receiverPlayer == none) {
        _.memory.Free(senderPlayer);
        return true;
    }
    messageAsText = __().text.FromColoredString(message);
    result = onMessageForSignal.Emit(receiverPlayer, senderPlayer, messageAsText);
    _.memory.Free(messageAsText);
    _.memory.Free(senderPlayer);
    _.memory.Free(receiverPlayer);
    return result;
}

defaultproperties {
    VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS  = 4
    VOICE_MESSAGES_BEFORE_ALERTS            = 8
    VOICE_MESSAGES_BEFORE_DIRECTIONS        = 14
    VOICE_MESSAGES_BEFORE_INSULTS           = 19
    VOICE_MESSAGES_BEFORE_TRADER            = 21
    VOICE_MESSAGES_BEFORE_AUTO              = 32
    VOICE_MESSAGES_TOTAL                    = 57
}