// (c) 2005 Electronic Arts Inc.
//

using System;
using System.Collections;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Text;
using System.Diagnostics;
using System.Collections.Generic;
using System.Runtime;
using System.Runtime.InteropServices ; 

using Microsoft.Win32;

using EA.GameTalkMessages;

namespace EA.GameTalkRouter
{
	/// <summary>
	/// This structure gives a hierarchal way of organizing Listeners based on their parsed channel name
	/// </summary>
	class ChannelIndexedListeners
	{
		public Dictionary<string, ChannelIndexedListeners> mChildContainer = new Dictionary<string, ChannelIndexedListeners>();
		public List<Listener> mListeners = new List<Listener>();
	}

	/// <summary>
	/// TcpServer is the server class. When "StartServer" method is called, we ask our TcpListener member 
	/// attempts to connect to the specified IP Address and port. The server then start listening for client socket requests.
	/// As soon as a request comes in from any client, a Client Socket Listening thread will be started. 
	/// That thread is responsible for client communication.
	/// </summary>
	public abstract class TcpServer
	{
		protected IPAddress mDefaultServer = null;
		protected IPEndPoint mDefaultIPEndPoint = null;
		private int mPort = 9876;
		private int Port
		{
			set
			{
				mPort = value;
			}
		}

		protected int ServerPort
		{
			get { return mPort; }
		}

		public const int mDefaultPort = 9876;

		public const int sExceptionHandlerIdleTime = 5;

		/// <summary>
		/// Local Variables Declaration.
		/// </summary>
		private TcpListener mTcpListener = null;
		private volatile bool mStopServer = false;
		private bool mStopPurging = false;
		private Thread mServerThread = null;
		private Thread mPurgingThread = null;

		private ServerEventHandler mServerEventHandler = null;

		private ILogger mLogger = null;

		/// <summary>
		/// Dictionary where we can buffer the messages intended for targets that are not yet connected
		/// Each value entry in this Dictionary is itself a Dictionary.  These nested Dictionary's are added with the 
		/// key that identifies the intended recipient of the messages.  These nested Dictionary's contain
		/// ServerBuffer objects (essentially just ArrayLists) that contain the messages from the various
		/// sources who sent these messages.  
		/// The reason the key used in the nested Dictionary is using the recipient's name is so as soon as the
		/// recipient connects - all we have to do is simply iterate over that Dictionary
		/// </summary>
		private Dictionary<string, Dictionary<string, ServerBuffer>> mBufferedMessages = new Dictionary<string, Dictionary<string, ServerBuffer>>();

        /// <summary>
        /// Ideally we would make this a Dictionary structure with a set limit of the number of channel delimiters
        /// for any listener registered - but we're not allowing that constraint
        /// </summary>
		private ChannelIndexedListeners mChannelIndexedListeners = new ChannelIndexedListeners();

		/// <summary>
		/// Used to control access to the mChannelIndexedListeners
		/// </summary>
		private ReaderWriterLock mChannelIndexedxListenerLock = new ReaderWriterLock();

		/// <summary>
		/// List of connected listeners
		/// Don't bother creating a container like mListenersList - except 
		/// indexed by the target name - since target names can be updated and that 
		/// just becomes a pain in the butt to maintain the appropriate names in the index
		/// </summary>
        protected List<Listener> mListenersList = new List<Listener>();

		//<ADDED EXCLUSIVELY SO WE CAN SEE IF WE HAVE A USABLE CONNECTION>
		//Creating the extern function...
		[DllImport("wininet.dll")]
		private extern static bool InternetGetConnectedState(out int Description, int ReservedValue);

		//Creating a function that uses the API function...
		public static bool IsConnectedToInternet( )
		{
			try
			{
				int Desc;
				return InternetGetConnectedState(out Desc, 0);
			}
			catch
			{
				//DebugLog("TcpServer: Exception thrown by IsConnectedToInternet: " + exception.Message);
			}
			// If we are unable to load the wininet.dll - we will disable this 
			// functionality by always assuming we are connected
			return true;
		}
		//</ADDED EXCLUSIVELY SO WE CAN SEE IF WE HAVE A USABLE CONNECTION>

		/// <summary>
		/// Shut down the GameTalkRouter
		/// </summary>
		public void Shutdown()
		{
			mStopServer = true;
		}

		private object mSyncObject = new object();

		public delegate void BufferMessageDelegate(MessageEventArgs m);
		public delegate bool IsClientWithSameNameConnectedDelegate(string clientName);
		public delegate TcpSocketListener GetConnectionForIpAndPortDelegate(string ipaddress, int portNum);
		public delegate void AddListenerDelegate(Listener listener);
		// Delegate for ConnectionInitiator to use to give TcpServer list of registered targets to update (if necessary)
		public delegate void UpdateRegisteredClientsDelegate(Target[] target);
		public delegate Listener[] GetAllListenersDelegate();

		private bool mIsAlive = true;
		public bool IsAlive
		{
			get { return mIsAlive; }
		}

		private void Initialize()
		{
			mServerEventHandler = new ServerEventHandler(ServerEventReceiver);
			ServerEventManager.Register(mServerEventHandler);
			string hostAddress = null;
			try
			{
				hostAddress = Network.ResolveIpAddress(Dns.GetHostName());
				mDefaultServer = IPAddress.Parse(hostAddress);
				// Use IPAddress.Any so that localhost can be used to connect to this server
				mDefaultIPEndPoint = new IPEndPoint(IPAddress.Any, mPort);
				mTcpListener = new TcpListener(mDefaultIPEndPoint);
			}
			catch(Exception exception)
			{
				mTcpListener = null;
				// rethrow the exception so that the application using GameTalkRouter can handle it appropriately
				throw exception;
			}

			// Wow!  Isn't there a way to specify the keep-alive on the socket instead of changing/updating the user's TCP/IP settings?
			RegistryKey key = null;
			try
			{
				// Set the timeout values for the KeepAlive
				key = Microsoft.Win32.Registry.LocalMachine.CreateSubKey(@"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters");
				int keepAliveTime = 300 * 1000; // 5 minutes
				key.SetValue("KeepAliveTime", keepAliveTime);
			}
			catch (Exception exception)
			{
				TcpServer.DebugLog("[RegisteredTcpSocket.SendAllQueuedMessages] exception: " + exception.Message);
			}
			finally
			{
				if (key != null)
					key.Close();
			}

			try
			{
				Logger.LogConfiguration("IpAddress:" + mDefaultServer + " Port:" + mDefaultIPEndPoint.Port.ToString());
			}
			catch { }
		}

		/// <summary>
		/// This constructor allows users to construct their own custom GameTalkRouter
		/// </summary>
		/// <param name="registryKeyStr"></param>
		/// <param name="portNum"></param>
		/// <param name="loggingPath"></param>
		public TcpServer(int portNum, string loggingPath, ConfigReader configReader)
		{
			ConfigReader.Instance = configReader;
			if (Configuration.Instance.UseNamedPipes)
				mLogger = new NamedPipesLogger();
			else
				mLogger = new Logger(loggingPath);
			Port = portNum;
			Initialize();
		}

		/// <summary>
		/// This constructor allows users to construct their own custom GameTalkRouter
		/// </summary>
		/// <param name="registryKeyStr"></param>
		/// <param name="portNum"></param>
		/// <param name="loggingPath"></param>
		/// <param name="miscLogFilename"></param>
		/// <param name="trafficLogFilename"></param>
		/// <param name="connectionsLogFilename"></param>
		public TcpServer(int portNum, string loggingPath, string miscLogFilename, 
			string trafficLogFilename, string connectionsLogFilename, ConfigReader configReader)
		{
			ConfigReader.Instance = configReader;
			if (Configuration.Instance.UseNamedPipes)
				mLogger = new NamedPipesLogger();
			else
				mLogger = new Logger(loggingPath, miscLogFilename, trafficLogFilename, connectionsLogFilename);
			Port = portNum;
			Initialize();
		}

		/// <summary>
		/// Destructor.
		/// </summary>
		~TcpServer()
		{
			StopServer();
		}

		/// <summary>
		/// Method that starts TCP/IP Server.
		/// </summary>
		public virtual void StartServer()
		{
			if (mTcpListener != null)
			{
				// Start the thread to listen to client requests.
				mTcpListener.Start();
				mServerThread = new Thread(new ThreadStart(ServerThreadStart));
				mServerThread.Name = "ServerThreadStart";
				mServerThread.Start();

				// Create a low priority thread that checks and deletes client marked for deletion.
				mPurgingThread = new Thread(new ThreadStart(PurgingThreadStart));
				mPurgingThread.Name = "PurgingThreadStart";
				mPurgingThread.Start();

				// Start a thread to see if the GameTalkServer loses its internet connection
				Thread statusThread = new Thread(new ThreadStart(ServerStatusThread));
				statusThread.Name = "ServerStatusThread";
				statusThread.Start();
			}
		}

		/// <summary>
		/// Method that stops the TCP/IP Server.
		/// </summary>
		protected virtual void StopServer()
		{
			bool performedShutdown = false;
			try
			{
				lock (mSyncObject)
				{
					// By unregistering our event-handler here (i.e. at the start of this method) we won't add any new connections or start new work for an existing connection
					ServerEventManager.Unregister(mServerEventHandler);
					if (mTcpListener != null)
					{
						performedShutdown = true;
						// It is important to Stop the server first before doing
						// any cleanup. If not so, clients might being added as
						// server is running, but supporting data structures
						// (such as mListenersList) are cleared. This might
						// cause exceptions.

						// Stop the TCP/IP Server.
						mStopServer = true;
						mTcpListener.Stop();

						// Wait for one second for the the thread to stop.
						mServerThread.Join(500);

						// If still alive; Get rid of the thread.
						if (mServerThread.IsAlive)
							mServerThread.Abort();

						mServerThread = null;

						mStopPurging = true;
						mPurgingThread.Join(500);
						if (mPurgingThread.IsAlive)
							mPurgingThread.Abort();

						mPurgingThread = null;

						// Free Server Object.
						mTcpListener = null;

						// shut-down all listeners (note: ShutdownAllListeners can end up firing an event and we unregistered our mServerEventHandler at the start of this method - so the event won't be received)
						ShutdownAllListeners();
					}
				}
			}
			catch (Exception exception)
			{
				Trace.WriteLine("exception: " + exception.Message);
			}
			finally
			{
				mIsAlive = false;
				// If we wanted the disposal of the Logger to be deterministic - we could 
				// explicitly call a method like mLogger.Shutdown and put its deregistration
				// (ServerEventManager.LoggerUnregister) in that method
				try
				{
					if (performedShutdown) // If this method gets invoked multiple times - no sense in duplicating the ClearLog event (also this trivially resolves an exception that was occurring when the Logger was getting unregistered before a duplicate call made it in here)
					{
						// We're shutting down - clear out our logs so the GameTalkMonitor/Viewer isn't displaying old information
						LoggingEventArgs e = new LoggingEventArgs(LoggingEventType.ClearLog);
						ServerEventManager.LoggingSend(this, e);
					}
				}
				catch { }
			}
		}

		/// <summary>
		/// Method that shuts down all listeners and clears the list.
		/// </summary>
		private void ShutdownAllListeners()
		{
			lock (mListenersList)
			{
				foreach (Listener listener in mListenersList)
				{
					listener.KillThisConnectionsThreads();
				}

				// Remove all elements from the list.
				mListenersList.Clear();
			}
		}

		protected void AddListener(Listener listener)
		{
			lock (mListenersList)
			{
				// Add the listener to our list
				mListenersList.Add(listener);
			}
#if(DEBUG)
			StringBuilder allConnectionsStr = new StringBuilder();
			Listener[] allSocketConnections = GetAllListeners();
			foreach (Listener listenerConnection in allSocketConnections)
			{
				allConnectionsStr.Append("[name: " + listenerConnection.TargetName + " ip address: " + listenerConnection.UniqueName + "]");
			}
			DebugLog("[TcpServer.AddListener] TCP connections: " + allConnectionsStr.ToString());
#endif
		}

		public static void DebugLog(string message)
		{
#if(DEBUG)
			try
			{
				LoggingEventArgs messageEvent = new LoggingEventArgs(LoggingEventType.LogHiddenMessage, message);
				ServerEventManager.LoggingSend(null, messageEvent);
			}
			catch { }
#endif
		}

		/// <summary>
		/// TCP/IP Server thread listening for client connection requests.
		/// </summary>
		private void ServerThreadStart()
		{
			Listener listener = null;
			while (!mStopServer)
			{
				try
				{
					if ((Configuration.Instance.CheckNetworkConnectivity()) && (!IsConnectedToInternet()))
					{
						mStopServer = true;
						Trace.WriteLine("GameTalkServer can not obtain connection");
						break;
					}

					// Use the Pending method to poll the underlying socket instance for client connection requests.
					if (mTcpListener.Pending())
					{
						//Accept the pending client connection and return a TcpClient object initialized for communication.
						TcpClient tcpClient = mTcpListener.AcceptTcpClient();
						listener = new TcpSocketListener(tcpClient.Client);
						AddListener(listener);
						// Start the listener on its own thread
						listener.StartReaderThreads();
					}
					Thread.Sleep(100);
				}
				catch (Exception exception)
				{
					mStopServer = true;
					TcpServer.DebugLog("[TcpServer.ServerThreadStart] exception: " + exception.Message);

					// If we're getting in here repetitively - it would be safer to Sleep/Idle.
					// So as not to tight loop and make GTS processor intensive when idle.
					Thread.Sleep(TcpServer.sExceptionHandlerIdleTime);
				}
			}
		}

		/// <summary>
		/// Whenever our background purging thread runs it will execute this so we don't keep
		/// trying to send to Listeners that are no longer connected
		/// </summary>
		/// <param name="indexedSocketListeners"></param>
		private void RemoveMarkedListenersFromIndex(ChannelIndexedListeners indexedSocketListeners)
		{
			// We never put Sockets in the top level - the sockets are always indexed by something - even if it is just 
			// the empty string
			Dictionary<string, ChannelIndexedListeners>.Enumerator dictionaryEnum = indexedSocketListeners.mChildContainer.GetEnumerator();
			while (dictionaryEnum.MoveNext())
			{
				KeyValuePair<string, ChannelIndexedListeners> keyValuePair = dictionaryEnum.Current;
				ChannelIndexedListeners childIndexedSocketListeners = keyValuePair.Value;
				// Don't replace this List with a list of indices to delete
				// As soon as you have two or more indices to delete - the actual deletion will fail
				List<Listener> socketListenersToDelete = new List<Listener>();
				for (int i = 0; i < childIndexedSocketListeners.mListeners.Count; i++)
				{
					Listener listener = childIndexedSocketListeners.mListeners[i];
					if (listener.IsMarkedForDeletion)
						socketListenersToDelete.Add(listener);
					else if (listener is RegisteredTcpSocket)
					{
						if ((keyValuePair.Key != "") && (!listener.IsAlive)) // If this isn't the empty string channel and this registered socket listener isn't running
							socketListenersToDelete.Add(listener);
					}
				}
				foreach (Listener socketListener in socketListenersToDelete)
					childIndexedSocketListeners.mListeners.Remove(socketListener);

				RemoveMarkedListenersFromIndex(childIndexedSocketListeners);
			}
		}

		/// <summary>
		/// Non recursive method that locks and invokes recursive removal method
		/// </summary>
		private void RemoveMarkedListenersFromIndex()
		{
			mChannelIndexedxListenerLock.AcquireWriterLock(-1);
			try
			{
				RemoveMarkedListenersFromIndex(mChannelIndexedListeners);
			}
			catch
			{
				throw;
			}
			finally
			{
				mChannelIndexedxListenerLock.ReleaseWriterLock();
			}
		}

		private void Purge()
		{
			RemoveMarkedListenersFromIndex();
			bool runGarbageCollector = false;
			List<Listener> removeFromListeners = new List<Listener>();
			// This is for connections that we want to stop
			List<Listener> stopListeners = new List<Listener>();
			// This is for RegisteredTcpSockets that we want to reregister on the empty channel (so that get all incoming messages)
			List<RegisteredTcpSocket> stopListenerAndReregister = new List<RegisteredTcpSocket>();
			bool anyActiveConnections = false;
			bool clearedConnections = false;
			// Check for any clients Listeners that are to be deleted and put them in a separate list in a thread safe fashion.
			lock (mListenersList)
			{
				foreach (Listener listener in mListenersList)
				{
					if (listener is RegisteredTcpSocket)
					{
						// Don't Poll something that isn't connected - 
						// RegisteredTcpSockets are unique in that they will be 
						// in the list and potentially unconnected
						// We could use listener.Connected - but that is slow
						if (((RegisteredTcpSocket)listener).IsAlive)
							listener.Poll();

						// If the connection isn't alive AND we don't want to permanently remove it from our list of listeners (then we'll want to reregister it on the empty channel)
						if ((!listener.IsAlive) && (!listener.IsMarkedForDeletion) && ((RegisteredTcpSocket)listener).HasAConnection)
							stopListenerAndReregister.Add((RegisteredTcpSocket)listener);
					}
					else
						listener.Poll();

					if (listener.IsMarkedForDeletion)
					{
						removeFromListeners.Add(listener);
						stopListeners.Add(listener);
						clearedConnections = true;
					}
					else if (listener.IsAlive)
						anyActiveConnections = true;
				}

				// Delete all the Listeners which are marked for deletion and are in the delete list.
				foreach (Listener listener in removeFromListeners)
				{
					mListenersList.Remove(listener);
				}
				// Can't use (mListenersList.Count == 0) to determine if anything is connected (because of RegistereTcpSockets)
			}
			// Not sure if this will result in the log not getting cleared when it should - but we need to do something like this so we don't immediately clear the log on start-up
			if ((!anyActiveConnections) && clearedConnections) // If we haven't cleared any connections - then we've probably just started up - let's not clear all messaging on start-up
			{
				LoggingEventArgs e = new LoggingEventArgs(LoggingEventType.ClearLog);
				ServerEventManager.LoggingSend(this, e);
			}

			// This can't be in the above lock of mListenersList - since the Listener/RegisteredTcpSocket
			// potentially locks things - which creates a potential for deadlock
			foreach (Listener listener in stopListeners)
			{
				listener.KillThisConnectionsThreads();
			}

			// This can't be in the above lock of mListenersList - since the Listener/RegisteredTcpSocket
			// potentially locks things - which creates a potential for deadlock
			foreach (RegisteredTcpSocket listener in stopListenerAndReregister)
			{
				// I could have the following return a bool (false) if (listener.mClientSocket == null) since nothing is really getting cleared
				listener.StopSocketAndReregister();
			}

			if ((stopListeners.Count > 0) || (stopListenerAndReregister.Count > 0)) // We've disconnected some connection - this should free/dealloc any lingering data from that Listener
				runGarbageCollector = true;

			removeFromListeners = null;
			stopListeners = null;
			stopListenerAndReregister = null;
			if (runGarbageCollector)
			{
				// GameTalkServer seems to be getting several hundred megs in memory - 
				// hopefully this will allow it to reclaim that memory when a client disconnects
				System.GC.WaitForPendingFinalizers();
				System.GC.Collect();
			}
		}

		/// <summary>
		/// Thread method for purging client listeners that are marked for deletion 
		/// (Example - TcpSocketListers with socket connection closed). This thead is a low priority thread 
		/// and sleeps for one second. It then checks for any obsolete client connection marked for deletion.
		/// </summary>
		private void PurgingThreadStart()
		{
			while (!mStopPurging)
			{
				try
				{
					Purge();
				}
				catch (Exception exception)
				{
					Logger.LogCommentMessage("[Purging Thread] exception: " + exception.Message);
				}
				finally
				{
					Thread.Sleep(1000);
				}
			}
		}

		private void ServerStatusThread()
		{
			// If we lose our internet connection - we will shut-down the server
			while (!mStopServer)
				Thread.Sleep(1000);

			StopServer();

			// I'd prefer to get a reference to the ServiceBase and:
			//ServiceBase.Stop();
			// But the service simply restarts itself using the above
			Environment.Exit(0);
		}

		protected void BufferMessage (MessageEventArgs m)
		{
			string toTargetLower = m.ToTargetLowerCase;
			Dictionary<string, ServerBuffer> recipientBuffer = null;
			bool foundRecipientBuffer = mBufferedMessages.TryGetValue(toTargetLower, out recipientBuffer);
			if (!foundRecipientBuffer) // this means no buffers have been set-up for this target
				return;

			string fromTargetLower = m.FromTargetLowerCase;
			ServerBuffer buffer = recipientBuffer[fromTargetLower];
			if (buffer == null) // this means no buffer has been set-up for this sender
				return;

			// Check to ensure the sender hasn't requested that we stop buffering
			if (buffer.BufferMessages)
			{
				// If we've reached the maximum size for this buffer - remove the oldest message
				// The only way the Count could be greater than than the max size is if the user/client updates the max
				// size to a number lower than the buffer's current size
				if (buffer.Count >= buffer.MaxSize)
				{
					int numMessagesOverBufferLimit = buffer.Count - buffer.MaxSize + 1;
					buffer.RemoveRange(0, numMessagesOverBufferLimit);
				}

				buffer.Add(m);
			}
		}

		/// <summary>
		/// This method will use the full string array for the intended target and see if it matches the first portion
		/// of the Listener name string array
		/// </summary>
		/// <param name="delimitedToTargetName"></param>
		/// <param name="delimitedListenerName"></param>
		/// <param name="fullMatch"></param>
		/// <returns></returns>
		public static bool TargetsMatch(string [] delimitedToTargetName, string [] delimitedListenerName, out bool fullMatch)
		{
			fullMatch = false;
			// If the intended recipient's name has more separators than the Listener's name, then we've got the wrong recipient
			if (delimitedToTargetName.Length > delimitedListenerName.Length)
				return false;

			int i = 0;
			for (; i < delimitedToTargetName.Length;i++)
			{
				if (delimitedToTargetName[i] != delimitedListenerName[i])
					return false;
			}

			if (i == delimitedListenerName.Length) // we matched on every part of the socketName
				fullMatch = true;

			return true;
		}

		private bool SendIfTargetNameMatches(Listener listener, MessageEventArgs m, string toTarget)
		{
			bool messageSent = false;
			bool fullMatch = false;
			if (m.SendOnlyToSpecificTarget) // We will only send if the name is an exact match
			{
				if (m.ToTargetLowerCase == listener.TargetNameLowerCase)
				{
					// we could add a check to ensure listener is still connected: listener.Connected - but this
					// may unnecessarily slow us down
					listener.SendMessage(m.FromTargetLowerCase, m.Message, true);
					messageSent = true;
					fullMatch = true;
				}
			}
			else if (TargetsMatch(m.ToTargetLowerArray, listener.TargetNameArray, out fullMatch) || (toTarget.Length == 0) ||
				(toTarget.Equals(GameTalkMessage.sBroadcastToAllString)) ||
				((toTarget.Equals(GameTalkMessage.sBroadcastToAllExceptSelfString)) && (listener.TargetNameLowerCase != m.FromTargetLowerCase)))
			{
				// "*" - WILL send back to the thing that sent it
				// "*!" will broadcast to everyone, but the sender

				// we could add a check to ensure listener is still connected: listener.Connected - but this
				// may unnecessarily slow us down
				listener.SendMessage(m.FromTargetLowerCase, m.Message, true);
				messageSent = true;
			}

			// We want to track if we (1) found a target with an exactly matching name and (2) if we sent to that target
			if ((!m.FoundTargetWithExactMatchingName) && (fullMatch))
			{
				m.FoundTargetWithExactMatchingName = fullMatch;
				m.SentToTargetWithExactMatchingName = messageSent && fullMatch;
			}

			return messageSent;
		}

		/// <summary>
		/// This method does no channel filtering - it doesn't do channel filtering because server messages can be sent
		/// at any time - including before the client has had a chance to set its channels
		/// Therefore, this method is used whenever the server has something to send to clients
		/// </summary>
		/// <param name="m"></param>
		/// <param name="toTarget"></param>
		/// <returns></returns>
		protected void SendMessageToMatchingTargets(MessageEventArgs m, string toTarget)
		{
			try
			{
				lock (mListenersList)
				{
					foreach (Listener listener in mListenersList)
					{
						// Don't worry about checking Connected status being slow - this method is only used to send messages from the GameTalkServer (not from any target)
						if (listener.Connected) // Don't send to unconnected registered target - this could result in an inadvertent connection attempt and/or stale messages being sent to a registered target once connected
							SendIfTargetNameMatches(listener, m, toTarget);
					}
				}
			}
			catch (ThreadInterruptedException)
			{
			}
		}

		/// <summary>
		/// This method filters based on channel - if the client hasn't registered a listener on the channel the message
		/// is being sent on - then the message won't be sent
		/// </summary>
		/// <param name="m"></param>
		/// <param name="toTarget"></param>
		/// <param name="channel"></param>
		/// <returns></returns>
		protected void SendMessageToMatchingTargetsFilterChannel(MessageEventArgs m, string toTarget,
			string channel)
		{
			string[] channelBits = channel.Split(new char[] { '.' });
			// We may want to consider copying the structure containing the sockets indexed by channel - so that locking/synchronization is not necessary
			mChannelIndexedxListenerLock.AcquireReaderLock(-1);

			try
			{
				ChannelIndexedListeners currentIndexedSocketListener = mChannelIndexedListeners;
				// We may sent this message to target(s)/client(s) at each level of our channel hierarchy
				foreach (string channelBit in channelBits)
				{
					if (!currentIndexedSocketListener.mChildContainer.ContainsKey(channelBit))
					{
						// This client/target could have already deregistered or never registered?
						break;
					}
					else
					{
						currentIndexedSocketListener = currentIndexedSocketListener.mChildContainer[channelBit];
						if (currentIndexedSocketListener == null)
						{
							// This client/target could have already deregistered or never registered?
							continue;
						}
						foreach (Listener listener in currentIndexedSocketListener.mListeners)
						{
							// If we haven't already sent a message to this listener and the target names match 
							// (according to our target matching semantics) - send it
							if ((!m.HasMessageBeenSent(listener.UniqueName)) &&
								(SendIfTargetNameMatches(listener, m, toTarget)))
								m.MessageHasBeenSent(listener.UniqueName, listener.TargetNameLowerCase);
						}
					}
				}
			}
			catch
			{
				throw;
			}
			finally
			{
				mChannelIndexedxListenerLock.ReleaseReaderLock();
			}
		}

		protected abstract void BroadcastMessage(MessageEventArgs m);

		protected Listener[] GetSocketListenersUsingTargetName(string targetName)
		{
			List<Listener> socketListeners = new List<Listener>();
			try
			{
				lock (mListenersList)
				{
					foreach (Listener currentSocketListener in mListenersList)
					{
						if (currentSocketListener.TargetNameLowerCase == targetName)
							socketListeners.Add(currentSocketListener);
					}
				}
			}
			catch (ThreadInterruptedException)
			{
			}

			return socketListeners.ToArray();
		}

		/// <summary>
		/// DANGEROUS METHOD!  This method presumes you have already acquired a lock
		/// on the mChannelIndexedListeners
		/// </summary>
		/// <param name="channel"></param>
		/// <returns></returns>
		private ChannelIndexedListeners GetIndexedSocketListenersBasedOnChannel(string channel)
		{
			string[] channelBits = channel.Split(new char[] { '.' });
			ChannelIndexedListeners currentIndexedSocketListener = mChannelIndexedListeners;
			bool fullyTraversedChannelBits = true;
			foreach (string channelBit in channelBits)
			{
				if (!currentIndexedSocketListener.mChildContainer.ContainsKey(channelBit))
				{
					// perhaps this has already been deregistered?
					fullyTraversedChannelBits = false;
					break;
				}
				else
					currentIndexedSocketListener = currentIndexedSocketListener.mChildContainer[channelBit];
			}
			// If we've reached the last channel - then we're now at the level where the Listener should be
			if (fullyTraversedChannelBits)
				return currentIndexedSocketListener;
			else
				return null;
		}

		private void AddChannelFilter(string channel, string fromTargetLower, bool forceTargetToListenOnAllChannels)
		{
			string[] channelBits = channel.Split(new char[] { '.' });
			// Do not put the following statement into the mChannelIndexedxListenerLock - otherwise deadlock potential
			Listener[] socketListeners = GetSocketListenersUsingTargetName(fromTargetLower);
			ChannelIndexedListeners currentIndexedSocketListener = mChannelIndexedListeners;
			mChannelIndexedxListenerLock.AcquireWriterLock(-1);
			try
			{
				foreach (string channelBit in channelBits)
				{
					if (!currentIndexedSocketListener.mChildContainer.ContainsKey(channelBit))
					{
						// no one has added this channel bit before - add it now
						ChannelIndexedListeners indexedSocketListeners = new ChannelIndexedListeners();
						currentIndexedSocketListener.mChildContainer[channelBit] = indexedSocketListeners;
						currentIndexedSocketListener = indexedSocketListeners;
					}
					else
						currentIndexedSocketListener = currentIndexedSocketListener.mChildContainer[channelBit];
				}

				if (socketListeners.Length == 0)
				{
					// How the hell can we get a channel registration for a target that doesn't exist (isn't connected)!?!
					string message = fromTargetLower + " tried to register on channel "+ channel +" - but that client isn't connected.";
					LoggingEventArgs messageEvent = new LoggingEventArgs(LoggingEventType.LogComment, message);
					ServerEventManager.LoggingSend(this, messageEvent);
				}
				else
				{
#if (DEBUG)
					Trace.WriteLine("[TcpServer] AddChannelFilter: num registered listeners: " + currentIndexedSocketListener.mListeners.Count + " on channel: \"" + channel+ "\" going to add: "+fromTargetLower);
#endif
					foreach (Listener socketListener in socketListeners)
					{
						// The following is a tad expensive (but this method isn't called often - so shouldn't cause an impact) and should be unnecessary (if everything is working properly - i.e. target shouldn't try to reregister on a channel they've already registered on)
						if (!currentIndexedSocketListener.mListeners.Contains(socketListener))
						{
							currentIndexedSocketListener.mListeners.Add(socketListener);
							socketListener.AddChannel(channel);
						}
						else if (!forceTargetToListenOnAllChannels) // In this situation - it is possible we attempted to make the client/target listen on a channel it is already registered on
							Logger.LogCommentMessage("[TcpServer] Tried to reregister channel: " + channel + " for target " + socketListener.TargetName);
					}
				}
			}
			catch 
			{
				throw;
			}
			finally
			{
				mChannelIndexedxListenerLock.ReleaseWriterLock();
			}
		}

		private void RemoveChannelFilter (string channel, string fromTargetLower)
		{
			mChannelIndexedxListenerLock.AcquireWriterLock(-1);
			try
			{
				ChannelIndexedListeners currentIndexedSocketListener = GetIndexedSocketListenersBasedOnChannel(channel);
				if (currentIndexedSocketListener != null)
				{
#if (DEBUG)
					Trace.WriteLine("[TcpServer] RemoveChannelFilter: num registered listeners: " + currentIndexedSocketListener.mListeners.Count + " on channel: " + channel);
#endif
					int j = 0;
					for (; j < currentIndexedSocketListener.mListeners.Count; j++)
					{
						Listener Listener = currentIndexedSocketListener.mListeners[j];
						if (Listener.TargetNameLowerCase == fromTargetLower)
						{
							break;
						}
					}
					if (j < currentIndexedSocketListener.mListeners.Count)
						currentIndexedSocketListener.mListeners.RemoveAt(j);
					else
						Logger.LogCommentMessage("[TcpServer.RemoveChannelFilter]: tried (and failed since it isn't here) to remove the channel: " + channel + " from index for target: " + fromTargetLower + " num SocketListeners listening on that channel: " + currentIndexedSocketListener.mListeners.Count);
				}
			}
			catch 
			{
				throw;
			}
			finally
			{
				mChannelIndexedxListenerLock.ReleaseWriterLock();
			}
		}

		public static void SendListOfCurrentlyConnectedClients(object sender, string recipientConnection, string channelToReplyOn)
		{
			GameTalkMessage currentlyConnectedClientsMessage = new GameTalkMessage(channelToReplyOn);
			// Since there are many types of Server Messages - the first key in this message 
			// tells the client which type of server message this is
			currentlyConnectedClientsMessage.AddKeyContent(GameTalkMessage.ClientServerMessages.ConnectedTargetsMessage.ToString(), string.Empty);
			MessageEventArgs  messageEvent = new MessageEventArgs(ServerEventType.SendListOfClientsToTarget, GameTalkMessage.sGameTalkServerID, recipientConnection, currentlyConnectedClientsMessage);
			ServerEventManager.Send(sender, messageEvent);
		}

		private void ClientConfigRequest(MessageEventArgs m)
		{
			if (m.Message.NumberOfKeys == 0) // Technically this is already checked earlier - but in case something else calls this
				return;

			ServerBuffer buffer = null;
			string fromTargetLower = m.FromTargetLowerCase;
			string contentKey = m.Message.GetKey(0);
			if(contentKey == GameTalkMessage.ClientServerMessages.StartBuffering.ToString())
			{
				// Client has requested that buffering be started for the intended target of this message
				if (m.Message.NumberOfKeys < 2) // If we don't have the target portion of the message - the message is incomplete
					return;

				string toTargetLower = m.Message.GetKey(1).ToLower();
				Dictionary<string, ServerBuffer> recipientBuffer = null;
				bool foundRecipientBuffer = mBufferedMessages.TryGetValue(toTargetLower, out recipientBuffer);
				if (!foundRecipientBuffer) // this means no buffers have been set-up for this target
				{
					recipientBuffer = new Dictionary<string, ServerBuffer>();
					mBufferedMessages.Add(toTargetLower, recipientBuffer);
				}

				bool foundSenderBuffer = recipientBuffer.TryGetValue(fromTargetLower, out buffer);
				if (!foundSenderBuffer)
				{
					buffer = new ServerBuffer();
					recipientBuffer.Add(fromTargetLower, buffer);
				}
				buffer.MaxSize = Convert.ToInt32((string) m.Message.GetContent(contentKey));
			}
			else if (contentKey == GameTalkMessage.ClientServerMessages.StopBuffering.ToString())
			{
				// Client has requested that buffering be started for the intended target of this message
				if (m.Message.NumberOfKeys < 2) // If we don't have the target portion of the message - the message is incomplete
					return;

				string toTargetLower = m.Message.GetKey(1).ToLower();
				Dictionary<string, ServerBuffer> recipientBuffer = null;
				bool foundRecipientBuffer = mBufferedMessages.TryGetValue(toTargetLower, out recipientBuffer);
				if ((foundRecipientBuffer) && (recipientBuffer.ContainsKey(fromTargetLower)))
				{
					buffer = recipientBuffer[fromTargetLower];
					buffer.BufferMessages = false;
				}
				else
				{
					string message = m.FromTarget+" tried to stop buffering " + m.ToTarget + " - which hadn't started buffering.";
					LoggingEventArgs messageEvent = new LoggingEventArgs(LoggingEventType.LogComment, message);
					ServerEventManager.LoggingSend(this, messageEvent);
				}
			}				
			else if(contentKey == GameTalkMessage.ClientServerMessages.ClearBuffer.ToString())
			{
				// Client has requested that the buffer for the intended target be cleared
				string content = (string) m.Message.GetContent(contentKey);
				bool clearBuffer = Convert.ToBoolean(content);
				if (clearBuffer)
				{
					if (buffer != null)
						buffer.Clear();
					else
					{
						string message = m.FromTarget+" tried to clear buffer for " + m.ToTarget + " - which hadn't start buffering.";
						LoggingEventArgs messageEvent = new LoggingEventArgs(LoggingEventType.LogComment, message);
						ServerEventManager.LoggingSend(this, messageEvent);
					}

					// If we want server to immediately reclaim the memory used by the previous instance of buffer
					System.GC.WaitForPendingFinalizers();
					System.GC.Collect();
				}
			}
			else if (contentKey == GameTalkMessage.ClientServerMessages.VersionRequest.ToString())
			{
				// Client has requested the version of GameTalk that the server is running
				// Don't care what the message key content is (i.e. this thing: m.Message.GetContent(contentKey))
				string fromClient = m.FromTargetLowerCase;
				GameTalkMessage serverVersionMessage = new GameTalkMessage(GameTalkMessage.sServerMessageID);
				string versionStr = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString();
				serverVersionMessage.AddKeyContentAsString(GameTalkMessage.ClientServerMessages.VersionResponse.ToString(), versionStr);
				MessageEventArgs messageEvent = new MessageEventArgs(ServerEventType.BroadcastMessage, GameTalkMessage.sGameTalkServerID, fromClient, serverVersionMessage);
                // This is one of two cases where someone other than ServerEventReceiver is invoking BroadcastMessage directly
				BroadcastMessage(messageEvent);
			}
            else if (contentKey == GameTalkMessage.ClientServerMessages.RegisteredTargetsRequest.ToString())
            {
                // Client has requested all the registered (in the registry) targets/clients
                string fromClient = m.FromTargetLowerCase;
                GameTalkMessage serverRegisteredTargetsMessage = new GameTalkMessage(GameTalkMessage.sServerMessageID);
				// Since there are many types of Server Messages - the first key in this message 
				// tells the client which type of server message this is
				serverRegisteredTargetsMessage.AddKeyContent(GameTalkMessage.ClientServerMessages.RegisteredTargetsResponse.ToString(), "");

				Target[] targets = Configuration.Instance.GetRegisteredTargets(mDefaultPort);
				foreach (Target target in targets)
					serverRegisteredTargetsMessage.AddKeyContent(target.Name, GenerateTargetMetaData(target.Platform, target.Version));

                MessageEventArgs messageEvent = new MessageEventArgs(ServerEventType.BroadcastMessage, GameTalkMessage.sGameTalkServerID, fromClient, serverRegisteredTargetsMessage);
                // This is one of two cases where someone other than ServerEventReceiver is invoking BroadcastMessage directly
                BroadcastMessage(messageEvent);
            }
            else if (contentKey == GameTalkMessage.ClientServerMessages.AddChannelFilter.ToString())
            {
                string channel = (string)m.Message.GetContent(contentKey);
				AddChannelFilter(channel, fromTargetLower, false);
            }
            else if (contentKey == GameTalkMessage.ClientServerMessages.RemoveChannelFilter.ToString())
            {
				string channel = (string)m.Message.GetContent(contentKey);
				RemoveChannelFilter(channel, fromTargetLower);
			}
			else if (contentKey == GameTalkMessage.ClientServerMessages.GetAllConnections.ToString())
            {
				SendListOfCurrentlyConnectedClients(this, fromTargetLower, GameTalkMessage.sServerMessageID);
			}
			else if (contentKey == GameTalkMessage.ClientServerMessages.RegisteredTargetAddRequest.ToString())
			{
				try
				{
					byte[] targetBytes = m.Message.GetContentAsBinary(0);
					MemoryStream memStream = new MemoryStream(targetBytes);
					System.Runtime.Serialization.Formatters.Binary.BinaryFormatter deserializer = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
					object targetObj = deserializer.Deserialize(memStream);
					memStream.Close();
					if (targetObj is Target)
						Configuration.Instance.AddOrUpdateRegisteredTarget((Target)targetObj);
				}
				catch (Exception exception)
				{
					Trace.WriteLine("exception: " + exception.Message + Environment.NewLine + exception.StackTrace);
				}
			}
		}

		private void SendAnyBufferedMessages (string toTarget, string toTargetLower, string [] toTargetLowerArray)
		{
			Dictionary<string, ServerBuffer> recipientBuffer = null;
			bool foundRecipientBuffer = mBufferedMessages.TryGetValue(toTargetLower, out recipientBuffer);
			if (!foundRecipientBuffer)
				return; // Nothing has been buffered for this target - nothing to send

			Listener toTargetSocket = null;
			bool fullMatch;
			lock (mListenersList)
			{
				foreach (Listener listener in this.mListenersList)
				{
					if (TargetsMatch(toTargetLowerArray, listener.TargetNameArray, out fullMatch))
					{
						toTargetSocket = listener;
						break;
					}
				}
			}

			if (toTargetSocket == null)
				return; // This should never happen - someone has called this method without the target being connected

			// get the messages that have been buffered for this target
			foreach (KeyValuePair<string, ServerBuffer> keyValuePair in recipientBuffer)
			{
				string fromTarget = keyValuePair.Key;
				ServerBuffer serverBuffer = keyValuePair.Value;
				while (serverBuffer.Count > 0)
				{
					MessageEventArgs m = (MessageEventArgs)serverBuffer[0];
					// send all the messages from the sender to the target
					// we could add a check to ensure listener is still connected: listener.Connected - but this
					// may unnecessarily slow us down
					// Also we may want to inform the SendMessage function this was a buffered message - therefore a different ServerEventType - exp: LogBufferedMessageSent
					toTargetSocket.SendMessage(m.FromTargetLowerCase, m.Message, true);
								
					// Clear this message from the buffer
					serverBuffer.RemoveAt(0);
				}
			}
		}

		/// <summary>
		/// GameTalk doesn't support null KeyContent - this will ensure string isn't null
		/// </summary>
		/// <param name="platform"></param>
		/// <param name="version"></param>
		/// <returns></returns>
		public static string GenerateTargetMetaData(string platform, string version)
		{
			if (platform == null)
				platform = string.Empty;

			if (version == null)
				version = string.Empty;

			return platform + ":" + version;
		}

		private void AddListofConnectedClientsToMessage(MessageEventArgs messageEventArgs)
		{
			lock (mListenersList)
			{
				foreach (Listener listener in this.mListenersList)
				{
					// No need to put the client (that is going to get this) in the list of 
                    // clients that are currently connected
					if (messageEventArgs.ToTargetLowerCase != listener.TargetNameLowerCase)
					{
						if (listener.Connected) // RegisteredTcpSocket can be in the list and not connected
						{
							string targetName = listener.TargetName;
							string platformName = listener.PlatformName;
							string version = listener.Version;
							if (!string.IsNullOrEmpty(targetName)) // Not sure why this occurs - but sometimes it seems the targetName can end up being null and a GameTalkMessage Key can not be null - possibly this is a strange race condition where we send out an annoucement that a target has connected just as it disconnects?
								messageEventArgs.Message.AddKeyContentAsString(targetName, GenerateTargetMetaData(platformName, version));
						}
					}
				}
			}
		}

		private void ServerEventReceiver(object sender, ServerEventArgs e)
		{
			if (e is MessageEventArgs)
			{
				MessageEventArgs m = (MessageEventArgs)e;

				// Need to test how an exception throw by one of the following methods will be handled
				if (m.Type == ServerEventType.BroadcastMessage)
					BroadcastMessage(m);
				else if (m.Type == ServerEventType.ClientConfigRequest)
					ClientConfigRequest(m);
				else if (m.Type == ServerEventType.SendBufferToTarget)
					SendAnyBufferedMessages(m.ToTarget, m.ToTargetLowerCase, m.ToTargetLowerArray);
				else if (m.Type == ServerEventType.SendListOfClientsToTarget)
				{
					AddListofConnectedClientsToMessage(m);
                    // If there are no other connected clients we don't send the message - the 
                    // first key is just to tell the client what kind of server message this is
                    if (m.Message.NumberOfKeys > 1)
					    BroadcastMessage(m);
				}
			}
			else if (e.Param is string)
			{
				string targetLower = (string)e.Param;
				if (e.Type == ServerEventType.RegisterClientToAllChannels)
				{
					AddChannelFilter(string.Empty, targetLower, true);
				}
				else if (e.Type == ServerEventType.UnregisterClientFromAllChannels)
				{
					RemoveChannelFilter(string.Empty, targetLower);
				}
			}
		}

		/// <summary>
		/// This does a precise name match - i.e. it doesn't use the partial "target matching semantics"
		/// </summary>
		/// <param name="clientName"></param>
		/// <returns></returns>
		protected bool IsClientWithSameNameConnected(string clientName)
		{
			lock (mListenersList)
			{
				foreach (Listener listener in mListenersList)
				{
					if (clientName == listener.TargetNameLowerCase)
						return true;
				}
			}
			return false;
		}

		/// <summary>
		/// Currently this is only used to diagnose an issue (exp - to see why a connection attempt failed) by getting a listing of all 
		/// Listeners - this would include all active connections AND RegisteredTcpSockets
		/// </summary>
		/// <returns></returns>
		protected Listener[] GetAllListeners()
		{
			Listener[] listeners = new Listener[0];
			lock (mListenersList)
			{
				listeners = mListenersList.ToArray();
			}
			return listeners;
		}

		/// <summary>
		/// Get TCP/IP connection corresponding to the specifed IP address and port number
		/// </summary>
		/// <param name="ipaddress"></param>
		/// <param name="portNum"></param>
		/// <returns></returns>
		protected TcpSocketListener GetConnectionForIpAndPort(string ipaddress, int portNum)
		{
			lock (mListenersList)
			{
				foreach (Listener connection in mListenersList)
				{
					TcpSocketListener socketListener = connection as TcpSocketListener;
					if (socketListener == null)
						continue;

					// We could string concat the ipAddress and portNum (with ":" between), then use the condition (ipAddressAndPortNum == connection.IpAddressAndPort) - but we'd be relying on the property IpAddressAndPort always returning the ip address and port like this "<IP Address>:</Port Number>" - unsafe
					if ((socketListener.IpAddress == ipaddress) && (socketListener.Port == portNum))
						return socketListener;
				}
			}
			return null;
		}

		protected void UpdateRegisteredTargets(Target[] targets)
		{
			List<Target> registeredTargetsToAdd = new List<Target>();
			lock (mListenersList)
			{
				List<RegisteredTcpSocket> removedRegisteredTargets = new List<RegisteredTcpSocket>();
				foreach (Listener Listener in mListenersList)
				{
					if (!(Listener is RegisteredTcpSocket))
						continue;
					RegisteredTcpSocket registeredTarget = (RegisteredTcpSocket)Listener;
					bool foundMatch = false;
					foreach (Target target in targets)
					{
						if (registeredTarget.TargetNameLowerCase == target.NameLowerCase)
						{
							registeredTarget.UpdateDetails(target);
							foundMatch = true;
							break;
						}
					}
					if (!foundMatch)
						removedRegisteredTargets.Add(registeredTarget);
				}

				foreach (RegisteredTcpSocket registeredTarget in removedRegisteredTargets)
				{
					// We mark it for deletion so it gets removed both from mListenersList and mChannelIndexedListeners
					registeredTarget.IsMarkedForDeletion = true;
				}

				foreach (Target target in targets)
				{
					bool foundMatch = false;
					foreach (Listener Listener in mListenersList)
					{
						if (!(Listener is RegisteredTcpSocket))
							continue;
						RegisteredTcpSocket registeredTarget = (RegisteredTcpSocket)Listener;
						if (registeredTarget.TargetNameLowerCase == target.NameLowerCase)
						{
							foundMatch = true;
							break;
						}
					}

					if (!foundMatch) // We don't instantiate RegisteredTcpSocket in this lock (to avoid nesting locks)
						registeredTargetsToAdd.Add(target);
				}
			}

			foreach (Target target in registeredTargetsToAdd)
			{
				RegisteredTcpSocket Listener = new RegisteredTcpSocket(target.Name, target.IpAddress, target.Port, target.Platform, new AddListenerDelegate(AddListener), target.SameIpAddressAndPortAsServer);
			}
		}

		/// <summary>
		/// As soon as a ServerBuffer is created it defaults to allow buffering
		/// </summary>
		class ServerBuffer : ArrayList
		{
			public bool BufferMessages = true;
			public int MaxSize = int.MaxValue;
		}
	}
}
