/// <summary>
        /// Inserts a chat user.
        /// </summary>
        /// <param name="chatRoom">The chat room which contains the user.</param>
        /// <param name="chatUser">The chat user to add.</param>
        /// <param name="cancellationToken">A token to observe while waiting for the task to complete.</param>
        /// <returns>
        /// A task that represents the asynchronous operation.
        /// </returns>
        public virtual Task AddUserAsync(ChatRoomInfo chatRoom, ChatUserInfo chatUser, CancellationToken cancellationToken)
        {
            ThrowIfDisposed();
            if (chatRoom == null)
            {
                throw new ArgumentNullException("chatRoom");
            }
            if (chatUser == null)
            {
                throw new ArgumentNullException("chatUser");
            }

            chatUser.RoomId = chatRoom.Id;

            var users = s_users.Get(chatRoom.Id) as ConcurrentDictionary <string, ChatUserInfo>;

            if (users == null)
            {
                users = new ConcurrentDictionary <string, ChatUserInfo>();
            }
            if (users.TryAdd(chatUser.UserName, chatUser))
            {
                s_users.Set(chatRoom.Id, users, DateTimeOffset.MaxValue);
            }
            return(Task.FromResult(0));
        }
        /// <summary>
        /// Removes a connection from the given chat user.
        /// </summary>
        /// <param name="connectionId">The connection to remove.</param>
        /// <param name="chatUser">The chat user.</param>
        /// <param name="cancellationToken">A token to observe while waiting for the task to complete.</param>
        /// <returns>
        /// A task that represents the asynchronous operation.
        /// </returns>
        public virtual Task RemoveConnectionFromUserAsync(string connectionId, ChatUserInfo chatUser, CancellationToken cancellationToken)
        {
            ThrowIfDisposed();
            if (string.IsNullOrEmpty(connectionId))
            {
                throw new ArgumentNullException("connectionId");
            }
            if (chatUser == null)
            {
                throw new ArgumentNullException("chatUser");
            }

            var roomKey           = chatUser.RoomId;
            var roomConnectionIds = s_roomConnections.Get(roomKey) as ImmutableHashSet <string>;

            if (roomConnectionIds != null)
            {
                roomConnectionIds = roomConnectionIds.Remove(connectionId);
                s_roomConnections.Set(roomKey, roomConnectionIds, DateTimeOffset.MaxValue);
            }

            var roomUserKey       = chatUser.RoomId + chatUser.UserName;
            var userConnectionIds = s_userConnections.Get(roomUserKey) as ImmutableHashSet <string>;

            if (userConnectionIds != null)
            {
                userConnectionIds = userConnectionIds.Remove(connectionId);
                s_userConnections.Set(roomUserKey, userConnectionIds, DateTimeOffset.MaxValue);
            }

            s_connections.Remove(connectionId);
            return(Task.FromResult(0));
        }
        /// <summary>
        /// Adds a connection ID to the given chat room.
        /// </summary>
        /// <param name="connectionId">The connection to add.</param>
        /// <param name="chatUser">The chat room.</param>
        /// <param name="cancellationToken">A token to observe while waiting for the task to complete.</param>
        /// <returns>
        /// A task that represents the asynchronous operation.
        /// </returns>
        public virtual Task AddConnectionToUserAsync(string connectionId, ChatUserInfo chatUser, CancellationToken cancellationToken)
        {
            ThrowIfDisposed();
            if (string.IsNullOrEmpty(connectionId))
            {
                throw new ArgumentNullException("connectionId");
            }
            if (chatUser == null)
            {
                throw new ArgumentNullException("chatUser");
            }

            var roomKey           = chatUser.RoomId;
            var roomUserKey       = chatUser.RoomId + chatUser.UserName;
            var roomConnectionIds = s_roomConnections.Get(roomKey) as ImmutableHashSet <string>;
            var userConnectionIds = s_userConnections.Get(roomUserKey) as ImmutableHashSet <string>;

            roomConnectionIds = roomConnectionIds != null?roomConnectionIds.Add(connectionId) : ImmutableHashSet.Create(connectionId);

            userConnectionIds = userConnectionIds != null?userConnectionIds.Add(connectionId) : ImmutableHashSet.Create(connectionId);

            // Add the connection to the global connection cache, the room connection cache,
            // and the user connection cache because the memory cache does not support AppFabric-style tagging
            // This is used by the GetConnectionByIdAsync method because SignalR Hub's OnDisconnected event
            // can only work with a ConnectionID

            s_connections.Set(connectionId, new ChatConnection {
                RoomId = chatUser.RoomId, UserName = chatUser.UserName
            }, DateTimeOffset.MaxValue);
            s_roomConnections.Set(roomKey, roomConnectionIds, DateTimeOffset.MaxValue);
            s_userConnections.Set(roomUserKey, userConnectionIds, DateTimeOffset.MaxValue);

            return(Task.FromResult(0));
        }
        /// <summary>
        /// Gets all the Connection IDs for the given chat user.
        /// </summary>
        /// <param name="chatUser">The chat user.</param>
        /// <param name="cancellationToken">A token to observe while waiting for the task to complete.</param>
        /// <returns>
        /// A task that represents the asynchronous operation.
        /// </returns>
        public virtual Task <IList <string> > GetConnectionsByUserAsync(ChatUserInfo chatUser, CancellationToken cancellationToken)
        {
            ThrowIfDisposed();
            var userConnectionStore = GetUserConnectionStore();

            if (chatUser == null)
            {
                throw new ArgumentNullException("chatUser");
            }
            return(userConnectionStore.GetConnectionsByUserAsync(chatUser, cancellationToken));
        }
        /// <summary>
        /// Gets all the connections for the given chat user.
        /// </summary>
        /// <param name="chatUser">The chat room.</param>
        /// <param name="userName">The user name.</param>
        /// <param name="cancellationToken">A token to observe while waiting for the task to complete.</param>
        /// <returns>
        /// A task that represents the asynchronous operation.
        /// </returns>
        public virtual Task <IList <string> > GetConnectionsByUserAsync(ChatUserInfo chatUser, CancellationToken cancellationToken)
        {
            ThrowIfDisposed();
            if (chatUser == null)
            {
                throw new ArgumentNullException("chatUser");
            }

            var connections = s_userConnections.Get(chatUser.RoomId + chatUser.UserName) as ImmutableHashSet <string>;

            if (connections != null)
            {
                return(Task.FromResult <IList <string> >(connections.ToList()));
            }
            return(Task.FromResult <IList <string> >(new List <string>()));
        }
        /// <summary>
        /// Removes a chat user.
        /// </summary>
        /// <param name="chatRoom">The chat site which contains the user.</param>
        /// <param name="chatUser">The chat user to remove.</param>
        /// <param name="cancellationToken">A token to observe while waiting for the task to complete.</param>
        /// <returns>
        /// A task that represents the asynchronous operation.
        /// </returns>
        public virtual async Task RemoveUserAsync(ChatRoomInfo chatRoom, ChatUserInfo chatUser, CancellationToken cancellationToken)
        {
            ThrowIfDisposed();
            var userStore = GetUserStore();

            if (chatRoom == null)
            {
                throw new ArgumentNullException("chatRoom");
            }
            if (chatUser == null)
            {
                throw new ArgumentNullException("chatUser");
            }
            await userStore.RemoveUserAsync(chatRoom, chatUser, cancellationToken);

            await UpdateAsync(chatRoom, cancellationToken);
        }
        /// <summary>
        /// Gets a collection of event notifications with the given values.
        /// </summary>
        /// <param name="projectId">The project which owns the events occured.</param>
        /// <param name="clientId">The client who is the actor.</param>
        /// <param name="offset">The index of the page of results to return. Use 1 to indicate the first page.</param>
        /// <param name="limit">The size of the page of results to return. <paramref name="pageIndex" /> is non-zero-based.</param>
        /// <param name="cancellationToken">A token to observe while waiting for the task to complete.</param>
        /// <returns>
        /// A task that represents the asynchronous operation.
        /// </returns>
        public virtual async Task <IList <ChatMessageInfo> > GetMessagesAsync(int projectId, string clientId, int offset, int limit, CancellationToken cancellationToken)
        {
            ThrowIfDisposed();
            var manager = GetLogManager();

            if (string.IsNullOrEmpty(clientId))
            {
                throw new ArgumentNullException("clientId");
            }

            var chatMessages = new List <ChatMessageInfo>();
            var logMessages  = await manager.FindAllMessagesAsync(projectId, clientId, offset, limit, cancellationToken);

            foreach (var logMessage in logMessages)
            {
                if (logMessage.CustomUri == null)
                {
                    continue;
                }
                ChatUserInfo from;
                bool         admin = false;
                if (logMessage.CustomUri != null)
                {
                    admin = logMessage.CustomUri.EndsWith(">>", StringComparison.Ordinal);
                }
                if (logMessage.Contact != null)
                {
                    from = new ChatUserInfo {
                        ContactId = logMessage.Contact.Id, UserName = logMessage.Contact.Email.Address, NickName = logMessage.Contact.NickName, Admin = admin
                    };
                }
                else
                {
                    from = new ChatUserInfo {
                        UserName = clientId, NickName = clientId, Admin = admin
                    };
                }
                chatMessages.Add(new ChatMessageInfo {
                    From = from, Message = logMessage.Message, CreatedDate = logMessage.StartDate
                });
            }
            return(chatMessages);
        }
        /// <summary>
        /// Gets or adds a new user to the given room.
        /// </summary>
        /// <param name="room">The room.</param>
        /// <param name="userName">The user name.</param>
        /// <param name="nickName">The nick name.</param>
        /// <param name="ipAddress">The IP address.</param>
        /// <param name="cancellationToken">A token to observe while waiting for the task to complete.</param>
        /// <returns>
        /// The chat user.
        /// </returns>
        private async Task <ChatUserInfo> GetOrAddRoomUserAsync(ChatRoomInfo room, string userName, string ipAddress,
                                                                CancellationToken cancellationToken = default(CancellationToken))
        {
            int?   contactId = null;
            string nickName  = null;

            if (!string.IsNullOrEmpty(userName))
            {
                var contact = await ChatManager.GetContactByUserNameAsync(room, userName, cancellationToken);

                if (contact != null)
                {
                    contactId = contact.Id;
                    userName  = contact.Email.Address;
                    nickName  = contact.NickName ?? contact.Email.Name ?? contact.FirstName ?? contact.LastName;
                }
            }
            if (string.IsNullOrEmpty(userName))
            {
                userName = ChatManager.GenerateClientId(ipAddress);
                nickName = nickName ?? userName;
            }
            var user = await ChatManager.GetUserByUserNameAsync(room, userName, cancellationToken);

            if (user == null)
            {
                user = new ChatUserInfo
                {
                    ContactId = contactId,
                    ClientId  = ChatManager.GenerateClientId(ipAddress),
                    IPAddress = ipAddress,
                    UserName  = userName,
                    NickName  = nickName
                };
                await ChatManager.AddUserAsync(room, user, cancellationToken);
            }
            return(user);
        }
        /// <summary>
        /// Removes a Connection ID from the given chat user.
        /// </summary>
        /// <param name="connectionId">The Connection ID to remove.</param>
        /// <param name="chatUser">The chat user.</param>
        /// <param name="cancellationToken">A token to observe while waiting for the task to complete.</param>
        /// <returns>
        /// A task that represents the asynchronous operation.
        /// </returns>
        public virtual async Task RemoveConnectionFromUserAsync(string connectionId, ChatUserInfo chatUser, CancellationToken cancellationToken)
        {
            ThrowIfDisposed();
            var userConnectionStore = GetUserConnectionStore();

            if (chatUser == null)
            {
                throw new ArgumentNullException("chatUser");
            }
            if (string.IsNullOrEmpty(connectionId))
            {
                throw new ArgumentNullException("connectionId");
            }
            var chatRoom = await FindByIdAsync(chatUser.RoomId, cancellationToken);

            if (chatRoom == null)
            {
                throw new InvalidOperationException("The chat room was not found.");
            }
            await userConnectionStore.RemoveConnectionFromUserAsync(connectionId, chatUser, cancellationToken);

            await UpdateAsync(chatRoom, cancellationToken);
        }