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

using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Collections;
using System.Collections.Generic;

namespace EA.GameTalk
{
	/// <summary>
	/// Summary description for Connection.
	/// </summary>
	public class Connection : System.Net.Sockets.TcpClient
	{
		/// <summary>
		/// Delegate to broadcast when a message is received.
		/// </summary>
		public delegate void MessageReceivedDelegate(byte[] buffer);

		/// <summary>
		/// Connection status.
		/// </summary>
		public enum ConnectionResult
		{
			/// <summary>
			/// Indicates we're not currently connected
			/// </summary>
			NotConnected,
			/// <summary>
			/// Indicates our connection wasn't acknowledged
			/// </summary>
			NotAcknowledge,
			/// <summary>
			/// Indicates our connection failed
			/// </summary>
			Failed,
			/// <summary>
			/// Indicates our connection succeeded
			/// </summary>
			Success,
		}
		
		/// <summary>
		/// The current host.
		/// </summary>
		private string mHost = string.Empty;

		/// <summary>
		/// The current port.
		/// </summary>
		private int mPort = -1;

		/// <summary>
		/// Read thread status flag - two different read threads (one that reads data from the
		/// wire and the other that processes that data) both use the following to know when
		/// they should shut-down
		/// </summary>
		private bool mReadThreadEnabled = true;

		/// <summary>
		/// The thread for reading data from the stream.
		/// </summary>
		private Thread mReadThread = null;

		/// <summary>
		/// The thread for processing the data that the reading thread accrues.
		/// </summary>
		private Thread mProcessThread = null;

		/// <summary>
		/// Message handler.
		/// </summary>
		private event MessageReceivedDelegate mMesssageReceivedHandler;

		private MessageReceivedDelegate mReceiveCallback = null;

		private SocketReader mSocketReader = null;

		/// <summary>
		/// Get the connection status.
		/// </summary>
		internal new bool Connected
		{
			get 
			{ 
				if (this.Client != null)
				{
					// Gets a value that indicates whether a Socket is connected to a remote host as of the last Send or Receive operation
					return this.Client.Connected;
				}
				else
				{
					return false;
				}
			}
		}

		/// <summary>
		/// Can the client write to the stream
		/// </summary>
		private bool CanWrite
		{
			get { return this.Client.Poll(-1, SelectMode.SelectWrite); }
		}

		private void Init(MessageReceivedDelegate messageReceived)
		{
			// Register the event handler
			mReceiveCallback = messageReceived;
			if (mReceiveCallback != null)
			{
				mMesssageReceivedHandler += mReceiveCallback;
			}

			// Specify the client to be removed right after close is called
			this.LingerState = new LingerOption(false, 0);

			// Send right away without delays
			this.NoDelay = true;

			mSocketReader = new SocketReader(this);
		}

		private void InitThreads()
		{
			// Setup the read thread
			mReadThread = new Thread(new ThreadStart(ReadThread));
			mReadThread.Name = "Client read thread";
			mReadThread.Start();

			mProcessThread = new Thread(new ThreadStart(ProcessBufferedData));
			mProcessThread.Name = "Client process thread";
			mProcessThread.Start();
		}

		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="messageReceived"></param>
		public Connection(MessageReceivedDelegate messageReceived) :
			base()
		{
			Init(messageReceived);

			InitThreads();
		}

		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="messageReceived"></param>
		/// <param name="multiThreaded"></param>
		public Connection(MessageReceivedDelegate messageReceived, bool multiThreaded) :
			base()
		{
			Init(messageReceived);

			if (multiThreaded)
				InitThreads();
		}

		/// <summary>
		/// Closes the connection
		/// </summary>
		public new void Close()
		{
			// The following will signal to both the read threads (ReadThread and ProcessThread)
			// that they should stop iterating
			mReadThreadEnabled = false;

			// Unregister the event handler
			if (mReceiveCallback != null)
			{
				mMesssageReceivedHandler -= mReceiveCallback;
			}

			lock (mPacketWriteBuffer)
			{
				Monitor.Pulse(mPacketWriteBuffer);
			}

			// If necessary, force the Read thread (that reads from the wire) to shut-down
			if ((mReadThread != null) && (mReadThread.IsAlive))
			{
                if (Client.Connected)
                {
					// To ensure that all data is sent and received before the socket is closed, we call Shutdown before calling the Disconnect method
                    Client.Shutdown(SocketShutdown.Both);
                    Client.Disconnect(true);
					// If we also call Close - this will prevent an exception from occurring when we do our thread join on the reader thread below
					// We don't only want to call Close - since Close releases the connection and automatically discards outgoing queued data
					Client.Close();
                }
				if (!mReadThread.Join(50))
				{
					Debug.WriteLine("mReadThread.Join timed out");
					mReadThread.Abort();
				}
			}

			// If necessary, force the Read thread (that processes the data) to shut-down
			if ((mProcessThread != null) && (mProcessThread.IsAlive))
			{
				if (!mProcessThread.Join(50))
				{
					Debug.WriteLine("mProcessThread.Join timed out");
					mProcessThread.Abort();
				}
			}
			
			base.Close();
		}

		/// <summary>
		/// Connect to the host on the specified port
		/// </summary>
		/// <param name="host">The host to connect to.</param>
		/// <param name="port">The port to connect to.</param>
		/// <returns>The result of the connection.</returns>
		public new ConnectionResult Connect(string host, int port)
		{
			ConnectionResult result = ConnectionResult.Failed;

			try
			{
				if ((!this.Client.Connected) ||
					(!host.Equals(mHost)) ||
					(port != mPort))
				{
					// Connect to target platform
                    base.Connect(host, port);

					// Remember settings
					mHost = string.Copy(host);
					mPort = port;

					result = ConnectionResult.Success;
				}
				else if (this.Client.Connected)
				{
					result = ConnectionResult.Success;
				}
			}
			catch (Exception e)
			{
				Trace.WriteLine(e.Message);
				result = ConnectionResult.NotConnected;
			}

			return result;
		}

		/// <summary>
		/// Send the data in the buffer.
		/// </summary>
		/// <param name="buffer">The data to be sent.</param>
		/// <returns>The result of the send.</returns>
		public ConnectionResult Send(byte[] buffer)
		{
			ConnectionResult result = ConnectionResult.Failed;

			if ((mHost.Length > 0) && (mPort > 0))
			{
				try
				{
					if (this.CanWrite)
					{
						int sent = this.Client.Send(buffer);
						while (sent < buffer.Length)
						{
							sent += this.Client.Send(buffer, sent, (int)buffer.Length - sent, SocketFlags.None);
						}
						result = ConnectionResult.Success;
					}
					else
					{
						if (this.Client.Poll(-1, SelectMode.SelectError))
						{
							Debug.Assert(false, "Client error!");
						}
						else
						{
							Debug.Assert(false, "Client could not write!");
						}
					}
				}
				catch (System.ObjectDisposedException e)
				{
					Trace.WriteLine(e.Message);
					result = ConnectionResult.NotConnected;
				}
				catch (Exception e)
				{
					Trace.WriteLine(e.Message);
					result = ConnectionResult.Failed;
				}
			}
			else
			{
				result = ConnectionResult.NotConnected;
			}
			return result;
		}

		private void DebugLog(string message)
		{
#if(DEBUG)
			Trace.WriteLine(message);
#endif
		}

		private List<byte[]> mPacketWriteBuffer = new List<byte[]>();

		internal List<byte[]> PacketWriteBuffer
		{
			get { return mPacketWriteBuffer; }
		}

		private List<byte[]> PullPacketsFromWriteBufferToReadBuffer()
		{
			List<byte[]> packetReadBuffers = null;
			try
			{
				lock (mPacketWriteBuffer)
				{
					// If we have no data in the mPacketWriteBuffer then wait on the Monitor
					if ((mPacketWriteBuffer.Count == 0) && (mReadThreadEnabled))
						Monitor.Wait(mPacketWriteBuffer);

					if (mReadThreadEnabled) // Check that we didn't get a Monitor.Pulse for shut-down
					{
						packetReadBuffers = mPacketWriteBuffer.GetRange(0, mPacketWriteBuffer.Count);
						mPacketWriteBuffer.Clear();
					}
				} // end of lock (mPacketWriteBuffer)
			}
			catch (Exception ex)
			{
				// We're on a different thread - we probably want to log what happened - and just continue
				// trying to process new messages
				// we could shut-down the reader thread if we get an error in here
				//mReadThreadEnabled = false;
				Trace.WriteLine("Exception: " + ex.Message + Environment.NewLine + ex.StackTrace);

				// If we're getting in here repetitively - it would be safer to Sleep/Idle.
				// So as not to tight loop and make the GameTalk client processor intensive when idle.
				Thread.Sleep(5);
			}
			return packetReadBuffers;
		}

		private void ProcessPacketsFromReadBuffer(byte[] packetReadBuffer)
		{
			MemoryStream bufferMemStream = null;
			BinaryReader bufferBinaryReader = null;
			try
			{
				bufferMemStream = new MemoryStream(packetReadBuffer);
				bufferBinaryReader = new BinaryReader(bufferMemStream);
				// Get the length of the entire message, always the first four bytes
				int messageLength = bufferBinaryReader.ReadInt32();
				Debug.Assert(messageLength != 0, "How can we have a zero length GameTalkMessage?");
				byte[] messageBytes = bufferBinaryReader.ReadBytes(messageLength);
				if (messageBytes.Length > 0)
				{
					if (this.mMesssageReceivedHandler != null)
					{
						this.mMesssageReceivedHandler(messageBytes);
					}
				}
			}
			catch (Exception ex)
			{
				// We're on a different thread - we probably want to log what happened - and just continue
				// trying to process new messages

				// we could shut-down the reader thread if we get an error in here
				//mReadThreadEnabled = false;

				Trace.WriteLine("Exception: " + ex.Message + Environment.NewLine + ex.StackTrace);
				// If we're getting in here repetitively - it would be safer to Sleep/Idle.
				// So as not to tight loop and make the GameTalk client processor intensive when idle.
				Thread.Sleep(5);
			}
			finally
			{
				if (bufferBinaryReader != null)
				{
					bufferBinaryReader.Close();
					bufferBinaryReader = null;
				}
				if (bufferMemStream != null)
				{
					bufferMemStream.Close();
					bufferMemStream = null;
				}
			}
		}

		/// <summary>
		/// The consumer thread's method
		/// </summary>
		private void ProcessBufferedData()
		{
			while (mReadThreadEnabled)
			{
				List<byte[]> packetReadBuffers = PullPacketsFromWriteBufferToReadBuffer();

				// If nothing has been read - there is nothing to process
				if (packetReadBuffers == null)
					continue;

				foreach(byte[] packetReadBuffer in packetReadBuffers)
					ProcessPacketsFromReadBuffer(packetReadBuffer);
			} //while (mReadThreadEnabled)
		}

		/// <summary>
		/// Producer thread for reading data from the socket.
		/// </summary>
		private void ReadThread()
		{
			// Potential performance improvement would be to store multiple GTMs in 
			// here and just provide an index for the location of each full GTM buffer
			byte[] singleGtmBuffer = null;
			while (mReadThreadEnabled)
			{
				try
				{
					mSocketReader.ReceiveData(ref singleGtmBuffer);
				}
				catch (Exception exception)
				{
					Trace.WriteLine("exception: " + exception.Message + Environment.NewLine + exception.StackTrace);
					// If we're getting in here repetitively - it would be safer to Sleep/Idle.
					// So as not to tight loop and make the GameTalk client processor intensive when idle.
					Thread.Sleep(5);
				}
			}
		}

		/// <summary>
		/// The user's entry into polling from the data read from the socket
		/// </summary>
		public void PollBufferedData()
		{
			List<byte[]> packetReadBuffers = PullPacketsFromWriteBufferToReadBuffer();

			// If nothing has been read - there is nothing to process
			if (packetReadBuffers == null)
				return;

			foreach (byte[] packetReadBuffer in packetReadBuffers)
				ProcessPacketsFromReadBuffer(packetReadBuffer);
		}

		/// <summary>
		/// Used to determine if there are anymore packets to process in the write buffer
		/// </summary>
		/// <returns></returns>
		public int NumPacketsToProcessFromWriteBuffer()
		{
			lock (mPacketWriteBuffer)
			{
				return mPacketWriteBuffer.Count;
			}
		}

		/// <summary>
		/// Should only be used in single-threaded mode.
		/// </summary>
		public void Poll()
		{
			byte[] singleGtmBuffer = null;
			mSocketReader.ClearCurrentMessageState();
			while (Client.Available > 0)
				mSocketReader.ReceiveData(ref singleGtmBuffer);

			if (NumPacketsToProcessFromWriteBuffer() > 0)
				PollBufferedData();
		}
	}
}
