import * as bootstrap from 'bootstrap';
import BaseController from './base-controller.js';
import GameLauncher from '../components/game-launcher.js';
import ProfileConfigurator from '../components/profile-configurator.js';
import BuddyChatHistoryManager from '../managers/buddy-chat-history-manager.js';
import GameList from '../components/game-list.js';
import BuddyList from '../components/buddy-list.js';
import ChatPanel from '../components/chat-panel.js';
import { SFSObject, SFSEvent, SFSBuddyEvent, EventParam, SFSUserVariable, SFSBuddyVariable, VariableType, ReservedBuddyVariables, InvitationReply } from 'sfs3-api';
import { SFSGameSettings, CreateSFSGameRequest, JoinRoomRequest, SetUserVariablesRequest, QuickJoinGameRequest, MatchExpression, StringMatch, NumberMatch, InvitationReplyRequest } from 'sfs3-api';
import { InitBuddyListRequest, AddBuddyRequest, RemoveBuddyRequest, BlockBuddyRequest, GoOnlineRequest, SetBuddyVariablesRequest, BuddyMessageRequest } from 'sfs3-api';

export default class LobbyController extends BaseController
{
    static DEFAULT_ROOM = 'Lobby';
    static GAME_ROOMS_GROUP_NAME = 'games';
    static BUDDYVAR_YEAR = SFSBuddyVariable.OFFLINE_PREFIX + 'year';
    static BUDDYVAR_MOOD = 'mood';
    static USERVAR_EXPERIENCE = 'exp';
	static USERVAR_RANKING = 'rank';

    constructor(app, view, name)
    {
        super(app, view, name);

        //------------------------------------------------------

        this.logoutButton = view.querySelector('#logoutButton');
        this.profileConfigurator = view.querySelector('#profileConfigurator');
        this.gameList = view.querySelector('#gameList');
        this.newGameButton = view.querySelector('#newGameButton');
        this.gameLauncher = view.querySelector('#gameLauncher');
        this.quickJoinGameButton = view.querySelector('#quickJoinGameButton');
        this.buddyList = view.querySelector('#buddyList');
        this.addBuddyForm = view.querySelector('#addBuddyForm');
        this.buddyNameInput = view.querySelector('#buddyNameInput');
        this.buddyListTab = view.querySelector('#buddyListTab')
        this.buddyListPane = view.querySelector('#buddyListPane')
        this.buddyChatTab = view.querySelector('#buddyChatTab')
        this.buddyChatPane = view.querySelector('#buddyChatPane');
        this.buddyChatPanel = view.querySelector('#buddyChatPanel');
        this.invitationMsg = view.querySelector('#invitationMsg');
        this.invitationSender = view.querySelector('#invitationSender');
        this.invitationRoom = view.querySelector('#invitationRoom');
        this.invitationExpireLabel = view.querySelector('#invitationExpireLabel');
        this.invitationAcceptButton = view.querySelector('#invitationAcceptButton');

        // Create invitation modal
        this.invitationModalEl = view.querySelector('#invitationModal');
        this.invitationModal = new bootstrap.Modal(this.invitationModalEl, {
            backdrop: true,
            focus: true,
            keyboard: true
        });
    }

    //----------------------------------------------------------
    // Overridden methods
    //----------------------------------------------------------

    initialize()
    {
        super.initialize();

        // Add logout button click listener
        this.boundOnLogoutButtonClick = this.onLogoutButtonClick.bind(this);
        this.logoutButton.addEventListener('click', this.boundOnLogoutButtonClick);

        // Add listeners to events fired by GameLauncher class
        this.boundOnGameLauncherShow = this.onGameLauncherShow.bind(this);
        this.gameLauncher.addEventListener(GameLauncher.SHOW_EVT, this.boundOnGameLauncherShow);

        this.boundOnStartGameClick = this.onStartGameClick.bind(this);
        this.gameLauncher.addEventListener(GameLauncher.START_CLICK_EVT, this.boundOnStartGameClick);

        // Add "quick join" button click listener
        this.boundOnQuickJoinButtonClick = this.onQuickJoinButtonClick.bind(this);
        this.quickJoinGameButton.addEventListener('click', this.boundOnQuickJoinButtonClick);

        // Add listener to join button click event fired by GameList class
        this.boundOnJoinGameClickEvent = this.onJoinGameClickEvent.bind(this);
        this.gameList.addEventListener(GameList.JOIN_CLICK_EVT, this.boundOnJoinGameClickEvent);

        // Add listeners to events fired by BuddyList class
        this.boundOnBuddyChatClickEvent = this.onBuddyChatClickEvent.bind(this);
        this.buddyList.addEventListener(BuddyList.CHAT_CLICK_EVT, this.boundOnBuddyChatClickEvent);

        this.boundOnBuddyInviteClickEvent = this.onBuddyInviteClickEvent.bind(this);
        this.buddyList.addEventListener(BuddyList.INVITE_CLICK_EVT, this.boundOnBuddyInviteClickEvent);

        this.boundOnBuddyBlockClickEvent = this.onBuddyBlockClickEvent.bind(this);
        this.buddyList.addEventListener(BuddyList.BLOCK_CLICK_EVT, this.boundOnBuddyBlockClickEvent);

        this.boundOnBuddyRemoveClickEvent = this.onBuddyRemoveClickEvent.bind(this);
        this.buddyList.addEventListener(BuddyList.REMOVE_CLICK_EVT, this.boundOnBuddyRemoveClickEvent);

        this.boundOnBuddyAddClickEvent = this.onBuddyAddClickEvent.bind(this);
        this.buddyList.addEventListener(BuddyList.ADD_CLICK_EVT, this.boundOnBuddyAddClickEvent);

        // Add listeners to events fired by ProfileConfigurator class
        this.boundOnOnlineToggleChangeEvent = this.onOnlineToggleChangeEvent.bind(this);
        this.profileConfigurator.addEventListener(ProfileConfigurator.ONLINE_CHANGE_EVT, this.boundOnOnlineToggleChangeEvent);

        this.boundOnBuddySettingChangeEvent = this.onBuddySettingChangeEvent.bind(this);
        this.profileConfigurator.addEventListener(ProfileConfigurator.BUDDY_SETTING_CHANGE_EVT, this.boundOnBuddySettingChangeEvent);

        this.boundOnPlayerSettingChangeEvent = this.onPlayerSettingChangeEvent.bind(this);
        this.profileConfigurator.addEventListener(ProfileConfigurator.PLAYER_SETTING_CHANGE_EVT, this.boundOnPlayerSettingChangeEvent);

        // Add buddy form submit listener
        this.boundOnAddBuddyFormSubmit = this.onAddBuddyFormSubmit.bind(this);
        this.addBuddyForm.addEventListener('submit', this.boundOnAddBuddyFormSubmit);
        
        // Add listeners to events fired by ChatPanel class
        this.boundOnBuddyChatPanelCloseClick = this.onBuddyChatPanelCloseClick.bind(this);
        this.buddyChatPanel.addEventListener(ChatPanel.CLOSE_CLICK_EVT, this.boundOnBuddyChatPanelCloseClick);

        this.boundOnBuddyChatPanelMsgSubmit = this.onBuddyChatPanelMsgSubmit.bind(this);
        this.buddyChatPanel.addEventListener(ChatPanel.MESSAGE_SUBMIT_EVT, this.boundOnBuddyChatPanelMsgSubmit);

        // Add buddy chat tab show listener
        this.boundOnBuddyChatTabShow = this.onBuddyChatTabShow.bind(this);
        this.buddyChatTab.addEventListener('shown.bs.tab', this.boundOnBuddyChatTabShow);

        // Add listener to invitation modal hide and accept button click
        this.boundOnInvitationModalHide = this.onInvitationModalHide.bind(this);
        this.invitationModalEl.addEventListener('hidden.bs.modal', this.boundOnInvitationModalHide);

        this.boundOnInvitationAcceptClick = this.onInvitationAcceptClick.bind(this);
        this.invitationAcceptButton.addEventListener('click', this.boundOnInvitationAcceptClick);

        // Add SFS event listeners
        this.sfs.addEventListener(SFSEvent.ROOM_CREATION_ERROR, this.onRoomCreationError, this);
        this.sfs.addEventListener(SFSEvent.ROOM_ADD, this.onRoomAdded, this);
        this.sfs.addEventListener(SFSEvent.ROOM_REMOVE, this.onRoomRemoved, this);
        this.sfs.addEventListener(SFSEvent.USER_COUNT_CHANGE, this.onUserCountChanged, this);
        this.sfs.addEventListener(SFSEvent.ROOM_JOIN, this.onRoomJoin, this);
        this.sfs.addEventListener(SFSEvent.ROOM_JOIN_ERROR, this.onRoomJoinError, this);
        this.sfs.addEventListener(SFSEvent.INVITATION, this.onInvitation, this);

		this.sfs.addEventListener(SFSBuddyEvent.BUDDY_LIST_INIT, this.onBuddyListInit, this);
		this.sfs.addEventListener(SFSBuddyEvent.BUDDY_ERROR, this.onBuddyError, this);
		this.sfs.addEventListener(SFSBuddyEvent.BUDDY_ONLINE_STATE_CHANGE, this.onBuddyOnlineStateChange, this);
		this.sfs.addEventListener(SFSBuddyEvent.BUDDY_VARIABLES_UPDATE, this.onBuddyVariablesUpdate, this);
		this.sfs.addEventListener(SFSBuddyEvent.BUDDY_BLOCK, this.onBuddyBlock, this);
		this.sfs.addEventListener(SFSBuddyEvent.BUDDY_REMOVE, this.onBuddyRemove, this);
		this.sfs.addEventListener(SFSBuddyEvent.BUDDY_ADD, this.onBuddyAdd, this);
		this.sfs.addEventListener(SFSBuddyEvent.BUDDY_MESSAGE, this.onBuddyMessage, this);
        
        // Populate list of available games
        const rooms = this.sfs.roomManager.getRoomList();
        this.gameList.populateList(rooms);

        // Display user name in profile modal
        this.profileConfigurator.displayUserName(this.sfs.mySelf.name);

        // Init or display player profile details
		this.initializePlayer();

        // Hide buddy list & chat panes
        this.showBuddyList(false);
        this.showBuddyChat(false);

        // Show unread chats counter
        this.updateUnreadBuddyChatsTabCounter();

        // Activate games tab
        bootstrap.Tab.getOrCreateInstance(this.view.querySelector(`#gamesTab`)).show();

        // Initialize Buddy List system
        // We have to check if it was already initialized if this scene is relaoded after leaving the Game scene
        if (!this.sfs.buddyManager.isInited)
            this.sfs.send(new InitBuddyListRequest());
        else
            this.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
		this.sfs.send(new JoinRoomRequest(LobbyController.DEFAULT_ROOM));

        // Show view
        this.showView();
    }

    destroy()
    {
        super.destroy(); // This also calls removeSmartFoxListeners method!

        // Remove logout button click listener
        this.logoutButton.removeEventListener('click', this.boundOnLogoutButtonClick);

        // Remove listeners to events fired by GameLauncher class
        this.gameLauncher.removeEventListener(GameLauncher.SHOW_EVT, this.boundOnGameLauncherShow);
        this.gameLauncher.removeEventListener(GameLauncher.START_CLICK_EVT, this.boundOnStartGameClick);

        // Remove "quick join" button click listener
        this.quickJoinGameButton.removeEventListener('click', this.boundOnQuickJoinButtonClick);

        // Remove buddy form submit listener
        this.addBuddyForm.addEventListener('submit', this.boundOnAddBuddyFormSubmit);

        // Remove listener to join button click event fired by GameList class
        this.gameList.removeEventListener(GameList.JOIN_CLICK_EVT, this.boundOnJoinGameClickEvent);

        // Remove listeners to events fired by BuddyList class
        this.buddyList.removeEventListener(BuddyList.CHAT_CLICK_EVT, this.boundOnBuddyChatClickEvent);
        this.buddyList.removeEventListener(BuddyList.INVITE_CLICK_EVT, this.boundOnBuddyInviteClickEvent);
        this.buddyList.removeEventListener(BuddyList.BLOCK_CLICK_EVT, this.boundOnBuddyBlockClickEvent);
        this.buddyList.removeEventListener(BuddyList.REMOVE_CLICK_EVT, this.boundOnBuddyRemoveClickEvent);
        this.buddyList.removeEventListener(BuddyList.ADD_CLICK_EVT, this.boundOnBuddyAddClickEvent);

        // Remove listeners to events fired by ProfileConfigurator class
        this.profileConfigurator.removeEventListener(ProfileConfigurator.ONLINE_CHANGE_EVT, this.boundOnOnlineToggleChangeEvent);
        this.profileConfigurator.removeEventListener(ProfileConfigurator.BUDDY_SETTING_CHANGE_EVT, this.boundOnBuddySettingChangeEvent);
        this.profileConfigurator.removeEventListener(ProfileConfigurator.PLAYER_SETTING_CHANGE_EVT, this.boundOnPlayerSettingChangeEvent);

        // Remove listeners to events fired by ChatPanel class
        this.buddyChatPanel.removeEventListener(ChatPanel.CLOSE_CLICK_EVT, this.boundOnBuddyChatPanelCloseClick);
        this.buddyChatPanel.removeEventListener(ChatPanel.MESSAGE_SUBMIT_EVT, this.boundOnBuddyChatPanelMsgSubmit);

        // Remove buddy chat tab show listener
        this.buddyChatTab.removeEventListener('shown.bs.tab', this.boundOnBuddyChatTabShow);

        // Remove listener to invitation modal hide and accept button click
        this.invitationModalEl.removeEventListener('hidden.bs.modal', this.boundOnInvitationModalHide);
        this.invitationAcceptButton.removeEventListener('click', this.boundOnInvitationAcceptClick);

        // Reset unread message counter
        this.updateUnreadBuddyMsgTabCounter(true);

        // Reset game list
        this.gameList.reset();

        // Reset buddy list
        this.buddyList.reset();
        
        // Reset chat panel
        this.buddyChatPanel.reset();

        this.invitationQueue = null;
    }

    removeSmartFoxListeners()
    {
        if (this.sfs != null)
        {
            this.sfs.removeEventListener(SFSEvent.ROOM_CREATION_ERROR, this.onRoomCreationError);
            this.sfs.removeEventListener(SFSEvent.ROOM_ADD, this.onRoomAdded);
            this.sfs.removeEventListener(SFSEvent.ROOM_REMOVE, this.onRoomRemoved);
            this.sfs.removeEventListener(SFSEvent.USER_COUNT_CHANGE, this.onUserCountChanged);
            this.sfs.removeEventListener(SFSEvent.ROOM_JOIN, this.onRoomJoin);
            this.sfs.removeEventListener(SFSEvent.ROOM_JOIN_ERROR, this.onRoomJoinError);
            this.sfs.removeEventListener(SFSEvent.INVITATION, this.onInvitation, this);

			this.sfs.removeEventListener(SFSBuddyEvent.BUDDY_LIST_INIT, this.onBuddyListInit);
			this.sfs.removeEventListener(SFSBuddyEvent.BUDDY_ERROR, this.onBuddyError);
			this.sfs.removeEventListener(SFSBuddyEvent.BUDDY_ONLINE_STATE_UPDATE, this.onBuddyOnlineStateChange);
			this.sfs.removeEventListener(SFSBuddyEvent.BUDDY_VARIABLES_UPDATE, this.onBuddyVariablesUpdate);
			this.sfs.removeEventListener(SFSBuddyEvent.BUDDY_BLOCK, this.onBuddyBlock);
			this.sfs.removeEventListener(SFSBuddyEvent.BUDDY_REMOVE, this.onBuddyRemove);
			this.sfs.removeEventListener(SFSBuddyEvent.BUDDY_ADD, this.onBuddyAdd);
			this.sfs.removeEventListener(SFSBuddyEvent.BUDDY_MESSAGE, this.onBuddyMessage);
        }
    }

    //----------------------------------------------------------
    // Other methods
    //----------------------------------------------------------

    showBuddyList(show)
    {
        // Show/hide buddy list tab and pane
        this.buddyListTab.hidden = !show;
        this.buddyListPane.hidden = !show;
    }

    showBuddyChat(show, buddy = null)
    {
        // Show/hide buddy list tab and pane
        this.buddyChatTab.hidden = !show;
        this.buddyChatPane.hidden = !show;

        if (show)
        {
            // Show buddy nick/name in tab
            const displayName = buddy.nickname ? buddy.nickname : buddy.name;
            this.view.querySelector(`#buddyChatTabLabel`).innerText = displayName;

            // Activate tab
            bootstrap.Tab.getOrCreateInstance(this.buddyChatTab).show();
        }
        else
        {
            // Remove buddy nick/name from tab
            this.view.querySelector(`#buddyChatTabLabel`).innerHTML = '';

            // Activate buddy list tab
            bootstrap.Tab.getOrCreateInstance(this.buddyListTab).show();
        }

        // Reset name in tag attribute
        this.buddyChatPanel.dataset.buddyName = (buddy != null ? buddy.name : '');
    }

    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 (this.sfs.mySelf.getVariable(LobbyController.USERVAR_EXPERIENCE) == null || this.sfs.mySelf.getVariable(LobbyController.USERVAR_RANKING) == null)
        {
            // If not, set player profile default values
            const expVar = new SFSUserVariable(LobbyController.USERVAR_EXPERIENCE, this.profileConfigurator.playerExperience, VariableType.STRING);
            const rankVar = new SFSUserVariable(LobbyController.USERVAR_RANKING, this.profileConfigurator.playerRanking, VariableType.SHORT);

            this.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.
        }
    }

    initializeBuddyClient()
    {
        // Display buddy list items
        for (let buddy of this.sfs.buddyManager.buddyList)
        {
            // All blocked buddies are displayed at the bottom of the list
            this.buddyList.addListItem(buddy, !buddy.isBlocked);

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

        // Set current user details in Buddy List system
        this.profileConfigurator.initBuddyProfile(this.sfs.buddyManager);

        // Display user state in Buddy List system
        this.displayUserStateAsBuddy();

        // Show/hide buddy list
        this.showBuddyList(this.sfs.buddyManager.myOnlineState);
    }

    /**
     * Show current user state in Buddy List system in footer.
     */
    displayUserStateAsBuddy()
    {
        const state = this.sfs.buddyManager.myOnlineState ? this.sfs.buddyManager.myState : 'Offline';
        this.profileConfigurator.displayUserState(state);
    }

    updateBuddyListEntry(buddy)
    {
        this.buddyList.updateListItem(buddy);

        // Update buddy in invite list in game launcher panel
		this.gameLauncher.updateInviteList(buddy);

        // Update buddy chat panel
        if (this.buddyChatPanel.dataset.buddyName == buddy.name)
            this.setBuddyChatPanelState(buddy);
    }

    initBuddyChatPanel(buddy, msgHistory)
    {
        // Set buddy name
        this.buddyChatPanel.dataset.buddyName = buddy.name;

        // Configure chat panel
        this.setBuddyChatPanelState(buddy);

        // Clear previous chat messages (panel is reused for different buddies)
        this.buddyChatPanel.reset();

        // Print chat history
        if (msgHistory != null)
            for (let messageObj of msgHistory)
                this.buddyChatPanel.printChatMessage(messageObj.message, messageObj.sender, messageObj.sentByMe, messageObj.date);

        // Show chat pane also setting buddy name in tag attribute
        this.showBuddyChat(true, buddy);

        // Focus on message input
        this.buddyChatPanel.setFocus();
    }
    
    /**
     * Update buddy chat panel based on the state of the buddy the current user is chatting with.
     */
    setBuddyChatPanelState(buddy)
    {
        // Nickname
        const newBuddyDisplayName = buddy.displayName;

        // Get current name displayed in chat
        const currBuddyDisplayName = this.buddyChatPanel.getAttribute('with');

        if (newBuddyDisplayName != currBuddyDisplayName)
        {
            this.buddyChatPanel.printSystemMessage(`${currBuddyDisplayName} is now known as ${newBuddyDisplayName}`);
            this.buddyChatPanel.setAttribute('with', newBuddyDisplayName);
        }

        // Enable/disable message input
        this.buddyChatPanel.setAttribute('disabled', !buddy.isOnline || buddy.isBlocked);
    }

    updateUnreadBuddyChatsTabCounter()
    {
        let value = BuddyChatHistoryManager.getUnreadChatsCount();

        // Set value
        this.view.querySelector('#unreadBuddyChatsCnt').innerText = value;

        // If value is 0, hide badge
        this.view.querySelector('#unreadBuddyChatsContainer').hidden = (value == 0);
    }

    updateUnreadBuddyMsgTabCounter(forceReset = false)
    {
        const counterNode = this.view.querySelector('#unreadBuddyMsgCnt');

        let value = 0;

        // If buddy chat is visible, always set number of unread messages to zero, otherwise increase current value
        const isChatVisible = this.buddyChatPane.checkVisibility();
        if (!isChatVisible && !forceReset)
        {
            // Get current value and increase it by one
            value = Number(counterNode.innerText) + 1;
        }

        // Set value
        counterNode.innerText = value;

        // If value is 0, hide badge
        this.view.querySelector('#unreadBuddyMsgContainer').hidden = (value == 0);
    }

    /**
     * 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.
     */
    startNewGame(isPublic, buddyName)
    {
        // Configure Room
        const roomName = this.sfs.mySelf.name + "'s game";

        const settings = new SFSGameSettings(roomName);
        settings.groupId = LobbyController.GAME_ROOMS_GROUP_NAME;
        settings.maxUsers = 2;
        settings.maxSpectators = 10;
        settings.minPlayersToStartGame = 2;
        settings.isPublic = isPublic;
        settings.leaveLastJoinedRoom = true;
        settings.notifyGameStarted = false;

        // 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 = [
                    this.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
            const invParams = new SFSObject();
            invParams.putString('room', roomName);
            invParams.putString('message', this.gameLauncher.invitationMessage);

            settings.invitationParams = invParams;
        }

        // Define players match expression to locate the users to invite
        const matchExp = new MatchExpression(LobbyController.USERVAR_EXPERIENCE, StringMatch.EQUALS, this.sfs.mySelf.getVariable(LobbyController.USERVAR_EXPERIENCE).value);
        matchExp.and(LobbyController.USERVAR_RANKING, NumberMatch.GREATER_THAN_OR_EQUAL_TO, this.sfs.mySelf.getVariable(LobbyController.USERVAR_RANKING).value);
        
        settings.playerMatchExpression = matchExp;

        // Request Room creation to server
        this.sfs.send(new CreateSFSGameRequest(settings));
    }
    
    /**
	 * Process the invitation in queue, displaying the invitation accept/refuse panel.
	 */
	processInvitations()
    {
        // If there's an active invitation, then the user is already dealing with it

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

                // Evaluate remaining time for replying
                const now = new Date();
                const secondsDifference = Math.floor((now.getTime() - iw.date.getTime()) / 1000);

                // 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
                    this.activeInvitationWrapper = iw;

                    // Update invitation UI
                    this.invitationMsg.innerText = iw.invitation.params.getString('message');
                    this.invitationMsg.hidden = this.invitationMsg.innerText == '';

                    this.invitationSender.innerText = iw.invitation.inviter.name;
                    this.invitationRoom.innerText = iw.invitation.params.getString('room');
                    this.invitationExpireLabel.innerText = iw.expiresInSeconds;

                    // Launch interval
                    this.invitationInterval = setInterval(() => {
                        // Update and display expiration seconds
                        this.activeInvitationWrapper.expiresInSeconds--;
                        const secs = this.activeInvitationWrapper.expiresInSeconds;
                        this.invitationExpireLabel.innerText = secs;

                        // Hide invitation (which refuses the invitation)
                        if (secs <= 0)
                            this.invitationModal.hide();
                    }, 1000);

                    // Show invitation modal
                    this.invitationModal.show();
                    break;
                }
            }
        }
    }

    acceptInvitation(accept = true)
    {
        // Stop expiration interval
        clearInterval(this.invitationInterval);

        // Retrieve invitation
        const invitation = this.activeInvitationWrapper.invitation;

        // Remove active invitation
        this.activeInvitationWrapper = null;

        // Accept/refuse invitation
        this.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
            for (let iw of this.invitationQueue)
                this.sfs.send(new InvitationReplyRequest(iw.invitation, InvitationReply.REFUSE));

            // Reset queue
            this.invitationQueue = null;
        }

        // If invitation was refused, process next invitation in the queue (if any)
        else
        {
            // Wait less than half a second to make it clear that the previous invitation was refused
            // before the next one (if any) is displayed
            setTimeout(() => {
                this.processInvitations();
            }, 250);
        }
    }

    //----------------------------------------------------------
    // UI event listeners
    //----------------------------------------------------------

    /**
     * On Logout button click, disconnect from SmartFoxServer.
     * This causes the SmartFox listeners added by this scene to be removed (see BaseController.destroy method)
     * and the Login scene to be loaded (see Application.onConnectionLost listener).
     */
	onLogoutButtonClick(e)
	{
        // Reset buddy chat history
        BuddyChatHistoryManager.resetAllHistory();

		// Disconnect from SmartFoxServer
        this.sfs.disconnect();
	}

    /**
     * On game launcher panel show, initialize panel.
     */
    onGameLauncherShow(e)
    {
        this.gameLauncher.init(this.sfs.buddyManager.myOnlineState, this.profileConfigurator.playerExperience, this.profileConfigurator.playerRanking);
    }

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

    /**
     * On Start button click in game launcher panel, create and join a new Game Room.
     */
    onStartGameClick(e)
    {
        const isGamePublic = e.detail.isPublic;
        const buddyName = e.detail.buddyName;

        this.startNewGame(isGamePublic, buddyName);
    }

    /**
     * On Play or Watch button click in Game List, join an existing game Room as a player or Spectator respectively.
     */
    onJoinGameClickEvent(e)
    {
        // Join game Room
        this.sfs.send(new JoinRoomRequest(e.detail.roomId, null, null, e.detail.asSpectator));
    }

    onInvitationModalHide(e)
    {
        // If an invitation is currently active, refuse it
        if (this.activeInvitationWrapper != null)
            this.acceptInvitation(false);
    }

    /**
     * On Accept button click, send reply to invitation.
     */
    onInvitationAcceptClick(e)
    {
        // Accept active invitation
        this.acceptInvitation();

        // Hide invitation modal
        this.invitationModal.hide();
    }

    /**
     * On Add buddy form submit, send request to add buddy to user's Buddy List.
     */
    onAddBuddyFormSubmit(e)
    {
        e.preventDefault();

        if (this.buddyNameInput.value != '')
        {
            // Request buddy adding to buddy list
            this.sfs.send(new AddBuddyRequest(this.buddyNameInput.value));

            // Reset buddy name input
            this.buddyNameInput.value = '';
            this.buddyNameInput.focus();
        }
    }

    /**
     * On Chat button click in Buddy List item, initialize and show chat panel.
     */
    onBuddyChatClickEvent(e)
    {
        const buddyName = e.detail.buddyName;

        // Skip if chat is already active with the same buddy
        if (this.buddyChatPane.dataset.buddyName != buddyName)
        {
            // Get chat history (if any) and reset new messages counter
            const buddyChatMessages = BuddyChatHistoryManager.getBuddyChatMessages(buddyName);
            BuddyChatHistoryManager.resetUnreadMessagesCount(buddyName);

            // Initialize and show panel
            this.initBuddyChatPanel(this.sfs.buddyManager.getBuddyByName(buddyName), buddyChatMessages);

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

    /**
     * On Invite button click in Buddy List item, start new game inviting buddy.
     */
    onBuddyInviteClickEvent(e)
    {
        const buddyName = e.detail.buddyName;

        this.startNewGame(false, buddyName);
    }

    /**
     * On Block/Unblock button click in Buddy List item, send request to block/unblock buddy in user's Buddy List.
     */
    onBuddyBlockClickEvent(e)
    {
        const buddyName = e.detail.buddyName;
        const isBlocked = this.sfs.buddyManager.getBuddyByName(buddyName).isBlocked;

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

    /**
     * On Remove button click in Buddy List item, send request to remove buddy from user's Buddy List.
     */
    onBuddyRemoveClickEvent(e)
    {
        const buddyName = e.detail.buddyName;

        // Request buddy removal from buddy list
		this.sfs.send(new RemoveBuddyRequest(buddyName));
    }

    /**
     * On Add button click in Buddy List item, send request to add a temporary buddy to user's Buddy List.
     */
    onBuddyAddClickEvent(e)
    {
        const buddyName = e.detail.buddyName;

        // Request buddy removal from buddy list
		this.sfs.send(new AddBuddyRequest(buddyName));
    }

    /**
     * On custom event fired by Profile Configurator control, toggle user online state in Buddy List system.
     */
    onOnlineToggleChangeEvent(e)
    {
        const goOnline = e.detail.goOnline;

        // Request online/offline state toggle
		this.sfs.send(new GoOnlineRequest(goOnline));
    }

    /**
     * On custom event fired by Profile Configurator control/s, set user's Buddy Variables.
     */
    onBuddySettingChangeEvent(e)
    {
        const varName = e.detail.variable;
        const value = e.detail.value;
        const type = value == null || value == '' ? VariableType.NULL : (typeof e.detail.value == 'number' ? VariableType.INT : VariableType.STRING);

        const buddyVars = new Array();
        buddyVars.push(new SFSBuddyVariable(varName, value, type));

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

    /**
     * On custom event fired by Profile Configurator control/s, set user's Buddy Variables.
     */
    onPlayerSettingChangeEvent(e)
    {
        const varName = e.detail.variable;
        const value = e.detail.value;
        const type = value == null || value == '' ? VariableType.NULL : (typeof e.detail.value == 'number' ? VariableType.SHORT : VariableType.STRING);

        const userVars = new Array();
		userVars.push(new SFSUserVariable(varName, value, type));
		
		// Set User Variables
		this.sfs.send(new SetUserVariablesRequest(userVars));
    }

    onBuddyChatTabShow(e)
    {
        this.updateUnreadBuddyMsgTabCounter();
        this.updateUnreadBuddyChatsTabCounter();
    }

    onBuddyChatPanelCloseClick(e)
    {
        this.showBuddyChat(false);
    }

    onBuddyChatPanelMsgSubmit(e)
    {
        const buddyName = this.buddyChatPanel.dataset.buddyName;
        const message = e.detail.message;

        // Add a custom parameter containing the recipient name
        const params = new SFSObject();
        params.putString('recipient', buddyName);

        // Retrieve buddy
        const buddy = this.sfs.buddyManager.getBuddyByName(buddyName);

        // Send message to buddy
        this.sfs.send(new BuddyMessageRequest(message, buddy, params));
    }

    //----------------------------------------------------------
    // SmartFoxServer event listeners
    //----------------------------------------------------------
    
    onRoomCreationError(evtParams)
    {
        // Show warning modal
        this.app.showWarningModal(`Game Room creation failed.\n${evtParams[EventParam.ErrorMessage]}.`);
    }

    onRoomAdded(evtParams)
    {
        const room = evtParams[EventParam.Room];

        // Add game list item
        this.gameList.addListItem(room);
    }

    onRoomRemoved(evtParams)
    {
        const room = evtParams[EventParam.Room];

        // Remove game list item
        this.gameList.removeListItem(room.id);
    }

    onUserCountChanged(evtParams)
    {
        const room = evtParams[EventParam.Room];

        // Update game list item
        this.gameList.setItemState(room);
    }

    onRoomJoin(evtParams)
    {
        const room = evtParams[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 (this.sfs.buddyManager.myOnlineState)
            {
                const buddyVars = new Array();
                buddyVars.push(new SFSBuddyVariable(ReservedBuddyVariables.BV_STATE, 'Away', VariableType.STRING));
                this.sfs.send(new SetBuddyVariablesRequest(buddyVars));
            }

            // Go to game view
            this.app.goToView('game');
        }
    }

    onRoomJoinError(evtParams)
    {
        // Show warning modal
        this.app.showWarningModal(`Room join failed.\n${evtParams[EventParam.ErrorMessage]}.`);
    }

    onInvitation(evtParams)
    {
        const invitation = evtParams[EventParam.Invitation];

        // Create a queue of invitations
        // We use an array because a user could receive multiple invitations within the time it takes to accept or refuse one
        if (this.invitationQueue == null)
            this.invitationQueue = new Array();

        // Add an object wrapping the invitation to the queue
        this.invitationQueue.push({
            invitation: invitation,
            expiresInSeconds: invitation.secondsForAnswer,
            date: new Date()
        });

        // Trigger invitation processing
        this.processInvitations();
    }
    
    onBuddyListInit(evtParams)
    {
        // Initialize buddy-related entities
        this.initializeBuddyClient();
    }

    onBuddyError(evtParams)
    {
        // Show warning modal
        this.app.showWarningModal(`Buddy list system error.\n${evtParams[EventParam.ErrorMessage]}.`);
    }

    onBuddyOnlineStateChange(evtParams)
    {
        const buddy = evtParams[EventParam.Buddy];
        const isItMe = evtParams[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)
            this.updateBuddyListEntry(buddy);
        else
        {
            this.displayUserStateAsBuddy();

            const isOnline = this.sfs.buddyManager.myOnlineState;

            // Show/hide buddy list
            this.showBuddyList(isOnline);

            if (!isOnline)
            {
                // Hide chat if current user went offline
                this.showBuddyChat(false);
            }
            else
            {
                // Update all buddy items if current user came online
                for (let b of this.sfs.buddyManager.buddyList)
                    this.updateBuddyListEntry(b);
            }
        }
    }

    onBuddyVariablesUpdate(evtParams)
    {
        const buddy = evtParams[EventParam.Buddy];
        const isItMe = evtParams[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)
            this.updateBuddyListEntry(buddy);
        else
            this.displayUserStateAsBuddy();
    }

    onBuddyBlock(evtParams)
    {
        const buddy = evtParams[EventParam.Buddy];

        this.updateBuddyListEntry(buddy);

        // Reset unread messages counter and update counter in buddy list tab
        BuddyChatHistoryManager.resetHistory(buddy.name);
        this.updateUnreadBuddyChatsTabCounter();
    }

    onBuddyRemove(evtParams)
    {
        const buddy = evtParams[EventParam.Buddy];

        this.buddyList.removeListItem(buddy.name);

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

        // Hide buddy chat
        if (this.buddyChatPanel.dataset.buddyName == buddy.name)
            this.showBuddyChat(false);

        // Reset unread messages counter and update counter in buddy list tab
        BuddyChatHistoryManager.resetHistory(buddy.name);
        this.updateUnreadBuddyChatsTabCounter();
    }

    onBuddyAdd(evtParams)
    {
        const buddy = evtParams[EventParam.Buddy];

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

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

    onBuddyMessage(evtParams)
    {
        const sender = evtParams[EventParam.Buddy];
        const isItMe = evtParams[EventParam.IsItMe];
        const message = evtParams[EventParam.Message];
        const recipient = evtParams[EventParam.Data].getString('recipient');

        const targetBuddyName = isItMe ? recipient : sender.name;
        let senderDisplayName;
        
        if (isItMe)
            senderDisplayName = (this.sfs.buddyManager.myNickname ? this.sfs.buddyManager.myNickname : this.sfs.mySelf.name);
        else
            senderDisplayName = sender.nickname ? sender.nickname : sender.name;

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

        // Show message or increase message counter
        if (this.buddyChatPanel.dataset.buddyName == chatMsg.target)
        {
            // Display message in chat panel
            this.buddyChatPanel.printChatMessage(chatMsg.message, chatMsg.sender, chatMsg.sentByMe, chatMsg.date);

            // On smaller screens, the active tab could be different from the buddy chat one
            // In this case, show the unread messages counter
            this.updateUnreadBuddyMsgTabCounter();
        }
        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)
            const unreadMsgCnt = BuddyChatHistoryManager.increaseUnreadMessagesCount(chatMsg.target);

            // Update buddy list item
            this.buddyList.setItemChatMsgCounter(chatMsg.target, unreadMsgCnt);
        }

        // On smaller screens, the active tab could be different from the buddy list one
        // In this case, show the unread chats counter
        this.updateUnreadBuddyChatsTabCounter();
    }
}
