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

using System;
using System.Diagnostics;
using System.IO;
using System.Collections.Generic;

using System.Timers;
using Microsoft.Win32;

using EA.GameTalkMessages;

namespace EA.GameTalkRouter
{
	/// <summary>
	/// Summary description for Logger.
    /// 
    /// When GameTalkServer is run by System/Service account
    ///  Environment.SpecialFolder.CommonApplicationData == C:\ProgramData
    ///  Environment.SpecialFolder.LocalApplicationData == C:\windows\system32\config\systemprofile\AppData\Local 
    ///  Environment.SpecialFolder.ApplicationData == C:\windows\system32\config\systemprofile\AppData\Roaming
    ///  
    /// Therefore, to ensure that regardless whether the GameTalkServer is run as application or as service - we need
    /// to use CommonApplicationData
    /// </summary>
	public class Logger : ILogger
	{
		private readonly string mPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),  @"GameTalkServer\");
		private LoggingEventHandler mLoggerEventHandler = null;
		private string mMiscLogFileName = "GameTalkServer.txt";
		private StreamWriter mStreamWriter = null;
		private string mTrafficLogFile1Name = "GameTalkServerTraffic1.txt";
		private string mTrafficLogFile2Name = "GameTalkServerTraffic2.txt";
		private ulong mDataWrittenToTrafficLog = 0;
		private const ulong mMaxSizeOfTrafficLog = 25 * 1024 * 1024;
		private StreamWriter mTrafficStreamWriter = null;
		private string mConnectionsLogFileName = "GameTalkServerConnections.txt";
		private StreamWriter mConnectionsStreamWriter = null;
		private TextWriterTraceListener mTraceListener = null;
		private const string mSyncFileForMonitorToDelete = "WaitForMonitorToDelete.txt";
		private const string mSyncFileForServerToDelete = "WaitForServerToDelete.txt";
		private bool mWaitingForGameTalkMonitorToDeleteFile = false;

		private volatile bool mStartListenerStarted = false;

		private object mSyncObject = new object();

		private List<string> mConfigLog = new List<string>();

		internal Logger()
		{
			string version = string.Empty;
			try
			{
				version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString();
			}
			catch (Exception) { }

			lock (mConfigLog)
			{
				mConfigLog.Add("Logger started: " + version);
			}

			// This is really only here for those who want the best performance from GameTalk throughput possible
			// If someone enables this - then to disable this 
			// 1. they'll have to stop the GameTalkServer
			// 2. change the value in the registry to something other than ConfigReader.LoggingLevel.NO_LOGGING
			// 3. start the GameTalkServer
			if (ConfigReader.Instance.LogLevel != GameTalkMessages.LoggingLevel.NO_LOGGING)
			{
				// Create our event handler
				mLoggerEventHandler = new LoggingEventHandler(ServerEventReceiver);
				ServerEventManager.LoggerRegister(mLoggerEventHandler);
			}
		}

		internal Logger(string loggingPath):this()
		{
			mPath = loggingPath + Path.DirectorySeparatorChar;

			// Starting up - clear out previous logs - irrelevant now
			StopListener();
		}

		internal Logger(string loggingPath, string miscLogFilename,
			string trafficLogFilename, string connectionsLogFilename):this(loggingPath)
		{
			if (FileNameValidated(miscLogFilename))
				mMiscLogFileName = miscLogFilename;

			if (FileNameValidated(mTrafficLogFile1Name))
				mTrafficLogFile1Name = trafficLogFilename.Replace(".", "1.");

			if (FileNameValidated(mTrafficLogFile2Name))
				mTrafficLogFile2Name = trafficLogFilename.Replace(".", "2.");

			if (FileNameValidated(mTrafficLogFile2Name))
				mConnectionsLogFileName = connectionsLogFilename;
		}

		~Logger()
		{
			ServerEventManager.LoggerUnregister(mLoggerEventHandler);
		}

		private bool FileNameValidated(string filename)
		{
			if ((filename != null) && 
				(filename.Contains(".")))
				return true;

			return false;
		}

		private void StartListener()
		{
			try
			{
				if (mStreamWriter != null)
					return;

				lock (mSyncObject)
				{
					if (mStreamWriter != null) // We check again - in case another thread got in here since we checked the above condition
						return;

					if (!Directory.Exists(mPath))
						Directory.CreateDirectory(mPath);

					FileStream fileStream = new FileStream(mPath + mMiscLogFileName, FileMode.Append, FileAccess.Write, FileShare.Read);
					mStreamWriter = new StreamWriter(fileStream, System.Text.Encoding.ASCII);
					mStreamWriter.AutoFlush = true;

					FileStream fileStream2 = new FileStream(mPath + mTrafficLogFile2Name, FileMode.Append, FileAccess.Write, FileShare.Read);
					mTrafficStreamWriter = new StreamWriter(fileStream2, System.Text.Encoding.ASCII);
					mTrafficStreamWriter.AutoFlush = true;

					FileStream fileStream3 = new FileStream(mPath + mConnectionsLogFileName, FileMode.Append, FileAccess.Write, FileShare.Read);
					mConnectionsStreamWriter = new StreamWriter(fileStream3, System.Text.Encoding.ASCII);
					mConnectionsStreamWriter.AutoFlush = true;

					mTraceListener = new ExceptionSafeTraceListener(mStreamWriter, "GameServer");
					Trace.Listeners.Add(mTraceListener);
					Trace.AutoFlush = true;

					lock (mConfigLog)
					{
						if (mConfigLog.Count > 0)
						{
							Write(mConfigLog[0]);

							for (int i = 1; i < mConfigLog.Count; i++)
								Write("Configuration: " + mConfigLog[i]);
						}
					}
				}
			}
			catch (Exception e)
			{
				Trace.WriteLine(e.Message);
				Trace.WriteLine(e.StackTrace);
			}
		}

		/// <summary>
		/// We need to pass the StreamWriter by "ref" since otherwise assigning to null 
		/// wouldn't actually assign the original reference to null.
		/// We return true if 
		/// 1. the StreamWriter parameter was referencing a valid StreamWriter object (i.e. wasn't null)
		/// 2. we are successfully able to Close the stream without exception
		/// </summary>
		/// <param name="streamWriter"></param>
		/// <param name="fileName"></param>
		/// <returns></returns>
		private bool CloseWriter(ref StreamWriter streamWriter, string fileName, bool deleteFile)
		{
			bool result = false;
			if (streamWriter != null)
			{
				streamWriter.Close();
				streamWriter = null;

				result = true;
			}

			if (deleteFile)
			{
				string fullFileName = mPath + fileName;
				try
				{
					if (File.Exists(fullFileName))
					{
						File.Delete(fullFileName);
					}
				}
				catch (Exception ex)
				{
					Trace.WriteLine("Exception: " + ex.ToString());
					Trace.WriteLine(ex.StackTrace);
				}
			}
			return result;
		}

		private void StopListener()
		{
			lock (mSyncObject)
			{
				// Do traffic and connection first because the mStreamWriter is used by Trace and we want to log exceptions
				CloseWriter(ref mTrafficStreamWriter, mTrafficLogFile1Name, true);
				CloseWriter(ref mTrafficStreamWriter, mTrafficLogFile2Name, true);
				CloseWriter(ref mConnectionsStreamWriter, mConnectionsLogFileName, true);

				try
				{
					if (File.Exists(mPath + mSyncFileForMonitorToDelete))
						File.Delete(mPath + mSyncFileForMonitorToDelete);
				}
				catch { }

				try
				{
					if (File.Exists(mPath + mSyncFileForServerToDelete))
						File.Delete(mPath + mSyncFileForServerToDelete);
				}
				catch { }

				// Remove the listener before we try to close the writer - since the CloseWriter method may try to write something using Trace
				// Unfortunate we won't see if there were any issues with closing the stream to the misc log file
				Trace.Listeners.Remove(mTraceListener);
				CloseWriter(ref mStreamWriter, mMiscLogFileName, true);
			}
		}

		/// <summary>
		/// Without synchronizing on this entire method - we have a VERY unlikely
		/// race condition - if a ServerEventType.ClearLog and a different e.Type
		/// are received by this receiver at the same time (on different threads)
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		public void ServerEventReceiver(object sender, LoggingEventArgs e)
		{
			if (LoggingEventType.ClearLog == e.Type)
			{
				if (!mStartListenerStarted)
					StopListener();
			}
			else
			{
				mStartListenerStarted = true;
				StartListener();
				mStartListenerStarted = false;

				// The following lock is likely to introduce a noticeable performance hit
				// The consequence of not having it is occasionally an exception is thrown
				// when trying to write to a null or uninitialized writer
				//lock (mSyncObject)
				{
					switch (e.Type)
					{
						case LoggingEventType.LogConnection:
							string connectionMessage = "Connection: " + e.Param.ToString().Trim();
							ConnectionWrite(connectionMessage);
							break;
						case LoggingEventType.LogConnectionName:
							string connectionNameMessage = "TargetName: " + e.Param.ToString().Trim();
							ConnectionWrite(connectionNameMessage);
							break;
						case LoggingEventType.LogDisconnection:
							string disconnectionMessage = "Disconnection: " + e.Param.ToString().Trim();
							ConnectionWrite(disconnectionMessage);
							break;
						case LoggingEventType.LogMessageReceived:
							if (ConfigReader.Instance.LogLevel == GameTalkMessages.LoggingLevel.NO_MESSAGE_LOGGING)
								return;

							string messageReceived = "MessageReceived: " + e.Param.ToString().Trim();
							TrafficWrite(messageReceived);
							break;
						case LoggingEventType.LogMessageSent:
							if (ConfigReader.Instance.LogLevel == GameTalkMessages.LoggingLevel.NO_MESSAGE_LOGGING)
								return;

							string messageSent = "MessageSent: " + e.Param.ToString().Trim();
							TrafficWrite(messageSent);
							break;
						case LoggingEventType.LogComment:
							string currentDateTime = "";
							try
							{
								currentDateTime = DateTime.Now.ToString();
							}
							catch { }
							Write(string.Format("Comment: At {0} {1}", currentDateTime, e.Param.ToString().Trim()));
							break;
						case LoggingEventType.LogHiddenMessage:
							TrafficWrite("DoNotDisplay: " + e.Param.ToString().Trim());
							break;
						case LoggingEventType.LogConfiguration:
							lock (mConfigLog)
							{
								mConfigLog.Add(e.Param.ToString().Trim());
							}
							break;
						case LoggingEventType.LogStartProcess:
							Write("StartProcess: "+ e.Param.ToString().Trim());
							break;
					}
				}
			}
		}

		/// <summary>
		/// When logging that a message has been sent - this is useful for performance reasons
		/// </summary>
		/// <param name="targetName"></param>
		/// <param name="gtm"></param>
		public static void LogSentMessage(string senderName, string targetName, GameTalkMessage gtm)
		{
			if (ConfigReader.Instance.LogLevel == GameTalkMessages.LoggingLevel.NO_MESSAGE_LOGGING)
				return;

			string text = string.Format("{0}:{1}:{2}:{3}", senderName, targetName, gtm.ChannelId, gtm.ToStringMinusChannel());
			LoggingEventArgs messageEvent = new LoggingEventArgs(LoggingEventType.LogMessageSent, text);
			ServerEventManager.LoggingSend(null, messageEvent);
		}

		/// <summary>
		/// When logging that a message has been received - this is useful for performance reasons
		/// </summary>
		/// <param name="recipientTarget"></param>
		/// <param name="gtm"></param>
		public static void LogReceivedMessage(string senderName, string recipientTarget, GameTalkMessage gtm)
		{
			if (ConfigReader.Instance.LogLevel == GameTalkMessages.LoggingLevel.NO_MESSAGE_LOGGING)
				return;

			string text = string.Format("{0}:{1}:{2}:{3}", senderName, recipientTarget, gtm.ChannelId, gtm.ToStringMinusChannel());
			LoggingEventArgs e = new LoggingEventArgs(LoggingEventType.LogMessageReceived, text);
			ServerEventManager.LoggingSend(null, e);
		}

		public static void LogCommentMessage(string message)
		{
			LoggingEventArgs messageEvent = new LoggingEventArgs(LoggingEventType.LogComment, message);
			ServerEventManager.LoggingSend(null, messageEvent);
		}

		/// <summary>
		/// Log configuration information - probably doesn't want to be displayed in same place as regular comments (i.e. LogComment)
		/// Also these messages are permanently cached for the life-time of the GameTalkServer so try to ensure that messages aren't being repeated
		/// </summary>
		/// <param name="message"></param>
		public static void LogConfiguration(string message)
		{
			LoggingEventArgs messageEvent = new LoggingEventArgs(LoggingEventType.LogConfiguration, message);
			ServerEventManager.LoggingSend(null, messageEvent);
		}

		private void Write(string text)
		{
			try
			{
				lock (mTraceListener)
				{
					Trace.WriteLine(text);
				}
			}
			catch { }
		}

		private void ConnectionWrite(string text)
		{
			try
			{
				// Not certain what will happen if a thread invokes StreamWriter.WriteLine 
				// while another thread is using it - hence the lock
				lock (mConnectionsStreamWriter)
				{
					mConnectionsStreamWriter.WriteLine(text);
				}
			}
			catch { }
		}

		private void ReinitiateTrafficStreamWriter()
		{
			// We just want to close the writer - don't need to pass in the filename if we
			// don't also want to delete the filename
			CloseWriter(ref mTrafficStreamWriter, null, false);

			string trafficLogFile1FullPath = mPath + mTrafficLogFile1Name;
			bool successfullyDeletedFile = false;
			while (!successfullyDeletedFile)
			{
				try
				{
					if (File.Exists(trafficLogFile1FullPath))
						File.Delete(trafficLogFile1FullPath);

					successfullyDeletedFile = true;
				}
				catch 
				{ 
					System.Threading.Thread.Sleep(0);
				}
			}
			FileInfo fileInfo = new FileInfo(mPath + mTrafficLogFile2Name);
			bool successFullyMovedFile = false;
			while (!successFullyMovedFile)
			{
				try
				{
					// If the file viewer is reading from this file at this moment
					// we won't be able to move it - so we just have to keep trying
					// to find our "moment"
					fileInfo.MoveTo(trafficLogFile1FullPath);
					successFullyMovedFile = true;
				}
				catch (System.IO.IOException)
				{
					// This likey means the GameTalkMonitor has a lock on the file
					System.Threading.Thread.Sleep(0);
				}
				catch (Exception)
				{
					// Don't know what happened
					if (!File.Exists(mPath + mTrafficLogFile2Name))
						successFullyMovedFile = true;
				}
			}

			FileStream fileStream = new FileStream(mPath + mTrafficLogFile2Name, FileMode.Append, FileAccess.Write, FileShare.Read);
			mTrafficStreamWriter = new StreamWriter(fileStream, System.Text.Encoding.ASCII);
			mTrafficStreamWriter.AutoFlush = true;
		}

		private void NotifyGameTalkMonitorOfSwap()
		{
			if (!File.Exists(mPath + mSyncFileForMonitorToDelete))
			{
				//Don't use File.Create since it doesn't close the stream immediately - quicker to:
				FileStream fileStream = new FileStream(mPath + mSyncFileForMonitorToDelete, FileMode.CreateNew);
				fileStream.Close();
				mWaitingForGameTalkMonitorToDeleteFile = true;
			}
		}

		private void DeleteFileCreatedByGameTalkMonitor()
		{
			bool successfullyDeleted = false;
			while (!successfullyDeleted)
			{
				try
				{
					if (File.Exists(mPath + mSyncFileForServerToDelete))
						File.Delete(mPath + mSyncFileForServerToDelete);
					
					successfullyDeleted = true;
				}
				catch (Exception)
				{
					System.Threading.Thread.Sleep(0);
				}
			}
		}

		private void TrafficWrite(string text)
		{
			try
			{
				// Not certain what will happen if a thread invokes StreamWriter.WriteLine
				// while another thread is using it - hence the lock
				// We can't lock on mTrafficStreamWriter because one thread could run
				// ReinitiateTrafficStreamWriter (which assigns it to null) just before 
				// another thread gets here
				lock (mTrafficLogFile2Name)
				{
					mDataWrittenToTrafficLog += (ulong)text.Length;
					// If we exceed the desired max size of the traffic log - then 
					// perform the "swap" once it is safe
					if ((ConfigReader.Instance.LimitLogFileSize) && (mDataWrittenToTrafficLog > mMaxSizeOfTrafficLog))
					{
						if (ConfigReader.Instance.CreateFileForMonitorToDelete)
						{
							// If we haven't already notified the GameTalkMonitor that we're 
							// waiting on it - then do so now
							if (!mWaitingForGameTalkMonitorToDeleteFile)
								NotifyGameTalkMonitorOfSwap();

							// If the file we created has been deleted - then something
							// (likely GameTalkMonitor) has seen it and deleted it
							if (!File.Exists(mPath + mSyncFileForMonitorToDelete))
							{
								ReinitiateTrafficStreamWriter();
								mDataWrittenToTrafficLog = (ulong)text.Length;

								// Let the GameTalkMonitor know that we've reset our streams
								// and swapped the files
								DeleteFileCreatedByGameTalkMonitor();
								mWaitingForGameTalkMonitorToDeleteFile = false;
							}
						}
						else
						{
							ReinitiateTrafficStreamWriter();
							mDataWrittenToTrafficLog = (ulong)text.Length;
						}
					}
					mTrafficStreamWriter.WriteLine(text);
				}
			}
			catch { }
		}

		internal class ExceptionSafeTraceListener : TextWriterTraceListener
		{
			public ExceptionSafeTraceListener(Stream stream, string name)
				: base(stream, name)
			{
			}

			public ExceptionSafeTraceListener(TextWriter writer, string name)
				: base(writer, name)
			{
			}

			public override void WriteLine(object o)
			{
				try
				{
					base.WriteLine(o);
				}
				catch
				{
					// Nice if we could log this - but logging this could recurse infinitely and besides which if we have an exception it probably means there is a problem with the logging stream
				}
			}

			public override void WriteLine(string message)
			{
				try
				{
					base.WriteLine(message);
				}
				catch
				{
					// Nice if we could log this - but logging this could recurse infinitely and besides which if we have an exception it probably means there is a problem with the logging stream
				}
			}
		}
	}
}
