using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Diagnostics;

using Microsoft.Win32;

using EA.GameTalkRouter;
using EA.GameTalkMessages;

namespace EA.GameTalkServer
{
	class FairQueue : Queue<MessageEventArgs>
	{
		private const uint mMsgThreadshold = 2;
		private Dictionary<string, uint> mRecipientCounter = new Dictionary<string, uint>();
		private object mSyncObj = new object();

		public void FairEnqueue(MessageEventArgs msg)
		{
			lock (mSyncObj)
			{
				// If we don't want to be dependent on the mRecipientTracker - this would probably work fine
				// Also it wouldn't require the FairDequeue method
				/*if (Count == 0)
					Enqueue(messageEventArgs);
				else if (Count < 100) // If we allow the queue to grow arbitrarily large - .NET never seems to reclaim the memory
				{
					if (messageEventArgs.ToTargetLowerCase != Peek().ToTargetLowerCase)
						Enqueue(messageEventArgs);
				}*/

				uint numOccurrences = 0;
				if (mRecipientCounter.TryGetValue(msg.ToTargetLowerCase, out numOccurrences))
					mRecipientCounter.Remove(msg.ToTargetLowerCase);

				if (numOccurrences < mMsgThreadshold)
				{
					++numOccurrences;
					Enqueue(msg);
				}

				mRecipientCounter.Add(msg.ToTargetLowerCase, numOccurrences);
			}
		}

		public MessageEventArgs FairDequeue()
		{
			lock (mSyncObj)
			{
				if (Count == 0)
					return null;

				MessageEventArgs msg = Dequeue();
				uint numOccurrences = 0;
				if (mRecipientCounter.TryGetValue(msg.ToTargetLowerCase, out numOccurrences)) // This check should be unnecessary unless the user didn't use our FairEnqueue to Enqueue
				{
					mRecipientCounter.Remove(msg.ToTargetLowerCase);
					--numOccurrences;
					if (numOccurrences != 0)
						mRecipientCounter.Add(msg.ToTargetLowerCase, numOccurrences);
				}
				
				return msg;
			}
		}
	}

	/// <summary>
	/// This class is here simply to try to create connections to registered clients
	/// If the intended recipient of a message is specific (not empty string or *) and 
	/// the target isn't connected - then it might be a registered target that we 
	/// can make the connection to.  Whether or not the current message was sent 
	/// to a client is no indication as to whether there is an unconnected client 
	/// that could also have got this message
	/// </summary>
	public class ConnectionInitiator
	{
		private FairQueue mMessageQueue = new FairQueue();
		private volatile bool mRun = true;
		private TcpServer.BufferMessageDelegate mBufferMessageDelegate;
		private TcpServer.IsClientWithSameNameConnectedDelegate mIsClientWithSameNameConnectedDelegate;
		private TcpServer.GetConnectionForIpAndPortDelegate mGetConnectionForIpAndPortDelegate;
		private TcpServer.AddListenerDelegate mAddListenerDelegate;
		private TcpServer.UpdateRegisteredClientsDelegate mUpdateRegisteredClientsDelegate;
		private TcpServer.GetAllListenersDelegate mGetAllListenersDelegate;

		private string mServerIpAddress = null;
		private int mServerPort = 0;

		private System.Timers.Timer mUpdateRegisteredTargetsTimer;

		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="bufferMessageDelegate"></param>
		/// <param name="isClientWithSameNameConnectedDelegate"></param>
		/// <param name="addListenerDelegate"></param>
		public ConnectionInitiator(TcpServer.BufferMessageDelegate bufferMessageDelegate,
			TcpServer.IsClientWithSameNameConnectedDelegate isClientWithSameNameConnectedDelegate,
			TcpServer.GetConnectionForIpAndPortDelegate getConnectionForIpAndPortDelegate,
			TcpServer.AddListenerDelegate addListenerDelegate,
			TcpServer.UpdateRegisteredClientsDelegate updateRegisteredClientsDelegate,
			TcpServer.GetAllListenersDelegate getAllListenersDelegate,
			string serverIpAddress, int serverPort)
		{
			mBufferMessageDelegate = bufferMessageDelegate;
			mIsClientWithSameNameConnectedDelegate = isClientWithSameNameConnectedDelegate;
			mGetConnectionForIpAndPortDelegate = getConnectionForIpAndPortDelegate;
			mAddListenerDelegate = addListenerDelegate;
			mUpdateRegisteredClientsDelegate = updateRegisteredClientsDelegate;
			mGetAllListenersDelegate = getAllListenersDelegate;
			mServerIpAddress = serverIpAddress;
			mServerPort = serverPort;

			mUpdateRegisteredTargetsTimer = new System.Timers.Timer(1000);
			mUpdateRegisteredTargetsTimer.Elapsed += new System.Timers.ElapsedEventHandler(UpdateRegisteredTargets);
			// Setting AutoReset to false is one way to prevent the timer's callback from being reinvoked while it is currently executing
			mUpdateRegisteredTargetsTimer.AutoReset = false;
			mUpdateRegisteredTargetsTimer.Enabled = true;
		}

		private bool IsLocalPcIpAddress(string ipAddress)
		{
			if ((ipAddress == mServerIpAddress) || (ipAddress.ToLower() == "localhost") || (ipAddress == "127.0.0.1"))
				return true;

			return false;
		}

		private Target[] GetAllRegisteredTargets()
		{
			try
			{
				Target[] targets = Configuration.Instance.GetRegisteredTargets(TcpServer.mDefaultPort);
				foreach (Target target in targets)
				{
					// Stop GameTalkServer from trying to connect to itself
					if ((IsLocalPcIpAddress(target.IpAddress)) && (target.Port == mServerPort))
						target.SameIpAddressAndPortAsServer = true;
				}
				return targets;
			}
			catch(Exception exception)
			{
				TcpServer.DebugLog("[ConnectionInitiator.GetAllRegisteredTargets] "+exception.Message);
			}

			return new Target[0];
		}

		private void UpdateRegisteredTargets(object sender, System.Timers.ElapsedEventArgs args)
		{
			try
			{
				mUpdateRegisteredTargetsTimer.Stop();
				Target[] targets = GetAllRegisteredTargets();
				// Need a delegate from TcpServer to update Registered Targets
				mUpdateRegisteredClientsDelegate(targets);
			}
			catch (Exception exception)
			{
				TcpServer.DebugLog("[ConnectionInitiator.UpdateRegisteredTargets] exception: "+exception.Message);
			}
			finally
			{
				try
				{
					mUpdateRegisteredTargetsTimer.Start();
				}
				catch (Exception innerException)
				{
					TcpServer.DebugLog("[ConnectionInitiator.UpdateRegisteredTargets] inner exception: " + innerException.Message);
				}
			}
		}

		/// <summary>
		/// Queue message for the ConnectionInitiator to deal with when it gets around to it
		/// </summary>
		/// <param name="messageEventArgs"></param>
		public void AddMessage(MessageEventArgs messageEventArgs)
		{
			mMessageQueue.FairEnqueue(messageEventArgs);
		}

		private void ConnectToTarget(Target target)
		{
			// check whether this ipAddress + port are already connected (the name may have changed - hence why we check using IP address and port number)
			TcpSocketListener tcpSocketListener = mGetConnectionForIpAndPortDelegate(target.IpAddress, target.Port);
			if ((tcpSocketListener != null) && (tcpSocketListener is RegisteredTcpSocket))
			{
				RegisteredTcpSocket registeredTcpSocketListener = (RegisteredTcpSocket)tcpSocketListener;
				if (!registeredTcpSocketListener.Connected)
				{
					if (registeredTcpSocketListener.Connect())
					{
						// Now send out all the messages that are relevant (exp - buffered messages) to a new client connecting
						registeredTcpSocketListener.OnReceiptOfConnectorsName(true);
						registeredTcpSocketListener.StartReaderThreads();

						// If GameTalkServer grows too large in memory - we could potentially 
						// add the following to reclaim memory whenever a new connection is made
						/*System.GC.WaitForPendingFinalizers();
						System.GC.Collect();*/
					}
					else // Clear existing messages - so messages sent long before target becomes connected aren't sent to that target
						registeredTcpSocketListener.ClearQueuedMessages();
				}
			}
			else if (tcpSocketListener == null)
			{
				StringBuilder allSocketConnectionsStr = new StringBuilder();
				Listener[] allListeners = mGetAllListenersDelegate();
				foreach (TcpSocketListener socketConnection in allListeners)
				{
					allSocketConnectionsStr.Append("[name: " + socketConnection.TargetName + " ip address: " + socketConnection.IpAddress + " port: " + socketConnection.Port + "]");
				}
				Logger.LogCommentMessage("Unable to find target with IP Address: " + target.IpAddress + " and port: " + target.Port + ".  TCP connections: " + allSocketConnectionsStr.ToString());
			}
			else
				Logger.LogCommentMessage("We can't initiate connection to IP Address: " + target.IpAddress + " and port: " + target.Port + " because it not a registered target (we might already be connected to it)");
		}

		/// <summary>
		/// This is the meat of ConnectionInitiator - we try to connect to unconnected 
		/// clients and buffer any messages for any client that is still unconnected
		/// </summary>
		/// <param name="m"></param>
		private void TryToConnectToTargets (MessageEventArgs m)
		{
			// The following logic doesn't have to be dependent on the registry
			// We could do a single iteration through all RegisteredTcpSockets and
			// get a list of matching targets back
			Target[] targets = GetAllRegisteredTargets();
			try
			{
				bool fullMatch;
				foreach (Target target in targets)
				{
					string[] registeredTargetNameArray = target.Name.ToLower().Split(new char[] { '.' });
					if (!TcpServer.TargetsMatch(m.ToTargetLowerArray, registeredTargetNameArray, out fullMatch))
						continue;

					ConnectToTarget(target);
				}
			}
			catch (Exception exception)
			{
				TcpServer.DebugLog("[ConnectionInitiator.TryToConnectToTargets] exception: " + exception.Message);
			}

			bool isClientWithSameNameConnected = mIsClientWithSameNameConnectedDelegate(m.ToTargetLowerCase);
			if (!isClientWithSameNameConnected)
			{
				// A client with this exact name isn't connected - if they have buffering enabled - buffer the message
				mBufferMessageDelegate(m);
			}
		}

		/// <summary>
		/// Start ConnectionInitiator as a low priorty thread that won't impact
		/// the existing performance of GameTalkServer
		/// </summary>
		public void Start()
		{
			while (mRun)
			{
				try
				{
					MessageEventArgs messageEventArgs = mMessageQueue.FairDequeue();
					if (messageEventArgs == null) // This should hit the Sleep - since the finally is in the loop
						continue;

					TryToConnectToTargets(messageEventArgs);
				}
				catch
				{
				}
				finally
				{
					Thread.Sleep(100);
				}
			}
		}

		/// <summary>
		/// Stop ConnectionInitiator
		/// </summary>
		public void Stop()
		{
			mUpdateRegisteredTargetsTimer.Stop();

			mRun = false;
		}
	}
}
