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.Game;
using Sfs3.Client.Entities.Match;
using Sfs3.Client.Entities.Invitation;
using Sfs3.Client.Requests.Invitations;

/**
 * Script attached to the main node in the scene.
 */
public partial class LobbyController : BaseController
{
    public static readonly string DEFAULT_ROOM = "Lobby";
    public static readonly string GAME_ROOMS_GROUP_NAME = "games";
    public static readonly string BUDDYVAR_YEAR = SFSBuddyVariable.OFFLINE_PREFIX + "year";
    public static readonly string BUDDYVAR_MOOD = "mood";
    public static readonly string USERVAR_EXPERIENCE = "exp";
    public static readonly string USERVAR_RANKING = "rank";

    public static readonly string EXTENSION_ID = "TicTacToe";
	public static readonly string EXTENSION_CLASS = "sfs3.extensions.examples.tictactoe.TicTacToeExtension";

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

    [Export]
    public ProfileConfigurator profileConfigurator;
    [Export]
    public GameLauncher gameLauncher;
    [Export]
    public PopupDialog warningPanel;
    [Export]
    public Label warningLabel;
    [Export]
    public GameList gameList;
    [Export]
    public Control buddyListContainer;
    [Export]
    public BuddyList buddyList;
    [Export]
    public LineEdit buddyNameInput;
    [Export]
    public ChatPanel buddyChatPanel;
    [Export]
    public PopupDialog invitationPanel;
    [Export]
    public Label invitationMsgLabel;
    [Export]
    public Label invitationDetailsLabel;
    [Export]
    public Label invitationExpireLabel;

    private Queue<InvitationWrapper> invitationQueue;
    private InvitationWrapper activeInvitationWrapper;
    private const string invitationExpireText = "This invitation will expire in {0} second{1}.";
    private float inviteExpireTimerSecs = 0f;
    private float inviteWaitTimerSecs = 0f;

    //----------------------------------------------------------
    // Callback methods
    //----------------------------------------------------------
    #region
    public override void _Ready()
    {
        base._Ready();

        sfs = Global.GetSfsClient();

        // Add SFS event listeners
        AddSmartFoxListeners();

        // Add listeners to events fired by GameLauncher class
        gameLauncher.ShowLauncherAction = OnGameLauncherShown;
        gameLauncher.StartGameAction = OnStartGamePressed;

        // Add listener to join button click event fired by GameList class
        gameList.JoinGameAction = OnJoinGamePressed;

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

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

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

        // Populate list of available games
        var rooms = sfs.RoomManager.GetRoomList();
        gameList.PopulateList(rooms);

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

        // Init or display player profile details
        InitializePlayer();

        // 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
            InitializeBuddyClient();

        // Join default Room
        // An initial Room where to join all users is needed to implement a public chat (not in this example)
        // or to automatically invite users to enter a private game when launched (see OnGameLauncherStart method below)
        // In this example we assume the Room already exists in the static SmartFoxServer configuration
        sfs.Send(new JoinRoomRequest(DEFAULT_ROOM));
    }

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

        if (inviteExpireTimerSecs > 0)
        {
            inviteExpireTimerSecs -= (float)delta;

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

            // Update expire message in invitation panel
            invitationExpireLabel.Text = String.Format(invitationExpireText, timerInt, timerInt == 1 ? "" : "s");

            if (timerInt == 0)
            {
                inviteExpireTimerSecs = 0f;
                invitationPanel.Hide();
            }
        }

        if (inviteWaitTimerSecs > 0)
        {
            inviteWaitTimerSecs -= (float)delta;

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

            if (timerInt == 0)
            {
                inviteWaitTimerSecs = 0f;

                ProcessInvitations();
            }
        }
    }
    #endregion

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

        sfs.Disconnect();
    }

    /**
     * On game launcher panel shown, initialize panel.
     */
    public void OnGameLauncherShown()
    {
        gameLauncher.Init(sfs.BuddyManager.MyOnlineState, profileConfigurator.PlayerExperience, profileConfigurator.PlayerRanking);
    }

    /**
     * On Start button click in game launcher panel, create and join a new Game Room.
     */
    public void OnStartGamePressed(bool isGamePublic, string buddyName)
    {
        StartNewGame(isGamePublic, buddyName);
    }

    /**
     * On Quick join button click, send request to join a random Game Room.
     */
    public void OnQuickJoinButtonClick()
    {
        // Quick join a game in the "games" group among those matching the current user's player profile
        sfs.Send(new QuickJoinGameRequest([GAME_ROOMS_GROUP_NAME], null, sfs.LastJoinedRoom));
    }

    /**
     * On Play or Watch button click in Game List, join an existing game Room as a player or Spectator respectively.
     */
    public void OnJoinGamePressed(int roomId, bool asSpectator)
    {
        sfs.Send(new JoinRoomRequest(roomId, null, null, asSpectator));
    }

    /**
     * On Invitation panel hidden, refuse invitation.
     */
    public void OnInvitationPanelHide()
    {
        // If an invitation is currently active, refuse it
        if (activeInvitationWrapper != null)
            AcceptInvitation(false);
    }

    /**
     * On Accept button click, send reply to invitation.
     */
    public void OnInvitationAcceptClick()
    {
        // Accept active invitation
        AcceptInvitation();

        // Hide invitation panel
        invitationPanel.Hide();
    }

    /**
     * On Refuse button click, hide invitation.
     */
    public void OnInvitationRefuseClick()
    {
        // Hide invitation panel, which triggers refusal
        invitationPanel.Hide();
    }

    /**
     * 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 Invite button click in Buddy List item, start new game inviting buddy.
     */
    public void OnInviteBuddyPressed(string buddyName)
    {
        StartNewGame(false, buddyName);
    }

    /**
     * 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));
    }

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

        List<UserVariable> userVars = [];
        userVars.Add(new SFSUserVariable(varName, value, type));

        // Set User Variables
        sfs.Send(new SetUserVariablesRequest(userVars));
    }

    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));
    }
    #endregion

    //----------------------------------------------------------
    // Helper methods
    //----------------------------------------------------------
    #region
    private void AddSmartFoxListeners()
    {
        sfs.AddEventListener(SFSEvent.ROOM_CREATION_ERROR, OnRoomCreationError);
        sfs.AddEventListener(SFSEvent.ROOM_ADD, OnRoomAdded);
        sfs.AddEventListener(SFSEvent.ROOM_REMOVE, OnRoomRemoved);
        sfs.AddEventListener(SFSEvent.USER_COUNT_CHANGE, OnUserCountChanged);
        sfs.AddEventListener(SFSEvent.ROOM_JOIN, OnRoomJoin);
        sfs.AddEventListener(SFSEvent.ROOM_JOIN_ERROR, OnRoomJoinError);
        sfs.AddEventListener(SFSEvent.INVITATION, OnInvitation);

        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);
    }

    public override void RemoveSmartFoxListeners()
    {
        if (sfs != null)
        {
            sfs.RemoveEventListener(SFSEvent.ROOM_CREATION_ERROR, OnRoomCreationError);
            sfs.RemoveEventListener(SFSEvent.ROOM_ADD, OnRoomAdded);
            sfs.RemoveEventListener(SFSEvent.ROOM_REMOVE, OnRoomRemoved);
            sfs.RemoveEventListener(SFSEvent.USER_COUNT_CHANGE, OnUserCountChanged);
            sfs.RemoveEventListener(SFSEvent.ROOM_JOIN, OnRoomJoin);
            sfs.RemoveEventListener(SFSEvent.ROOM_JOIN_ERROR, OnRoomJoinError);
            sfs.RemoveEventListener(SFSEvent.INVITATION, OnInvitation);

            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);
        }
    }

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

    private void InitializePlayer()
    {
        // IMPORTANT NOTE
        // The Experience and Ranking details are custom parameters used in this example to show how to filter players through SmartFoxServer's matchmaking system
        // when sending invitations to play a game. Of course they (or any other parameter with a similar purpose) shouldn't be set manually, but they should be
        // determined by the game logic, saved in a database and set in User Variables by a server-side Extension on user login.
        // In this example, for sake of simplicity, such details are set to default values when the page is loaded and they are lost on user logout.

        // Check if player details are set in User Variables
        if (sfs.MySelf.GetVariable(USERVAR_EXPERIENCE) == null || sfs.MySelf.GetVariable(USERVAR_RANKING) == null)
        {
            // If not, set player profile default values
            var expVar = new SFSUserVariable(USERVAR_EXPERIENCE, profileConfigurator.PlayerExperience, VariableType.STRING);
            var rankVar = new SFSUserVariable(USERVAR_RANKING, profileConfigurator.PlayerRanking, VariableType.SHORT);

            sfs.Send(new SetUserVariablesRequest([expVar, rankVar]));

            // Note that the LobbyController doesn't listen to the USER_VARIABLES_UPDATE event.
            // This is because the above variables are used for the matchmaking only,
            // and theres no actual server-side Extension updating them based on the game logic.
        }
    }

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

            // Also update buddy list in game launcher panel
            gameLauncher.UpdateInviteList(buddy);
        }

        // 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);

        // Update buddy in invite list in game launcher panel
        gameLauncher.UpdateInviteList(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();
    }

    /**
     * On:
     * - Start game button click on the Start game panel, or
     * - Invite buddy button click on a Buddy List Item prefab instance,
     * create and join a new game Room.
     */
    private void StartNewGame(bool isPublic, string buddyName)
    {
        // Configure Room
        string roomName = sfs.MySelf.Name + "'s game";

        var settings = new SFSGameSettings(roomName);
        settings.GroupId = GAME_ROOMS_GROUP_NAME;
        settings.MaxUsers = 2;
        settings.MaxSpectators = 10;
        settings.MinPlayersToStartGame = 2;
        settings.IsPublic = isPublic;
        settings.LeaveLastJoinedRoom = true;
        settings.NotifyGameStarted = false;
        settings.Extension = new RoomExtension(EXTENSION_ID, EXTENSION_CLASS);

        // Additional settings specific to private games
        if (!isPublic) // This check is actually redundant: if the game is public, the invitation-related settings are ignored
        {
            // Invite a buddy
            if (buddyName != null)
                settings.InvitedPlayers = [
                    sfs.BuddyManager.GetBuddyByName(buddyName)
                ];

            // Search the "default" group, which in this example contains the static default Room only (Lobby)
            settings.SearchableRooms = ["default"];

            // Additional invitation parameters
            var invParams = new SFSObject();
            invParams.PutString("room", roomName);
            invParams.PutString("message", gameLauncher.InvitationMessage);

            settings.InvitationParams = invParams;
        }

        // Define players match expression to locate the users to invite
        var matchExp = new MatchExpression(USERVAR_EXPERIENCE, StringMatch.EQUALS, sfs.MySelf.GetVariable(USERVAR_EXPERIENCE).Value);
        matchExp.And(USERVAR_RANKING, NumberMatch.GREATER_THAN_OR_EQUAL_TO, sfs.MySelf.GetVariable(USERVAR_RANKING).Value);

        settings.PlayerMatchExpression = matchExp;

        // Request Room creation to server
        sfs.Send(new CreateSFSGameRequest(settings));
    }

    /**
	 * Process the invitation in queue, displaying the invitation accept/refuse panel.
	 */
    private void ProcessInvitations()
    {
        // If there's an active invitation, then the user is already dealing with it

        // Otherwise we can go on with the processing
        if (activeInvitationWrapper == null)
        {
            while (invitationQueue.Count > 0)
            {
                // Get next invitation wrapper object in queue
                var iw = invitationQueue.Dequeue();

                // Evaluate remaining time for replying
                var now = DateTime.Now;
                int secondsDifference = (int)Math.Floor((now - iw.Date).TotalSeconds);

                // Update expiration time
                iw.ExpiresInSeconds -= secondsDifference;

                // Display invitation only if expiration will occur in 3 seconds or more, otherwise discard it
                if (iw.ExpiresInSeconds >= 3)
                {
                    // Set active invitation
                    activeInvitationWrapper = iw;

                    // Update invitation UI
                    invitationMsgLabel.Text = iw.Invitation.Params.GetString("message");
                    invitationMsgLabel.Visible = invitationMsgLabel.Text != "";

                    invitationDetailsLabel.Text = String.Format("You have been invited by {0} to play {1}.", iw.Invitation.Inviter.Name, iw.Invitation.Params.GetString("room"));
                    invitationExpireLabel.Text = String.Format(invitationExpireText, iw.ExpiresInSeconds, iw.ExpiresInSeconds == 1 ? "" : "s");

                    // Run timer to auto-refuse invitation (see _process)
                    inviteExpireTimerSecs = activeInvitationWrapper.ExpiresInSeconds;

                    // Show invitation modal
                    invitationPanel.Visible = true;
                    break;
                }
            }
        }
    }
    
    private void AcceptInvitation(bool accept = true)
    {
        // Stop expiration timer
        inviteExpireTimerSecs = 0f;
        
        // Retrieve invitation
        var invitation = activeInvitationWrapper.Invitation;

        // Remove active invitation
        activeInvitationWrapper = null;

        // Accept/refuse invitation
        sfs.Send(new InvitationReplyRequest(invitation, accept ? InvitationReply.ACCEPT : InvitationReply.REFUSE));

        // If invitation was accepted, refuse all remaining invitations in the queue
        if (accept)
        {
            // Refuse other invitations
            foreach (var iw in invitationQueue)
                sfs.Send(new InvitationReplyRequest(iw.Invitation, InvitationReply.REFUSE));

            // Reset queue
            invitationQueue = null;
        }

        // If invitation was refused, run timer and then process next invitation in the queue (if any)
        // We wait a second to make it clear that the previous invitation was refused before the next one (if any) is displayed
        else
        {
            inviteWaitTimerSecs = 1f;
        }
    }
    #endregion

    //----------------------------------------------------------
    // SmartFoxServer event listeners
    //----------------------------------------------------------
    #region
    private void OnRoomCreationError(ApiEvent evt)
    {
        // Show warning
        warningLabel.Text = String.Format("Game Room creation failed.\n{0}.", evt.Params[EventParam.ErrorMessage]);
        warningPanel.Show();
    }

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

        // Display game list item
        gameList.AddListItem(room);
    }

    public void OnRoomRemoved(ApiEvent evt)
    {
        Room room = (Room)evt.Params[EventParam.Room];

        // Remove game list item
        gameList.RemoveListItem(room.Id);
    }

    public void OnUserCountChanged(ApiEvent evt)
    {
        Room room = (Room)evt.Params[EventParam.Room];

        // Update game list item
        gameList.SetItemState(room.Id);
    }

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

        // If a Game Room was joined, go to the game view, otherwise ignore this event
        if (room.IsGame)
        {
            // Set state to "Away" in Buddy List system
            if (sfs.BuddyManager.MyOnlineState)
            {
                List<BuddyVariable> buddyVars = [];
                buddyVars.Add(new SFSBuddyVariable(ReservedBuddyVariables.BV_STATE, "Away", VariableType.STRING));
                sfs.Send(new SetBuddyVariablesRequest(buddyVars));
            }

            // Load game scene
            GetTree().ChangeSceneToFile("scenes/Game.tscn");
        }
    }

    private void OnRoomJoinError(ApiEvent evt)
    {
        // Show warning
        warningLabel.Text = String.Format("Room join failed.\n{0}.", evt.Params[EventParam.ErrorMessage]);
        warningPanel.Show();
    }

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

        // Create a queue of invitations
        // We use a list because a user could receive multiple invitations within the time it takes to accept or refuse one
        if (invitationQueue == null)
            invitationQueue = [];

        // Add an object wrapping the invitation to the queue
        invitationQueue.Enqueue(new InvitationWrapper(invitation, invitation.SecondsForAnswer));

        // Trigger invitation processing
        ProcessInvitations();
    }

    private void OnBuddyListInit(ApiEvent evt)
    {
        // Initialize buddy-related entities
        InitializeBuddyClient();
    }

    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);

        // Update buddy in invite list in game launcher panel
        gameLauncher.UpdateInviteList(buddy, true);

        // Hide buddy chat
        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);

        // Add buddy to invite list ingame launcher panel
        gameLauncher.UpdateInviteList(buddy);
    }

    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);
        }
    }
    #endregion
}

internal class InvitationWrapper
{
    public Invitation Invitation;
    public int ExpiresInSeconds;
    public DateTime Date;

    public InvitationWrapper(Invitation invitation, int expiresInSeconds)
    {
        Invitation = invitation;
        ExpiresInSeconds = expiresInSeconds;
        Date = DateTime.Now;
    }

}