using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.Diagnostics;
using System.Windows.Forms;
using System.IO.Pipes;
using System.IO;
using System.Threading;
using System.Runtime.InteropServices;

using EA.GameTalkMessages;

namespace EA.GameTalkViewer
{
	class NamedPipesReader
	{
		// Importing this dll would allow us to check if a pipe exists before bothering to try to connect
		// I do worry a bit about deployment - example - should I be using a different dll on windows 7 vs windows 8
		[return: MarshalAs(UnmanagedType.Bool)]
		[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
		private static extern bool WaitNamedPipe(string name, int timeout);

		private volatile bool mActive = true;
		SynchronizationContext mUISynchronizationContext;

		volatile NamedPipeClientStream mTrafficPipeClient = null;
		volatile StreamReader mTrafficStreamReader = null;

		volatile NamedPipeClientStream mConnectionsPipeClient = null;
		volatile StreamReader mConnectionsStreamReader = null;

		volatile NamedPipeClientStream mMiscPipeClient = null;
		volatile StreamReader mMiscStreamReader = null;

		CommonCallbacks.ProcessMiscStringDelegate mProcessMiscDelegate = null;
		CommonCallbacks.ProcessConnectionsStringDelegate mProcessConnectionsDelegate = null;
		CommonCallbacks.ProcessTrafficStringDelegate mProcessTrafficDelegate = null;

		Thread mTrafficReaderThread = null;
		Thread mConnectionReaderThread = null;
		Thread mMiscReaderThread = null;

		readonly int NewLineLength = Environment.NewLine.ToString().Length;

		public NamedPipesReader(SynchronizationContext uISynchronizationContext,
			CommonCallbacks.ProcessMiscStringDelegate processMiscDelegate,
			CommonCallbacks.ProcessConnectionsStringDelegate processConnectionsDelegate,
			CommonCallbacks.ProcessTrafficStringDelegate processTrafficDelegate)
		{
			mUISynchronizationContext = uISynchronizationContext;
			mProcessMiscDelegate = processMiscDelegate;
			mProcessConnectionsDelegate = processConnectionsDelegate;
			mProcessTrafficDelegate = processTrafficDelegate;

			mTrafficReaderThread = new Thread(ReadTraffic);
			mTrafficReaderThread.Start();

			mConnectionReaderThread = new Thread(ReadConnections);
			mConnectionReaderThread.Start();

			mMiscReaderThread = new Thread(ReadMisc);
			mMiscReaderThread.Start();
		}

		public bool Active
		{
			set
			{
				mActive = value;

				try
				{
					if (mTrafficStreamReader != null)
					{
						mTrafficStreamReader.Close();
						mTrafficStreamReader.Dispose();
					}
					mTrafficPipeClient.Close();
					mTrafficPipeClient.Dispose();
				}
				catch { }
				try
				{
					if (mConnectionsStreamReader != null)
					{
						mConnectionsStreamReader.Close();
						mConnectionsStreamReader.Dispose();
					}
					mConnectionsPipeClient.Close();
					mConnectionsPipeClient.Dispose();
				}
				catch { }
				try
				{
					if (mMiscStreamReader != null)
					{
						mMiscStreamReader.Close();
						mMiscStreamReader.Dispose();
					}
					mMiscPipeClient.Close();
					mMiscPipeClient.Dispose();
				}
				catch { }

				try
				{
					// If issuing this repeatedly is more reliable than only doing it once - we can bump up the number of iterations - but afaik if it doesn't work once - I don't think it is likely to work on successive tries
					for (int i = 0; (i < 1) && mTrafficReaderThread.IsAlive; i++)
					{
						mTrafficReaderThread.Interrupt();
						mTrafficReaderThread.Abort();
					}
				}
				catch { }
				try
				{
					// If issuing this repeatedly is more reliable than only doing it once - we can bump up the number of iterations - but afaik if it doesn't work once - I don't think it is likely to work on successive tries
					for (int i = 0; (i < 1) && mConnectionReaderThread.IsAlive; i++)
					{
						mConnectionReaderThread.Interrupt();
						mConnectionReaderThread.Abort();
					}
				}
				catch { }
				try
				{
					// If issuing this repeatedly is more reliable than only doing it once - we can bump up the number of iterations - but afaik if it doesn't work once - I don't think it is likely to work on successive tries
					for (int i = 0; (i < 1) && mMiscReaderThread.IsAlive; i++)
					{
						mMiscReaderThread.Interrupt();
						mMiscReaderThread.Abort();
					}
				}
				catch { }
			}
		}

		public bool ThreadsAlive
		{
			get
			{
				if ((mTrafficReaderThread != null) && (mTrafficReaderThread.IsAlive))
					return true;

				if ((mConnectionReaderThread != null) && (mConnectionReaderThread.IsAlive))
					return true;

				if ((mMiscReaderThread != null) && (mMiscReaderThread.IsAlive))
					return true;
				return false;
			}
		}

		private void ReadTraffic()
		{
			bool lastReadIncomplete = false;
			while (mActive)
			{
				Thread.Sleep(50);
				try
				{
					if (mTrafficPipeClient == null)
					{
						mTrafficPipeClient = new NamedPipeClientStream(".", NamedPipes.kGameTalkTraffic, PipeDirection.InOut, PipeOptions.Asynchronous);
						//mTrafficStreamReader = new StreamReader(mTrafficPipeClient);
					}

					if (!mTrafficPipeClient.IsConnected)
					{
						try
						{
							if (NamedPipeDoesNotExist(NamedPipes.kGameTalkTraffic)) // If the GameTalkServer isn't running (or for some other reason isn't waiting for a connection) MS has a bug where this ends up tight looping and kills processor usage
								continue;

							// This is a bit annoying - this throws an exception (when it times out) - but if we use the mundane Connect method (with no timeout argument) - we can't kill the thread and thus have to use the heinous Environment.Exit(...) method
							mTrafficPipeClient.Connect(500);
							mTrafficStreamReader = new StreamReader(mTrafficPipeClient);
						}
						catch // Who cares if this times out
						{
							continue;
						}
					}

					if (!mTrafficPipeClient.IsConnected || !mTrafficPipeClient.CanRead)
						continue;

					/*byte[] bytes = new byte[1024];
					pipeClient.Read(bytes, 0, 1024);
					ProcessTraffic(bytes);
					pipeClient.Close();*/

					/*EA.GameTalkMessages.StreamString streamString = new EA.GameTalkMessages.StreamString(pipeClient);
					ProcessTraffic(streamString.ReadString());*/

					//string traffic = mTrafficStreamReader.ReadToEnd();
					char[] trafficBuffer = new char[1024];
					int amountRead = 0;
					while ((amountRead = mTrafficStreamReader.Read(trafficBuffer, 0, 1024)) == 1024)
						Trace.WriteLine("ignoring 1024 bytes of traffic");
					string traffic = new string(trafficBuffer);
					if (lastReadIncomplete)
					{
						int firstNewLine = traffic.IndexOf(Environment.NewLine);
						if (firstNewLine == -1)
							continue;

						traffic = traffic.Substring(firstNewLine + NewLineLength);
					}
					int lastNewLine = traffic.LastIndexOf(Environment.NewLine, amountRead, amountRead);
					if (lastNewLine == -1)
						continue;

					if (lastNewLine != (amountRead - NewLineLength))
					{
						traffic = traffic.Substring(0, lastNewLine + NewLineLength);
						lastReadIncomplete = true;
					}
					else
						traffic = traffic.Substring(0, amountRead);

					mProcessTrafficDelegate(traffic);

					/*
					string traffic = null;
					while ((traffic = mTrafficStreamReader.ReadLine()) != null)
					{
						mProcessTrafficStringDelegate(traffic);
					}*/
				}
				catch (Exception exception)
				{
					string exceptionStr = "exception: " + exception.Message + Environment.NewLine + exception.StackTrace;
					// If you want this to get logged in the Misc Panel - just uncomment the "Trace.Listeners.Add(...)" in GameTalkNotify.cs
					Trace.WriteLine(exceptionStr);
				}
			}
		}

		private void ReadConnections()
		{
			while (mActive)
			{
				try
				{
					Thread.Sleep(50);
					if (mConnectionsPipeClient == null)
					{
						mConnectionsPipeClient = new NamedPipeClientStream(".", NamedPipes.kGameTalkConnections, PipeDirection.InOut, PipeOptions.Asynchronous);
						//mConnectionsStreamReader = new StreamReader(mConnectionsPipeClient);
					}

					if (!mConnectionsPipeClient.IsConnected)
					{
						try
						{
							if (NamedPipeDoesNotExist(NamedPipes.kGameTalkConnections)) // If the GameTalkServer isn't running (or for some other reason isn't waiting for a connection) MS has a bug where this ends up tight looping and kills processor usage
								continue;

							// This is a bit annoying - this throws an exception (when it times out) - but if we use the mundane Connect method (with no timeout argument) - we can't kill the thread and thus have to use the heinous Environment.Exit(...) method
							mConnectionsPipeClient.Connect(500);
							mConnectionsStreamReader = new StreamReader(mConnectionsPipeClient);
						}
						catch // Who cares if this times out
						{
							continue;
						}
					}

					if (!mConnectionsPipeClient.IsConnected || !mConnectionsPipeClient.CanRead)
						continue;

					/*byte[] bytes = new byte[1024];
					pipeClient.Read(bytes, 0, 1024);
					ProcessConnections(bytes);
					pipeClient.Close();*/

					/*EA.GameTalkMessages.StreamString streamString = new EA.GameTalkMessages.StreamString(pipeClient);
					ProcessConnections(streamString.ReadString());*/

					string connections = null;
					while ((connections = mConnectionsStreamReader.ReadLine()) != null)
					{
						mProcessConnectionsDelegate(connections);
					}
				}
				catch (Exception exception)
				{
					string exceptionStr = "exception: " + exception.Message + Environment.NewLine + exception.StackTrace;
					// If you want this to get logged in the Misc Panel - just uncomment the "Trace.Listeners.Add(...)" in GameTalkNotify.cs
					Trace.WriteLine(exceptionStr);
				}
			}
		}

		private void ReadMisc()
		{
			while (mActive)
			{
				try
				{
					Thread.Sleep(50);
					if (mMiscPipeClient == null)
					{
						mMiscPipeClient = new NamedPipeClientStream(".", NamedPipes.kGameTalkMisc, PipeDirection.InOut, PipeOptions.Asynchronous);
						//mMiscStreamReader = new StreamReader(mMiscPipeClient);
					}

					if (!mMiscPipeClient.IsConnected)
					{
						try
						{
							if (NamedPipeDoesNotExist(NamedPipes.kGameTalkMisc)) // If the GameTalkServer isn't running (or for some other reason isn't waiting for a connection) MS has a bug where this ends up tight looping and kills processor usage
								continue;

							// This is a bit annoying - this throws an exception (when it times out) - but if we use the mundane Connect method (with no timeout argument) - we can't kill the thread and thus have to use the heinous Environment.Exit(...) method
							mMiscPipeClient.Connect(500);
							mMiscStreamReader = new StreamReader(mMiscPipeClient);
						}
						catch // Who cares if this times out
						{
							continue;
						}
					}

					if (!mMiscPipeClient.IsConnected || !mMiscPipeClient.CanRead)
						continue;

					string miscMessages = null;
					// Not sure of the right way to make this non-blocking - ReadLineAsync seems to introduce some complexity with debugging
					while ((miscMessages = mMiscStreamReader.ReadLine()) != null)
					{
						mProcessMiscDelegate(miscMessages);
					}
				}
				catch (Exception exception)
				{
					string exceptionStr = "exception: " + exception.Message + Environment.NewLine + exception.StackTrace;
					// If you want this to get logged in the Misc Panel - just uncomment the "Trace.Listeners.Add(...)" in GameTalkNotify.cs
					Trace.WriteLine(exceptionStr);
				}
			}
		}

		/// <summary>
		/// Provides an indication if the named pipe exists. 
		/// This has to prove the pipe does not exist. If there is any doubt, this
		/// returns that it does exist and it is up to the caller to attempt to connect
		/// to that server. The means that there is a wide variety of errors that can occur that
		/// will be ignored - for example, a pipe name that contains invalid characters will result
		/// in a return value of false.
		/// 
		/// </summary>
		/// <param name="pipeName">The pipe to connect to</param>
		/// <returns>false if it can be proven it does not exist, otherwise true</returns>
		/// <remarks>
		/// Attempts to check if the pipe server exists without incurring the cost
		/// of calling NamedPipeClientStream.Connect. This is because Connect either 
		/// times out and throws an exception or goes into a tight spin loop burning
		/// up cpu cycles if the server does not exist.
		/// 
		/// Common Error codes from WinError.h
		/// ERROR_FILE_NOT_FOUND 2L
		/// ERROR_BROKEN_PIPE =  109 (0x6d)
		/// ERROR_BAD_PATHNAME  161L The specified path is invalid.
		/// ERROR_BAD_PIPE =  230  (0xe6) The pipe state is invalid.
		/// ERROR_PIPE_BUSY =  231 (0xe7) All pipe instances are busy.
		/// ERROR_NO_DATA =   232   (0xe8) the pipe is being closed
		/// ERROR_PIPE_NOT_CONNECTED 233L No process is on the other end of the pipe.
		/// ERROR_PIPE_CONNECTED        535L There is a process on other end of the pipe.
		/// ERROR_PIPE_LISTENING        536L Waiting for a process to open the other end of the pipe.
		/// 
		/// </remarks>
		static public bool NamedPipeDoesNotExist(string pipeName)
		{
			try
			{
				int timeout = 0;
				string normalizedPath = System.IO.Path.GetFullPath(
				 string.Format(@"\\.\pipe\{0}", pipeName));
				bool exists = WaitNamedPipe(normalizedPath, timeout);
				if (!exists)
				{
					int error = Marshal.GetLastWin32Error();
					if (error == 0) // pipe does not exist
						return true;
					else if (error == 2) // win32 error code for file not found
						return true;
					// all other errors indicate other issues
				}
				return false;
			}
			catch (Exception ex)
			{
				//Exception.Publish("Failure in WaitNamedPipe()", ex);
				Debug.WriteLine("ex: " + ex.Message + Environment.NewLine + ex.StackTrace);
				return true; // assume it exists
			}
		}

	}
}
