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 CallPanel from '../components/call-panel.js';
import ChatPanel from '../components/chat-panel.js';
import { SFSObject, SFSEvent, SFSBuddyEvent, SFSAudioEvent, EventParam, SFSBuddyVariable, VariableType, ReservedBuddyVariables } from 'sfs3-api';
import { CreateRoomRequest, RoomSettings, InvitationReply, InvitationReplyRequest, JoinRoomInvitationRequest, LeaveRoomRequest } from 'sfs3-api';
import { InitBuddyListRequest, AddBuddyRequest, RemoveBuddyRequest, BlockBuddyRequest, GoOnlineRequest, SetBuddyVariablesRequest, BuddyMessageRequest } from 'sfs3-api';
import { InitAudioRequest, StartAudioStreamRequest, StopAudioStreamRequest, ToggleMuteAudioStreamRequest } from 'sfs3-api';

const CallState = Object.freeze({
  AVAILABLE: 'available',
  PENDING: 'pending',
  IN_PROGRESS: 'in-progress'
});

export default class LobbyController extends BaseController
{
    static BUDDYVAR_YEAR = SFSBuddyVariable.OFFLINE_PREFIX + 'year';
    static BUDDYVAR_MOOD = 'mood';
    static INVITATION_EXPIRY_SECS = 20;

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

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

        this.logoutButton = view.querySelector('#logoutButton');
        this.profileConfigurator = view.querySelector('#profileConfigurator');
        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.buddyCallTab = view.querySelector('#buddyCallTab');
        this.buddyCallPane = view.querySelector('#buddyCallPane');
        this.buddyCallPanel = view.querySelector('#buddyCallPanel');
        this.buddyChatTab = view.querySelector('#buddyChatTab');
        this.buddyChatPane = view.querySelector('#buddyChatPane');
        this.buddyChatPanel = view.querySelector('#buddyChatPanel');
        this.callInvitationSenderLabel = view.querySelector('#callInvitationSenderLabel');
        this.acceptCallButton = view.querySelector('#acceptCallButton');
        this.refuseCallButton = view.querySelector('#refuseCallButton');

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

        // Initialize state
        this.callState = CallState.AVAILABLE;
        this.activeInvitation = null;
        this.callInvitationTimeout = null;
        this.pendingInviteUserName = null;

        // Load ringtone sound
        this.ringTone = new Audio('assets/sounds/ringtone.wav');
        this.ringTone.preload = 'auto';
        this.ringTone.volume = 0.5;
    }

    //----------------------------------------------------------
    // 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 BuddyList class
        this.boundOnBuddyChatClickEvent = this.onBuddyChatClickEvent.bind(this);
        this.buddyList.addEventListener(BuddyList.CHAT_CLICK_EVT, this.boundOnBuddyChatClickEvent);

        this.boundOnBuddyCallClickEvent = this.onBuddyCallClickEvent.bind(this);
        this.buddyList.addEventListener(BuddyList.CALL_CLICK_EVT, this.boundOnBuddyCallClickEvent);

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

        // 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 listeners to events fired by CallPanel class
        this.boundOnBuddyCallPanelEndCallClick = this.onBuddyCallPanelEndCallClick.bind(this);
        this.buddyCallPanel.addEventListener(CallPanel.END_CALL_EVT, this.boundOnBuddyCallPanelEndCallClick);

        this.boundOnBuddyCallPanelToggleMuteClick = this.onBuddyCallPanelToggleMuteClick.bind(this);
        this.buddyCallPanel.addEventListener(CallPanel.TOGGLE_MUTE_EVT, this.boundOnBuddyCallPanelToggleMuteClick);

        // Add listener to invitation modal accept and refuse buttons click
        this.boundOnAcceptCallClick = this.onAcceptCallClick.bind(this);
        this.acceptCallButton.addEventListener('click', this.boundOnAcceptCallClick);

        this.boundOnRefuseCallClick = this.onRefuseCallClick.bind(this);
        this.refuseCallButton.addEventListener('click', this.boundOnRefuseCallClick);

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

        // Add SFS event listeners
        this.sfs.addEventListener(SFSEvent.ROOM_JOIN, this.onRoomJoin, this);
        this.sfs.addEventListener(SFSEvent.ROOM_REMOVE, this.onRoomRemove, this);
        this.sfs.addEventListener(SFSEvent.USER_ENTER_ROOM, this.onUserEnterRoom, this);
        this.sfs.addEventListener(SFSEvent.USER_EXIT_ROOM, this.onUserExitRoom, this);
        this.sfs.addEventListener(SFSEvent.INVITATION, this.onInvitation, this);
        this.sfs.addEventListener(SFSEvent.INVITATION_REPLY, this.onInvitationReply, 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);

        this.sfs.addEventListener(SFSAudioEvent.INIT, this.onAudioInit, this);
        this.sfs.addEventListener(SFSAudioEvent.STREAM_START, this.onAudioStreamStart, this);
        this.sfs.addEventListener(SFSAudioEvent.STREAM_STOP, this.onAudioStreamStop, this);
        this.sfs.addEventListener(SFSAudioEvent.STREAM_ADD, this.onAudioStreamAdd, this);
        this.sfs.addEventListener(SFSAudioEvent.STREAM_REMOVE, this.onAudioStreamRemove, this);
        this.sfs.addEventListener(SFSAudioEvent.TOGGLE_MUTE, this.onToggleMute, this);
        this.sfs.addEventListener(SFSAudioEvent.ERROR, this.onAudioError, this);

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

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

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

        // Activate games tab
        bootstrap.Tab.getOrCreateInstance(this.view.querySelector(`#buddyListTab`)).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();

        // Show view
        this.showView();
    }

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

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

        // Remove buddy form submit listener
        this.addBuddyForm.addEventListener('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);

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

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

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

    removeSmartFoxListeners()
    {
        if (this.sfs != null)
        {
            this.sfs.removeEventListener(SFSEvent.ROOM_JOIN, this.onRoomJoin);
            this.sfs.removeEventListener(SFSEvent.ROOM_REMOVE, this.onRoomRemove);
            this.sfs.removeEventListener(SFSEvent.USER_ENTER_ROOM, this.onUserEnterRoom);
            this.sfs.removeEventListener(SFSEvent.USER_EXIT_ROOM, this.onUserExitRoom);
            this.sfs.removeEventListener(SFSEvent.INVITATION, this.onInvitation, this);
            this.sfs.removeEventListener(SFSEvent.INVITATION_REPLY, this.onInvitationReply, 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);

            this.sfs.removeEventListener(SFSAudioEvent.INIT, this.onAudioInit);
            this.sfs.removeEventListener(SFSAudioEvent.STREAM_START, this.onAudioStreamStart);
            this.sfs.removeEventListener(SFSAudioEvent.STREAM_STOP, this.onAudioStreamStop);
            this.sfs.removeEventListener(SFSAudioEvent.STREAM_ADD, this.onAudioStreamAdd);
            this.sfs.removeEventListener(SFSAudioEvent.STREAM_REMOVE, this.onAudioStreamRemove);
            this.sfs.removeEventListener(SFSAudioEvent.TOGGLE_MUTE, this.onToggleMute);
            this.sfs.removeEventListener(SFSAudioEvent.ERROR, this.onAudioError);
        }
    }

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

    showBuddyCall(show)
    {
        // Show/hide buddy call tab and pane
        this.buddyCallTab.hidden = !show;
        this.buddyCallPane.hidden = !show;

        if (show)
        {
            // Activate call tab
            bootstrap.Tab.getOrCreateInstance(this.buddyCallTab).show();
        }
        else
        {
            // Activate buddy list tab
            bootstrap.Tab.getOrCreateInstance(this.buddyListTab).show();
        }
    }

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

    initializeBuddyClient()
    {
        // Display buddy list items
        // All blocked buddies are displayed at the bottom of the list
        for (let buddy of this.sfs.buddyManager.buddyList)
            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);
    }

    startAudioStream()
    {
        this.sfs.send(new StartAudioStreamRequest(null, this.buddyCallPanel.muted));
    }

    sendPendingInvitation()
    {
        // Show user in call panel
        this.buddyCallPanel.addParticipant(this.pendingInviteUserName);

        // Make sure buddy to invite did not disconnect during the Room create + join process
        if (this.sfs.buddyManager.getBuddyByName(this.pendingInviteUserName).isOnline)
        {
            const invitedUsers = [this.pendingInviteUserName];

            const invParams = new SFSObject();
            invParams.putString("room", this.sfs.lastJoinedRoom.name);

            // Send the request
            this.sfs.send(new JoinRoomInvitationRequest(this.sfs.lastJoinedRoom, invitedUsers, invParams, LobbyController.INVITATION_EXPIRY_SECS));

            this.app.trace("Invitation to call sent to " + this.pendingInviteUserName);
        }
        else
        {
            this.handleBuddyLeftCall(this.pendingInviteUserName, "Offline");
        }

        // Reset reference
        this.pendingInviteUserName = null;
    }

    hideCallInvitation()
    {
        clearTimeout(this.callInvitationTimeout);

        // Remove active invitation
        this.activeInvitation = null;

        // Hide modal
        this.callInvitationModal.hide();

        // Stop sound
        this.ringTone.pause();
    }

    refuseInvitation()
    {
        // Refuse invitation
        this.sfs.send(new InvitationReplyRequest(this.activeInvitation, InvitationReply.REFUSE));

        // Hide modal
        this.hideCallInvitation();
    }

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

        // If current user is now alone, wait a few seconds then leave room
        if (this.sfs.lastJoinedRoom?.userCount == 1)
            this.leaveCall(3000);
    }

    async leaveCall(delayMillis)
    {
        if (this.sfs.lastJoinedRoom != null)
        {
            this.app.trace("Leaving call Room");

            // Disable call panel buttons
            this.buddyCallPanel.setAttribute('disabled', true);

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

            // Wait some time
            if (delayMillis > 0)
                await new Promise(resolve => setTimeout(resolve, delayMillis));

            // Hide call panel
            this.showBuddyCall(false);

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

            // Leave Room
            this.sfs.send(new LeaveRoomRequest(this.sfs.lastJoinedRoom));

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

    generateTimestampString()
    {
        const now = new Date();
        const year = now.getFullYear();
        const month = String(now.getMonth() + 1).padStart(2, '0');
        const day = String(now.getDate()).padStart(2, '0');
        const hours = String(now.getHours()).padStart(2, '0');
        const minutes = String(now.getMinutes()).padStart(2, '0');
        const seconds = String(now.getSeconds()).padStart(2, '0');
        const milliseconds = String(now.getMilliseconds()).padStart(3, '0');

        return `${year}${month}${day}${hours}${minutes}${seconds}${milliseconds}`;
    }

    generateRandomString()
    {
        const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        return Array.from({ length: 20 })
                    .map(() => chars.charAt(Math.floor(Math.random() * chars.length)))
                    .join('');
    }

    //----------------------------------------------------------
    // 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 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 Call button click in Buddy List item, initialize and show call panel.
     */
    onBuddyCallClickEvent(e)
    {
        const buddyName = e.detail.buddyName;

        // If a call attempt (which may include Room creation, joining and sending invitation)
        // is already in progress, skip all next attempts 
        if (this.callState == CallState.PENDING)
            return;
        
        // Also avoid inviting the same user multiple times
        if (this.buddyCallPanel.hasParticipant(buddyName))
            return;
        
        //-----------------------------------------------------

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

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

            // Configure Room
            const roomName = "call-" + this.generateTimestampString();

            const settings = new RoomSettings(roomName);
            settings.maxUsers = 4;
            settings.password = this.generateRandomString();
            settings.allowOwnerOnlyInvitation = false;
            settings.allowAudioStreaming = true;
            settings.isGame = true; // This flag is useful for our flow, because a game Room is removed as soon as it is left empty

            // Request Room creation to server
            this.sfs.send(new CreateRoomRequest(settings, true));
        }
        else
        {
            this.sendPendingInvitation();
        }
    }

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

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

    onAcceptCallClick(e)
    {
        // Accept/refuse invitation
        this.sfs.send(new InvitationReplyRequest(this.activeInvitation, InvitationReply.ACCEPT));

        // Hide modal
        this.hideCallInvitation();
    }

    onRefuseCallClick(e)
    {
        this.refuseInvitation();
    }

    onBuddyCallPanelEndCallClick(e)
    {
        // Stop stream, leave room, close and reset call panel
        this.leaveCall(0);
    }

    onBuddyCallPanelToggleMuteClick(e)
    {
        if (this.sfs.audioManager.isStreaming())
            this.sfs.send(new ToggleMuteAudioStreamRequest());
    }

    //----------------------------------------------------------
    // SmartFoxServer event listeners
    //----------------------------------------------------------

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

        this.app.trace("Call Room joined");

        // Enable call panel buttons
        this.buddyCallPanel.setAttribute('disabled', false);

        // Show call panel
        this.showBuddyCall(true);

        // Update state
        this.callState = CallState.IN_PROGRESS;

        // If Room already contains other users, then current user was invited to join
        // Show other users and start audio stream
        if (room.userList.length > 1)
        {
            // Display all other participants
            for (let user of room.userList)
            {
                if (user != this.sfs.mySelf)
                    this.buddyCallPanel.addParticipant(user.name);
            }

            // Start audio stream
            this.startAudioStream();
        }

        // Otherwise send pending invitation
        else
            this.sendPendingInvitation();
    }

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

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

            this.refuseInvitation();
        }
    }

    onUserEnterRoom(evtParams)
    {
        const room = evtParams[EventParam.Room];
        const user = evtParams[EventParam.User];

        // Display participant to call (unless already displayed before, during invitation)
        if (!this.buddyCallPanel.hasParticipant(user.name))
            this.buddyCallPanel.addParticipant(user.name);
        
        if (!this.sfs.audioManager.isStreaming())
            this.startAudioStream();
    }

    onUserExitRoom(evtParams)
    {
        const room = evtParams[EventParam.Room];
        const user = evtParams[EventParam.User];

        if (user != this.sfs.mySelf)
        {
            this.app.trace(`User ${user.name} left the call`);

            this.handleBuddyLeftCall(user.name, "Call ended");
        }
    }

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

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

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

            // Update invitation UI
            this.callInvitationSenderLabel.textContent = invitation.inviter.name;

            // Run timeout to hide the invitation panel when the invitation expires
            this.callInvitationTimeout = setTimeout(() => {
                this.hideCallInvitation();
                // We could send a refusal just like if the user pressed the refuse button
                // Or, like in this case, we can let the invitation expire and the inviter
                // will be notified automatically
            }, invitation.secondsForAnswer * 1000);

            // Show invitation modal
            this.callInvitationModal.show();

            // Play sound
            this.ringTone.currentTime = 0; // Start from beginning
            this.ringTone.play();
        }
    }

    onInvitationReply(evtParams)
    {
        const invitee = evtParams[EventParam.Invitee];
        const invReply = evtParams[EventParam.Reply];

        if (invReply != InvitationReply.ACCEPT)
        {
            this.app.trace("Invitation to call expired or refused by " + invitee.name);
            
            const reason = invReply == InvitationReply.REFUSE ? "Occupied" : "Not available";

            this.handleBuddyLeftCall(invitee.name, reason);
        }
    }
    
    onBuddyListInit(evtParams)
    {
        // Initialize buddy-related entities
        this.initializeBuddyClient();

        // Initialize audio
        this.sfs.send(new InitAudioRequest());
    }

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

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

    async onAudioInit(evtParams)
    {
        this.app.trace("Audio Streaming System initialized");

        try
        {
            // Request user permission to access audio, so we can display the list of devices
            // Actually best practice is to request permission after a user gesture (for example click on a 'settings' button
            // to access the list of devices, or click on a 'start stream' button right before starting audio capture from microphone)
            await this.sfs.audioManager.requestUserPermission();
            
            // Update devices in profile configurator panel
            this.profileConfigurator.initAudioControls(this.sfs.audioManager);
        }
        catch (err)
        {
            this.app.trace(err.message);

            if (err.cause)
                this.app.trace("Inner error:\n\t" + err.cause);
        }
    }

    onAudioStreamStart(evtParams)
    {
        const asi = evtParams[EventParam.MyAudioStream];

        // Show my audio meter
        this.buddyCallPanel.showMyAudioMeter(asi);

        this.app.trace("My audio stream started");
    }

    onAudioStreamStop(evtParams)
    {
        this.app.trace("My audio stream stopped");
    }

    onAudioStreamAdd(evtParams)
    {
        const asi = evtParams[EventParam.AudioStream];
            
        // Show user audio meter
        this.buddyCallPanel.updateParticipant(asi);

        this.app.trace("Audio stream added; user: " + asi.user);
    }

    onAudioStreamRemove(evtParams)
    {
        const asi = evtParams[EventParam.AudioStream];

        this.app.trace("Audio stream removed; user: " + asi.user);
    }

    onToggleMute(evtParams)
    {
        const asi = evtParams[EventParam.AudioStream];

        if (asi.isMuted)
            this.app.trace("Audio stream muted; user: " + asi.user);
        else
            this.app.trace("Audio stream unmuted; user: " + asi.user);
    }

    onAudioError(evtParams)
    {
        const errorMsg = evtParams[EventParam.ErrorMessage];

        this.app.trace(`Audio error: ${errorMsg}`);
    }
}
