#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
#endif

using UnityEngine;
using UnityEngine.SceneManagement;

using Sfs2X;
using Sfs2X.Core;
using Sfs2X.Requests;
using Sfs2X.Util;

#if UNITY_EDITOR
[InitializeOnLoad]
public class UnityEditorStartSceneSetter
{
    static UnityEditorStartSceneSetter()
    {
        // In Unity Editor, set Play Mode scene to LOGIN scene
        EditorSceneManager.playModeStartScene = AssetDatabase.LoadAssetAtPath<SceneAsset>("Assets/Scenes/Login.unity");
    }
}
#endif

///=================================================================================================================///

/**
 * Singleton class holding a reference to the SmartFox class instances providing a connection to the SmartFoxCluster Lobby node and Game node.
 * This is needed to share the client-server connections among the project's multiple scenes.
 * This class also takes care of loading the required scenes based on the connection state.
 */
public class GlobalManager : MonoBehaviour
{
    private static GlobalManager _instance;

    public static GlobalManager Instance
    {
        get
        {
            if (_instance == null)
                _instance = new GameObject("GlobalManager").AddComponent<GlobalManager>();
            
            return _instance;
        }
    }

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

    private SmartFox sfsLobby;
    private SmartFox sfsGame;
    private BaseSceneController currentSceneCtrl;
    private string lobbyUsername;
    private string gameUsername;
    private string gamePassword;
    private string connLostMsg;

    //----------------------------------------------------------
    // Unity callback methods
    //----------------------------------------------------------
    #region
    private void Awake()
    {
        // Do not destroy this object on scene change
        DontDestroyOnLoad(this);

        // Make sure the application runs in background
        Application.runInBackground = true;

        Debug.Log("Global Manager ready");
    }

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

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

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

    private void OnDestroy()
    {
        Debug.Log("Global Manager destroyed");
    }

    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

    //----------------------------------------------------------
    // Public methods
    //----------------------------------------------------------
    #region
    /**
     * Register the current scene controller class instance.
     */
    public void SetCurrentSceneController(BaseSceneController controller)
    {
        if (currentSceneCtrl != controller)
            currentSceneCtrl = controller;
    }

    /**
     * Return and delete the last connection lost message.
     */
    public string ConnectionLostMessage
    {
        get
        {
            string m = connLostMsg;
            connLostMsg = null;
            return m;
        }
    }

    /**
	 * Create and return the SmartFox class instance for the lobby node.
	 * Username is needed because the Global Manager takes care of performing the login step automatically.
	 */
    public SmartFox CreateLobbyClient(string username)
    {
        lobbyUsername = username;

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

        return sfsLobby;
    }

    /**
	 * Return the SmartFox class instance for the lobby node.
	 */
    public SmartFox GetLobbyClient()
    {
        return sfsLobby;
    }

    /**
	 * Return the SmartFox class instance for the game node.
	 */
    public SmartFox GetGameClient()
    {
        return sfsGame;
    }
    #endregion

    //----------------------------------------------------------
    // Private methods
    //----------------------------------------------------------
    #region
    /**
	 * 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);

        Debug.Log("Global Manager - 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);

            Debug.Log("Global Manager - 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);

        Debug.Log("Global Manager - 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);

            Debug.Log("Global Manager - Game node event listeners removed");
        }
    }

    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
                message += "The reason of the disconnection is unknown.";
        }

        return message;
    }
    #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"])
        {
            Debug.Log("Connection to lobby node established; mode is: " + sfsLobby.ConnectionMode);
            Debug.Log("SFS2X API version: " + sfsLobby.Version);

            // Login
            sfsLobby.Send(new LoginRequest(lobbyUsername));
        }
        else
        {
            Debug.Log("Connection to lobby node failed; is the server running at all?");

            // Show error message in current scene (which can be the LOGIN scene only)
            currentSceneCtrl.ShowError("Connection to lobby node failed; is the server running at all?");

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

    private void OnLobbyNodeConnectionLost(BaseEvent evt)
    {
        string connLostReason = (string)evt.Params["reason"];
        string currSceneName = SceneManager.GetActiveScene().name;

        Debug.Log("Connection to lobby node lost; reason is: " + connLostReason);

        // 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; they will be sent to the LOGIN scene when leaving the game
        if (currSceneName == "Game" && connLostReason != ClientDisconnectionReason.MANUAL)
            Debug.Log("A game is currently in progress; disconnection ignored (user can keep playing and will be sent to the Login scene when leaving the game)");

        // In all other cases, go to the LOGIN scene which will display an error message (except in case of intentional disconnection)
        else
        {
            if (currSceneName != "Login")
            {
                Debug.Log("Switching to Login scene");
                SceneManager.LoadScene("Login");

                if (connLostReason != ClientDisconnectionReason.MANUAL)
                {
                    // Save disconnection message, which can be retrieved by the LOGIN scene to display a warning
                    // NOTE: we can't call currentSceneCtrl.ShowError() because scene controller is not yet ready (until the Awake event)
                    connLostMsg = ComposeConnLostMessage("An unexpected disconnection from lobby node occurred.", connLostReason);
                }
            }
        }

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

    private void OnLobbyNodeLogin(BaseEvent evt)
    {
        Debug.Log("Login to lobby node successful");

        // Load lobby scene
        SceneManager.LoadScene("Lobby");
    }

    private void OnLobbyNodeLoginError(BaseEvent evt)
    {
        Debug.Log("Login to lobby node failed");

        // Show error message in current scene (which can be the LOGIN scene only)
        currentSceneCtrl.ShowError("Login to lobby node failed due to the following error: " + (string)evt.Params["errorMessage"]);

        // Disconnect
        sfsLobby.Disconnect();
    }

    private void OnGameNodeConnectionRequired(BaseEvent evt)
    {
        Debug.Log("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)
    {
        Debug.Log("Load balancer error");

        // Notify failed attempt to join game to the current scene (which can be the LOBBY scene only)
        currentSceneCtrl.OnGameJoinFailed();
    }
    #endregion

    #region GAME NODE
    private void OnGameNodeConnection(BaseEvent evt)
    {
        // Check if the connection was established or not
        if ((bool)evt.Params["success"])
        {
            Debug.Log("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
        {
            Debug.Log("Connection to game node failed");

            // Reset
            RemoveSmartFoxGameListeners();
            sfsGame = null;

            // Notify failed attempt to join game to the current scene (which can be the LOBBY scene only)
            currentSceneCtrl.OnGameJoinFailed();
        }
    }

    private void OnGameNodeConnectionLost(BaseEvent evt)
    {
        string connLostReason = (string)evt.Params["reason"];
        string currSceneName = SceneManager.GetActiveScene().name;

        Debug.Log("Connection to game node lost; reason is: " + connLostReason);

        // If lobby connection is not avalable, go to the LOGIN scene
        if (sfsLobby == null || !sfsLobby.IsConnected)
        {
            Debug.Log("No lobby node connection currently active; switching to Login scene");
            SceneManager.LoadScene("Login");

            // Save disconnection message, which can be retrieved by the LOGIN scene to display a warning
            // NOTE: we can't call currentSceneCtrl.ShowError() because scene controller is not yet ready (until the Awake event)
            if (connLostReason != ClientDisconnectionReason.MANUAL)
                connLostMsg = ComposeConnLostMessage("An unexpected disconnection from game node occurred and no lobby connection is currently available.", connLostReason);
            else
                connLostMsg = ComposeConnLostMessage("No active lobby connection available.", null);
        }

        // Otherwise go to the LOBBY scene
        else
        {
            // Disable lag monitor for the lobby node
            sfsLobby.EnableLagMonitor(false);

            // This check is needed because some errors in the Lobby scene itself (i.e. Game Room creation error)
            // would cause its reload and the error message disappear suddenly
            if (SceneManager.GetActiveScene().name != "Lobby")
            {
                Debug.Log("Switching to Lobby scene");
                SceneManager.LoadScene("Lobby");
            }

            if (connLostReason != ClientDisconnectionReason.MANUAL)
            {
                // Save disconnection message, which can be retrieved by the LOBBY scene to display a warning; see NOTE above
                connLostMsg = ComposeConnLostMessage("An unexpected disconnection from game node occurred.", connLostReason);
            }
        }

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

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

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

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

        // Disconnect
        sfsGame.Disconnect();

        // Notify failed attempt to join game to the current scene (which can be the LOBBY scene only)
        currentSceneCtrl.OnGameJoinFailed();
    }

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

        // Disconnect
        sfsGame.Disconnect();

        // Notify failed attempt to join game to the current scene (which can be the LOBBY scene only)
        currentSceneCtrl.OnGameJoinFailed();
    }

    private void OnGameRoomJoin(BaseEvent evt)
    {
        Debug.Log("Game Room joined successfully; switching to Game scene");

        // Load game scene
        SceneManager.LoadScene("Game");
    }

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

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

        // Notify failed attempt to join game to the current scene (which can be the LOBBY scene only)
        currentSceneCtrl.OnGameJoinFailed();
    }
    #endregion
}
