using Godot;

using Sfs3.Client.Core;
using Sfs3.Client.Requests;
using Sfs3.Client.Entities;
using System.Collections.Generic;

using System.Linq;
using System;
using Sfs3.Client.Requests.Buddylist;
using Sfs3.Client.Entities.Variables;
using Sfs3.Client.Entities.Data;
using Sfs3.Client.Requests.Audio;
using Sfs3.Client.Entities.Invitation;
using System.Threading.Tasks;
using Sfs3.Client.Requests.Invitations;
using Sfs3.Client.Entities.Audio;

/**
 * Script attached to the main node in the scene.
 */
public partial class LobbyController : BaseController
{
    public static readonly string BUDDYVAR_YEAR = SFSBuddyVariable.OFFLINE_PREFIX + "year";
    public static readonly string BUDDYVAR_MOOD = "mood";
    private const int INVITATION_EXPIRY_SECS = 20;

    //----------------------------------------------------------
    // UI elements
    //----------------------------------------------------------
    [ExportCategory("UI Settings")]

    [Export]
    public ProfileConfigurator profileConfigurator;
	[Export]
	public PopupDialog warningPanel;
    [Export]
    public Label warningLabel;
    [Export]
    public Control buddyListContainer;
    [Export]
    public BuddyList buddyList;
    [Export]
    public LineEdit buddyNameInput;
    [Export]
    public ChatPanel buddyChatPanel;
    [Export]
    public CallPanel buddyCallPanel;
    [Export]
    public Window callInvitationPanel;
    [Export]
    public Label callInvitationSenderLabel;
    [Export]
    public AudioStreamPlayer callInvitationRingtone;

    private enum CallState
    {
        AVAILABLE,
        PENDING,
        IN_PROGRESS
    }

    private CallState callState = CallState.AVAILABLE;
    private Invitation activeInvitation;
    private float inviteExpireTimerSecs = 0f;
    private string pendingInviteUserName = null;

    //----------------------------------------------------------
    // Callback Methods
    //----------------------------------------------------------
    #region
    public override void _Ready()
    {
        base._Ready();
        
        sfs = Global.GetSfsClient();

        // Add SFS event listeners
        AddSmartFoxListeners();

        // Add listeners to events fired by BuddyList class
        buddyList.StartChatAction = OnStartBuddyChatPressed;
        buddyList.StartCallAction = OnStartBuddyCallPressed;
        buddyList.BlockAction = OnBlockBuddyPressed;
        buddyList.RemoveAction = OnRemoveBuddyPressed;
        buddyList.AddAction = OnAddBuddyPressed;

        // Add listeners to events fired by ProfileConfigurator class
        profileConfigurator.OnlineChangeAction = OnOnlineToggleChanged;
        profileConfigurator.BuddySettingChangeAction = OnBuddySettingChanged;

        // Add listeners to events fired by ChatPanel class
        buddyChatPanel.SendMessageAction = OnBuddyChatMessageSubmit;

        // Add listeners to events fired by CallPanel class
        buddyCallPanel.StopCallAction = OnStopCallPressed;
        buddyCallPanel.MuteAction = OnMuteToggleChanged;

        // Display user name in profile modal
        profileConfigurator.DisplayUserName(sfs.MySelf.Name);

        // Initialize Buddy List system
        // We have to check if it was already initialized if this scene is relaoded after leaving the Game scene
        if (!sfs.BuddyManager.IsInited)
            sfs.Send(new InitBuddyListRequest());
        else
            OnBuddyListInit(null);
    }

    public override void _Process(double delta)
    {
        base._Process(delta);

        if (activeInvitation != null && inviteExpireTimerSecs > 0)
        {
            inviteExpireTimerSecs -= (float)delta;

            int timerInt = (int)Math.Round(inviteExpireTimerSecs, 0);

            if (timerInt == 0)
            {
                // Stop expiration timer
                inviteExpireTimerSecs = 0f;

                // Remove active invitation
                activeInvitation = null;

                // Hide panel
                callInvitationPanel.Hide();
                callInvitationRingtone.Stop();

                // We could send a refusal just like if the user pressed the refuse button
                // Or, like in this case, we can let the invitation expire and the inviter
                // will be notified automatically
            }
        }
    }
    #endregion

    //----------------------------------------------------------
    // UI event listeners
    //----------------------------------------------------------
    #region
    
    public void OnLogoutButtonClick()
    {
        BuddyChatHistoryManager.ResetAllHistory();

        sfs.Disconnect();
    }
    
    /**
     * On Add buddy button click, send request to add buddy to user's Buddy List.
     */
    public void OnAddBuddyButtonClick()
    {
        OnBuddyNameInputSubmit(buddyNameInput.Text);

        buddyNameInput.GrabFocus();
    }

    /**
	 * On buddy name input submit, send request to add buddy to user's Buddy List.
	 */
    public void OnBuddyNameInputSubmit(string userName)
    {
        if (userName != "")
        {
            // Request buddy adding to buddy list
            sfs.Send(new AddBuddyRequest(userName));

            // Reset buddy name input
            buddyNameInput.Text = "";
            buddyNameInput.GrabFocus();
        }
    }

    /**
     * On Chat button press in Buddy List item, initialize and show chat panel.
     */
    public void OnStartBuddyChatPressed(string buddyName)
    {
        // Skip if chat is already active with the same buddy
        if (buddyChatPanel.UserName != buddyName)
        {
            // Get chat history (if any) and reset new messages counter
            var buddyChatMessages = BuddyChatHistoryManager.GetBuddyChatMessages(buddyName);
            BuddyChatHistoryManager.ResetUnreadMessagesCount(buddyName);

            // Initialize and show panel
            InitBuddyChatPanel(sfs.BuddyManager.GetBuddyByName(buddyName), buddyChatMessages);

            // Move buddy list item to top of the list and reset new messages counter
            buddyList.MoveListItemToTop(buddyName);
            buddyList.SetItemChatMsgCounter(buddyName, 0);
        }
    }

    /**
     * On Call button press in Buddy List item, initialize and show call panel.
     */
    public void OnStartBuddyCallPressed(string buddyName)
    {
        // If a call attempt (which may include Room creation, joining and sending invitation)
        // is already in progress, skip all next attempts 
        if (callState == CallState.PENDING)
            return;
        
        // Also avoid inviting the same user multiple times
        if (buddyCallPanel.HasParticipant(buddyName))
            return;
        
        //-----------------------------------------------------

        // Save ref. to user name to invite
        pendingInviteUserName = buddyName;

        // If current user is not yet participating in a call, create and join a room, then invite buddy
        // Otherwise directly invite buddy
        if (sfs.LastJoinedRoom == null)
        {
            // Set state
            callState = CallState.PENDING;

            // Configure Room
            string roomName = "call-" + DateTime.Now.ToString("yyyyMMddHHmmssfff");

            var settings = new RoomSettings(roomName)
            {
                MaxUsers = 4,
                Password = GenerateRandomString(),
                AllowOwnerOnlyInvitation = false,
                AllowAudioStreaming = true,
                IsGame = true, // This flag is useful for our flow, because a game Room is removed as soon as it is left empty
            };

            // Request Room creation to server
            sfs.Send(new CreateRoomRequest(settings, true));
        }
        else
        {
            SendPendingInvitation();
        }
    }

    /**
     * On Block/Unblock button press in Buddy List item, send request to block/unblock buddy in user's Buddy List.
     */
    public void OnBlockBuddyPressed(string buddyName)
    {
        bool isBlocked = sfs.BuddyManager.GetBuddyByName(buddyName).IsBlocked;

        // Request buddy block/unblock
        sfs.Send(new BlockBuddyRequest(buddyName, !isBlocked));
    }

    /**
     * On Remove button press in Buddy List item, send request to remove buddy from user's Buddy List.
     */
    public void OnRemoveBuddyPressed(string buddyName)
    {
        // Request buddy removal from buddy list
		sfs.Send(new RemoveBuddyRequest(buddyName));
    }

    /**
     * On Add button press in Buddy List item, send request to add a temporary buddy to user's Buddy List.
     */
    public void OnAddBuddyPressed(string buddyName)
    {
        // Request buddy removal from buddy list
		sfs.Send(new AddBuddyRequest(buddyName));
    }

    /**
     * On custom event fired by Profile Configurator control, toggle user online state in Buddy List system.
     */
    public void OnOnlineToggleChanged(bool goOnline)
    {
        // Request online/offline state toggle
        sfs.Send(new GoOnlineRequest(goOnline));
    }

    /**
     * On custom event fired by Profile Configurator control/s, set user's Buddy Variables.
     */
    public void OnBuddySettingChanged(string varName, object value)
    {
        var type = value == null ? VariableType.NULL : (value is int ? VariableType.INT : VariableType.STRING);

        List<BuddyVariable> buddyVars = [];
        buddyVars.Add(new SFSBuddyVariable(varName, value, type));

        // Set Buddy Variables
        sfs.Send(new SetBuddyVariablesRequest(buddyVars));
    }

    public void OnBuddyChatMessageSubmit(string msg)
    {
        string buddyName = buddyChatPanel.UserName;

        // Add a custom parameter containing the recipient name
        ISFSObject paramsObj = new SFSObject();
        paramsObj.PutString("recipient", buddyName);

        // Retrieve buddy
        Buddy buddy = sfs.BuddyManager.GetBuddyByName(buddyName);

        // Send message to buddy
        sfs.Send(new BuddyMessageRequest(msg, buddy, paramsObj));
    }

    /**
     * On Accept button click, accept invitation.
     */
    public void OnInvitationAcceptClick()
    {
        // Accept/refuse invitation
        sfs.Send(new InvitationReplyRequest(activeInvitation, InvitationReply.ACCEPT));

        // Remove active invitation
        activeInvitation = null;

        // Hide panel
        callInvitationPanel.Hide();
        callInvitationRingtone.Stop();
    }

    /**
     * On Refuse button click, refuse invitation.
     */
    public void OnInvitationRefuseClick()
    {
        RefuseInvitation();
    }

    public void OnStopCallPressed()
    {
        // Stop stream, leave room, close and reset call panel
        LeaveCall(0);
    }

    public void OnMuteToggleChanged(bool isMuted)
    {
        if (sfs.AudioManager.IsStreaming())
            sfs.Send(new ToggleMuteAudioStreamRequest());
    }
    #endregion

    //----------------------------------------------------------
    // Helper methods
    //----------------------------------------------------------
    #region
    private void AddSmartFoxListeners()
    {
        sfs.AddEventListener(SFSEvent.ROOM_JOIN, OnRoomJoin);
        sfs.AddEventListener(SFSEvent.ROOM_REMOVE, OnRoomRemove);
        sfs.AddEventListener(SFSEvent.USER_ENTER_ROOM, OnUserEnterRoom);
        sfs.AddEventListener(SFSEvent.USER_EXIT_ROOM, OnUserExitRoom);
        sfs.AddEventListener(SFSEvent.INVITATION, OnInvitation);
        sfs.AddEventListener(SFSEvent.INVITATION_REPLY, OnInvitationReply);

		sfs.AddEventListener(SFSBuddyEvent.BUDDY_LIST_INIT, OnBuddyListInit);
		sfs.AddEventListener(SFSBuddyEvent.BUDDY_ERROR, OnBuddyError);
		sfs.AddEventListener(SFSBuddyEvent.BUDDY_ONLINE_STATE_CHANGE, OnBuddyOnlineStateChange);
		sfs.AddEventListener(SFSBuddyEvent.BUDDY_VARIABLES_UPDATE, OnBuddyVariablesUpdate);
		sfs.AddEventListener(SFSBuddyEvent.BUDDY_BLOCK, OnBuddyBlock);
		sfs.AddEventListener(SFSBuddyEvent.BUDDY_REMOVE, OnBuddyRemove);
		sfs.AddEventListener(SFSBuddyEvent.BUDDY_ADD, OnBuddyAdd);
		sfs.AddEventListener(SFSBuddyEvent.BUDDY_MESSAGE, OnBuddyMessage);

        sfs.AddEventListener(SFSAudioEvent.INIT, OnAudioInit);
        sfs.AddEventListener(SFSAudioEvent.STREAM_START, OnAudioStreamStart);
        sfs.AddEventListener(SFSAudioEvent.STREAM_STOP, OnAudioStreamStop);
        sfs.AddEventListener(SFSAudioEvent.STREAM_ADD, OnAudioStreamAdd);
        sfs.AddEventListener(SFSAudioEvent.STREAM_REMOVE, OnAudioStreamRemove);
        sfs.AddEventListener(SFSAudioEvent.TOGGLE_MUTE, OnToggleMute);
    }

    public override void RemoveSmartFoxListeners()
    {
        if (sfs != null)
        {
            sfs.RemoveEventListener(SFSEvent.ROOM_JOIN, OnRoomJoin);
            sfs.RemoveEventListener(SFSEvent.ROOM_REMOVE, OnRoomRemove);
            sfs.RemoveEventListener(SFSEvent.USER_ENTER_ROOM, OnUserEnterRoom);
            sfs.RemoveEventListener(SFSEvent.USER_EXIT_ROOM, OnUserExitRoom);
            sfs.RemoveEventListener(SFSEvent.INVITATION, OnInvitation);
            sfs.RemoveEventListener(SFSEvent.INVITATION_REPLY, OnInvitationReply);

            sfs.RemoveEventListener(SFSBuddyEvent.BUDDY_LIST_INIT, OnBuddyListInit);
            sfs.RemoveEventListener(SFSBuddyEvent.BUDDY_ERROR, OnBuddyError);
            sfs.RemoveEventListener(SFSBuddyEvent.BUDDY_ONLINE_STATE_CHANGE, OnBuddyOnlineStateChange);
            sfs.RemoveEventListener(SFSBuddyEvent.BUDDY_VARIABLES_UPDATE, OnBuddyVariablesUpdate);
            sfs.RemoveEventListener(SFSBuddyEvent.BUDDY_BLOCK, OnBuddyBlock);
            sfs.RemoveEventListener(SFSBuddyEvent.BUDDY_REMOVE, OnBuddyRemove);
            sfs.RemoveEventListener(SFSBuddyEvent.BUDDY_ADD, OnBuddyAdd);
            sfs.RemoveEventListener(SFSBuddyEvent.BUDDY_MESSAGE, OnBuddyMessage);

            sfs.RemoveEventListener(SFSAudioEvent.INIT, OnAudioInit);
            sfs.RemoveEventListener(SFSAudioEvent.STREAM_START, OnAudioStreamStart);
            sfs.RemoveEventListener(SFSAudioEvent.STREAM_STOP, OnAudioStreamStop);
            sfs.RemoveEventListener(SFSAudioEvent.STREAM_ADD, OnAudioStreamAdd);
            sfs.RemoveEventListener(SFSAudioEvent.STREAM_REMOVE, OnAudioStreamRemove);
            sfs.RemoveEventListener(SFSAudioEvent.TOGGLE_MUTE, OnToggleMute);
        }
    }

    private void ShowBuddyList(bool show)
    {
        buddyListContainer.Visible = show;
    }

    private void InitializeBuddyClient()
    {
        // Display buddy list items
        // All blocked buddies are displayed at the bottom of the list
        foreach (Buddy buddy in sfs.BuddyManager.BuddyList)
            buddyList.AddListItem(buddy, !buddy.IsBlocked);

        // Set current user details in Buddy List system
        profileConfigurator.InitBuddyProfile(sfs.BuddyManager);

        // Display user state in Buddy List system
        DisplayUserStateAsBuddy();

        // Show/hide buddy list
        ShowBuddyList(sfs.BuddyManager.MyOnlineState);
    }

    /**
     * Show current user state in Buddy List system in footer.
     */
    private void DisplayUserStateAsBuddy()
    {
        string state = sfs.BuddyManager.MyOnlineState ? sfs.BuddyManager.MyState : "Offline";
        profileConfigurator.DisplayUserState(state);
    }

    private void UpdateBuddyListEntry(Buddy buddy)
    {
        buddyList.UpdateListItem(buddy);

        // Also update buddy chat panel
        if (buddyChatPanel.UserName == buddy.Name)
        {
            // Update nickname if changed
            string newBuddyDisplayName = buddy.DisplayName;

            // Get current name displayed in chat
            string currBuddyDisplayName = buddyChatPanel.DisplayName;

            if (newBuddyDisplayName != currBuddyDisplayName)
            {
                buddyChatPanel.PrintSystemMessage(currBuddyDisplayName + " is now known as " + newBuddyDisplayName);
                buddyChatPanel.SetUser(buddy.Name, buddy.Nickname);
            }

            // Enable/disable message input
            buddyChatPanel.Disabled = !buddy.IsOnline || buddy.IsBlocked;
        }
    }

    private void InitBuddyChatPanel(Buddy buddy, List<BuddyChatMessage> msgHistory)
    {
        // Reset panel (it is reused for different buddies)
        buddyChatPanel.Reset();

        // Set buddy name
        buddyChatPanel.SetUser(buddy.Name, buddy.Nickname);

        // Print chat history
        if (msgHistory != null)
            foreach (var messageObj in msgHistory)
                buddyChatPanel.PrintChatMessage(messageObj.Message, messageObj.Sender, messageObj.SentByMe, messageObj.Date);

        // Show chat panel
        buddyChatPanel.SetVisible();

        // Focus on message input
        buddyChatPanel.SetFocus();
    }

    private void StartAudioStream()
    {
        sfs.Send(new StartAudioStreamRequest(isMuted: buddyCallPanel.muteButton.ButtonPressed));
    }

    private void SendPendingInvitation()
    {
        // Show user in call panel
        buddyCallPanel.AddParticipant(pendingInviteUserName);

        // Make sure buddy to invite did not disconnect during the Room create + join process
        if (sfs.BuddyManager.GetBuddyByName(pendingInviteUserName).IsOnline)
        {
            List<string> invitedUsers = [pendingInviteUserName];

            var invParams = new SFSObject();
            invParams.PutString("room", sfs.LastJoinedRoom.Name);

            // Send the request
            sfs.Send(new JoinRoomInvitationRequest(sfs.LastJoinedRoom, invitedUsers, invParams, INVITATION_EXPIRY_SECS));

            Global.Trace("Invitation to call sent to " + pendingInviteUserName);
        }
        else
        {
            HandleBuddyLeftCall(pendingInviteUserName, "Offline");
        }

        // Reset reference
        pendingInviteUserName = null;
    }
    
    private void RefuseInvitation()
    {
        // Stop expiration timer
        inviteExpireTimerSecs = 0f;

        // Refuse invitation
        sfs.Send(new InvitationReplyRequest(activeInvitation, InvitationReply.REFUSE));

        // Remove active invitation
        activeInvitation = null;

        // Hide panel
        callInvitationPanel.Hide();
        callInvitationRingtone.Stop();
    }

    private async Task LeaveCall(int delayMillis)
    {
        if (sfs.LastJoinedRoom != null)
        {
            Global.Trace("Leaving call Room");

            // Disable call panel buttons
            buddyCallPanel.Disabled = true;

            if (sfs.AudioManager.IsStreaming())
                sfs.Send(new StopAudioStreamRequest());
            // Note: actively stopping the stream is not actually needed,
            // as it is done automatically by the SFS API when the Room is left

            // Wait some time
            if (delayMillis > 0)
                await Task.Delay(delayMillis);
            
            // Hide call panel
            buddyCallPanel.SetVisible(false);

            // Reset call panel (empty participants list)
            buddyCallPanel.Reset();

            // Leave Room
            sfs.Send(new LeaveRoomRequest(sfs.LastJoinedRoom));

            // Update state
            callState = CallState.AVAILABLE;
        }
    }

    /**
     * Method called if either a user refused the invitation to a call,
     * or the invitation expired,
     * or they joined the call and left it afterwards
     */
    private void HandleBuddyLeftCall(string userName, string reason)
    {
        // Change list entry status and remove after a few seconds
        buddyCallPanel.RemoveParticipant(userName, reason);

        // If current user is now alone, wait a few seconds then leave room
        if (sfs.LastJoinedRoom?.UserCount == 1)
            LeaveCall(3000);
    }

    private string GenerateRandomString()
    {
        const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        var random = new Random();
        return new string(Enumerable.Repeat(chars, 20)
            .Select(s => s[random.Next(s.Length)]).ToArray());
    }
    #endregion

    //----------------------------------------------------------
    // SmartFoxServer event listeners
    //----------------------------------------------------------
    #region

    private void OnRoomJoin(ApiEvent evt)
    {
        Room room = (Room)evt.Params[EventParam.Room];

        Global.Trace("Call Room joined");

        // Enable call panel buttons
        buddyCallPanel.Disabled = false;

        // Show call panel
        buddyCallPanel.SetVisible(true);

        // Update state
        callState = CallState.IN_PROGRESS;

        // If Room already contains other users, then current user was invited to join.
        // Show other users and start audio stream
        if (room.UserList.Count > 1)
        {
            // Display all other participants
            foreach (User user in room.UserList)
            {
                if (user != sfs.MySelf)
                    buddyCallPanel.AddParticipant(user.Name);
            }

            // Start audio stream
            StartAudioStream();
        }

        // Otherwise send pending invitation
        else
            SendPendingInvitation();
    }

    private void OnRoomRemove(ApiEvent evt)
    {
        Room room = (Room)evt.Params[EventParam.Room];

        // If the removed Room is the one user was invited to join, refuse invitation
        // Note: we can't just discard it and let it expire, because if the sender tries to send another invitation
        // for the same Room, the server prevents it until the previous invitation is expired
        if (activeInvitation != null && room.Name == activeInvitation.Params.GetString("room"))
        {
            Global.Trace("Call Room to which user was invited to was removed; refusing invitation");

            RefuseInvitation();
        }
    }

    private void OnUserEnterRoom(ApiEvent evt)
    {
        Room room = (Room)evt.Params[EventParam.Room];
        User user = (User)evt.Params[EventParam.User];

        // Display participant to call (unless already displayed before, during invitation)
        if (!buddyCallPanel.HasParticipant(user.Name))
            buddyCallPanel.AddParticipant(user.Name);
        
        if (!sfs.AudioManager.IsStreaming())
            StartAudioStream();
    }

    private void OnUserExitRoom(ApiEvent evt)
    {
        Room room = (Room)evt.Params[EventParam.Room];
        User user = (User)evt.Params[EventParam.User];

        if (user != sfs.MySelf)
        {
            Global.Trace("User " + user.Name + " left the call");

            HandleBuddyLeftCall(user.Name, "Call ended");
        }
    }

    private void OnInvitation(ApiEvent evt)
    {
        Invitation invitation = (Invitation)evt.Params[EventParam.Invitation];

        // Invitation is automatically refused if:
        // - call setup is in progress (call state is: PENDING)
        // - user is already participating in a call (call state is: IN_PROGRESS)
        // - user has already received another invitation they have still to reply to (active invitation is not null)

        if (callState != CallState.AVAILABLE || activeInvitation != null)
        {
            // Refuse invitation; this triggers an INVITATION_REPLY event too
            sfs.Send(new InvitationReplyRequest(invitation, InvitationReply.REFUSE));
        }
        else
        {
            activeInvitation = invitation;

            // Update invitation UI
            callInvitationSenderLabel.Text = invitation.Inviter.Name;

            // Run timer to hide the invitation panel when the invitation expires (see _process)
            inviteExpireTimerSecs = invitation.SecondsForAnswer;

            // Show invitation modal
            callInvitationPanel.Show();
            callInvitationRingtone.Play();
        }
    }

    private void OnInvitationReply(ApiEvent evt)
    {
        User invitee = (User)evt.Params[EventParam.Invitee];
        InvitationReply invReply = (InvitationReply)evt.Params[EventParam.Reply];

        if (invReply != InvitationReply.ACCEPT)
        {
            Global.Trace("Invitation to call expired or refused by " + invitee.Name);

            string reason = invReply switch
            {
                InvitationReply.REFUSE => "Occupied",
                _ => "Not available",
            };

            HandleBuddyLeftCall(invitee.Name, reason);
        }
    }
    
    private void OnBuddyListInit(ApiEvent evt)
    {
        Global.Trace("Buddy List System initialized");

        // Initialize buddy-related entities
        InitializeBuddyClient();

        // Initialize audio
        sfs.Send(new InitAudioRequest());
    }

    private void OnBuddyError(ApiEvent evt)
    {
        warningLabel.Text = String.Format("Buddy list system error.\n{0}.", evt.Params[EventParam.ErrorMessage]);
        warningPanel.Show();
    }

    private void OnBuddyOnlineStateChange(ApiEvent evt)
    {
        Buddy buddy = (Buddy)evt.Params[EventParam.Buddy];
        bool isItMe = (bool)evt.Params[EventParam.IsItMe];

        // As this event is fired in case of online state update for both the current user and their buddies,
        // we have to check who the event refers to

        if (!isItMe)
            UpdateBuddyListEntry(buddy);
        else
        {
            DisplayUserStateAsBuddy();

            bool isOnline = sfs.BuddyManager.MyOnlineState;

            // Show/hide buddy list
            ShowBuddyList(isOnline);

            if (!isOnline)
            {
                // Hide chat if current user went offline
                buddyChatPanel.ResetAndHide();
            }
            else
            {
                // Update all buddy items if current user came online
                foreach (Buddy b in sfs.BuddyManager.BuddyList)
                    UpdateBuddyListEntry(b);
            }
        }
    }

    private void OnBuddyVariablesUpdate(ApiEvent evt)
    {
        Buddy buddy = (Buddy)evt.Params[EventParam.Buddy];
        bool isItMe = (bool)evt.Params[EventParam.IsItMe];

        // As this event is fired in case of Buddy Variables update for both the current user and their buddies,
        // we have to check who the event refers to

        if (!isItMe)
            UpdateBuddyListEntry(buddy);
        else
            DisplayUserStateAsBuddy();
    }

    private void OnBuddyBlock(ApiEvent evt)
    {
        Buddy buddy = (Buddy)evt.Params[EventParam.Buddy];

        UpdateBuddyListEntry(buddy);

        // Reset unread messages counter
        BuddyChatHistoryManager.ResetHistory(buddy.Name);
    }

    private void OnBuddyRemove(ApiEvent evt)
    {
        Buddy buddy = (Buddy)evt.Params[EventParam.Buddy];

        buddyList.RemoveListItem(buddy.Name);

        if (buddyChatPanel.UserName == buddy.Name)
            buddyChatPanel.ResetAndHide();

        // Reset unread messages counter and update counter in buddy list tab
        BuddyChatHistoryManager.ResetHistory(buddy.Name);
    }

    private void OnBuddyAdd(ApiEvent evt)
    {
        Buddy buddy = (Buddy)evt.Params[EventParam.Buddy];

        // Add buddy list item at the top of the list
        buddyList.AddListItem(buddy, true);
    }

    private void OnBuddyMessage(ApiEvent evt)
    {
        Buddy sender = (Buddy)evt.Params[EventParam.Buddy];
        bool isItMe = (bool)evt.Params[EventParam.IsItMe];
        string message = (string)evt.Params[EventParam.Message];
        string recipient = ((SFSObject)evt.Params[EventParam.Data]).GetString("recipient");

        string targetBuddyName = isItMe ? recipient : sender.Name;
        string senderDisplayName = isItMe ? sfs.BuddyManager.MyDisplayName : sender.DisplayName;

        // Add message to queue
        var chatMsg = BuddyChatHistoryManager.AddMessage(targetBuddyName, senderDisplayName, isItMe, message);

        // Show message or increase message counter
        if (buddyChatPanel.UserName == chatMsg.Target)
        {
            // Display message in chat panel
            buddyChatPanel.PrintChatMessage(chatMsg.Message, chatMsg.Sender, chatMsg.SentByMe, chatMsg.Date);
        }
        else
        {
            // Increase unread messages count
            // NOTE: there's no need to make sure the sender is not the current user,
            // as in this example the current user can't send buddy messages in other ways than
            // the chat panel (which would make the above if statement true)
            int unreadMsgCnt = BuddyChatHistoryManager.IncreaseUnreadMessagesCount(chatMsg.Target);

            // Update buddy list item
            buddyList.SetItemChatMsgCounter(chatMsg.Target, unreadMsgCnt);
        }
    }

    private void OnAudioInit(ApiEvent evt)
    {
        Global.Trace("Audio Streaming System initialized");

        // Add utility scene node to SFS API
        // This is used if audio mode is set to NATIVE_GODOT only (see LoginController)
        if (!sfs.Config.Audio.UseEmbeddedAudioLib)
            sfs.AudioManager.SetSceneObject(GetNode("SfsNode"));

        // Update devices in profile configurator panel
        profileConfigurator.InitAudioControls(sfs.AudioManager);
    }

    private void OnAudioStreamStart(ApiEvent evt)
    {
        AudioStreamInfo asi = (AudioStreamInfo)evt.GetParam(EventParam.MyAudioStream);

        // Show my audio meter
        buddyCallPanel.ShowMyAudioMeter(asi);

         Global.Trace("My audio stream started");
    }

    private void OnAudioStreamStop(ApiEvent evt)
    {
        Global.Trace("My audio stream stopped");
    }

    private void OnAudioStreamAdd(ApiEvent evt)
    {
        AudioStreamInfo asi = (AudioStreamInfo)evt.GetParam(EventParam.AudioStream);
        
        // Show user audio meter
        buddyCallPanel.UpdateParticipant(asi);

        Global.Trace("Audio stream added; user: " + asi.User);
    }

    private void OnAudioStreamRemove(ApiEvent evt)
    {
        AudioStreamInfo asi = (AudioStreamInfo)evt.GetParam(EventParam.AudioStream);

        Global.Trace("Audio stream removed; user: " + asi.User);
    }

    private void OnToggleMute(ApiEvent evt)
    {
        AudioStreamInfo asi = (AudioStreamInfo)evt.GetParam(EventParam.AudioStream);

        if (asi.IsMuted)
            Global.Trace("Audio stream muted; user: " + asi.User);
        else
            Global.Trace("Audio stream unmuted; user: " + asi.User);
    }
    #endregion
}