using System;
using System.Collections;
using System.Collections.Generic;

using UnityEngine;
using UnityEngine.UI;

using Sfs2X;
using Sfs2X.Core;
using Sfs2X.Entities;
using Sfs2X.Entities.Variables;
using Sfs2X.Entities.Match;
using Sfs2X.Requests;
using Sfs2X.Requests.Cluster;
using Sfs2X.Util;

public class MainController : MonoBehaviour
{
	//----------------------------------------------------------
	// Editor public properties
	//----------------------------------------------------------

	[Tooltip("IP address or domain name of the SmartFoxCluster Lobby instance")]
	public string host = "127.0.0.1";

	[Tooltip("Display SmartFoxServer client debug messages")]
	public bool debug = false;

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

	public InputField nameInput;
	public Button enterButton;
	public Button joinCreateButton;
	public GameObject waitingControls;
	public Button lobbyLeaveButton;
	public Button lobbyKillButton;
	public Button gameLeaveButton;
	public Button gameKillButton;
	public Button interactButton;
	public Text lobbyConsoleText;
	public ScrollRect lobbyScrollView;
	public Toggle lobbyAutoscrollToggle;
	public Text gameConsoleText;
	public ScrollRect gameScrollView;
	public Toggle gameAutoscrollToggle;

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

    public const int TCP_PORT = 9933;		// Predefined TCP port in Overcast
    public const int HTTP_PORT = 8080;      // Predefined HTTP port in Overcast
    public const string ZONE = "Cluster";   // Predefined Zone name for SmartFoxCluster's Lobby

    private SmartFox sfsLobby;
	private SmartFox sfsGame;
	
	private string gameUsername;
	private string gamePassword;

	//----------------------------------------------------------
	// Unity callback methods
	//----------------------------------------------------------
	#region
	private void Start()
	{
		// Make the application run in background
		Application.runInBackground = true;

		// Focus on username input
		nameInput.Select();
		nameInput.ActivateInputField();
	}

	private void Update()
	{
		// Process the SmartFox events queues

		if (sfsLobby != null)
			sfsLobby.ProcessEvents();

		if (sfsGame != null)
			sfsGame.ProcessEvents();
	}

	private void OnApplicationQuit()
	{
		// On application quit, make sure a disconnection from all SmartFoxServer instances is executed
		// This is required because an active socket connection during the application quit process can cause a crash on some platforms

		if (sfsGame != null && sfsGame.IsConnected)
			sfsGame.Disconnect();

		if (sfsLobby != null && sfsLobby.IsConnected)
			sfsLobby.Disconnect();
	}
	#endregion

	//----------------------------------------------------------
	// UI event listeners
	//----------------------------------------------------------
	#region
	/**
	 * On username input edit end, if the Enter key was pressed, connect to SmartFoxServer Lobby node.
	 */
	public void OnNameInputEndEdit()
	{
		if (Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter))
			Connect();
	}

	/**
	 * On Enter button click, connect to SmartFoxServer Lobby node.
	 */
	public void OnEnterButtonClick()
	{
		Connect();
	}

	/**
	 * On Leave button click, disconnect from SmartFoxServer Lobby node.
	 */
	public void OnLeaveLobbyButtonClick()
	{
        sfsLobby.Disconnect();
	}

	/**
	 * On Kill button click, abruptly interrupt connection with SmartFoxServer Lobby node.
	 */
	public void OnKillLobbyButtonClick()
	{
		sfsLobby.KillConnection();
	}

	/**
	 * On Join or Create Room button click, quick join an existing public Room, or create one.
	 */
	public void OnJoinOrCreateButtonClick()
    {
		JoinOrCreateRoom();
    }

    /**
	 * On Cancel button click, stop waiting for available Game node or Room to join.
	 */
    public void OnCancelJoinButtonClick()
    {
		CancelWaitToJoin();

        // Reset user interface
        ResetUI();

		TraceLobby("Attempt to join or create Game Room cancelled");
    }

    /**
	 * On Leave button click, disconnect from SmartFoxServer Game node.
	 */
    public void OnLeaveGameButtonClick()
	{
		sfsGame.Disconnect();
	}

	/**
	 * On Kill button click, abruptly interrupt connection with SmartFoxServer Game node.
	 */
	public void OnKillGameButtonClick()
	{
		sfsGame.KillConnection();
	}

	/**
	 * On Interact button click, set a user variable.
	 */
	public void OnGameInteractButtonClick()
    {
		int cnt = 0;

		if (sfsGame.MySelf.GetVariable("cnt") != null)
			cnt = sfsGame.MySelf.GetVariable("cnt").GetIntValue();

		cnt++;

		UserVariable cntVar = new SFSUserVariable("cnt", cnt);
		sfsGame.Send(new SetUserVariablesRequest(new List<UserVariable>() { cntVar }));
	}
	#endregion

	//----------------------------------------------------------
	// Helper methods
	//----------------------------------------------------------
	#region
	private void TraceLobby(string msg, bool isWarning = false)
	{
		Trace(lobbyConsoleText, msg, isWarning);

		if (lobbyAutoscrollToggle.isOn)
			StartCoroutine(ScrollToBottom(lobbyScrollView));
	}

	private void TraceGame(string msg, bool isWarning = false)
	{
		Trace(gameConsoleText, msg, isWarning);

		if (gameAutoscrollToggle.isOn)
			StartCoroutine(ScrollToBottom(gameScrollView));
	}

	private void Trace(Text console, string msg, bool isWarning)
	{
		console.text += (console.text != "" ? "\n" : "");

		if (isWarning)
			console.text += "<color=red>";

		console.text += msg;

		if (isWarning)
			console.text += "</color>";
	}

	IEnumerator ScrollToBottom(ScrollRect scrollView)
	{
		yield return new WaitForEndOfFrame();

		scrollView.verticalNormalizedPosition = 0;
	}

	private string ComposeConnLostMessage(string prefix, string connLostReason)
	{
		string message = prefix;

		if (connLostReason != null)
		{
			message += " ";

			if (connLostReason == ClientDisconnectionReason.IDLE)
				message += "It looks like you have been idle for too much time.";
			else if (connLostReason == ClientDisconnectionReason.KICK)
				message += "You have been kicked by an administrator or moderator.";
			else if (connLostReason == ClientDisconnectionReason.BAN)
				message += "You have been banned by an administrator or moderator.";
			else if (connLostReason == ClientDisconnectionReason.MANUAL)
				message += "Manual disconnection requested.";
			else
				message += "The reason of the disconnection is unknown.";
		}

		return message;
	}

	/**
	 * Connect to SmartFoxServer Lobby.
	 */
	private void Connect()
	{
		// Reset user interface
		nameInput.interactable = false;
		enterButton.interactable = false;

		// Initialize SmartFox client used to connect to the cluster lobby node
#if !UNITY_WEBGL
		sfsLobby = new SmartFox();
#else
		sfsLobby = new SmartFox(UseWebSocket.WS_BIN);
#endif

		// Add event listeners
		AddSmartFoxLobbyListeners();

		// Configure SmartFox internal logger
		sfsLobby.Logger.EnableConsoleTrace = debug;

        // Set connection parameters
        ConfigData cfg = new ConfigData();
        cfg.Host = host;
        cfg.Port = TCP_PORT;
        cfg.Zone = ZONE;
        cfg.Debug = debug;

#if UNITY_WEBGL
		cfg.Port = HTTP_PORT;
#endif

        // Connect to SmartFoxServer
        sfsLobby.Connect(cfg);
	}

    /**
	 * Quick join an existing public Room, or create one.
	 */
    public void JoinOrCreateRoom()
    {
        // Disable button
        joinCreateButton.interactable = false;

        // Show waiting animation
        waitingControls.SetActive(true);

        TraceLobby("Sending request to join or create a public Room");

        // Set the common Room settings to create a new game Room if needed
        ClusterRoomSettings settings = new ClusterRoomSettings("GR_" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
        settings.GroupId = "games";
        settings.MaxUsers = 10;
        settings.MaxSpectators = 0;
        settings.IsGame = true;
        settings.IsPublic = true;

        // NOTE
        // A public Room needs criteria to be "findable" by other users; as an example, we use the Room name
        // (a private Room can't be found by users: they must be invited to join)

        // Set the match expression to search for an existing game Room
        MatchExpression exp = new MatchExpression(RoomProperties.NAME, StringMatch.STARTS_WITH, "GR_");

        // Send request
        sfsLobby.Send(new ClusterJoinOrCreateRequest(exp, new List<string>() { "games" }, settings));
    }

    /**
	 * Reset buttons state based on connection state to Lobby and Game nodes.
	 */
    private void ResetUI()
    {
		bool isLobbyConnected = (sfsLobby != null && sfsLobby.IsConnected);
		bool isGameConnected = (sfsGame != null && sfsGame.IsConnected);

		// LOBBY
		nameInput.interactable = !isLobbyConnected && !isGameConnected;
		enterButton.interactable = !isLobbyConnected && !isGameConnected;
		lobbyLeaveButton.interactable = isLobbyConnected;
		lobbyKillButton.interactable = isLobbyConnected;
		joinCreateButton.interactable = isLobbyConnected && !isGameConnected;

		// GAME
		gameLeaveButton.interactable = isGameConnected;
		gameKillButton.interactable = isGameConnected;
		interactButton.interactable = isGameConnected;
	}
	
	/**
	 * Add all SmartFoxServer-related event listeners required for the lobby node.
	 */
	private void AddSmartFoxLobbyListeners()
	{
		sfsLobby.AddEventListener(SFSEvent.CONNECTION, OnLobbyNodeConnection);
		sfsLobby.AddEventListener(SFSEvent.CONNECTION_LOST, OnLobbyNodeConnectionLost);
		sfsLobby.AddEventListener(SFSEvent.LOGIN, OnLobbyNodeLogin);
		sfsLobby.AddEventListener(SFSEvent.LOGIN_ERROR, OnLobbyNodeLoginError);

		sfsLobby.AddEventListener(SFSClusterEvent.CONNECTION_REQUIRED, OnGameNodeConnectionRequired);
		sfsLobby.AddEventListener(SFSClusterEvent.LOAD_BALANCER_ERROR, OnLoadBalancerError);

		TraceLobby("Lobby node event listeners added");
	}

	/**
	 * Remove all SmartFoxServer-related event listeners required for the lobby node.
	 */
	private void RemoveSmartFoxLobbyListeners()
	{
		if (sfsLobby != null)
		{
			sfsLobby.RemoveEventListener(SFSEvent.CONNECTION, OnLobbyNodeConnection);
			sfsLobby.RemoveEventListener(SFSEvent.CONNECTION_LOST, OnLobbyNodeConnectionLost);
			sfsLobby.RemoveEventListener(SFSEvent.LOGIN, OnLobbyNodeLogin);
			sfsLobby.RemoveEventListener(SFSEvent.LOGIN_ERROR, OnLobbyNodeLoginError);

            sfsLobby.RemoveEventListener(SFSClusterEvent.CONNECTION_REQUIRED, OnGameNodeConnectionRequired);
            sfsLobby.RemoveEventListener(SFSClusterEvent.LOAD_BALANCER_ERROR, OnLoadBalancerError);

			TraceLobby("Lobby node event listeners removed");
		}
	}

	/**
	 * Add all SmartFoxServer-related event listeners required for the game node.
	 */
	private void AddSmartFoxGameListeners()
	{
        sfsGame.AddEventListener(SFSEvent.CONNECTION, OnGameNodeConnection);
        sfsGame.AddEventListener(SFSEvent.CONNECTION_LOST, OnGameNodeConnectionLost);
        sfsGame.AddEventListener(SFSEvent.LOGIN, OnGameNodeLogin);
        sfsGame.AddEventListener(SFSEvent.LOGIN_ERROR, OnGameNodeLoginError);
        sfsGame.AddEventListener(SFSEvent.ROOM_CREATION_ERROR, OnGameRoomCreationError);
        sfsGame.AddEventListener(SFSEvent.ROOM_JOIN, OnGameRoomJoin);
        sfsGame.AddEventListener(SFSEvent.ROOM_JOIN_ERROR, OnGameRoomJoinError);
		sfsGame.AddEventListener(SFSEvent.USER_ENTER_ROOM, OnGameRoomUserEnter);
		sfsGame.AddEventListener(SFSEvent.USER_EXIT_ROOM, OnGameRoomUserExit);
		sfsGame.AddEventListener(SFSEvent.USER_VARIABLES_UPDATE, OnGameUserVarUpdate);

		TraceGame("Game node event listeners added");
	}

	/**
	 * Remove all SmartFoxServer-related event listeners required for the game node.
	 */
	private void RemoveSmartFoxGameListeners()
	{
		if (sfsGame != null)
		{
            sfsGame.RemoveEventListener(SFSEvent.CONNECTION, OnGameNodeConnection);
            sfsGame.RemoveEventListener(SFSEvent.CONNECTION_LOST, OnGameNodeConnectionLost);
            sfsGame.RemoveEventListener(SFSEvent.LOGIN, OnGameNodeLogin);
            sfsGame.RemoveEventListener(SFSEvent.LOGIN_ERROR, OnGameNodeLoginError);
            sfsGame.RemoveEventListener(SFSEvent.ROOM_CREATION_ERROR, OnGameRoomCreationError);
            sfsGame.RemoveEventListener(SFSEvent.ROOM_JOIN, OnGameRoomJoin);
            sfsGame.RemoveEventListener(SFSEvent.ROOM_JOIN_ERROR, OnGameRoomJoinError);
			sfsGame.RemoveEventListener(SFSEvent.USER_ENTER_ROOM, OnGameRoomUserEnter);
			sfsGame.RemoveEventListener(SFSEvent.USER_EXIT_ROOM, OnGameRoomUserExit);
			sfsGame.RemoveEventListener(SFSEvent.USER_VARIABLES_UPDATE, OnGameUserVarUpdate);

			TraceGame("Game node event listeners removed");
		}
	}

    /**
	 * Schedule request to join a game.
	 */
    private void ScheduleJoinOrCreateRoom()
    {
        TraceLobby("Trying again to join or create a Game Room in 2 seconds...");

        // Schedule another request to join or create a game
        Invoke("JoinOrCreateRoom", 2);
    }

    /**
	 * Cancel next scheduled request to join a game.
	 */
    private void CancelWaitToJoin()
	{
        CancelInvoke("JoinOrCreateRoom");

        // Hide waiting animation
        waitingControls.SetActive(false);
    }
	#endregion

	//----------------------------------------------------------
	// SmartFoxServer event listeners
	//----------------------------------------------------------
	#region LOBBY NODE
	private void OnLobbyNodeConnection(BaseEvent evt)
	{
		// Check if the connection was established or not
		if ((bool)evt.Params["success"])
		{
			TraceLobby("Connection to lobby node established; mode is: " + sfsLobby.ConnectionMode);
			TraceLobby("SFS2X API version: " + sfsLobby.Version);

			// Login
			sfsLobby.Send(new LoginRequest(nameInput.text));
		}
		else
		{
			TraceLobby("Connection to lobby node failed; is the server running at all?", true);

			// Reset
			RemoveSmartFoxLobbyListeners();
			sfsLobby = null;
		}
	}

	private void OnLobbyNodeConnectionLost(BaseEvent evt)
	{
		string connLostReason = (string)evt.Params["reason"];
		bool isManual = connLostReason == ClientDisconnectionReason.MANUAL;

		TraceLobby(ComposeConnLostMessage("An disconnection from lobby node occurred.", connLostReason), !isManual);

		// On lobby non-manual disconnection, if a game is in progress (which means a connection to the game node is still active)
		// we can ignore this event and let the user keep playing the game
		if (sfsGame != null && sfsGame.IsConnected)
			TraceLobby("Client still connected to game node: user can keep interacting with the game node");

        // Cancel scheduled request to join a game
        CancelWaitToJoin();

        // Reset user interface
        ResetUI();

		// Reset
		RemoveSmartFoxLobbyListeners();
		sfsLobby = null;
	}

	private void OnLobbyNodeLogin(BaseEvent evt)
	{
		TraceLobby("Login to lobby node successful");

		User user = (User)evt.Params["user"];

		nameInput.text = user.Name;

		// Reset user interface
		ResetUI();
	}

	private void OnLobbyNodeLoginError(BaseEvent evt)
	{
		TraceLobby("Login to lobby node failed due to the following error: " + (string)evt.Params["errorMessage"], true);

		// Disconnect
		sfsLobby.Disconnect();
	}
	
	private void OnGameNodeConnectionRequired(BaseEvent evt)
	{
		TraceLobby("Connection to game node required");

		// Retrieve connection settings
		ConfigData cfg = (ConfigData)evt.Params["configData"];
		
		// Retrieve and save login details
		gameUsername = (string)evt.Params["userName"];
		gamePassword = (string)evt.Params["password"];

		// Initialize SmartFox client used to connect to the cluster game node
#if !UNITY_WEBGL
		sfsGame = new SmartFox();
#else
		sfsGame = new SmartFox(UseWebSocket.WS_BIN);
#endif

		// Add event listeners
		AddSmartFoxGameListeners();

		// Establish a connection to the game node; a game room will be joined automatically after login
		sfsGame.Connect(cfg);
	}

	private void OnLoadBalancerError(BaseEvent evt)
	{
		TraceLobby("A load balancer error occurred: you should investigate the cluster state", true);
		
		// Schedule another request to join or create a game room
		ScheduleJoinOrCreateRoom();
    }
	#endregion
	
	#region GAME NODE
	private void OnGameNodeConnection(BaseEvent evt)
	{
		// Check if the connection was established or not
		if ((bool)evt.Params["success"])
		{
			TraceGame("Connection to game node established; mode is: " + sfsGame.ConnectionMode);

			// Enable lag monitor for the lobby node
			// In conjunction with the "Enable keepalive" setting on the Cluster Zone,
			// this keeps the connection to the lobby node alive while the user is playing a game on a game node
			sfsLobby.EnableLagMonitor(true);

			// Login
			sfsGame.Send(new LoginRequest(gameUsername, gamePassword));
		}
		else
		{
			TraceLobby("Connection to game node failed", true);

            // Reset
            RemoveSmartFoxGameListeners();
			sfsGame = null;

            // Schedule another request to join or create a game room
            ScheduleJoinOrCreateRoom();
        }
	}

	private void OnGameNodeConnectionLost(BaseEvent evt)
	{
		string connLostReason = (string)evt.Params["reason"];
		bool isManual = connLostReason == ClientDisconnectionReason.MANUAL;

		TraceGame(ComposeConnLostMessage("A disconnection from game node occurred.", connLostReason), !isManual);

		// If lobby connection is available, disable lag monitor
		if (sfsLobby != null && sfsLobby.IsConnected)
			sfsLobby.EnableLagMonitor(false);
		else
			TraceGame("No active lobby connection available; new login required", true);

		// Reset user interface
		ResetUI();

		// Reset
		RemoveSmartFoxGameListeners();
		sfsGame = null;
	}

	private void OnGameNodeLogin(BaseEvent evt)
	{
		TraceGame("Login to game node successful; waiting for Room auto-join event");

		// Nothing to do; a Room-autojoin is triggered by the server
	}

	private void OnGameNodeLoginError(BaseEvent evt)
	{
		TraceGame("Login to game node failed due to the following error: " + (string)evt.Params["errorMessage"], true);

		// Disconnect
		sfsGame.Disconnect();

        // Schedule another request to join or create a game room
        ScheduleJoinOrCreateRoom();
    }

	private void OnGameRoomCreationError(BaseEvent evt)
	{
		TraceGame("Game Room creation failed: " + (string)evt.Params["errorMessage"], true);

		// Disconnect
		sfsGame.Disconnect();

        // Schedule another request to join or create a game room
        ScheduleJoinOrCreateRoom();
    }

	private void OnGameRoomJoin(BaseEvent evt)
	{
		Room room = (Room)evt.Params["room"];

		TraceGame("Game Room '" + room.Name + "' joined successfully");

		// Reset user interface
		ResetUI();

		// Hide wait state
		CancelWaitToJoin();
    }

	private void OnGameRoomJoinError(BaseEvent evt)
	{
		TraceGame("Game Room join failed: " + (string)evt.Params["errorMessage"], true);

		// Disconnect from game node
		sfsGame.Disconnect();

        // Schedule another request to join or create a game room
        ScheduleJoinOrCreateRoom();
    }

	private void OnGameRoomUserEnter(BaseEvent evt)
	{
		User user = (User)evt.Params["user"];
		Room room = (Room)evt.Params["room"];

		TraceGame("User " + user.Name + " entered Game Room '" + room.Name + "'");
	}

	private void OnGameRoomUserExit(BaseEvent evt)
	{
		User user = (User)evt.Params["user"];
		Room room = (Room)evt.Params["room"];

		TraceGame("User " + user.Name + " left Game Room '" + room.Name + "'");
	}

	private void OnGameUserVarUpdate(BaseEvent evt)
	{
		User user = (User)evt.Params["user"];
		List<string> changedVars = (List<string>)evt.Params["changedVars"];

		if (changedVars.Contains("cnt"))
		{
			int cnt = user.GetVariable("cnt").GetIntValue();

			if (user.IsItMe)
				TraceGame("You clicked the interact button " + cnt + " time" + (cnt != 1 ? "s" : ""));
			else
				TraceGame("User " + user.Name + " clicked the interact button " + cnt + " time" + (cnt != 1 ? "s" : ""));
		}
	}
	#endregion
}
