import * as bootstrap from 'bootstrap';
import BaseController from './base-controller.js';
import ProfileConfigurator from '../components/profile-configurator.js';
import BuddyChatHistoryManager from '../managers/buddy-chat-history-manager.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 { ExtensionRequest, SetUserVariablesRequest, InvitationReplyRequest } from 'sfs3-api';
import { InitBuddyListRequest, AddBuddyRequest, RemoveBuddyRequest, BlockBuddyRequest, GoOnlineRequest, SetBuddyVariablesRequest, BuddyMessageRequest } from 'sfs3-api';

export default class LobbyController extends BaseController
{
	// The experience is a custom User Variable used as a matchmaking parameter when
	// inviting players to a game from the pre-game screen.
	static USERVAR_EXPERIENCE = 'xp';

	// Zone Extension request/response used to start or join a game (server-side matchmaking)
	static ZONE_EXT_REQ_INIT = 'init';
	static ZONE_EXT_RESP_INIT_ERR = 'initErr';

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

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

		this.logoutButton = view.querySelector('#logoutButton');
		this.profileConfigurator = view.querySelector('#profileConfigurator');
		this.startButtons = view.querySelectorAll('[data-level]');
		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 level start buttons click listener
		this.boundOnStartButtonClick = this.onStartButtonClick.bind(this);
		this.startButtons.forEach(bt => bt.addEventListener('click', this.boundOnStartButtonClick));

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

		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.EXTENSION_RESPONSE, this.onExtensionResponse, this);
		this.sfs.addEventListener(SFSEvent.ROOM_JOIN, this.onRoomJoin, this);
		this.sfs.addEventListener(SFSEvent.ROOM_JOIN_ERROR, this.onRoomJoinError, this);
		this.sfs.addEventListener(SFSEvent.ROOM_CREATION_ERROR, this.onRoomCreationError, this);
		this.sfs.addEventListener(SFSEvent.USER_VARIABLES_UPDATE, this.onUserVariablesUpdate, 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);

		// Enable level start buttons
		this.enableStartButtons(true);

		// 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 play tab
		bootstrap.Tab.getOrCreateInstance(this.view.querySelector('#playTab')).show();

		// Initialize Buddy List system
		// We have to check if it was already initialized if this view is shown again after leaving a game
		if (!this.sfs.buddyManager.isInited)
			this.sfs.send(new InitBuddyListRequest());
		else
			this.initializeBuddyClient();

		// NOTE: games are started/joined through the Zone Extension "init" request (server-side
		// matchmaking), and invitations are sent from the pre-game screen; no default Room is joined here.

		// Show view
		this.showView();
	}

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

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

		// Remove level start buttons click listener
		this.startButtons.forEach(bt => bt.removeEventListener('click', this.boundOnStartButtonClick));

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

		// Remove listeners to events fired by BuddyList class
		this.buddyList.removeEventListener(BuddyList.CHAT_CLICK_EVT, this.boundOnBuddyChatClickEvent);
		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 buddy list
		this.buddyList.reset();

		// Reset chat panel
		this.buddyChatPanel.reset();

		this.invitationQueue = null;
	}

	removeSmartFoxListeners()
	{
		if (this.sfs != null)
		{
			this.sfs.removeEventListener(SFSEvent.EXTENSION_RESPONSE, this.onExtensionResponse);
			this.sfs.removeEventListener(SFSEvent.ROOM_JOIN, this.onRoomJoin);
			this.sfs.removeEventListener(SFSEvent.ROOM_JOIN_ERROR, this.onRoomJoinError);
			this.sfs.removeEventListener(SFSEvent.ROOM_CREATION_ERROR, this.onRoomCreationError);
			this.sfs.removeEventListener(SFSEvent.USER_VARIABLES_UPDATE, this.onUserVariablesUpdate);
			this.sfs.removeEventListener(SFSEvent.INVITATION, this.onInvitation);

			this.sfs.removeEventListener(SFSBuddyEvent.BUDDY_LIST_INIT, this.onBuddyListInit);
			this.sfs.removeEventListener(SFSBuddyEvent.BUDDY_ERROR, this.onBuddyError);
			this.sfs.removeEventListener(SFSBuddyEvent.BUDDY_ONLINE_STATE_CHANGE, 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
	//----------------------------------------------------------

	/**
	 * Enable/disable the level start buttons.
	 */
	enableStartButtons(enable)
	{
		this.startButtons.forEach(bt => bt.disabled = !enable);
	}

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

	showBuddyChat(show, buddy = null)
	{
		// Show/hide buddy chat 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 data attribute
		this.buddyChatPanel.dataset.buddyName = (buddy != null ? buddy.name : '');
	}

	/**
	 * Initialize the player profile.
	 *
	 * IMPORTANT NOTE
	 * The Experience is a custom parameter used in this example to show how to filter players through
	 * SmartFoxServer's matchmaking system when sending invitations to play a game. Of course it (or any
	 * other parameter with a similar purpose) shouldn't be set manually, but it should be determined by
	 * the game logic, saved in a database and set in User Variables by a server-side Extension on login.
	 * In this example, for the sake of simplicity, it is set to a default value when the lobby is shown
	 * and it is lost on user logout.
	 */
	initializePlayer()
	{
		// Check if the player experience is set in User Variables
		if (this.sfs.mySelf.getVariable(LobbyController.USERVAR_EXPERIENCE) == null)
		{
			// If not, set the default value; this in turn causes a USER_VARIABLES_UPDATE event to be fired,
			// upon which we display the player experience in the profile configurator panel
			const expVar = new SFSUserVariable(LobbyController.USERVAR_EXPERIENCE, this.profileConfigurator.playerExperience, VariableType.INT);

			this.sfs.send(new SetUserVariablesRequest([expVar]));
		}
		else
		{
			// If yes, display player details in the profile configurator panel
			this.profileConfigurator.initPlayerProfile(this.sfs.mySelf);
		}
	}

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

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

	/**
	 * Process the invitation in queue, displaying the invitation accept/refuse modal.
	 */
	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 SFS listeners added by this controller to be removed (see BaseController.destroy)
	 * and the Login view to be shown (see Application.onConnectionLost listener).
	 */
	onLogoutButtonClick(e)
	{
		// Reset buddy chat history
		BuddyChatHistoryManager.resetAllHistory();

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

	/**
	 * On level button click, send a request to the server-side Zone Extension to start a new game.
	 * The Zone Extension searches for an existing Room to join the user in; if it can't be found, a new Room is created.
	 */
	onStartButtonClick(e)
	{
		const levelName = e.currentTarget.dataset.level;

		// Disable start buttons
		this.enableStartButtons(false);

		// Add level name to request parameters
		const reqParams = new SFSObject();
		reqParams.putString('name', levelName);

		// Send request to the Zone Extension (no Room specified)
		this.sfs.send(new ExtensionRequest(LobbyController.ZONE_EXT_REQ_INIT, reqParams));
	}

	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 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 adding to 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, 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 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, set user's experience in User Variables.
	 */
	onPlayerSettingChangeEvent(e)
	{
		const varName = e.detail.variable;
		const value = e.detail.value;

		const userVars = new Array();
		userVars.push(new SFSUserVariable(varName, value, VariableType.INT));

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

	onExtensionResponse(evtParams)
	{
		const cmd = evtParams[EventParam.Cmd];

		if (cmd == LobbyController.ZONE_EXT_RESP_INIT_ERR)
		{
			// Show warning modal
			this.app.showWarningModal('Invalid level identifier.');

			// Re-enable start buttons
			this.enableStartButtons(true);
		}
	}

	onRoomJoin(evtParams)
	{
		// A Game Room was joined (through the Zone Extension "init" request or by accepting an invitation)

		// 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]}.`);

		// Re-enable start buttons
		this.enableStartButtons(true);
	}

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

		// Re-enable start buttons
		this.enableStartButtons(true);
	}

	onUserVariablesUpdate(evtParams)
	{
		const user = evtParams[EventParam.User];

		// Display player details in profile configurator panel
		if (user.isItMe)
			this.profileConfigurator.initPlayerProfile(user);
	}

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

			// Keep the profile state dropdown in sync (e.g. when returning from a game,
			// where the state is set back to "Available" only after the server confirms it)
			this.profileConfigurator.updateSelectedState(this.sfs.buddyManager.myState);
		}
	}

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

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

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