﻿using AsmodeeDigital.Common.Plugin.Domain.Data;
using AsmodeeDigital.Common.Plugin.Manager.Event;
using AsmodeeDigital.Common.Plugin.Utils;
using AsmodeeDigital.Common.Plugin.Utils.Extensions;
using AsmodeeDigital.PlayReal.Plugin.Domain.GameState;
using AsmodeeDigital.PlayReal.Plugin.Domain.Message;
using com.daysofwonder;
using com.daysofwonder.async;
using com.daysofwonder.game.push;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Unity.IO.Compression;
using UnityEngine;

namespace AsmodeeDigital.PlayReal.Plugin.Network
{
    /// <summary>
    /// Create a connection to Days Of Wonders Server. Send asynchronous request to DoW server and fire events when a response is received from the server
    /// </summary>
    public class ServerConnection : IDisposable
    {
        #region Events
        /// <summary>
        /// Event fired when the socket is connected to the server
        /// </summary>
        public event Action ConnectedToServerEvent;
        /// <summary>
        /// Event fired when the socket is disconnected from the server
        /// </summary>
        public event Action DisconnectedFromServerEvent;
        /// <summary>
        /// Event fired when the user is authenticated on the server after a disconnection
        /// </summary>
        public event Action ReconnectedToServerEvent;
        /// <summary>
        /// Event fired when the reconnection is not possible after many trials
        /// </summary>
        public event Action CantReconnectToServerEvent;
        /// <summary>
        /// Event fired when the player is authenticated
        /// </summary>
        public event Action<AsyncConnectedRequest> AsyncConnectedEvent;
        /// <summary>
        /// Event fired when there is an error in the authentication
        /// </summary>
        public event Action<AsyncConnectionErrorRequest> AsyncConnectionErrorEvent;
        /// <summary>
        /// Event fired when a game in which the player is invited is created
        /// </summary>
        public event Action<GameCreatedRequest> GameCreatedEvent;
        /// <summary>
        /// Event fired when an entry log is written by the component
        /// </summary>
        public event Action<string> LogMessageEvent;
        /// <summary>
        /// Event fired when the server ask the player to perform a game action. the player became the "active player"
        /// </summary>
        public event Action<ActionRequiredRequest> ActionRequiredEvent;
        /// <summary>
        /// Event fired when
        /// </summary>
        public event Action<StatusReport> GameStatusReportEvent;
        /// <summary>
        /// Event fired after a WhatsNewPussycat call. Contains all of the game informations in which a player is involved
        /// </summary>
        public event Action<List<StatusReport>> GameStatusReportAllEvent;
        /// <summary>
        /// Event fired when an invited player answered to an invitation
        /// </summary>
        public event Action<InvitationAnsweredRequest> InvitationAnsweredEvent;
        /// <summary>
        /// Event fired when one or more player presence status changes
        /// </summary>
        public event Action<PlayerPresenceUpdateRequest> PlayerPresenceUpdatedEvent;
        /// <summary>
        /// Event fired after a AskPlayerInfoRequest
        /// </summary>
        public event Action<LobbyPlayerInfoRequest> LobbyPlayerInfoEvent;
        /// <summary>
        /// Event fired after the AskPlayerInfoRequest. It contains the full details of a given player.
        /// </summary>
        public event Action<PlayerInfoRequest> PlayerInfoEvent;
        /// <summary>
        /// Event fired when a heartbeat request is returned by the server
        /// </summary>
        public event Action<PingRequest> PingEvent;
        /// <summary>
        /// Event fired after the AsyncBuddyListRequest. Contains all the informations of friends
        /// </summary>
        public event Action<AsyncBuddyListContentRequest> AsyncBuddyListContentEvent;
        /// <summary>
        /// Event fired after a CommitActionRequest
        /// </summary>
        public event Action<ActionCommitedRequest> ActionCommitedEvent;
        /// <summary>
        /// Event fired by the server when a player has been replaced by a robot, or when the player comes back (in Synchronous games)
        /// </summary>
        public event Action<PlayerReplacedRequest> PlayerReplacedEvent;
        /// <summary>
        /// Event fired by the server if there’s no player (all timed out, or all withdrawn) to keep the game alive
        /// </summary>
        public event Action<GameAbortedRequest> GameAbortedEvent;
        /// <summary>
        /// Event fired by the server if the server never got a CommitActionRequest from the “active” player in the required time, or this player forfeited, this message is sent to the next player in turn with the hope that it could ever implement a bot that could take this player’s turn
        /// </summary>
        public event Action<PlayerTimeoutRequest> PlayerTimeoutEvent;
        /// <summary>
        /// Event fired by the server When one of the player sent a MulticastDataRequest the server will send back a ClientDataRequest to the multicasted clients
        /// </summary>
        public event Action<ClientDataRequest> ClientDataEvent;
        /// <summary>
        /// Event fired by the server to all present players after receiving a GameOverRequest. It allows the client to display ranking updates, achievements earned, final scores. Non present players can get access to the same information in the game status report from a WhatsNewPussyCatRequest.
        /// </summary>
        public event Action<GameOutcomeRequest> GameOutcomeEvent;
        /// <summary>
        /// Event fired by the server upon reception of a GameForfeitRequest to all the connected players, or if all players in a game have timed out
        /// </summary>
        public event Action<GameForfeitedRequest> GameForfeitedEvent;
        /// <summary>
        /// Event fired by the server after a EnterLobbyRequest.
        /// </summary>
        public event Action<LobbyEnteredRequest> LobbyEnteredEvent;
        /// <summary>
        /// Sent by the server from time to time to send the whole in lobby Player list Note that the player_list field is a byte array resulting of the compression of the PlayerList message.
        /// </summary>
        public event Action<List<SmallPlayer>> LobbyPlayerListEvent;
        /// <summary>
        /// Sent by the server from time to time to send the currently open game list
        /// </summary>
        public event Action<GameList> LobbyGameListEvent;
        /// <summary>
        /// Sent by the server to confirm that the client has exited the lobby
        /// </summary>
        public event Action<LobbyExitedRequest> LobbyExitedEvent;
        /// <summary>
        /// Sent by the server in response to a MulticastChatRequest to the players of the game game_id as long as they were listed in MulticastChatRequest.recipient_ids or recipient_ids is empty.
        /// </summary>
        public event Action<ClientChatRequest> ClientChatEvent;
        /// <summary>
        /// Sent to in response to GetChatHistoryRequest
        /// </summary>
        public event Action<ClientChatHistoryRequest> ClientChatHistoryEvent;
        /// <summary>
        /// Sent in response to a given AsyncIgnoreListRequest
        /// </summary>
        public event Action<AsyncIgnoreListContentRequest> AsyncIgnoreListContentEvent;
        /// <summary>
        /// Sent by the server after receiving an LobbyCreateGameRequest for confirmation that the operation succeeded.
        /// </summary>
        public event Action<LobbyGameCreatedRequest> LobbyGameCreatedEvent;
        /// <summary>
        /// Sent to the joining player if the server denies the join
        /// </summary>
        public event Action<LobbyJoinDeniedRequest> LobbyJoinDeniedEvent;
        /// <summary>
        /// Sent to all the player presents in the game (and the joining player) when a player joins a given game and it succeed
        /// </summary>
        public event Action<LobbyNewPlayerRequest> LobbyNewPlayerEvent;
        /// <summary>
        /// Sent by the server in response to the LobbyLeaveGameRequest to all the players in the game and the leaving player
        /// </summary>
        public event Action<LobbyPlayerLeftGameRequest> LobbyPlayerLeftGameEvent;
        /// <summary>
        /// Sent in response to a client request that generated an error.
        /// </summary>
        public event Action<ErrorRequest> ErrorEvent;
        /// <summary>
        /// Sent to the MulticastChatRequest sender if her message has been blocked or if she’s been muted
        /// </summary>
        public event Action<ClientChatBlockedRequest> ClientChatBlockedEvent;
        /// <summary>
        /// Sent by the server during statistics broadcast, or after an initial AskServerStatisticsRequest
        /// </summary>
        public event Action<ServerStatisticsRequest> ServerStatisticsEvent;
        /// <summary>
        /// Sent by the server to the client in response to a successfull AsyncUnlinkedDeviceRequest
        /// </summary>
        public event Action<AsyncDeviceUnlinkedRequest> AsyncDeviceUnlinkedEvent;
        /// <summary>
        /// Despite its name, this request is sent by the server when the presence of some monitored players change.
        /// </summary>
        public event Action<AsyncBuddyPresencePartialUpdateRequest> AsyncBuddyPresencePartialUpdateEvent;
        /// <summary>
        /// Sent by the server when a buddy has been added
        /// </summary>
        public event Action<AsyncBuddyAddedRequest> AsyncBuddyAddedEvent;
        /// <summary>
        /// Sent by the server when a buddy has been removed
        /// </summary>
        public event Action<AsyncBuddyRemovedRequest> AsyncBuddyRemovedEvent;
        /// <summary>
        /// Sent by the server in response to the GetClocksRequest
        /// </summary>
        public event Action<ClocksStatusRequest> ClocksStatusEvent;
        /// <summary>
        /// Sent by the server when an ignore have been added
        /// </summary>
        public event Action<AsyncIgnoreAddedRequest> AsyncIgnoreAddedEvent;
        /// <summary>
        /// Sent by the server when an ignore has been removed
        /// </summary>
        public event Action<AsyncIgnoreRemovedRequest> AsyncIgnoreRemovedEvent;
        /// <summary>
        /// Sent by the server after receiving a StartObserveGameRequest or a StopObserveGameRequest
        /// </summary>
        public event Action<GameObservedRequest> GameObservedEvent;
        #endregion

        /// <summary>
        /// Enable the log system
        /// </summary>
        public bool LogEnabled { get; set; }

        /// <summary>
        /// Start each log entry with time
        /// </summary>
        public bool LogWithTime { get; set; }

        /// <summary>
        /// Name of each log entry (can be empty) usefull for automatic testing
        /// </summary>
        public String LogName { get; set; }

        /// <summary>
        /// Enable the log for the bytes
        /// </summary>
        public bool LogPacketEnabled = false;

        /// <summary>
        /// Enable the log for the packets
        /// </summary>
        public bool LogAllBytesEnabled = false;

        /// <summary>
        /// True if the application is quitting. Setted in Dispose
        /// </summary>
        public bool IsQuitting = false;

        /// <summary>
        /// State of the connection the the server
        /// </summary>
        public ConnectionState ConnectionState = ConnectionState.NotInitialized;

        /// <summary>
        /// Type model of all proto serialized class of the game using ServerConnection
        /// </summary>
        public PlayRealSerializer GameDataModel = new PlayRealSerializer();

        /// <summary>
        /// Endpoint of DoW server
        /// </summary>
        private IPEndPoint _endPoint;

        /// <summary>
        /// Socket connected to DoW server
        /// </summary>
        public Socket _socket = null;

        /// <summary>
        /// SslStream used in a ssl connection
        /// </summary>
        private SslStream _sslStream = null;

        /// <summary>
        /// Stream of the socket
        /// </summary>
        private NetworkStream _networkStream = null;

        /// <summary>
        /// Is this the first packet sent to DoW server?
        /// </summary>
        private bool _allreadySent = false;

        /// <summary>
        /// Bytes buffered from the server stream
        /// </summary>
        private BufferPacket _bufferPacket = new BufferPacket();

        /// <summary>
        /// Connection parameters to DoW server
        /// </summary>
        private DoWNetworkParameters _networkParameters;

        /// <summary>
        /// Internal timer in charge of continuous ping to DoW server
        /// </summary>
        private System.Timers.Timer _timerPing;

        /// <summary>
        /// Internal timer in charge of reconnection
        /// </summary>
        private System.Timers.Timer _timerReconnect;

        /// <summary>
        /// Maximum recconnection attempt after a disconnection
        /// </summary>
        private int _maxReconnectionAttempt = 10;

        /// <summary>
        /// Count of reconnection attempt after a disconnection
        /// </summary>
        private int _reconnectionAttempt = 0;

        /// <summary>
        /// Force the reconnection
        /// </summary>
        private bool _forceReconnection = false;

        /// <summary>
        /// Name of the connected user (for auto-reconnection)
        /// </summary>
        private string _lastUserName = string.Empty;

        /// <summary>
        /// Password of the connected user (for auto-reconnection)
        /// </summary>
        private string _lastPasswordMD5 = string.Empty;

        /// <summary>
        /// Device type used for authentication  (for auto-reconnection)
        /// </summary>
        private Devices.Type _lastDeviceType;

        /// <summary>
        /// Device token used for authentication (for auto-reconnection)
        /// </summary>
        private byte[] _lastDeviceToken;

        /// <summary>
        /// Last session (for auto-reconnection)
        /// </summary>
        private Session _lastSession = null;

        public ServerConnection(DoWNetworkParameters networkParameters)
        {
            _networkParameters = networkParameters;

            LogEnabled = true;
            LogWithTime = false;

            ConnectedToServerEvent += ServerConnection_ConnectedToServerEvent;
            AsyncConnectedEvent += ServerConnection_AsyncConnectedEvent;
        }

        #region Connection
        /// <summary>
        /// Connect to server specified in DoWNetworkParameters passed in constructor
        /// </summary>
        public void Connect()
        {
            IPHostEntry ipHostInfo = Dns.GetHostEntry(_networkParameters.HostName);
            IPAddress host = ipHostInfo.AddressList[0];
            Debug.LogFormat(
                "Connecting to Scalable Server through IPv{0} at {1}",
                (host.AddressFamily == AddressFamily.InterNetworkV6) ? "6" : "4",
                _networkParameters.HostName
            );

            _endPoint = new IPEndPoint(host, _networkParameters.HostPort);
            _socket = new Socket(host.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

            ConnectionState = ConnectionState.WaitingConnection;
            _socket.BeginConnect(_endPoint, new AsyncCallback(ConnectCallback), _socket);
            _allreadySent = false;

            //--- Ping server every 10 seconds
            _timerPing = new System.Timers.Timer(_networkParameters.PingDelay * 1000f);
            _timerPing.Elapsed += TimerPing_Elapsed;
            _timerPing.Enabled = true;
            //---
        }

        /// <summary>
        /// Check the Pin Public Key
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="certificate"></param>
        /// <param name="chain"></param>
        /// <param name="sslPolicyErrors"></param>
        /// <returns></returns>
        public bool PinPublicKey(object sender, X509Certificate certificate, X509Chain chain,
                            SslPolicyErrors sslPolicyErrors)
        {
            if (null == certificate)
                return false;

            //Warn : If there is an error with GetPublicKeyString it's because the target in unity is Web.
            String pk = certificate.GetPublicKeyString();

            for (int i = 0; i < _networkParameters.PinPublicKeys.Length; i++)
            {
                if (pk.Equals(_networkParameters.PinPublicKeys[i]))
                    return true;
            }

            Debug.LogError("The public key do not correspond to the key received from the server");

            return false;
        }

        /// <summary>
        /// Callback called when the component is connected to DoW server
        /// </summary>
        /// <param name="ar"></param>
        private void ConnectCallback(IAsyncResult ar)
        {
            try
            {
                ConnectionState = ConnectionState.Connected;

                Socket client = (Socket)ar.AsyncState;
                client.EndConnect(ar);

                _networkStream = new NetworkStream(_socket);

                if (_networkParameters.UseSSL)
                {
                    _sslStream = new SslStream(_networkStream, true, new RemoteCertificateValidationCallback(PinPublicKey));
                    _sslStream.AuthenticateAsClient("always_wrong");
                }

                if (ConnectedToServerEvent != null)
                {
                    EventManager.Instance.QueueEvent(ConnectedToServerEvent);
                }
            }
            catch (Exception e)
            {
                Log(e);
            }
        }
        #endregion

        #region Reconnection
        private void DisconnectedFromServer()
        {
            if (_forceReconnection)
                return;

            Log("Disconnected from the server", false, LogType.Warning);

            if (DisconnectedFromServerEvent != null)
                EventManager.Instance.QueueEvent(DisconnectedFromServerEvent);

            Reconnect();
        }

        public void Reconnect()
        {
            if (_forceReconnection)
                return;

            _forceReconnection = true;
            ConnectionState = ConnectionState.NotConnected;
            StartTimerReconnect();
        }

        private void StartTimerReconnect()
        {
            if (_timerReconnect == null)
            {
                _reconnectionAttempt = 0;
                _timerReconnect = new System.Timers.Timer(_networkParameters.AutoReconnectDelay * 1000f);
                _timerReconnect.Elapsed += _timerReconnect_Elapsed;
                _timerReconnect.Start();

                //---> Immediate reconnection attempt
                _timerReconnect_Elapsed(null, null);
            }
        }

        private void StopTimerReconnect()
        {
            if (_timerReconnect != null)
            {
                _timerReconnect.Stop();
                _timerReconnect.Dispose();
                _timerReconnect = null;
            }
        }

        private void _timerReconnect_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            if (ConnectionState == ConnectionState.Connected)
                ConnectionState = ConnectionState.NotConnected;

            if (ConnectionState == ConnectionState.WaitingConnection || ConnectionState == ConnectionState.WaitingAuthentication)
                return;

            //---> Max attempt reached
            if (_reconnectionAttempt >= _maxReconnectionAttempt)
            {
                Log(String.Format("Can't reconnect to the server after {0} trials", _maxReconnectionAttempt), false, LogType.Error);
                StopTimerReconnect();

                if (CantReconnectToServerEvent != null)
                    EventManager.Instance.QueueEvent(CantReconnectToServerEvent);

                return;
            }

            _reconnectionAttempt++;

            Log(String.Format("Reconnection attempt ({0}/{1})", _reconnectionAttempt, _maxReconnectionAttempt), false, LogType.Warning);

            if (ConnectionState == ConnectionState.NotConnected)
                Connect();
        }

        private void ServerConnection_ConnectedToServerEvent()
        {
            if (_forceReconnection)
            {
                Log("Reconnection : Connected to server");

                StopTimerReconnect();

                AuthenticateUser();
            }
        }

        private void ServerConnection_AsyncConnectedEvent(AsyncConnectedRequest obj)
        {
            ConnectionState = ConnectionState.Authenticated;

            if (_forceReconnection)
            {
                Log("Reconnection : Authenticated");

                _forceReconnection = false;

                if (ReconnectedToServerEvent != null)
                    EventManager.Instance.QueueEvent(ReconnectedToServerEvent);
            }
        }
        #endregion

        #region Send
        /// <summary>
        /// Send packet asynchronously to the server. When Sending is completed (in SendCallback) call the optional callback
        /// </summary>
        /// <param name="packet">Packet to send to DoW server</param>
        /// <param name="callBackSent">Optional method to call after the packet has been sent</param>
        private void SendToServer(Packet packet, Action callBackSent = null)
        {
            if (IsQuitting)
                return;

            if (ConnectionState < ConnectionState.Connected)
                throw new Exception("Not authenticated to DoW server");

            try
            {
                StateObject stateSend = new StateObject();

                stateSend.CallbackSent = callBackSent;

                if (_networkParameters.UseSSL)
                    stateSend.WorkStream = _sslStream;
                else
                    stateSend.WorkStream = _networkStream;

                //--- Serialize and copy the packet in a bytes array
                byte[] buffer;
                using (var mStream = new MemoryStream())
                {
                    GameDataModel.Serialize(mStream, packet);
                    buffer = mStream.ToArray();
                }
                //---

                //--- Reversed 4 lengths bytes for buffer length
                byte[] intSize = BitConverter.GetBytes(buffer.Length);
                Array.Reverse(intSize);
                //---

                //--- 4 bytes size + buffer
                byte[] finaleBuffer = new byte[buffer.Length + 4];
                Array.Copy(intSize, 0, finaleBuffer, 0, 4);
                Array.Copy(buffer, 0, finaleBuffer, 4, buffer.Length);
                //---

                //---> Start Async Write
                stateSend.WorkStream.BeginWrite(finaleBuffer, 0, finaleBuffer.Length, new AsyncCallback(SendCallback), stateSend);
            }
            catch (IOException ex)
            {
                Log(ex);
                DisconnectedFromServer();
            }
            catch (Exception ex)
            {
                Log(ex);
            }
        }

        /// <summary>
        /// Callback called when a packet has been sent
        /// </summary>
        /// <param name="ar">Asynchronous request result</param>
        private void SendCallback(IAsyncResult ar)
        {
            if (ConnectionState < ConnectionState.Connected)
                return;

            try
            {
                StateObject stateSend = (StateObject)ar.AsyncState;

                stateSend.WorkStream.EndWrite(ar);

                if (stateSend.CallbackSent != null)
                    stateSend.CallbackSent();

                //---> First connection for receiving data
                if (!_allreadySent)
                {
                    Receive();
                    _allreadySent = true;
                }
            }
            catch (IOException ex)
            {
                Log(ex);
                DisconnectedFromServer();
            }
            catch (Exception e)
            {
                Log(e);
            }
        }
        #endregion

        #region Receive
        /// <summary>
        /// Start receiving data
        /// </summary>
        /// <param name="stateReceive">State object</param>
        private void Receive()
        {
            if (ConnectionState < ConnectionState.Connected)
                return;

            try
            {
                StateObject stateReceive = new StateObject();

                if (_networkParameters.UseSSL)
                    stateReceive.WorkStream = _sslStream;
                else
                    stateReceive.WorkStream = _networkStream;

                stateReceive.WorkStream.BeginRead(stateReceive.Buffer, 0, StateObject.BufferSize, new AsyncCallback(ReceiveCallback), stateReceive);
            }
            catch (IOException ex)
            {
                Log(ex);
                DisconnectedFromServer();
            }
            catch (Exception e)
            {
                Log(e);
            }
        }

        /// <summary>
        /// Callback called when a packet has been received
        /// </summary>
        /// <param name="ar"></param>
        private void ReceiveCallback(IAsyncResult ar)
        {
            if (ConnectionState < ConnectionState.Connected)
                return;

            try
            {
                // Retrieve the state object and the client socket 
                // from the asynchronous state object.
                // Then read data from the remote device.
                StateObject stateReceive = (StateObject)ar.AsyncState;

                int bytesRead = stateReceive.WorkStream.EndRead(ar);

                if (bytesRead > 0)
                {
                    for (int i = 0; i < bytesRead; i++)
                    {
                        _bufferPacket.Bytes.Add(stateReceive.Buffer[i]);
                    }

                    ComputeStateReceive(_bufferPacket);
                }

                //--- Log
                if (LogAllBytesEnabled)
                {
                    LogAllBytes();
                }
                //---

                while (_bufferPacket.BytesToRead == 0)
                {
                    //---> Get message from the byte buffer
                    _bufferPacket.StreamPacket = new MemoryStream(_bufferPacket.Bytes.ToArray(), 4, _bufferPacket.PacketSize.Value, false, true);

                    //--- Log
                    if (LogPacketEnabled)
                    {
                        LogPacket();
                    }
                    //---

                    //---> Remove bytes already read
                    _bufferPacket.Bytes.RemoveRange(0, _bufferPacket.PacketSize.Value + 4);

                    ComputeStateReceive(_bufferPacket);

                    Packet packet = (Packet)GameDataModel.Deserialize(_bufferPacket.StreamPacket, null, typeof(Packet));

                    PacketProcessor(packet);
                }

                if (ConnectionState < ConnectionState.Connected)
                    return;

                Receive();
            }
            catch (IOException ex)
            {
                Log(ex);
                DisconnectedFromServer();
            }
            catch (Exception ex)
            {
                Log(ex);
                LogAllBytes();
                LogPacket();
            }
        }

        private void ComputeStateReceive(BufferPacket bufferPacket)
        {
            lock (bufferPacket)
            {
                //--- Compute the size of the packet to read
                bufferPacket.PacketSize = GetSize(bufferPacket.Bytes.ToArray());
                bufferPacket.BytesToRead = bufferPacket.PacketSize + 4;
                //---

                //---> Subtract the count bytes from the remaining count
                if (bufferPacket.BytesToRead > 0)
                {
                    bufferPacket.BytesToRead -= bufferPacket.Bytes.Count;

                    if (bufferPacket.BytesToRead < 0)
                        bufferPacket.BytesToRead = 0;
                }
            }
        }
        #endregion

        #region Log
        /// <summary>
        /// Log exception
        /// </summary>
        /// <param name="ex">Exception to log</param>
        private void Log(Exception ex)
        {
            Log(string.Format(
                "{0}\r\n{1}\r\nInner exception :\r\n{2}\r\n{3}\r\n{4}",
                ex.GetType().ToString(),
                ex.Message,
                ex.InnerException != null ? ex.InnerException.GetType().ToString() : string.Empty,
                ex.InnerException != null ? ex.InnerException.Message : String.Empty,
                ex.StackTrace),
                false, LogType.Error);
        }

        /// <summary>
        /// Log standard message and raise event on client side
        /// </summary>
        /// <param name="message">Message to log</param>
        /// <param name="onlyConsole">If true, do not raise LogMessage event</param>
        private void Log(string message, bool onlyConsole = false, LogType logType = LogType.Log)
        {
            if (LogMessageEvent != null && (!onlyConsole))// || !Application.isEditor))
                EventManager.Instance.QueueEvent(LogMessageEvent, message);

            if (!LogEnabled)
                return;

            string startMessage = string.Empty;

            if (LogWithTime)
                startMessage = String.Format("{0:HH:mm:ss} - ", DateTime.Now);

            if (!string.IsNullOrEmpty(LogName))
                startMessage += String.Format("{0} : ", LogName);

            switch (logType)
            {
                case LogType.Exception:
                case LogType.Error:
                    Debug.LogErrorFormat("{0}{1}", startMessage, message);
                    break;
                case LogType.Warning:
                    Debug.LogWarningFormat("{0}{1}", startMessage, message);
                    break;
                case LogType.Assert:
                case LogType.Log:
                    Debug.LogFormat("{0}{1}", startMessage, message);
                    break;

                default:
                    break;
            }

            //LogPacket(message + "\r\n");
        }

        private void LogPacket()
        {
            Log("Log packet - Start");

            if (_bufferPacket.BytesToRead == 0)
            {
                Log(String.Format("Packet received - size : {0}\r\n", _bufferPacket.PacketSize.ToString()));

                if (_bufferPacket.StreamPacket != null)
                {
                    StringBuilder strBuilder = new StringBuilder();

                    for (int i = 0; i < _bufferPacket.StreamPacket.Length; i++)
                    {
                        int byteRead = _bufferPacket.StreamPacket.ReadByte();
                        //---> Log bytes as HexaDecimal
                        strBuilder.Append(byteRead.ToString("X2") + " ");
                    }

                    _bufferPacket.StreamPacket.Seek(0, SeekOrigin.Begin);
                    Log(strBuilder.ToString());
                }
            }
            else
            {
                Log("Packet not completed");
            }

            Log("Log packet - End");
        }

        private void LogAllBytes()
        {
            Log("Log all bytes - Start");

            Log(String.Format("Bytes received - size : {0}\r\n", _bufferPacket.Bytes.Count.ToString()));

            if (_bufferPacket.BytesToRead.HasValue)
                Log(String.Format("Bytes remaining to complete the packet : {0}\r\n", _bufferPacket.BytesToRead.ToString()));

            if (_bufferPacket.Bytes.Count > 0)
            {
                StringBuilder strBuilder = new StringBuilder();

                for (int i = 0; i < _bufferPacket.Bytes.Count; i++)
                {
                    //---> Log bytes as HexaDecimal
                    strBuilder.Append(_bufferPacket.Bytes[i].ToString("X2") + " ");
                }

                Log(strBuilder.ToString());
            }

            Log("Log all bytes - End");
        }

        private void LogPayload<T>(Message payload, T payloadMessage)
        {
            //--- Only keep the class name
            string messageType = payloadMessage.ToString();

            int indexLastPoint = messageType.LastIndexOf(".");
            if (indexLastPoint != -1)
                messageType = messageType.Substring(indexLastPoint + 1, messageType.Length - indexLastPoint - 1);
            //---

            Log("Message : " + payload.request_number + " - " + messageType, true);
        }
        #endregion

        /// <summary>
        /// Ping DoW server every n seconds to keep connection alive
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void TimerPing_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            if (ConnectionState == ConnectionState.Authenticated)
                PingToServer();
        }

        /// <summary>
        /// Get size contained in a reversed 4 bytes array
        /// </summary>
        /// <param name="array">4 bytes minimum array</param>
        /// <returns></returns>
        private int? GetSize(byte[] array)
        {
            if (array.Length < 4)
                return null;

            byte[] sizeByte = new byte[4];
            Array.Copy(array, sizeByte, 4);
            Array.Reverse(sizeByte);
            int size = BitConverter.ToInt16(sizeByte, 0);

            if (size == 0)
                return null;

            return size;
        }

        /// <summary>
        /// Deflate and deserialize zipped protobuf datas
        /// </summary>
        /// <typeparam name="T">Type of the object</typeparam>
        /// <param name="datas">Zipped datas</param>
        /// <returns></returns>
        public T GetObjectFromDeflateDatas<T>(byte[] datas)
        {
            T value = default(T);

            using (MemoryStream streamOriginal = new MemoryStream(datas))
            {
                //---> Need 2 readbyte before reading the deflate stream. Because of a bug in the implementation of HTTP request in .net framework
                //     More infos here : http://george.chiramattel.com/blog/2007/09/deflatestream-block-length-does-not-match.html
                streamOriginal.ReadByte();
                streamOriginal.ReadByte();

                using (DeflateStream deflateStream = new DeflateStream(streamOriginal, CompressionMode.Decompress))
                {
                    value = (T)GameDataModel.Deserialize(deflateStream, null, typeof(T));
                }
            }

            return value;
        }

        /// <summary>
        /// Serialize data in a bytes array
        /// All class must be defined in gameDataModel
        /// </summary>
        /// <typeparam name="T">Generic type defined in gameDataModel</typeparam>
        /// <param name="data">Object of type <typeparamref name="T"/> to serialize</param>
        /// <returns></returns>
        private byte[] SeralizeData<T>(T data)
        {
            byte[] bytes = null;

            if (data != null)
            {
                using (MemoryStream msGameData = new MemoryStream())
                {
                    GameDataModel.Serialize(msGameData, data);
                    bytes = msGameData.ToArray();
                }
            }

            return bytes;
        }

        /// <summary>
        /// Raise events associated to the message contained in the packet
        /// </summary>
        /// <param name="packetReceived"></param>
        private void PacketProcessor(Packet packetReceived)
        {
            //---> Authenticate
            if (packetReceived.payload.async_connected_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.async_connected_request);

                if (AsyncConnectedEvent != null)
                    EventManager.Instance.QueueEvent(AsyncConnectedEvent, packetReceived.payload.async_connected_request);
            }
            //---> Authenticate Error
            else if (packetReceived.payload.async_connection_error_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.async_connection_error_request);

                if (AsyncConnectionErrorEvent != null)
                    EventManager.Instance.QueueEvent(AsyncConnectionErrorEvent, packetReceived.payload.async_connection_error_request);
            }
            //---> Game created
            else if (packetReceived.payload.game_created_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.game_created_request);

                if (GameCreatedEvent != null)
                    EventManager.Instance.QueueEvent(GameCreatedEvent, packetReceived.payload.game_created_request);
            }
            //---> Invitation answered
            else if (packetReceived.payload.invitation_answered_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.invitation_answered_request);

                if (InvitationAnsweredEvent != null)
                    EventManager.Instance.QueueEvent(InvitationAnsweredEvent, packetReceived.payload.invitation_answered_request);
            }
            //---> Player presence
            else if (packetReceived.payload.player_presence_update_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.player_presence_update_request);

                if (PlayerPresenceUpdatedEvent != null)
                    EventManager.Instance.QueueEvent(PlayerPresenceUpdatedEvent, packetReceived.payload.player_presence_update_request);
            }
            //---> Action Required
            else if (packetReceived.payload.action_required_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.action_required_request);

                if (ActionRequiredEvent != null)
                    EventManager.Instance.QueueEvent(ActionRequiredEvent, packetReceived.payload.action_required_request);
            }
            //---> Game status report
            else if (packetReceived.payload.game_status_report_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.game_status_report_request);

                if (GameStatusReportEvent != null)
                {
                    foreach (StatusReport statusReport in packetReceived.payload.game_status_report_request.reports)
                    {
                        EventManager.Instance.QueueEvent(GameStatusReportEvent, statusReport);
                    }
                }

                if (GameStatusReportAllEvent != null && packetReceived.payload.game_status_report_request.reports != null && packetReceived.payload.game_status_report_request.reports.Count > 0)
                {
                    EventManager.Instance.QueueEvent(GameStatusReportAllEvent, packetReceived.payload.game_status_report_request.reports);
                }
            }
            //---> Lobby player info
            else if (packetReceived.payload.lobby_player_info_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.lobby_player_info_request);

                if (LobbyPlayerInfoEvent != null)
                    EventManager.Instance.QueueEvent(LobbyPlayerInfoEvent, packetReceived.payload.lobby_player_info_request);
            }
            //TODO : player_info_request ne semble plus utilisé
            //---> Player Info
            else if (packetReceived.payload.player_info_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.player_info_request);

                if (PlayerInfoEvent != null)
                    EventManager.Instance.QueueEvent(PlayerInfoEvent, packetReceived.payload.player_info_request);
            }
            //---> Ping back
            else if (packetReceived.payload.ping_request != null)
            {
                if (PingEvent != null)
                    EventManager.Instance.QueueEvent(PingEvent, packetReceived.payload.ping_request);
            }
            //---> Buddy list
            else if (packetReceived.payload.async_buddy_list_content_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.async_buddy_list_content_request);

                if (AsyncBuddyListContentEvent != null)
                    EventManager.Instance.QueueEvent(AsyncBuddyListContentEvent, packetReceived.payload.async_buddy_list_content_request);
            }
            ////---> Game Over
            //else if (packetReceived.payload.game_over_request != null)
            //{
            //    message = "game_over_request";
            //    if (GameOverEvent != null)
            //        EventManager.Instance.QueueEvent(GameOverEvent, packetReceived.payload.game_over_request);
            //}
            else if (packetReceived.payload.action_commited_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.action_commited_request);

                if (ActionCommitedEvent != null)
                    EventManager.Instance.QueueEvent(ActionCommitedEvent, packetReceived.payload.action_commited_request);
            }
            else if (packetReceived.payload.player_replaced_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.player_replaced_request);

                if (PlayerReplacedEvent != null)
                    EventManager.Instance.QueueEvent(PlayerReplacedEvent, packetReceived.payload.player_replaced_request);
            }
            else if (packetReceived.payload.game_aborted_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.game_aborted_request);

                if (GameAbortedEvent != null)
                    EventManager.Instance.QueueEvent(GameAbortedEvent, packetReceived.payload.game_aborted_request);
            }
            else if (packetReceived.payload.player_timeout_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.player_timeout_request);

                if (PlayerTimeoutEvent != null)
                    EventManager.Instance.QueueEvent(PlayerTimeoutEvent, packetReceived.payload.player_timeout_request);
            }
            else if (packetReceived.payload.action_commited_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.action_commited_request);

                if (ActionCommitedEvent != null)
                    EventManager.Instance.QueueEvent(ActionCommitedEvent, packetReceived.payload.action_commited_request);
            }
            //---> Error Request
            else if (packetReceived.payload.error_request != null)
            {
                if (ErrorEvent != null)
                    EventManager.Instance.QueueEvent(ErrorEvent, packetReceived.payload.error_request);

                Log(String.Format("Error Request : {0} - {1}", packetReceived.payload.error_request.code.ToString(), packetReceived.payload.error_request.msg), false, LogType.Warning);
            }
            else if (packetReceived.payload.client_data_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.client_data_request);

                if (ClientDataEvent != null)
                    EventManager.Instance.QueueEvent(ClientDataEvent, packetReceived.payload.client_data_request);
            }
            else if (packetReceived.payload.game_outcome_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.game_outcome_request);

                if (GameOutcomeEvent != null)
                    EventManager.Instance.QueueEvent(GameOutcomeEvent, packetReceived.payload.game_outcome_request);
            }
            else if (packetReceived.payload.game_forfeited_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.game_forfeited_request);

                if (GameForfeitedEvent != null)
                    EventManager.Instance.QueueEvent(GameForfeitedEvent, packetReceived.payload.game_forfeited_request);
            }
            else if (packetReceived.payload.lobby_entered_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.lobby_entered_request);

                if (LobbyEnteredEvent != null)
                    EventManager.Instance.QueueEvent(LobbyEnteredEvent, packetReceived.payload.lobby_entered_request);
            }
            else if (packetReceived.payload.lobby_player_list_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.lobby_player_list_request);

                if (LobbyPlayerListEvent != null)
                {
                    SmallPlayer[] players = GetObjectFromDeflateDatas<SmallPlayer[]>(packetReceived.payload.lobby_player_list_request.player_list);
                    EventManager.Instance.QueueEvent(LobbyPlayerListEvent, players.ToList());
                }
            }
            else if (packetReceived.payload.lobby_game_list_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.lobby_game_list_request);

                if (LobbyGameListEvent != null)
                {
                    GameList gameList = GetObjectFromDeflateDatas<GameList>(packetReceived.payload.lobby_game_list_request.game_list);
                    EventManager.Instance.QueueEvent(LobbyGameListEvent, gameList);
                }
            }
            else if (packetReceived.payload.lobby_exited_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.lobby_exited_request);

                if (LobbyExitedEvent != null)
                    EventManager.Instance.QueueEvent(LobbyExitedEvent, packetReceived.payload.lobby_exited_request);
            }
            else if (packetReceived.payload.client_chat_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.client_chat_request);

                if (ClientChatEvent != null)
                    EventManager.Instance.QueueEvent(ClientChatEvent, packetReceived.payload.client_chat_request);
            }
            else if (packetReceived.payload.client_chat_history_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.client_chat_history_request);

                if (ClientChatHistoryEvent != null)
                    EventManager.Instance.QueueEvent(ClientChatHistoryEvent, packetReceived.payload.client_chat_history_request);
            }
            else if (packetReceived.payload.async_ignore_list_content_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.async_ignore_list_content_request);

                if (AsyncIgnoreListContentEvent != null)
                    EventManager.Instance.QueueEvent(AsyncIgnoreListContentEvent, packetReceived.payload.async_ignore_list_content_request);
            }
            else if (packetReceived.payload.lobby_game_created_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.lobby_game_created_request);

                if (LobbyGameCreatedEvent != null)
                    EventManager.Instance.QueueEvent(LobbyGameCreatedEvent, packetReceived.payload.lobby_game_created_request);
            }
            else if (packetReceived.payload.lobby_join_denied_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.lobby_join_denied_request);

                if (LobbyJoinDeniedEvent != null)
                    EventManager.Instance.QueueEvent(LobbyJoinDeniedEvent, packetReceived.payload.lobby_join_denied_request);
            }
            else if (packetReceived.payload.lobby_new_player_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.lobby_new_player_request);

                if (LobbyNewPlayerEvent != null)
                    EventManager.Instance.QueueEvent(LobbyNewPlayerEvent, packetReceived.payload.lobby_new_player_request);
            }
            else if (packetReceived.payload.lobby_player_left_game_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.lobby_player_left_game_request);

                if (LobbyPlayerLeftGameEvent != null)
                    EventManager.Instance.QueueEvent(LobbyPlayerLeftGameEvent, packetReceived.payload.lobby_player_left_game_request);
            }
            else if (packetReceived.payload.client_chat_blocked_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.client_chat_blocked_request);

                if (ClientChatBlockedEvent != null)
                    EventManager.Instance.QueueEvent(ClientChatBlockedEvent, packetReceived.payload.client_chat_blocked_request);
            }
            else if (packetReceived.payload.server_statistics_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.server_statistics_request);

                if (ServerStatisticsEvent != null)
                    EventManager.Instance.QueueEvent(ServerStatisticsEvent, packetReceived.payload.server_statistics_request);
            }
            else if (packetReceived.payload.async_device_unlinked_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.async_device_unlinked_request);

                if (AsyncDeviceUnlinkedEvent != null)
                    EventManager.Instance.QueueEvent(AsyncDeviceUnlinkedEvent, packetReceived.payload.async_device_unlinked_request);
            }
            else if (packetReceived.payload.async_buddy_presence_partial_update_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.async_buddy_presence_partial_update_request);

                if (AsyncBuddyPresencePartialUpdateEvent != null)
                    EventManager.Instance.QueueEvent(AsyncBuddyPresencePartialUpdateEvent, packetReceived.payload.async_buddy_presence_partial_update_request);
            }
            else if (packetReceived.payload.async_buddy_added_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.async_buddy_added_request);

                if (AsyncBuddyAddedEvent != null)
                    EventManager.Instance.QueueEvent(AsyncBuddyAddedEvent, packetReceived.payload.async_buddy_added_request);
            }
            else if (packetReceived.payload.async_buddy_removed_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.async_buddy_removed_request);

                if (AsyncBuddyRemovedEvent != null)
                    EventManager.Instance.QueueEvent(AsyncBuddyRemovedEvent, packetReceived.payload.async_buddy_removed_request);
            }
            else if (packetReceived.payload.clocks_status_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.clocks_status_request);

                if (ClocksStatusEvent != null)
                    EventManager.Instance.QueueEvent(ClocksStatusEvent, packetReceived.payload.clocks_status_request);
            }
            else if (packetReceived.payload.async_ignore_added_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.async_ignore_added_request);

                if (AsyncIgnoreAddedEvent != null)
                    EventManager.Instance.QueueEvent(AsyncIgnoreAddedEvent, packetReceived.payload.async_ignore_added_request);
            }
            else if (packetReceived.payload.async_ignore_removed_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.async_ignore_removed_request);

                if (AsyncIgnoreRemovedEvent != null)
                    EventManager.Instance.QueueEvent(AsyncIgnoreRemovedEvent, packetReceived.payload.async_ignore_removed_request);
            }
            else if (packetReceived.payload.game_observed_request != null)
            {
                LogPayload(packetReceived.payload, packetReceived.payload.game_observed_request);

                if (GameObservedEvent != null)
                    EventManager.Instance.QueueEvent(GameObservedEvent, packetReceived.payload.game_observed_request);
            }
            else
            {
                Log("Message unknown id : " + packetReceived.payload.request_number);
            }
        }

        #region Requests
        ///// <summary>
        ///// Send an request to authenticate a user with a login and a password
        ///// </summary>
        ///// <param name="login">Login of the user</param>
        ///// <param name="password">Password of the user</param>
        ///// <param name="gameType">Game type</param>
        ///// <param name="session">Previous session of the user. Can be null</param>
        //public void AuthenticateUser(string login, string password, Session session)
        //{
        //    AuthenticateUser(login, password, session, 0, null);
        //}

        /// <summary>
        /// Send an request to authenticate a user with a login and a password
        /// </summary>
        /// <param name="login">Login of the user</param>
        /// <param name="password">Password of the user</param>
        /// <param name="gameType">Game type</param>
        /// <param name="session">Previous session of the user. Can be null</param>
        /// <param name="deviceType">Device type</param>
        /// <param name="deviceToken">Device token for push notification</param>
        public void AuthenticateUser(string login, string password, Session session, Devices.Type deviceType, byte[] deviceToken)
        {
            _lastUserName = login;
            MD5 md5 = MD5.Create();
            byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(password);
            _lastPasswordMD5 = BitConverter.ToString(md5.ComputeHash(inputBytes)).Replace("-", "");
            _lastSession = session;
            _lastDeviceType = deviceType;
            _lastDeviceToken = deviceToken;

            AuthenticateUser();
        }

        /// <summary>
        /// Send an request to authenticate a user with the saved parameters
        /// </summary>
        private void AuthenticateUser()
        {
            try
            {
                Packet packet = new Packet();
                packet.payload = new Message();
                packet.payload.request_number = 400;
                packet.payload.async_auth_request = new AsyncAuthRequest();
                packet.payload.async_auth_request.name = _lastUserName;
                packet.payload.async_auth_request.pass = _lastPasswordMD5;
                packet.payload.async_auth_request.game_type = _networkParameters.GameType;
                packet.payload.async_auth_request.session = _lastSession;

                if (_lastDeviceToken != null)
                {
                    packet.payload.async_auth_request.device = new com.daysofwonder.game.push.DeviceType();
                    packet.payload.async_auth_request.device.type = _lastDeviceType;
                    packet.payload.async_auth_request.device.token = _lastDeviceToken;
                }

                SendToServer(packet);
            }
            catch (Exception ex)
            {
                Log(ex);
            }
        }

        /// <summary>
        /// Disconnect the user from DoW server and close socket
        /// </summary>
        public void Disconnect()
        {
            if (ConnectionState < ConnectionState.Connected)
                return;

            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 401;

            AsyncDisconnectRequest asyncDisconnect = new AsyncDisconnectRequest();
            packet.payload.async_disconnect_request = asyncDisconnect;

            SendToServer(packet, () =>
            {
                CloseConnection();

                if (DisconnectedFromServerEvent != null)
                    EventManager.Instance.QueueEvent(DisconnectedFromServerEvent);
            });
        }

        /// <summary>
        /// Send a request to create a room
        /// </summary>
        ///<param name="gameConf">Game configuration</param>
        ///<param name="DoWIds">DoW ids of invited players</param>
        public void CreateInvitationGame(GameConfiguration gameConf, List<int> DoWIds)
        {
            try
            {
                Packet packet = new Packet();
                packet.payload = new Message();
                packet.payload.request_number = 405;

                EngageGameWithFriendsRequest game = new EngageGameWithFriendsRequest();
                packet.payload.engage_game_with_friends_request = game;
                game.friends.AddRange(DoWIds);

                game.configuration = gameConf;

                SendToServer(packet);
            }
            catch (Exception ex)
            {
                Log(ex);
            }
        }

        /// <summary>
        /// Send a request to create a room
        /// </summary>
        ///<param name="gameConf">Game configuration</param>
        ///<param name="password">Password of a private game, can be empty if the game is open</param>
        public void CreateOnlineGame(GameConfiguration gameConf, string password)
        {
            try
            {
                Packet packet = new Packet();
                packet.payload = new Message();
                packet.payload.request_number = 607;

                LobbyCreateGameRequest game = new LobbyCreateGameRequest();
                packet.payload.lobby_create_game_request = game;

                game.configuration = gameConf;
                game.password = password;

                SendToServer(packet);
            }
            catch (Exception ex)
            {
                Log(ex);
            }
        }

        /// <summary>
        /// Send a request to accept an invitation on a given game
        /// </summary>
        /// <param name="gameId">Game ID of the room</param>
        public void AcceptInvitation(long gameId)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 540;

            AnswerInvitationRequest answerInvitation = new AnswerInvitationRequest();
            packet.payload.answer_invitation_request = answerInvitation;
            answerInvitation.game_id = gameId;
            answerInvitation.accept = true;

            SendToServer(packet);
        }

        /// <summary>
        /// Send a request to decline an invitation on a given game
        /// </summary>
        /// <param name="gameId">Game ID of the room</param>
        public void DeclineInvitation(long gameId)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 540;

            AnswerInvitationRequest answerInvitation = new AnswerInvitationRequest();
            packet.payload.answer_invitation_request = answerInvitation;
            answerInvitation.game_id = gameId;
            answerInvitation.accept = false;

            SendToServer(packet);
        }

        /// <summary>
        /// Send a request to get an up-to-date status of all data of either a given game or for all games this player is part of
        /// </summary>
        /// <param name="gameId">Optional Game ID. If null, data of all games are returned</param>
        public void WhatsNewPussycat(long? gameId = null)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 511;

            WhatsNewPussycatRequest whatsNew = new WhatsNewPussycatRequest();
            packet.payload.whats_new_pussycat_request = whatsNew;
            if (gameId.HasValue)
                whatsNew.game_id = gameId.Value;

            SendToServer(packet);
        }

        /// <summary>
        /// When a player has finished his action, she sends a CommitActionRequest with the current game state and the list of next active players in turn.
        /// </summary>
        /// <typeparam name="T">Type of the GameState. Must be derived from GameStateBase</typeparam>
        /// <param name="gameId">Game ID</param>
        /// <param name="playerId">Player ID</param>
        /// <param name="nextPlayers">Ordered list of next players (Local Player ID)</param>
        /// <param name="gameData">Game State with all events</param>
        public void CommitAction<T>(Int64 gameId, int playerId, List<int> nextPlayers, T gameData) where T : GameStateBase
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 501;

            CommitActionRequest commitAction = new CommitActionRequest();
            packet.payload.commit_action_request = commitAction;

            commitAction.game_id = gameId;
            commitAction.player_id = playerId;
            commitAction.next_player_ids.AddRange(nextPlayers);
            commitAction.next_state = SeralizeData(gameData);

            SendToServer(packet);
        }

        /// <summary>
        /// Send a request to add a buddy to the buddy list
        /// </summary>
        /// <param name="DowId">DoW player ID (www_id)</param>
        public void AddBuddy(int DowId)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 518;

            packet.payload.async_buddy_management_request = new AsyncBuddyManagementRequest();
            packet.payload.async_buddy_management_request.operation = AsyncBuddyManagementRequest.Operation.ADD;
            AsyncBuddy asyncBudy = new AsyncBuddy();
            packet.payload.async_buddy_management_request.buddy = asyncBudy;
            asyncBudy.player_id = DowId;

            SendToServer(packet);
        }

        /// <summary>
        /// Send a request to remove a buddy from the buddy list
        /// </summary>
        /// <param name="DowId">DoW player ID (www_id)</param>
        public void RemoveBuddy(int DowId)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 518;

            packet.payload.async_buddy_management_request = new AsyncBuddyManagementRequest();
            packet.payload.async_buddy_management_request.operation = AsyncBuddyManagementRequest.Operation.REMOVE;
            AsyncBuddy asyncBudy = new AsyncBuddy();
            packet.payload.async_buddy_management_request.buddy = asyncBudy;
            asyncBudy.player_id = DowId;

            SendToServer(packet);
        }

        /// <summary>
        /// Send a request to add a buddy to the buddy list
        /// </summary>
        /// <param name="DowId">DoW player ID (www_id)</param>
        public void AddToIgnore(int DowId)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 562;

            packet.payload.async_ignore_management_request = new AsyncIgnoreManagementRequest();
            packet.payload.async_ignore_management_request.operation = AsyncIgnoreManagementRequest.Operation.ADD;
            AsyncIgnore asyncIgnore = new AsyncIgnore();
            packet.payload.async_ignore_management_request.ignore = asyncIgnore;
            asyncIgnore.player_id = DowId;

            SendToServer(packet);
        }

        /// <summary>
        /// Send a request to remove a buddy from the buddy list
        /// </summary>
        /// <param name="DowId">DoW player ID (www_id)</param>
        public void RemoveFromIgnore(int DowId)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 562;

            packet.payload.async_ignore_management_request = new AsyncIgnoreManagementRequest();
            packet.payload.async_ignore_management_request.operation = AsyncIgnoreManagementRequest.Operation.REMOVE;
            AsyncIgnore asyncIgnore = new AsyncIgnore();
            packet.payload.async_ignore_management_request.ignore = asyncIgnore;
            asyncIgnore.player_id = DowId;

            SendToServer(packet);
        }

        /// <summary>
        /// Sent by the client when more details are needed for a given lobby player. This is used for instance when clicking on a player in the lobby player list.
        /// </summary>
        /// <param name="playerId">DoW player ID (www_id)</param>
        public void AskPlayerInfoRequest(int playerId)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 605;

            packet.payload.ask_player_info_request = new AskPlayerInfoRequest();
            packet.payload.ask_player_info_request.player_id = playerId;

            SendToServer(packet);
        }

        /// <summary>
        /// Heartbeat request. Send this request regularly to the server, which will echo it back. This allows the server to notice when a client is disconnected (and the reverse). This also allows to measure the round-trip time between the client and the server, and possibly alert the player if it is too large.
        /// </summary>
        public void PingToServer()
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 777;

            packet.payload.ping_request = new PingRequest();
            packet.payload.ping_request.timestamp = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds;

            SendToServer(packet);
        }

        /// <summary>
        /// Sent by the client to ask the connected user buddy list
        /// </summary>
        public void AsyncBuddyListRequest()
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 515;

            packet.payload.async_buddy_list_request = new AsyncBuddyListRequest();

            SendToServer(packet);
        }

        /// <summary>
        /// Sent by the client to fetch the user ignore list
        /// </summary>
        public void AsyncIgnoreListRequest()
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 560;

            packet.payload.async_ignore_list_request = new AsyncIgnoreListRequest();

            SendToServer(packet);
        }

        /// <summary>
        /// A player can decide to send arbitrary data to a set or subset of the players in the same game
        /// Data have to be derived from IMessage interface and defined in gameDataModel
        /// </summary>
        /// <typeparam name="T">Type of the message</typeparam>
        /// <param name="gameId">Game ID</param>
        /// <param name="playersId">Local players id who will receive the message</param>
        /// <param name="data">Data to send to other players. Must be of type IMessage</param>
        public void MultiCastDataRequest<T>(Int64 gameId, List<int> playersId, T data) where T : IMessage
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 502;

            MulticastDataRequest multicastDataRequest = new MulticastDataRequest();
            packet.payload.multicast_data_request = multicastDataRequest;

            multicastDataRequest.game_id = gameId;

            if (playersId != null)
                multicastDataRequest.recipient_ids.AddRange(playersId);

            multicastDataRequest.data = SeralizeData(data);

            SendToServer(packet);
        }

        /// <summary>
        /// Sent by the last playing player to the server with the game results to signal that the game is over. the game will then be in state “GameOutcome” and send back GameOutcomeRequest Upon a timeout or reception of GameOutcomeConfirmationRequest of all players, then the game will be in GameOver state and won’t be reported to WhatsNewPussyCatRequest anymore.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="gameId">Game ID</param>
        /// <param name="playersWinnerOrderedAndScore">List of Local Player ID + Score, ordered from winner to loser</param>
        /// <param name="gameData">Game state</param>
        public void GameOverRequest<T>(Int64 gameId, List<Tuple<int, int>> playersWinnerOrderedAndScore, T gameData, bool isRanked) where T : GameStateBase
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 507;

            GameOverRequest gameOverRequest = new GameOverRequest();
            gameOverRequest.game_id = gameId;
            gameOverRequest.data = SeralizeData(gameData);
            gameOverRequest.is_ranked = isRanked;

            for (int i = 0; i < playersWinnerOrderedAndScore.Count; i++)
            {
                FinalScore finalScore = new FinalScore();
                finalScore.game_rank = i + 1;
                finalScore.player_id = playersWinnerOrderedAndScore[i].Item1;
                finalScore.game_score = playersWinnerOrderedAndScore[i].Item2;
                //finalScore.nb_games = ?

                if (playersWinnerOrderedAndScore[i].Item2 == playersWinnerOrderedAndScore[0].Item2)
                    finalScore.winner = true;

                gameOverRequest.finalScores.Add(finalScore);
            }

            packet.payload.game_over_request = gameOverRequest;

            SendToServer(packet);
        }

        /// <summary>
        /// Sent by a client to validate the game over result
        /// </summary>
        /// <param name="gameId">Game ID</param>
        /// <param name="playerId">Local player id</param>
        public void GameOutcomeConfirmationRequest(Int64 gameId, int playerId)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 522;

            packet.payload.game_outcome_confirmation_request = new GameOutcomeConfirmationRequest();
            packet.payload.game_outcome_confirmation_request.game_id = gameId;
            packet.payload.game_outcome_confirmation_request.player_id = playerId;

            SendToServer(packet);
        }

        /// <summary>
        /// Sent by the a player when she wants to withdraw definitely from the current game. This might abort the whole game if all players have timed out, forfeited or left, in which case the clients will receive a GameAbortedRequest after receiving a GameForfeitedRequest.
        /// </summary>
        /// <param name="gameId">Game ID</param>
        public void GameForfeitRequest(Int64 gameId)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 513;

            packet.payload.game_forfeit_request = new GameForfeitRequest();
            packet.payload.game_forfeit_request.game_id = gameId;

            SendToServer(packet);
        }

        /// <summary>
        /// Sent by the client to leave a synchronous game (ie a Single Session) this will allow the server to schedule a robot for this player. In response the server will broadcast a PlayerReplacedRequest
        /// </summary>
        /// <param name="gameId">Game ID</param>
        public void LeaveSyncGameRequest(Int64 gameId)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 538;

            packet.payload.leave_sync_game_request = new LeaveSyncGameRequest();
            packet.payload.leave_sync_game_request.game_id = gameId;

            SendToServer(packet);
        }

        /// <summary>
        /// Sent by the client to rejoin a left game In response the server will broadcast a PlayerReplacedRequest
        /// </summary>
        /// <param name="gameId">Game ID</param>
        public void ResumeSyncGameRequest(Int64 gameId)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 539;

            packet.payload.resume_sync_game_request = new ResumeSyncGameRequest();
            packet.payload.resume_sync_game_request.game_id = gameId;

            SendToServer(packet);
        }

        /// <summary>
        /// Sent by the client to inform the server presence system that the connected player just entered a specific game interface. Note: the server makes sure that the player can be only in one game at a time
        /// </summary>
        /// <param name="gameId">Game ID or null to quit the current game</param>
        public void SwitchedToGameRequest(Int64? gameId = null)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 534;

            packet.payload.switched_to_game_request = new SwitchedToGameRequest();

            if (gameId.HasValue)
                packet.payload.switched_to_game_request.game_id = gameId.Value;

            SendToServer(packet);
        }

        /// <summary>
        /// Sent by the client when it wants to enter the lobby This automatically subscribes the client to the following streams:
        ///  - chat messages
        ///  - player list updates
        ///  - games offer updates
        /// </summary>
        public void EnterLobbyRequest()
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 600;

            packet.payload.enter_lobby_request = new EnterLobbyRequest();

            SendToServer(packet);
        }

        /// <summary>
        /// Sent by the client when it wants to exit the lobby This automatically unsubscribes the client from all the streams of lobby updates
        /// </summary>
        public void ExitLobbyRequest()
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 602;

            packet.payload.exit_lobby_request = new ExitLobbyRequest();

            SendToServer(packet);
        }

        /// <summary>
        /// Sends a chat to the server, the server will then multicast the text to the other players in the given game. If no game_id are given, then the chat message is sent to the lobby (provided the connected user entered the lobby).
        /// </summary>
        /// <param name="chat">Chat message</param>
        /// <param name="gameId">Game ID</param>
        public void MultiCastChatRequest(string chat, Int64? gameId = null)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 524;

            packet.payload.multicast_chat_request = new MulticastChatRequest();

            if (gameId.HasValue)
                packet.payload.multicast_chat_request.game_id = gameId.Value;

            packet.payload.multicast_chat_request.text = chat;
            //packet.payload.multicast_chat_request.recipient_ids = recipientIds;

            SendToServer(packet);
        }

        /// <summary>
        /// Sent to retrieve the chat history of the lobby or of a game
        /// </summary>
        /// <param name="gameId">Game ID</param>
        public void GetChatHistoryRequest(Int64? gameId = null)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 535;

            packet.payload.get_chat_history_request = new GetChatHistoryRequest();

            if (gameId.HasValue)
                packet.payload.get_chat_history_request.game_id = gameId.Value;

            SendToServer(packet);
        }

        /// <summary>
        /// Join an open game in the lobby. The player must be in the lobby first. This request can return an LobbyJoinDeniedRequest if joining is denied.
        /// </summary>
        /// <param name="gameId">Game ID</param>
        public void LobbyJoinGameRequest(Int64 gameId, string password = "", byte[] userData = null)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 610;

            packet.payload.lobby_join_game_request = new LobbyJoinGameRequest();

            packet.payload.lobby_join_game_request.game_id = gameId;
            packet.payload.lobby_join_game_request.password = password;
            packet.payload.lobby_join_game_request.user_data = userData;

            SendToServer(packet);
        }

        /// <summary>
        /// Sent by the client to leave a waiting open game.
        /// </summary>
        /// <param name="gameId">Game ID</param>
        public void LobbyLeaveGameRequest(Int64 gameId)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 613;

            packet.payload.lobby_leave_game_request = new LobbyLeaveGameRequest();

            packet.payload.lobby_leave_game_request.game_id = gameId;

            SendToServer(packet);
        }

        /// <summary>
        /// Sent by a client to validate it showed the abort to the players
        /// </summary>
        /// <param name="gameId">Game ID</param>
        /// <param name="playerId">Local player ID</param>
        public void GameAbortedConfirmationRequest(Int64 gameId, int playerId)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 531;

            packet.payload.game_aborted_confirmation_request = new GameAbortedConfirmationRequest();

            packet.payload.game_aborted_confirmation_request.game_id = gameId;
            packet.payload.game_aborted_confirmation_request.player_id = playerId;

            SendToServer(packet);
        }

        /// <summary>
        /// Sent by the client to unlink a device from the current active session.
        /// The idea is that if a user logs out from his client and logs in with a different dow account, we would have the same device token id pointing to different accounts thus it is possible that a given device receive notification for a different user, which is bad.
        /// </summary>
        /// <param name="deviceType">Device type</param>
        /// <param name="deviceToken">Device token</param>
        public void AsyncUnlinkDeviceRequest(Devices.Type deviceType, byte[] deviceToken)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 412;
            packet.payload.async_unlink_device_request = new AsyncUnlinkDeviceRequest();

            packet.payload.async_unlink_device_request.device = new com.daysofwonder.game.push.DeviceType();
            packet.payload.async_unlink_device_request.device.type = deviceType;
            packet.payload.async_unlink_device_request.device.token = deviceToken;
            SendToServer(packet);
        }

        /// <summary>
        /// Sent by the client when he wants to have some statistics about the server (like number of hosted games, connected clients...) when subscribe is true, the server sends a continuous stream of statistics updates
        /// </summary>
        /// <param name="subscribe">True to subscribe a continuous stream of statistics, False to get once and unsuscribe</param>
        public void AskServerStatisticsRequest(bool subscribe)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 408;
            packet.payload.ask_server_statistics_request = new AskServerStatisticsRequest();

            packet.payload.ask_server_statistics_request.subscribe = subscribe;

            SendToServer(packet);
        }

        /// <summary>
        /// Sent by the client to monitor the presence of a set of players.
        /// Once sent and when subscribed to the presence system, the client will receive presence changes for those players.
        /// </summary>
        /// <param name="playerIds">List of dow player id to monitor presence of</param>
        public void RegisterPresenceRequest(List<int> playerIds)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 527;
            packet.payload.register_presence_request = new RegisterPresenceRequest();

            packet.payload.register_presence_request.player_ids.AddRange(playerIds);

            SendToServer(packet);
        }

        /// <summary>
        /// Sent by the client to stop monitoring the presence of a player
        /// </summary>
        /// <param name="playerIds">List of dow player id to stop monitoring presence</param>
        public void UnregisterPresenceRequest(List<int> playerIds)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 528;
            packet.payload.unregister_presence_request = new UnregisterPresenceRequest();

            packet.payload.unregister_presence_request.player_ids.AddRange(playerIds);

            SendToServer(packet);
        }

        /// <summary>
        /// Sent by the client to start monitoring the presence of players given by RegisterPresenceRequest
        /// Once sent, the client will receive AsyncBuddyPresencePartialUpdateRequest whenever the presence of one or more player changes.
        /// </summary>
        public void SubscribePresenceServiceRequest()
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 532;
            packet.payload.subscribe_presence_service_request = new SubscribePresenceServiceRequest();

            SendToServer(packet);
        }

        /// <summary>
        /// Sent by the client to stop monitoring the presence of players given by RegisterPresenceRequest
        /// </summary>
        public void UnsubscribePresenceServiceRequest()
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 533;
            packet.payload.unsubscribe_presence_service_request = new UnsubscribePresenceServiceRequest();

            SendToServer(packet);
        }

        /// <summary>
        /// Sent by the client when one client wants to get the values of the player clocks
        /// </summary>
        /// <param name="gameId">Game ID</param>
        public void GetClocksRequest(Int64 gameId)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 546;
            packet.payload.get_clocks_request = new GetClocksRequest();

            packet.payload.get_clocks_request.game_id = gameId;

            SendToServer(packet);
        }

        /// <summary>
        /// Send this to observe a specific game.
        /// In return the server will send the GameObservedRequest, and if successfull the stream of observed events as if the player was part of the game.
        /// To stop observing a game, send a StopObserveGameRequest
        /// </summary>
        /// <param name="gameId">Game ID</param>
        /// <param name="observer">Observer ID</param>
        /// <param name="session">Session ID</param>
        public void StartObserveGameRequest(Int64 gameId, int observer, Int64 session)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 650;
            packet.payload.start_observe_game_request = new StartObserveGameRequest();

            packet.payload.start_observe_game_request.game_id = gameId;
            packet.payload.start_observe_game_request.observer = observer;
            packet.payload.start_observe_game_request.observer_session = session;

            SendToServer(packet);
        }

        /// <summary>
        /// Stop observing a game that the connected user was observing. This stops the stream of this game event
        /// </summary>
        /// <param name="gameId">Game ID</param>
        /// <param name="session">Session ID</param>
        public void StopObserveGameRequest(Int64 gameId, Int64 session)
        {
            Packet packet = new Packet();
            packet.payload = new Message();
            packet.payload.request_number = 651;
            packet.payload.stop_observe_game_request = new StopObserveGameRequest();

            packet.payload.stop_observe_game_request.game_id = gameId;
            packet.payload.stop_observe_game_request.session = session;

            SendToServer(packet);
        }

        #endregion

        public void Dispose()
        {
            IsQuitting = true;

            if (_timerPing != null)
                _timerPing.Elapsed -= TimerPing_Elapsed;

            StopTimerReconnect();

            ConnectedToServerEvent -= ServerConnection_ConnectedToServerEvent;
            AsyncConnectedEvent -= ServerConnection_AsyncConnectedEvent;
        }

        /// <summary>
        /// Close socket and dispose all streams
        /// </summary>
        private void CloseConnection()
        {
            Log("Close connection");

            ConnectionState = ConnectionState.NotConnected;
            _allreadySent = false;
            if (_timerPing != null)
                _timerPing.Elapsed -= TimerPing_Elapsed;
            StopTimerReconnect();

            if (_sslStream != null)
            {
                _sslStream.Close();
                _sslStream.Dispose();
                _sslStream = null;
            }

            if (_networkStream != null)
            {
                _networkStream.Close();
                _networkStream.Dispose();
                _networkStream = null;
            }

            if (_socket != null)
            {
                _socket.Shutdown(SocketShutdown.Both);
                _socket.Close();
                _socket = null;
            }
        }

        public class BufferPacket
        {
            /// <summary>
            /// Amount of remaining bytes to read
            /// </summary>
            public int? BytesToRead = null;

            /// <summary>
            /// Array of bytes read from the stream
            /// </summary>
            public List<byte> Bytes = new List<byte>();

            /// <summary>
            /// Size of the incoming message
            /// </summary>
            public int? PacketSize = null;

            public MemoryStream StreamPacket = null;
        }

        /// <summary>
        /// State object for receiving data from DoW server
        /// </summary>
        public class StateObject
        {
            /// <summary>
            /// Stream of communication
            /// </summary>
            public Stream WorkStream = null;

            /// <summary>
            /// Size of receive buffer. 
            /// </summary>
            public const int BufferSize = 256;

            /// <summary>
            /// Receive buffer
            /// </summary>
            public byte[] Buffer = new byte[BufferSize];

            /// <summary>
            /// Method called when data are sent
            /// </summary>
            public Action CallbackSent = null;
        }
    }
}
