using System;
using System.Collections.Generic;

using UnityEngine;
using UnityEngine.UI;

using Sfs2X;
using Sfs2X.Core;
using Sfs2X.Entities;
using Sfs2X.Entities.Data;
using Sfs2X.Entities.Invitation;
using Sfs2X.Entities.Variables;
using Sfs2X.Requests;
using Sfs2X.Requests.Buddylist;
using Sfs2X.Requests.Game;

/**
 * Script attached to the Controller object in the Lobby scene.
 */
public class LobbySceneController : BaseSceneController
{
	public static string USERVAR_EXPERIENCE = "xp";

	private const string ZONE_EXT_REQ_INIT = "init";
	private const string ZONE_EXT_RESP_INIT_ERR = "initErr";

	//----------------------------------------------------------
	// UI elements
	//----------------------------------------------------------

	public Text loggedInAsLabel;
	public Text userStatusLabel;
	public UserProfilePanel userProfilePanel;
	public InvitationPanel invitationPanel;
	public WarningPanel warningPanel;
	public GameObject waitingContainer;

    public GameObject startButtonsContainer;

	public GameObject buddiesPanel;
	public Transform buddyListContent;
	public BuddyListItem buddyListItemPrefab;
	public InputField buddyNameInput;
	public BuddyChatPanel buddyChatPanel;

	public Queue<InvitationWrapper> invitationQueue;

	//----------------------------------------------------------
	// Private properties
	//----------------------------------------------------------

	private SmartFox sfsLobby;
	private Dictionary<string, BuddyListItem> buddyListItems;
	private string selectedLevel;

    //----------------------------------------------------------
    // Unity callback methods
    //----------------------------------------------------------
    #region
    private void Start()
	{
		Debug.Log("LOBBY SCENE - Scene loaded");

		// Get reference to static SmartFox client instance for the lobby
		sfsLobby = gm.GetLobbyClient();

		// Add lobby event listeners
		AddSmartFoxLobbyListeners();

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

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

		// Display username in footer and user profile panel
		loggedInAsLabel.text = "Logged in as <b>" + sfsLobby.MySelf.Name + "</b>";
		userProfilePanel.InitUserProfile(sfsLobby.MySelf.Name);

		// Check if a connection lost message should be displayed
		CheckConnectionLost();
	}
	#endregion

	//----------------------------------------------------------
	// Abstract methods from BaseSceneController implementation
	//----------------------------------------------------------
	#region
	override protected void RemoveSmartFoxListeners()
	{
		// Remove lobby-related SmartFox event listeners
		RemoveSmartFoxLobbyListeners();
	}

	override public void ShowError(string errorMsg)
	{
		// Display connection or login error
		warningPanel.Show(errorMsg);
	}

	override protected void HideModals()
	{
		userProfilePanel.Hide();
		invitationPanel.Hide();
		warningPanel.Hide();
    }

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

    override public void OnGameJoinFailed()
    {
        // Schedule another request to join or create a game
        Invoke("RequestStartGame", 2);
    }
    #endregion

    //----------------------------------------------------------
    // UI event listeners
    //----------------------------------------------------------
    #region
    /**
	 * On Logout button click, disconnect from SmartFoxServer.
	 * This causes the SmartFox listeners added by this scene to be removed (see BaseSceneController.OnDestroy method)
	 * and the Login scene to be loaded (see GlobalManager.OnLobbyNodeConnectionLost method).
	 */
    public void OnLogoutButtonClick()
	{
		// Disconnect from lobby node
		sfsLobby.Disconnect();
	}

	/**
	 * On level button click, send a request to the server side Zone Extension to join an existing game on a game node, or create a new one.
	 */
	public void OnStartGameClick(string levelName)
	{
		// Disable start buttons
        EnableStartButtons(false);

        // Show waiting message
        waitingContainer.SetActive(true);

		// Set selected level
		selectedLevel = levelName;

		// Send request to Zone Extension 
		RequestStartGame();
    }

    /**
	 * On Cancel button click, stop requesting to join or create a game Room to lobby.
	 */
    public void OnCancelButtonClick()
    {
        CancelInvoke("RequestStartGame");

        // Hide waiting message
        waitingContainer.SetActive(false);

        // Enable start buttons
        EnableStartButtons(true);
    }

    /**
	 * On User icon click, show User Profile Panel prefab instance.
	 */
    public void OnUserIconClick()
	{
		userProfilePanel.Show();
	}

	/**
	 * On buddy name input edit end, if the Enter key was pressed, send request to add buddy to user's Buddy List.
	 */
	public void OnBuddyNameInputEndEdit()
	{
		if (Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter))
			OnAddBuddyButtonClick();
	}

	/**
	 * On Add buddy button click, send request to add buddy to user's Buddy List.
	 */
	public void OnAddBuddyButtonClick()
	{
		if (buddyNameInput.text != "")
		{
			// Request buddy adding to buddy list
			sfsLobby.Send(new AddBuddyRequest(buddyNameInput.text));
			buddyNameInput.text = "";
		}
	}

	/**
	 * On Add buddy button click in Buddy List Item prefab instance, send request to add temporary buddy to user's Buddy List.
	 */
	public void OnAddBuddyButtonClick(string buddyName)
	{
		sfsLobby.Send(new AddBuddyRequest(buddyName));
	}

	/**
	 * On Block buddy button click in Buddy List Item prefab instance, send request to block/unblock buddy in user's Buddy List.
	 */
	public void OnBlockBuddyButtonClick(string buddyName)
	{
		bool isBlocked = sfsLobby.BuddyManager.GetBuddyByName(buddyName).IsBlocked;

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

	/**
	 * On Remove buddy button click in Buddy List Item prefab instance, send request to remove buddy from user's Buddy List.
	 */
	public void OnRemoveBuddyButtonClick(string buddyName)
	{
		// Request buddy removal from buddy list
		sfsLobby.Send(new RemoveBuddyRequest(buddyName));
	}

	/**
	 * On Chat button click in Buddy List Item prefab instance, initialize and show chat panel.
	 */
	public void OnChatBuddyButtonClick(string buddyName)
	{
		// Skip if chat is already active with the same buddy
		if (buddyChatPanel.BuddyName != buddyName)
		{
			// Get chat history (if any) and reset new messages counter
			List<BuddyChatMessage> buddyChatMessages = BuddyChatHistoryManager.GetBuddyChatMessages(buddyName);
			BuddyChatHistoryManager.ResetUnreadMessagesCount(buddyName);

			// Initialize and show panel
			buddyChatPanel.Init(sfsLobby.BuddyManager.GetBuddyByName(buddyName), buddyChatMessages);

			// Move buddy list item to top of the list and reset new messages counter
			buddyListItems.TryGetValue(buddyName, out BuddyListItem buddyListItem);

			if (buddyListItem != null)
			{
				buddyListItem.gameObject.transform.SetAsFirstSibling();
				buddyListItem.SetChatMsgCounter(0);
			}
		}
	}

	/**
	 * On custom event fired by Buddy Chat Panel prefab instance, send message to buddy.
	 */
	public void OnBuddyMessageSubmit(string buddyName, string message)
	{
		// Add a custom parameter containing the recipient name
		ISFSObject _params = new SFSObject();
		_params.PutUtfString("recipient", buddyName);

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

		// Send message to buddy
		sfsLobby.Send(new BuddyMessageRequest(message, buddy, _params));
	}

	/**
	 * On custom event fired by User Profile Panel prefab instance, toggle user online state in Buddy List system.
	 */
	public void OnOnlineToggleChange(bool isChecked)
	{
		// Send requesto to toggle online/offline state
		sfsLobby.Send(new GoOnlineRequest(isChecked));
	}

	/**
	 * On custom event fired by User Profile Panel prefab instance, set user's Buddy Variables.
	 */
	public void OnBuddyDetailChange(string varName, object value)
	{
		List<BuddyVariable> buddyVars = new List<BuddyVariable>();
		buddyVars.Add(new SFSBuddyVariable(varName, value));

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

	/**
	 * On custom event fired by User Profile Panel prefab instance, set user's User Variables.
	 */
	public void OnPlayerDetailChange(string varName, object value)
	{
		List<UserVariable> userVars = new List<UserVariable>();
		userVars.Add(new SFSUserVariable(varName, value));

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

	/**
	 * On custom event fired by Invitation Panel prefab instance, send reply to invitation.
	 */
	public void OnInvitationReplyClick(Invitation invitation, bool accept)
	{
		// Accept/refuse invitation
		sfsLobby.Send(new InvitationReplyRequest(invitation, (accept ? InvitationReply.ACCEPT : InvitationReply.REFUSE)));

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

			// Reset queue
			invitationQueue.Clear();
		}

		// If invitation was refused, process next invitation in the queue (if any)
		else
			ProcessInvitations();
	}
	#endregion

	//----------------------------------------------------------
	// Helper methods
	//----------------------------------------------------------
	#region
	/**
	 * Add all SmartFoxServer lobby-related event listeners required by the scene.
	 */
	private void AddSmartFoxLobbyListeners()
	{
		Debug.Log("LOBBY SCENE - Adding lobby node event listeners");

		sfsLobby.AddEventListener(SFSEvent.EXTENSION_RESPONSE, OnExtensionResponse);
		sfsLobby.AddEventListener(SFSEvent.USER_VARIABLES_UPDATE, OnUserVariablesUpdate);
		sfsLobby.AddEventListener(SFSEvent.INVITATION, OnInvitation);

		sfsLobby.AddEventListener(SFSBuddyEvent.BUDDY_LIST_INIT, OnBuddyListInit);
		sfsLobby.AddEventListener(SFSBuddyEvent.BUDDY_ERROR, OnBuddyError);
		sfsLobby.AddEventListener(SFSBuddyEvent.BUDDY_ONLINE_STATE_UPDATE, OnBuddyOnlineStateUpdate);
		sfsLobby.AddEventListener(SFSBuddyEvent.BUDDY_VARIABLES_UPDATE, OnBuddyVariablesUpdate);
		sfsLobby.AddEventListener(SFSBuddyEvent.BUDDY_BLOCK, OnBuddyBlock);
		sfsLobby.AddEventListener(SFSBuddyEvent.BUDDY_REMOVE, OnBuddyRemove);
		sfsLobby.AddEventListener(SFSBuddyEvent.BUDDY_ADD, OnBuddyAdd);
		sfsLobby.AddEventListener(SFSBuddyEvent.BUDDY_MESSAGE, OnBuddyMessage);
	}

	/**
	 * Remove all SmartFoxServer lobby-related event listeners added by the scene.
	 */
	private void RemoveSmartFoxLobbyListeners()
	{
		Debug.Log("LOBBY SCENE - Removing lobby node event listeners");

		sfsLobby.RemoveEventListener(SFSEvent.EXTENSION_RESPONSE, OnExtensionResponse);
		sfsLobby.RemoveEventListener(SFSEvent.USER_VARIABLES_UPDATE, OnUserVariablesUpdate);
		sfsLobby.RemoveEventListener(SFSEvent.INVITATION, OnInvitation);

		sfsLobby.RemoveEventListener(SFSBuddyEvent.BUDDY_LIST_INIT, OnBuddyListInit);
		sfsLobby.RemoveEventListener(SFSBuddyEvent.BUDDY_ERROR, OnBuddyError);
		sfsLobby.RemoveEventListener(SFSBuddyEvent.BUDDY_ONLINE_STATE_UPDATE, OnBuddyOnlineStateUpdate);
		sfsLobby.RemoveEventListener(SFSBuddyEvent.BUDDY_VARIABLES_UPDATE, OnBuddyVariablesUpdate);
		sfsLobby.RemoveEventListener(SFSBuddyEvent.BUDDY_BLOCK, OnBuddyBlock);
		sfsLobby.RemoveEventListener(SFSBuddyEvent.BUDDY_REMOVE, OnBuddyRemove);
		sfsLobby.RemoveEventListener(SFSBuddyEvent.BUDDY_ADD, OnBuddyAdd);
		sfsLobby.RemoveEventListener(SFSBuddyEvent.BUDDY_MESSAGE, OnBuddyMessage);
	}

	/**
	 * Enable/disable buttons to start a new game.
	 */
	private void EnableStartButtons(bool enable)
    {
		var startButtons = startButtonsContainer.GetComponentsInChildren<Button>();
		foreach (var bt in startButtons)
			bt.interactable = enable;
	}

	/**
	 * Initialize 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 joining players in an existing 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 upon user login.
	 * In this example, for sake of simplicity, such information is set to a default value when the Lobby scene is loaded and it is lost upon user logout.
	 */
	private void InitPlayerProfile()
	{
        // Check if player experience is set in User Variables
        if (sfsLobby.MySelf.GetVariable(USERVAR_EXPERIENCE) == null)
        {
            // If not, set player profile default value; this in turn causes a UserVariablesUpdate event to be fired,
            // upon which we will display the player details in the user profile panel
            SFSUserVariable expVar = new SFSUserVariable(USERVAR_EXPERIENCE, 0);

			sfsLobby.Send(new SetUserVariablesRequest(new List<UserVariable>() { expVar }));
        }
        else
        {
            // If yes, display player details in user profile panel
            userProfilePanel.InitPlayerProfile(sfsLobby.MySelf);
        }
    }

	/**
	 * Show current user state in Buddy List system.
	 */
	private void DisplayUserStateAsBuddy()
	{
		if (sfsLobby.BuddyManager.MyOnlineState)
			userStatusLabel.text = sfsLobby.BuddyManager.MyState;
		else
			userStatusLabel.text = "Offline";
	}

	/**
	 * Initialize buddy-related entities.
	 */
	private void InitializeBuddyClient()
	{
		// Init buddy-related data structures
		buddyListItems = new Dictionary<string, BuddyListItem>();

		// For the buddy list we use a scrollable area containing a separate prefab for each buddy
		// The prefab contains clickable buttons to chat with the buddy, block or remove them
		List<Buddy> buddies = sfsLobby.BuddyManager.BuddyList;

		// Display buddy list items
		// All blocked buddies are displayed at the bottom of the list
		foreach (Buddy buddy in buddies)
			AddBuddyListItem(buddy, !buddy.IsBlocked);

		// Set current user details in buddy system
		userProfilePanel.InitBuddyProfile(sfsLobby.BuddyManager);

		// Display user state in buddy list system
		DisplayUserStateAsBuddy();

		// Show/hide buddies panel
		buddiesPanel.SetActive(sfsLobby.BuddyManager.MyOnlineState);
	}

	/**
	 * Create Buddy List Item prefab instance and add to buddy list.
	 */
	private void AddBuddyListItem(Buddy buddy, bool toTop = false)
	{
		string buddyName = buddy.Name;

		// Check if buddy list item already exists
		// This could happen if a temporary buddy is added permanently
		if (buddyListItems.ContainsKey(buddyName))
		{
			BuddyListItem buddyListItem = buddyListItems[buddyName];
			buddyListItem.SetState(buddy);
		}
		else
		{
			// Create buddy list item
			BuddyListItem buddyListItem = Instantiate(buddyListItemPrefab);
			buddyListItems.Add(buddyName, buddyListItem);

			// Init buddy list item
			buddyListItem.Init(buddy);

			// Set unread messages counter
			// (buddy could have sent messages while user was in the game scene)
			buddyListItem.SetChatMsgCounter(BuddyChatHistoryManager.GetUnreadMessagesCount(buddy.Name));

			// Add listeners to buttons
			buddyListItem.removeButton.onClick.AddListener(() => OnRemoveBuddyButtonClick(buddyName));
			buddyListItem.addButton.onClick.AddListener(() => OnAddBuddyButtonClick(buddyName));
			buddyListItem.blockButton.onClick.AddListener(() => OnBlockBuddyButtonClick(buddyName));
			buddyListItem.chatButton.onClick.AddListener(() => OnChatBuddyButtonClick(buddyName));

			// Add buddy list item to container
			buddyListItem.gameObject.transform.SetParent(buddyListContent, false);

			if (toTop)
				buddyListItem.gameObject.transform.SetAsFirstSibling();
		}
	}

	/**
	 * Update Buddy List Item prefab instance when buddy state changes.
	 */
	private void UpdateBuddyListItem(Buddy buddy)
	{
		// Get reference to buddy list item corresponding to Buddy
		buddyListItems.TryGetValue(buddy.Name, out BuddyListItem buddyListItem);

		if (buddyListItem != null)
		{
			// Check if the update was cause by a block/unblock request
			if (buddy.IsBlocked != buddyListItem.isBlocked)
			{
				// Move buddy to the bottom of the list
				if (buddy.IsBlocked)
					buddyListItem.gameObject.transform.SetAsLastSibling();
				else
					buddyListItem.gameObject.transform.SetAsFirstSibling();
			}

			// Update buddy list item
			buddyListItem.SetState(buddy);

			// Update buddy chat panel
			if (buddyChatPanel.BuddyName == buddy.Name)
				buddyChatPanel.SetState(buddy);
		}
	}

	/**
	 * Process the invitation in queue, displaying the invitation accept/refuse panel.
	 */
	private void ProcessInvitations()
	{
		// If the invitation panel is visible, then the user is already dealing with an invitation
		// Otherwise we can go on with the processing
		if (!invitationPanel.IsVisible)
		{
			while (invitationQueue.Count > 0)
			{
				// Get next invitation in queue
				InvitationWrapper iw = invitationQueue.Dequeue();

				// Evaluate remaining time for replying
				DateTime now = DateTime.Now;
				TimeSpan ts = now - iw.date;

				// Update expiration time
				iw.expiresInSeconds -= (int)Math.Floor(ts.TotalSeconds);

				// Display invitation only if expiration will occur in 3 seconds or more, otherwise discard it
				if (iw.expiresInSeconds >= 3)
				{
					invitationPanel.Show(iw);
					break;
				}
			}
		}
	}

    /**
	 * Send request to join or create game to Zone Extension.
	 */
    private void RequestStartGame()
	{
		// Add level name to request parameters
		ISFSObject reqParams = new SFSObject();
		reqParams.PutUtfString("name", selectedLevel);

		// Send request to Zone Extension
		sfsLobby.Send(new ExtensionRequest(ZONE_EXT_REQ_INIT, reqParams));
    }
    #endregion

    //----------------------------------------------------------
    // SmartFoxServer event listeners
    //----------------------------------------------------------
    #region
    private void OnExtensionResponse(BaseEvent evt)
    {
		String cmd = (String)evt.Params["cmd"];

		if (cmd == ZONE_EXT_RESP_INIT_ERR)
		{
			// Show Warning Panel prefab instance
			ShowError("Invalid level identifier");

			// Re-enable start buttons
			EnableStartButtons(true);
		}
	}

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

	private void OnBuddyError(BaseEvent evt)
	{
		// Show Warning Panel prefab instance
		ShowError("Buddy list system error: " + (string)evt.Params["errorMessage"]);
	}

	public void OnBuddyOnlineStateUpdate(BaseEvent evt)
	{
		Buddy buddy = (Buddy)evt.Params["buddy"];
		bool isItMe = (bool)evt.Params["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)
			UpdateBuddyListItem(buddy);
		else
		{
			DisplayUserStateAsBuddy();

			// Show/hide buddies panel
			buddiesPanel.SetActive(sfsLobby.BuddyManager.MyOnlineState);

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

	public void OnBuddyVariablesUpdate(BaseEvent evt)
	{
		Buddy buddy = (Buddy)evt.Params["buddy"];
		bool isItMe = (bool)evt.Params["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)
			UpdateBuddyListItem(buddy);
		else
			DisplayUserStateAsBuddy();
	}

	public void OnBuddyBlock(BaseEvent evt)
	{
		Buddy buddy = (Buddy)evt.Params["buddy"];

		UpdateBuddyListItem(buddy);
	}

	public void OnBuddyRemove(BaseEvent evt)
	{
		Buddy buddy = (Buddy)evt.Params["buddy"];

		// Get reference to buddy list item corresponding to Buddy
		buddyListItems.TryGetValue(buddy.Name, out BuddyListItem buddyListItem);

		// Remove buddy list item
		if (buddyListItem != null)
		{
			// Remove listeners
			buddyListItem.chatButton.onClick.RemoveAllListeners();
			buddyListItem.blockButton.onClick.RemoveAllListeners();
			buddyListItem.removeButton.onClick.RemoveAllListeners();

			// Remove buddy list item from dictionary
			buddyListItems.Remove(buddy.Name);

			// Destroy game object
			GameObject.Destroy(buddyListItem.gameObject);

			// Block chat interaction
			if (buddyChatPanel.BuddyName == buddy.Name)
				buddyChatPanel.Hide();
		}
	}

	public void OnBuddyAdd(BaseEvent evt)
	{
		Buddy buddy = (Buddy)evt.Params["buddy"];

		// Add buddy list item at the top of the list
		AddBuddyListItem(buddy, true);
	}

	public void OnBuddyMessage(BaseEvent evt)
	{
		// Add message to queue
		BuddyChatMessage chatMsg = BuddyChatHistoryManager.AddMessage(evt.Params);

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

			// Update buddy list item
			BuddyListItem buddyListItem = buddyListItems[chatMsg.buddyName];
			buddyListItem.gameObject.transform.SetAsFirstSibling();
			buddyListItem.SetChatMsgCounter(unreadMsgCnt);
		}
	}

	public void OnUserVariablesUpdate(BaseEvent evt)
	{
		User user = (User)evt.Params["user"];

		// Display player details in user profile panel
		if (user.IsItMe)
			userProfilePanel.InitPlayerProfile(user);
	}

	public void OnInvitation(BaseEvent evt)
	{
		Invitation invitation = (Invitation)evt.Params["invitation"];

		// Add invitation wrapper to queue
		// We use a quue because a user could receive multiple invitations within the time it takes to accept or refuse one
		if (invitationQueue == null)
			invitationQueue = new Queue<InvitationWrapper>();

		invitationQueue.Enqueue(new InvitationWrapper(invitation));

		// Trigger invitation processing
		ProcessInvitations();
	}
	#endregion
}
